unforld 1 not health yet
This commit is contained in:
@@ -28,6 +28,9 @@ class Igny8AdminConfig(AdminConfig):
|
||||
|
||||
def ready(self):
|
||||
super().ready()
|
||||
# Import Unfold AFTER apps are ready
|
||||
from unfold.admin import ModelAdmin as UnfoldModelAdmin
|
||||
|
||||
# Register Django internals in admin (read-only where appropriate)
|
||||
from django.contrib.admin.models import LogEntry
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
@@ -35,8 +38,8 @@ class Igny8AdminConfig(AdminConfig):
|
||||
from django.contrib.sessions.models import Session
|
||||
|
||||
_safe_register(LogEntry, ReadOnlyAdmin)
|
||||
_safe_register(Permission, admin.ModelAdmin)
|
||||
_safe_register(Group, admin.ModelAdmin)
|
||||
_safe_register(Permission, UnfoldModelAdmin)
|
||||
_safe_register(Group, UnfoldModelAdmin)
|
||||
_safe_register(ContentType, ReadOnlyAdmin)
|
||||
_safe_register(Session, ReadOnlyAdmin)
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
"""
|
||||
Celery Task Monitoring Admin
|
||||
Celery Task Monitoring Admin - Unfold Style
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.contrib import messages
|
||||
from django_celery_results.models import TaskResult
|
||||
from rangefilter.filters import DateRangeFilter
|
||||
from unfold.admin import ModelAdmin
|
||||
from unfold.contrib.filters.admin import RangeDateFilter
|
||||
|
||||
|
||||
class CeleryTaskResultAdmin(admin.ModelAdmin):
|
||||
"""Admin interface for monitoring Celery tasks"""
|
||||
class CeleryTaskResultAdmin(ModelAdmin):
|
||||
"""Admin interface for monitoring Celery tasks with Unfold styling"""
|
||||
|
||||
list_display = [
|
||||
'task_id',
|
||||
@@ -22,8 +23,8 @@ class CeleryTaskResultAdmin(admin.ModelAdmin):
|
||||
list_filter = [
|
||||
'status',
|
||||
'task_name',
|
||||
('date_created', DateRangeFilter),
|
||||
('date_done', DateRangeFilter),
|
||||
('date_created', RangeDateFilter),
|
||||
('date_done', RangeDateFilter),
|
||||
]
|
||||
search_fields = ['task_id', 'task_name', 'task_args']
|
||||
readonly_fields = [
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
"""
|
||||
Custom AdminSite for IGNY8 to organize models into proper groups
|
||||
Custom AdminSite for IGNY8 to organize models into proper groups using Unfold
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.apps import AdminConfig
|
||||
from django.apps import apps
|
||||
from django.urls import path
|
||||
from django.urls import path, reverse_lazy
|
||||
from django.shortcuts import redirect
|
||||
from unfold.admin import ModelAdmin as UnfoldModelAdmin
|
||||
from unfold.sites import UnfoldAdminSite
|
||||
|
||||
|
||||
class Igny8AdminSite(admin.AdminSite):
|
||||
class Igny8AdminSite(UnfoldAdminSite):
|
||||
"""
|
||||
Custom AdminSite that organizes models into the planned groups:
|
||||
1. Billing & Tenancy
|
||||
2. Sites & Users
|
||||
3. Global Reference Data
|
||||
4. Planner
|
||||
5. Writer Module
|
||||
6. Thinker Module
|
||||
7. System Configuration
|
||||
Custom AdminSite based on Unfold that organizes models into the planned groups
|
||||
"""
|
||||
site_header = 'IGNY8 Administration'
|
||||
site_title = 'IGNY8 Admin'
|
||||
|
||||
@@ -36,6 +36,11 @@ ALLOWED_HOSTS = [
|
||||
]
|
||||
|
||||
INSTALLED_APPS = [
|
||||
# Django Unfold admin theme - MUST be before django.contrib.admin
|
||||
'unfold',
|
||||
'unfold.contrib.filters',
|
||||
'unfold.contrib.import_export',
|
||||
'unfold.contrib.simple_history',
|
||||
# Core Django apps - Custom admin with IGNY8 branding
|
||||
'igny8_core.admin.apps.Igny8AdminConfig', # Custom admin config
|
||||
'django.contrib.auth',
|
||||
@@ -51,6 +56,7 @@ INSTALLED_APPS = [
|
||||
'import_export',
|
||||
'rangefilter',
|
||||
'django_celery_results',
|
||||
'simple_history',
|
||||
# IGNY8 apps
|
||||
'igny8_core.auth.apps.Igny8CoreAuthConfig', # Use app config with custom label
|
||||
'igny8_core.ai.apps.AIConfig', # AI Framework
|
||||
@@ -108,6 +114,7 @@ MIDDLEWARE = [
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'simple_history.middleware.HistoryRequestMiddleware', # Audit trail
|
||||
'igny8_core.middleware.request_id.RequestIDMiddleware', # Request ID tracking (must be early)
|
||||
'igny8_core.auth.middleware.AccountContextMiddleware', # Multi-account support
|
||||
# AccountContextMiddleware sets request.account from JWT
|
||||
@@ -607,6 +614,40 @@ CELERY_CACHE_BACKEND = 'django-cache'
|
||||
# Import/Export Settings
|
||||
IMPORT_EXPORT_USE_TRANSACTIONS = True
|
||||
|
||||
# ==============================================================================
|
||||
# UNFOLD ADMIN CONFIGURATION
|
||||
# ==============================================================================
|
||||
# Modern Django admin theme with advanced features
|
||||
# Documentation: https://unfoldadmin.com/
|
||||
|
||||
UNFOLD = {
|
||||
"SITE_TITLE": "IGNY8 Administration",
|
||||
"SITE_HEADER": "IGNY8 Admin",
|
||||
"SITE_URL": "/",
|
||||
"SITE_SYMBOL": "speed", # Symbol from Material icons
|
||||
"SHOW_HISTORY": True, # Show history for models with simple_history
|
||||
"SHOW_VIEW_ON_SITE": True, # Show "View on site" button
|
||||
"COLORS": {
|
||||
"primary": {
|
||||
"50": "248 250 252",
|
||||
"100": "241 245 249",
|
||||
"200": "226 232 240",
|
||||
"300": "203 213 225",
|
||||
"400": "148 163 184",
|
||||
"500": "100 116 139",
|
||||
"600": "71 85 105",
|
||||
"700": "51 65 85",
|
||||
"800": "30 41 59",
|
||||
"900": "15 23 42",
|
||||
"950": "2 6 23",
|
||||
},
|
||||
},
|
||||
"SIDEBAR": {
|
||||
"show_search": True, # Show search in sidebar
|
||||
"show_all_applications": True, # Show all apps (we'll organize via custom get_app_list)
|
||||
},
|
||||
}
|
||||
|
||||
# Billing / Payments configuration
|
||||
STRIPE_PUBLIC_KEY = os.getenv('STRIPE_PUBLIC_KEY', '')
|
||||
STRIPE_SECRET_KEY = os.getenv('STRIPE_SECRET_KEY', '')
|
||||
|
||||
@@ -17,7 +17,8 @@ drf-spectacular>=0.27.0
|
||||
stripe>=7.10.0
|
||||
|
||||
# Django Admin Enhancements
|
||||
django-admin-interface==0.26.0
|
||||
django-unfold==0.73.1
|
||||
django-import-export==3.3.1
|
||||
django-admin-rangefilter==0.11.1
|
||||
django-celery-results==2.5.1
|
||||
django-simple-history==3.4.0
|
||||
|
||||
@@ -1,204 +1,246 @@
|
||||
/*global gettext, interpolate, ngettext, Actions*/
|
||||
'use strict';
|
||||
"use strict";
|
||||
{
|
||||
function show(selector) {
|
||||
document.querySelectorAll(selector).forEach(function(el) {
|
||||
el.classList.remove('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
function hide(selector) {
|
||||
document.querySelectorAll(selector).forEach(function(el) {
|
||||
el.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
function showQuestion(options) {
|
||||
hide(options.acrossClears);
|
||||
show(options.acrossQuestions);
|
||||
hide(options.allContainer);
|
||||
}
|
||||
|
||||
function showClear(options) {
|
||||
show(options.acrossClears);
|
||||
hide(options.acrossQuestions);
|
||||
document.querySelector(options.actionContainer).classList.remove(options.selectedClass);
|
||||
show(options.allContainer);
|
||||
hide(options.counterContainer);
|
||||
}
|
||||
|
||||
function reset(options) {
|
||||
hide(options.acrossClears);
|
||||
hide(options.acrossQuestions);
|
||||
hide(options.allContainer);
|
||||
show(options.counterContainer);
|
||||
}
|
||||
|
||||
function clearAcross(options) {
|
||||
reset(options);
|
||||
const acrossInputs = document.querySelectorAll(options.acrossInput);
|
||||
acrossInputs.forEach(function(acrossInput) {
|
||||
acrossInput.value = 0;
|
||||
});
|
||||
document.querySelector(options.actionContainer).classList.remove(options.selectedClass);
|
||||
}
|
||||
|
||||
function checker(actionCheckboxes, options, checked) {
|
||||
if (checked) {
|
||||
showQuestion(options);
|
||||
} else {
|
||||
reset(options);
|
||||
}
|
||||
actionCheckboxes.forEach(function(el) {
|
||||
el.checked = checked;
|
||||
el.closest('tr').classList.toggle(options.selectedClass, checked);
|
||||
});
|
||||
}
|
||||
|
||||
function updateCounter(actionCheckboxes, options) {
|
||||
const sel = Array.from(actionCheckboxes).filter(function(el) {
|
||||
return el.checked;
|
||||
}).length;
|
||||
const counter = document.querySelector(options.counterContainer);
|
||||
// data-actions-icnt is defined in the generated HTML
|
||||
// and contains the total amount of objects in the queryset
|
||||
const actions_icnt = Number(counter.dataset.actionsIcnt);
|
||||
counter.textContent = interpolate(
|
||||
ngettext('%(sel)s of %(cnt)s selected', '%(sel)s of %(cnt)s selected', sel), {
|
||||
sel: sel,
|
||||
cnt: actions_icnt
|
||||
}, true);
|
||||
const allToggle = document.getElementById(options.allToggleId);
|
||||
allToggle.checked = sel === actionCheckboxes.length;
|
||||
if (allToggle.checked) {
|
||||
showQuestion(options);
|
||||
} else {
|
||||
clearAcross(options);
|
||||
}
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
actionContainer: "div.actions",
|
||||
counterContainer: "span.action-counter",
|
||||
allContainer: "div.actions span.all",
|
||||
acrossInput: "div.actions input.select-across",
|
||||
acrossQuestions: "div.actions span.question",
|
||||
acrossClears: "div.actions span.clear",
|
||||
allToggleId: "action-toggle",
|
||||
selectedClass: "selected"
|
||||
};
|
||||
|
||||
window.Actions = function(actionCheckboxes, options) {
|
||||
options = Object.assign({}, defaults, options);
|
||||
let list_editable_changed = false;
|
||||
let lastChecked = null;
|
||||
let shiftPressed = false;
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
shiftPressed = event.shiftKey;
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', (event) => {
|
||||
shiftPressed = event.shiftKey;
|
||||
});
|
||||
|
||||
document.getElementById(options.allToggleId).addEventListener('click', function(event) {
|
||||
checker(actionCheckboxes, options, this.checked);
|
||||
updateCounter(actionCheckboxes, options);
|
||||
});
|
||||
|
||||
document.querySelectorAll(options.acrossQuestions + " a").forEach(function(el) {
|
||||
el.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
const acrossInputs = document.querySelectorAll(options.acrossInput);
|
||||
acrossInputs.forEach(function(acrossInput) {
|
||||
acrossInput.value = 1;
|
||||
});
|
||||
showClear(options);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll(options.acrossClears + " a").forEach(function(el) {
|
||||
el.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
document.getElementById(options.allToggleId).checked = false;
|
||||
clearAcross(options);
|
||||
checker(actionCheckboxes, options, false);
|
||||
updateCounter(actionCheckboxes, options);
|
||||
});
|
||||
});
|
||||
|
||||
function affectedCheckboxes(target, withModifier) {
|
||||
const multiSelect = (lastChecked && withModifier && lastChecked !== target);
|
||||
if (!multiSelect) {
|
||||
return [target];
|
||||
}
|
||||
const checkboxes = Array.from(actionCheckboxes);
|
||||
const targetIndex = checkboxes.findIndex(el => el === target);
|
||||
const lastCheckedIndex = checkboxes.findIndex(el => el === lastChecked);
|
||||
const startIndex = Math.min(targetIndex, lastCheckedIndex);
|
||||
const endIndex = Math.max(targetIndex, lastCheckedIndex);
|
||||
const filtered = checkboxes.filter((el, index) => (startIndex <= index) && (index <= endIndex));
|
||||
return filtered;
|
||||
};
|
||||
|
||||
Array.from(document.getElementById('result_list').tBodies).forEach(function(el) {
|
||||
el.addEventListener('change', function(event) {
|
||||
const target = event.target;
|
||||
if (target.classList.contains('action-select')) {
|
||||
const checkboxes = affectedCheckboxes(target, shiftPressed);
|
||||
checker(checkboxes, options, target.checked);
|
||||
updateCounter(actionCheckboxes, options);
|
||||
lastChecked = target;
|
||||
} else {
|
||||
list_editable_changed = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector('#changelist-form button[name=index]').addEventListener('click', function(event) {
|
||||
if (list_editable_changed) {
|
||||
const confirmed = confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."));
|
||||
if (!confirmed) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const el = document.querySelector('#changelist-form input[name=_save]');
|
||||
// The button does not exist if no fields are editable.
|
||||
if (el) {
|
||||
el.addEventListener('click', function(event) {
|
||||
if (document.querySelector('[name=action]').value) {
|
||||
const text = list_editable_changed
|
||||
? gettext("You have selected an action, but you haven’t saved your changes to individual fields yet. Please click OK to save. You’ll need to re-run the action.")
|
||||
: gettext("You have selected an action, and you haven’t made any changes on individual fields. You’re probably looking for the Go button rather than the Save button.");
|
||||
if (!confirm(text)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Sync counter when navigating to the page, such as through the back
|
||||
// button.
|
||||
window.addEventListener('pageshow', (event) => updateCounter(actionCheckboxes, options));
|
||||
};
|
||||
|
||||
// Call function fn when the DOM is loaded and ready. If it is already
|
||||
// loaded, call the function now.
|
||||
// http://youmightnotneedjquery.com/#ready
|
||||
function ready(fn) {
|
||||
if (document.readyState !== 'loading') {
|
||||
fn();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', fn);
|
||||
}
|
||||
}
|
||||
|
||||
ready(function() {
|
||||
const actionsEls = document.querySelectorAll('tr input.action-select');
|
||||
if (actionsEls.length > 0) {
|
||||
Actions(actionsEls);
|
||||
}
|
||||
function show(options, selector) {
|
||||
options.parent.querySelectorAll(selector).forEach(function (el) {
|
||||
el.classList.remove("hidden");
|
||||
});
|
||||
}
|
||||
|
||||
function hide(options, selector) {
|
||||
options.parent.querySelectorAll(selector).forEach(function (el) {
|
||||
el.classList.add("hidden");
|
||||
});
|
||||
}
|
||||
|
||||
function showQuestion(options) {
|
||||
hide(options, options.acrossClears);
|
||||
show(options, options.acrossQuestions);
|
||||
hide(options, options.allContainer);
|
||||
}
|
||||
|
||||
function showClear(options) {
|
||||
show(options, options.acrossClears);
|
||||
hide(options, options.acrossQuestions);
|
||||
options.parent
|
||||
.querySelector(options.actionContainer)
|
||||
.classList.remove(options.selectedClass);
|
||||
show(options, options.allContainer);
|
||||
hide(options, options.counterContainer);
|
||||
}
|
||||
|
||||
function reset(options) {
|
||||
hide(options, options.acrossClears);
|
||||
hide(options, options.acrossQuestions);
|
||||
hide(options, options.allContainer);
|
||||
show(options, options.counterContainer);
|
||||
}
|
||||
|
||||
function clearAcross(options) {
|
||||
reset(options);
|
||||
const acrossInputs = options.parent.querySelectorAll(options.acrossInput);
|
||||
acrossInputs.forEach(function (acrossInput) {
|
||||
acrossInput.value = 0;
|
||||
acrossInput.dispatchEvent(new Event("input"));
|
||||
});
|
||||
options.parent
|
||||
.querySelector(options.actionContainer)
|
||||
.classList.remove(options.selectedClass);
|
||||
}
|
||||
|
||||
function checker(actionCheckboxes, options, checked) {
|
||||
if (checked) {
|
||||
showQuestion(options);
|
||||
} else {
|
||||
reset(options);
|
||||
}
|
||||
actionCheckboxes.forEach(function (el) {
|
||||
el.checked = checked;
|
||||
el.closest("tr").classList.toggle(options.selectedClass, checked);
|
||||
});
|
||||
}
|
||||
|
||||
function updateCounter(actionCheckboxes, options) {
|
||||
const sel = Array.from(actionCheckboxes).filter(function (el) {
|
||||
return el.checked;
|
||||
}).length;
|
||||
const counter = options.parent.querySelector(options.counterContainer);
|
||||
// data-actions-icnt is defined in the generated HTML
|
||||
// and contains the total amount of objects in the queryset
|
||||
const actions_icnt = Number(counter.dataset.actionsIcnt);
|
||||
counter.textContent = interpolate(
|
||||
ngettext(
|
||||
"%(sel)s of %(cnt)s selected",
|
||||
"%(sel)s of %(cnt)s selected",
|
||||
sel
|
||||
),
|
||||
{
|
||||
sel: sel,
|
||||
cnt: actions_icnt,
|
||||
},
|
||||
true
|
||||
);
|
||||
const allToggle = options.parent.querySelector(".action-toggle");
|
||||
allToggle.checked = sel === actionCheckboxes.length;
|
||||
if (allToggle.checked) {
|
||||
showQuestion(options);
|
||||
} else {
|
||||
clearAcross(options);
|
||||
}
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
actionContainer: "div.actions",
|
||||
counterContainer: "span.action-counter",
|
||||
allContainer: "div.actions span.all",
|
||||
acrossInput: "div.actions input.select-across",
|
||||
acrossQuestions: "div.actions span.question",
|
||||
acrossClears: "div.actions span.clear",
|
||||
allToggleId: "action-toggle",
|
||||
selectedClass: "selected",
|
||||
};
|
||||
|
||||
window.Actions = function (actionCheckboxes, options) {
|
||||
options = Object.assign({}, defaults, options);
|
||||
let list_editable_changed = false;
|
||||
let lastChecked = null;
|
||||
let shiftPressed = false;
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
shiftPressed = event.shiftKey;
|
||||
});
|
||||
|
||||
document.addEventListener("keyup", (event) => {
|
||||
shiftPressed = event.shiftKey;
|
||||
});
|
||||
|
||||
const allToggle = options.parent.querySelector(".action-toggle");
|
||||
allToggle.addEventListener("click", function (event) {
|
||||
checker(actionCheckboxes, options, this.checked);
|
||||
updateCounter(actionCheckboxes, options);
|
||||
});
|
||||
|
||||
options.parent
|
||||
.querySelectorAll(options.acrossQuestions + " a")
|
||||
.forEach(function (el) {
|
||||
el.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
const acrossInputs = options.parent.querySelectorAll(
|
||||
options.acrossInput
|
||||
);
|
||||
acrossInputs.forEach(function (acrossInput) {
|
||||
acrossInput.value = 1;
|
||||
acrossInput.dispatchEvent(new Event("input"));
|
||||
});
|
||||
showClear(options);
|
||||
});
|
||||
});
|
||||
|
||||
options.parent
|
||||
.querySelectorAll(options.acrossClears + " a")
|
||||
.forEach(function (el) {
|
||||
el.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
options.parent.querySelector(".action-toggle").checked = false;
|
||||
clearAcross(options);
|
||||
checker(actionCheckboxes, options, false);
|
||||
updateCounter(actionCheckboxes, options);
|
||||
});
|
||||
});
|
||||
|
||||
function affectedCheckboxes(target, withModifier) {
|
||||
const multiSelect = lastChecked && withModifier && lastChecked !== target;
|
||||
if (!multiSelect) {
|
||||
return [target];
|
||||
}
|
||||
const checkboxes = Array.from(actionCheckboxes);
|
||||
const targetIndex = checkboxes.findIndex((el) => el === target);
|
||||
const lastCheckedIndex = checkboxes.findIndex((el) => el === lastChecked);
|
||||
const startIndex = Math.min(targetIndex, lastCheckedIndex);
|
||||
const endIndex = Math.max(targetIndex, lastCheckedIndex);
|
||||
const filtered = checkboxes.filter(
|
||||
(el, index) => startIndex <= index && index <= endIndex
|
||||
);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
const resultList = options.parent.querySelector(".result-list").tBodies;
|
||||
Array.from(resultList).forEach(function (el) {
|
||||
el.addEventListener("change", function (event) {
|
||||
const target = event.target;
|
||||
if (target.classList.contains("action-select")) {
|
||||
const checkboxes = affectedCheckboxes(target, shiftPressed);
|
||||
checker(checkboxes, options, target.checked);
|
||||
updateCounter(actionCheckboxes, options);
|
||||
lastChecked = target;
|
||||
} else {
|
||||
list_editable_changed = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
options.parent
|
||||
.querySelector("button[name=index]")
|
||||
.addEventListener("click", function (event) {
|
||||
if (list_editable_changed) {
|
||||
const confirmed = confirm(
|
||||
gettext(
|
||||
"You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."
|
||||
)
|
||||
);
|
||||
if (!confirmed) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const el = options.parent.querySelector("input[name=_save]");
|
||||
|
||||
// The button does not exist if no fields are editable.
|
||||
if (el) {
|
||||
el.addEventListener("click", function (event) {
|
||||
if (document.querySelector("[name=action]").value) {
|
||||
const text = list_editable_changed
|
||||
? gettext(
|
||||
"You have selected an action, but you haven’t saved your changes to individual fields yet. Please click OK to save. You’ll need to re-run the action."
|
||||
)
|
||||
: gettext(
|
||||
"You have selected an action, and you haven’t made any changes on individual fields. You’re probably looking for the Go button rather than the Save button."
|
||||
);
|
||||
if (!confirm(text)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Sync counter when navigating to the page, such as through the back
|
||||
// button.
|
||||
window.addEventListener("pageshow", (event) =>
|
||||
updateCounter(actionCheckboxes, options)
|
||||
);
|
||||
};
|
||||
|
||||
// Call function fn when the DOM is loaded and ready. If it is already
|
||||
// loaded, call the function now.
|
||||
// http://youmightnotneedjquery.com/#ready
|
||||
function ready(fn) {
|
||||
if (document.readyState !== "loading") {
|
||||
fn();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", fn);
|
||||
}
|
||||
}
|
||||
|
||||
ready(function () {
|
||||
document.querySelectorAll(".result-list-wrapper").forEach(function (el) {
|
||||
const actionsEls = el.querySelectorAll("tr input.action-select");
|
||||
|
||||
if (actionsEls.length > 0) {
|
||||
Actions(actionsEls, {
|
||||
parent: el,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,252 +1,301 @@
|
||||
/*global SelectBox, interpolate*/
|
||||
// Handles related-objects functionality: lookup link for raw_id_fields
|
||||
// and Add Another links.
|
||||
'use strict';
|
||||
"use strict";
|
||||
{
|
||||
const $ = django.jQuery;
|
||||
let popupIndex = 0;
|
||||
const relatedWindows = [];
|
||||
const $ = django.jQuery;
|
||||
let popupIndex = 0;
|
||||
const relatedWindows = [];
|
||||
|
||||
function dismissChildPopups() {
|
||||
relatedWindows.forEach(function(win) {
|
||||
if(!win.closed) {
|
||||
win.dismissChildPopups();
|
||||
win.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setPopupIndex() {
|
||||
if(document.getElementsByName("_popup").length > 0) {
|
||||
const index = window.name.lastIndexOf("__") + 2;
|
||||
popupIndex = parseInt(window.name.substring(index));
|
||||
} else {
|
||||
popupIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function addPopupIndex(name) {
|
||||
return name + "__" + (popupIndex + 1);
|
||||
}
|
||||
|
||||
function removePopupIndex(name) {
|
||||
return name.replace(new RegExp("__" + (popupIndex + 1) + "$"), '');
|
||||
}
|
||||
|
||||
function showAdminPopup(triggeringLink, name_regexp, add_popup) {
|
||||
const name = addPopupIndex(triggeringLink.id.replace(name_regexp, ''));
|
||||
const href = new URL(triggeringLink.href);
|
||||
if (add_popup) {
|
||||
href.searchParams.set('_popup', 1);
|
||||
}
|
||||
const win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes');
|
||||
relatedWindows.push(win);
|
||||
win.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
function showRelatedObjectLookupPopup(triggeringLink) {
|
||||
return showAdminPopup(triggeringLink, /^lookup_/, true);
|
||||
}
|
||||
|
||||
function dismissRelatedLookupPopup(win, chosenId) {
|
||||
const name = removePopupIndex(win.name);
|
||||
const elem = document.getElementById(name);
|
||||
if (elem.classList.contains('vManyToManyRawIdAdminField') && elem.value) {
|
||||
elem.value += ',' + chosenId;
|
||||
} else {
|
||||
elem.value = chosenId;
|
||||
}
|
||||
$(elem).trigger('change');
|
||||
const index = relatedWindows.indexOf(win);
|
||||
if (index > -1) {
|
||||
relatedWindows.splice(index, 1);
|
||||
}
|
||||
function dismissChildPopups() {
|
||||
relatedWindows.forEach(function (win) {
|
||||
if (!win.closed) {
|
||||
win.dismissChildPopups();
|
||||
win.close();
|
||||
}
|
||||
|
||||
function showRelatedObjectPopup(triggeringLink) {
|
||||
return showAdminPopup(triggeringLink, /^(change|add|delete)_/, false);
|
||||
}
|
||||
|
||||
function updateRelatedObjectLinks(triggeringLink) {
|
||||
const $this = $(triggeringLink);
|
||||
const siblings = $this.nextAll('.view-related, .change-related, .delete-related');
|
||||
if (!siblings.length) {
|
||||
return;
|
||||
}
|
||||
const value = $this.val();
|
||||
if (value) {
|
||||
siblings.each(function() {
|
||||
const elm = $(this);
|
||||
elm.attr('href', elm.attr('data-href-template').replace('__fk__', value));
|
||||
elm.removeAttr('aria-disabled');
|
||||
});
|
||||
} else {
|
||||
siblings.removeAttr('href');
|
||||
siblings.attr('aria-disabled', true);
|
||||
}
|
||||
}
|
||||
|
||||
function updateRelatedSelectsOptions(currentSelect, win, objId, newRepr, newId, skipIds = []) {
|
||||
// After create/edit a model from the options next to the current
|
||||
// select (+ or :pencil:) update ForeignKey PK of the rest of selects
|
||||
// in the page.
|
||||
|
||||
const path = win.location.pathname;
|
||||
// Extract the model from the popup url '.../<model>/add/' or
|
||||
// '.../<model>/<id>/change/' depending the action (add or change).
|
||||
const modelName = path.split('/')[path.split('/').length - (objId ? 4 : 3)];
|
||||
// Select elements with a specific model reference and context of "available-source".
|
||||
const selectsRelated = document.querySelectorAll(`[data-model-ref="${modelName}"] [data-context="available-source"]`);
|
||||
|
||||
selectsRelated.forEach(function(select) {
|
||||
if (currentSelect === select || skipIds && skipIds.includes(select.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let option = select.querySelector(`option[value="${objId}"]`);
|
||||
|
||||
if (!option) {
|
||||
option = new Option(newRepr, newId);
|
||||
select.options.add(option);
|
||||
// Update SelectBox cache for related fields.
|
||||
if (window.SelectBox !== undefined && !SelectBox.cache[currentSelect.id]) {
|
||||
SelectBox.add_to_cache(select.id, option);
|
||||
SelectBox.redisplay(select.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
option.textContent = newRepr;
|
||||
option.value = newId;
|
||||
});
|
||||
}
|
||||
|
||||
function dismissAddRelatedObjectPopup(win, newId, newRepr) {
|
||||
const name = removePopupIndex(win.name);
|
||||
const elem = document.getElementById(name);
|
||||
if (elem) {
|
||||
const elemName = elem.nodeName.toUpperCase();
|
||||
if (elemName === 'SELECT') {
|
||||
elem.options[elem.options.length] = new Option(newRepr, newId, true, true);
|
||||
updateRelatedSelectsOptions(elem, win, null, newRepr, newId);
|
||||
} else if (elemName === 'INPUT') {
|
||||
if (elem.classList.contains('vManyToManyRawIdAdminField') && elem.value) {
|
||||
elem.value += ',' + newId;
|
||||
} else {
|
||||
elem.value = newId;
|
||||
}
|
||||
}
|
||||
// Trigger a change event to update related links if required.
|
||||
$(elem).trigger('change');
|
||||
} else {
|
||||
const toId = name + "_to";
|
||||
const toElem = document.getElementById(toId);
|
||||
const o = new Option(newRepr, newId);
|
||||
SelectBox.add_to_cache(toId, o);
|
||||
SelectBox.redisplay(toId);
|
||||
if (toElem && toElem.nodeName.toUpperCase() === 'SELECT') {
|
||||
const skipIds = [name + "_from"];
|
||||
updateRelatedSelectsOptions(toElem, win, null, newRepr, newId, skipIds);
|
||||
}
|
||||
}
|
||||
const index = relatedWindows.indexOf(win);
|
||||
if (index > -1) {
|
||||
relatedWindows.splice(index, 1);
|
||||
}
|
||||
win.close();
|
||||
}
|
||||
|
||||
function dismissChangeRelatedObjectPopup(win, objId, newRepr, newId) {
|
||||
const id = removePopupIndex(win.name.replace(/^edit_/, ''));
|
||||
const selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]);
|
||||
const selects = $(selectsSelector);
|
||||
selects.find('option').each(function() {
|
||||
if (this.value === objId) {
|
||||
this.textContent = newRepr;
|
||||
this.value = newId;
|
||||
}
|
||||
}).trigger('change');
|
||||
updateRelatedSelectsOptions(selects[0], win, objId, newRepr, newId);
|
||||
selects.next().find('.select2-selection__rendered').each(function() {
|
||||
// The element can have a clear button as a child.
|
||||
// Use the lastChild to modify only the displayed value.
|
||||
this.lastChild.textContent = newRepr;
|
||||
this.title = newRepr;
|
||||
});
|
||||
const index = relatedWindows.indexOf(win);
|
||||
if (index > -1) {
|
||||
relatedWindows.splice(index, 1);
|
||||
}
|
||||
win.close();
|
||||
}
|
||||
|
||||
function dismissDeleteRelatedObjectPopup(win, objId) {
|
||||
const id = removePopupIndex(win.name.replace(/^delete_/, ''));
|
||||
const selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]);
|
||||
const selects = $(selectsSelector);
|
||||
selects.find('option').each(function() {
|
||||
if (this.value === objId) {
|
||||
$(this).remove();
|
||||
}
|
||||
}).trigger('change');
|
||||
const index = relatedWindows.indexOf(win);
|
||||
if (index > -1) {
|
||||
relatedWindows.splice(index, 1);
|
||||
}
|
||||
win.close();
|
||||
}
|
||||
|
||||
window.showRelatedObjectLookupPopup = showRelatedObjectLookupPopup;
|
||||
window.dismissRelatedLookupPopup = dismissRelatedLookupPopup;
|
||||
window.showRelatedObjectPopup = showRelatedObjectPopup;
|
||||
window.updateRelatedObjectLinks = updateRelatedObjectLinks;
|
||||
window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup;
|
||||
window.dismissChangeRelatedObjectPopup = dismissChangeRelatedObjectPopup;
|
||||
window.dismissDeleteRelatedObjectPopup = dismissDeleteRelatedObjectPopup;
|
||||
window.dismissChildPopups = dismissChildPopups;
|
||||
window.relatedWindows = relatedWindows;
|
||||
|
||||
// Kept for backward compatibility
|
||||
window.showAddAnotherPopup = showRelatedObjectPopup;
|
||||
window.dismissAddAnotherPopup = dismissAddRelatedObjectPopup;
|
||||
|
||||
window.addEventListener('unload', function(evt) {
|
||||
window.dismissChildPopups();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
setPopupIndex();
|
||||
$("a[data-popup-opener]").on('click', function(event) {
|
||||
event.preventDefault();
|
||||
opener.dismissRelatedLookupPopup(window, $(this).data("popup-opener"));
|
||||
});
|
||||
$('body').on('click', '.related-widget-wrapper-link[data-popup="yes"]', function(e) {
|
||||
e.preventDefault();
|
||||
if (this.href) {
|
||||
const event = $.Event('django:show-related', {href: this.href});
|
||||
$(this).trigger(event);
|
||||
if (!event.isDefaultPrevented()) {
|
||||
showRelatedObjectPopup(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
$('body').on('change', '.related-widget-wrapper select', function(e) {
|
||||
const event = $.Event('django:update-related');
|
||||
$(this).trigger(event);
|
||||
if (!event.isDefaultPrevented()) {
|
||||
updateRelatedObjectLinks(this);
|
||||
}
|
||||
});
|
||||
$('.related-widget-wrapper select').trigger('change');
|
||||
$('body').on('click', '.related-lookup', function(e) {
|
||||
e.preventDefault();
|
||||
const event = $.Event('django:lookup-related');
|
||||
$(this).trigger(event);
|
||||
if (!event.isDefaultPrevented()) {
|
||||
showRelatedObjectLookupPopup(this);
|
||||
}
|
||||
});
|
||||
function setPopupIndex() {
|
||||
if (document.getElementsByName("_popup").length > 0) {
|
||||
const index = window.name.lastIndexOf("__") + 2;
|
||||
popupIndex = parseInt(window.name.substring(index));
|
||||
} else {
|
||||
popupIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function addPopupIndex(name) {
|
||||
return name + "__" + (popupIndex + 1);
|
||||
}
|
||||
|
||||
function removePopupIndex(name) {
|
||||
return name.replace(new RegExp("__" + (popupIndex + 1) + "$"), "");
|
||||
}
|
||||
|
||||
function showAdminPopup(triggeringLink, name_regexp, add_popup) {
|
||||
const name = addPopupIndex(triggeringLink.id.replace(name_regexp, ""));
|
||||
const href = new URL(triggeringLink.href);
|
||||
if (add_popup) {
|
||||
href.searchParams.set("_popup", 1);
|
||||
}
|
||||
const win = window.open(
|
||||
href,
|
||||
name,
|
||||
"height=768,width=1024,resizable=yes,scrollbars=yes"
|
||||
);
|
||||
relatedWindows.push(win);
|
||||
win.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
function showRelatedObjectLookupPopup(triggeringLink) {
|
||||
return showAdminPopup(triggeringLink, /^lookup_/, true);
|
||||
}
|
||||
|
||||
function dismissRelatedLookupPopup(win, chosenId) {
|
||||
const name = removePopupIndex(win.name);
|
||||
const elem = document.getElementById(name);
|
||||
if (elem.classList.contains("vManyToManyRawIdAdminField") && elem.value) {
|
||||
elem.value += "," + chosenId;
|
||||
} else {
|
||||
document.getElementById(name).value = chosenId;
|
||||
}
|
||||
const index = relatedWindows.indexOf(win);
|
||||
if (index > -1) {
|
||||
relatedWindows.splice(index, 1);
|
||||
}
|
||||
win.close();
|
||||
}
|
||||
|
||||
function showRelatedObjectPopup(triggeringLink) {
|
||||
return showAdminPopup(triggeringLink, /^(change|add|delete)_/, false);
|
||||
}
|
||||
|
||||
function updateRelatedObjectLinks(triggeringLink) {
|
||||
const $this = $(triggeringLink);
|
||||
// !CHANGED from original
|
||||
// const siblings = $this.nextAll(
|
||||
// ".view-related, .change-related, .delete-related"
|
||||
// );
|
||||
|
||||
const siblings = $this
|
||||
.closest(".related-widget-wrapper")
|
||||
.find(".view-related, .change-related, .delete-related");
|
||||
|
||||
if (!siblings.length) {
|
||||
return;
|
||||
}
|
||||
const value = $this.val();
|
||||
if (value) {
|
||||
siblings.each(function () {
|
||||
const elm = $(this);
|
||||
elm.attr(
|
||||
"href",
|
||||
elm.attr("data-href-template").replace("__fk__", value)
|
||||
);
|
||||
elm.removeAttr("aria-disabled");
|
||||
});
|
||||
} else {
|
||||
siblings.removeAttr("href");
|
||||
siblings.attr("aria-disabled", true);
|
||||
}
|
||||
}
|
||||
|
||||
function updateRelatedSelectsOptions(
|
||||
currentSelect,
|
||||
win,
|
||||
objId,
|
||||
newRepr,
|
||||
newId,
|
||||
skipIds = []
|
||||
) {
|
||||
// After create/edit a model from the options next to the current
|
||||
// select (+ or :pencil:) update ForeignKey PK of the rest of selects
|
||||
// in the page.
|
||||
|
||||
const path = win.location.pathname;
|
||||
// Extract the model from the popup url '.../<model>/add/' or
|
||||
// '.../<model>/<id>/change/' depending the action (add or change).
|
||||
const modelName = path.split("/")[path.split("/").length - (objId ? 4 : 3)];
|
||||
// Select elements with a specific model reference and context of "available-source".
|
||||
const selectsRelated = document.querySelectorAll(
|
||||
`[data-model-ref="${modelName}"] [data-context="available-source"]`
|
||||
);
|
||||
|
||||
selectsRelated.forEach(function (select) {
|
||||
if (
|
||||
currentSelect === select ||
|
||||
(skipIds && skipIds.includes(select.id))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let option = select.querySelector(`option[value="${objId}"]`);
|
||||
|
||||
if (!option) {
|
||||
option = new Option(newRepr, newId);
|
||||
select.options.add(option);
|
||||
// Update SelectBox cache for related fields.
|
||||
if (
|
||||
window.SelectBox !== undefined &&
|
||||
!SelectBox.cache[currentSelect.id]
|
||||
) {
|
||||
SelectBox.add_to_cache(select.id, option);
|
||||
SelectBox.redisplay(select.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
option.textContent = newRepr;
|
||||
option.value = newId;
|
||||
});
|
||||
}
|
||||
|
||||
function dismissAddRelatedObjectPopup(win, newId, newRepr) {
|
||||
const name = removePopupIndex(win.name);
|
||||
const elem = document.getElementById(name);
|
||||
if (elem) {
|
||||
const elemName = elem.nodeName.toUpperCase();
|
||||
if (elemName === "SELECT") {
|
||||
elem.options[elem.options.length] = new Option(
|
||||
newRepr,
|
||||
newId,
|
||||
true,
|
||||
true
|
||||
);
|
||||
updateRelatedSelectsOptions(elem, win, null, newRepr, newId);
|
||||
} else if (elemName === "INPUT") {
|
||||
if (
|
||||
elem.classList.contains("vManyToManyRawIdAdminField") &&
|
||||
elem.value
|
||||
) {
|
||||
elem.value += "," + newId;
|
||||
} else {
|
||||
elem.value = newId;
|
||||
}
|
||||
}
|
||||
// Trigger a change event to update related links if required.
|
||||
$(elem).trigger("change");
|
||||
} else {
|
||||
const toId = name + "_to";
|
||||
const toElem = document.getElementById(toId);
|
||||
const o = new Option(newRepr, newId);
|
||||
SelectBox.add_to_cache(toId, o);
|
||||
SelectBox.redisplay(toId);
|
||||
if (toElem && toElem.nodeName.toUpperCase() === "SELECT") {
|
||||
const skipIds = [name + "_from"];
|
||||
updateRelatedSelectsOptions(toElem, win, null, newRepr, newId, skipIds);
|
||||
}
|
||||
}
|
||||
const index = relatedWindows.indexOf(win);
|
||||
if (index > -1) {
|
||||
relatedWindows.splice(index, 1);
|
||||
}
|
||||
win.close();
|
||||
}
|
||||
|
||||
function dismissChangeRelatedObjectPopup(win, objId, newRepr, newId) {
|
||||
const id = removePopupIndex(win.name.replace(/^edit_/, ""));
|
||||
const selectsSelector = interpolate("#%s, #%s_from, #%s_to", [id, id, id]);
|
||||
const selects = $(selectsSelector);
|
||||
selects
|
||||
.find("option")
|
||||
.each(function () {
|
||||
if (this.value === objId) {
|
||||
this.textContent = newRepr;
|
||||
this.value = newId;
|
||||
}
|
||||
})
|
||||
.trigger("change");
|
||||
updateRelatedSelectsOptions(selects[0], win, objId, newRepr, newId);
|
||||
selects
|
||||
.next()
|
||||
.find(".select2-selection__rendered")
|
||||
.each(function () {
|
||||
// The element can have a clear button as a child.
|
||||
// Use the lastChild to modify only the displayed value.
|
||||
this.lastChild.textContent = newRepr;
|
||||
this.title = newRepr;
|
||||
});
|
||||
const index = relatedWindows.indexOf(win);
|
||||
if (index > -1) {
|
||||
relatedWindows.splice(index, 1);
|
||||
}
|
||||
win.close();
|
||||
}
|
||||
|
||||
function dismissDeleteRelatedObjectPopup(win, objId) {
|
||||
const id = removePopupIndex(win.name.replace(/^delete_/, ""));
|
||||
const selectsSelector = interpolate("#%s, #%s_from, #%s_to", [id, id, id]);
|
||||
const selects = $(selectsSelector);
|
||||
selects
|
||||
.find("option")
|
||||
.each(function () {
|
||||
if (this.value === objId) {
|
||||
$(this).remove();
|
||||
}
|
||||
})
|
||||
.trigger("change");
|
||||
const index = relatedWindows.indexOf(win);
|
||||
if (index > -1) {
|
||||
relatedWindows.splice(index, 1);
|
||||
}
|
||||
win.close();
|
||||
}
|
||||
|
||||
window.showRelatedObjectLookupPopup = showRelatedObjectLookupPopup;
|
||||
window.dismissRelatedLookupPopup = dismissRelatedLookupPopup;
|
||||
window.showRelatedObjectPopup = showRelatedObjectPopup;
|
||||
window.updateRelatedObjectLinks = updateRelatedObjectLinks;
|
||||
window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup;
|
||||
window.dismissChangeRelatedObjectPopup = dismissChangeRelatedObjectPopup;
|
||||
window.dismissDeleteRelatedObjectPopup = dismissDeleteRelatedObjectPopup;
|
||||
window.dismissChildPopups = dismissChildPopups;
|
||||
|
||||
// Kept for backward compatibility
|
||||
window.showAddAnotherPopup = showRelatedObjectPopup;
|
||||
window.dismissAddAnotherPopup = dismissAddRelatedObjectPopup;
|
||||
|
||||
window.addEventListener("unload", function (evt) {
|
||||
window.dismissChildPopups();
|
||||
});
|
||||
|
||||
$(document).ready(function () {
|
||||
setPopupIndex();
|
||||
$("a[data-popup-opener]").on("click", function (event) {
|
||||
event.preventDefault();
|
||||
opener.dismissRelatedLookupPopup(window, $(this).data("popup-opener"));
|
||||
});
|
||||
$("body").on(
|
||||
"click",
|
||||
'.related-widget-wrapper-link[data-popup="yes"]',
|
||||
function (e) {
|
||||
e.preventDefault();
|
||||
if (this.href) {
|
||||
const event = $.Event("django:show-related", { href: this.href });
|
||||
$(this).trigger(event);
|
||||
if (!event.isDefaultPrevented()) {
|
||||
showRelatedObjectPopup(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
$("body").on("change", ".related-widget-wrapper select", function (e) {
|
||||
const event = $.Event("django:update-related");
|
||||
$(this).trigger(event);
|
||||
if (!event.isDefaultPrevented()) {
|
||||
updateRelatedObjectLinks(this);
|
||||
}
|
||||
});
|
||||
$(".related-widget-wrapper select").trigger("change");
|
||||
$("body").on("click", ".related-lookup", function (e) {
|
||||
e.preventDefault();
|
||||
const event = $.Event("django:lookup-related");
|
||||
$(this).trigger(event);
|
||||
if (!event.isDefaultPrevented()) {
|
||||
showRelatedObjectLookupPopup(this);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,345 +15,485 @@
|
||||
* 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) {
|
||||
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;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
/**
|
||||
* 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");
|
||||
} else {
|
||||
addButton.parent().hide();
|
||||
// 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.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.
|
||||
// !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,
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
// !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 toggleDeleteButtonVisibility = function (inlineGroup) {
|
||||
if (minForms.val() !== "" && minForms.val() - totalForms.val() >= 0) {
|
||||
inlineGroup.find(".inline-deletelink").hide();
|
||||
} else {
|
||||
inlineGroup.find(".inline-deletelink").show();
|
||||
}
|
||||
};
|
||||
|
||||
// !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);
|
||||
});
|
||||
} 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();
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
$(event.target).find(".admin-autocomplete").djangoAdminSelect2();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/* 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) {
|
||||
const $rows = $(this);
|
||||
|
||||
const reinitDateTimeShortCuts = function() {
|
||||
// Reinitialize the calendar and clock widgets by force
|
||||
if (typeof DateTimeShortcuts !== "undefined") {
|
||||
$(".datetimeshortcuts").remove();
|
||||
DateTimeShortcuts.init();
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
return $rows;
|
||||
};
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
return $rows;
|
||||
};
|
||||
|
||||
$(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;
|
||||
}
|
||||
});
|
||||
$(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
21
backend/staticfiles/unfold/css/simplebar/LICENSE
Normal file
21
backend/staticfiles/unfold/css/simplebar/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Jonathan Nicol
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
230
backend/staticfiles/unfold/css/simplebar/simplebar.css
Normal file
230
backend/staticfiles/unfold/css/simplebar/simplebar.css
Normal file
@@ -0,0 +1,230 @@
|
||||
[data-simplebar] {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
align-content: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.simplebar-wrapper {
|
||||
overflow: hidden;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
max-width: inherit;
|
||||
max-height: inherit;
|
||||
}
|
||||
|
||||
.simplebar-mask {
|
||||
direction: inherit;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.simplebar-offset {
|
||||
direction: inherit !important;
|
||||
box-sizing: inherit !important;
|
||||
resize: none !important;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.simplebar-content-wrapper {
|
||||
direction: inherit;
|
||||
box-sizing: border-box !important;
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 100%; /* Required for horizontal native scrollbar to not appear if parent is taller than natural height */
|
||||
width: auto;
|
||||
max-width: 100%; /* Not required for horizontal scroll to trigger */
|
||||
max-height: 100%; /* Needed for vertical scroll to trigger */
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.simplebar-content-wrapper::-webkit-scrollbar,
|
||||
.simplebar-hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.simplebar-content:before,
|
||||
.simplebar-content:after {
|
||||
content: ' ';
|
||||
display: table;
|
||||
}
|
||||
|
||||
.simplebar-placeholder {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.simplebar-height-auto-observer-wrapper {
|
||||
box-sizing: inherit !important;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 1px;
|
||||
position: relative;
|
||||
float: left;
|
||||
max-height: 1px;
|
||||
overflow: hidden;
|
||||
z-index: -1;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
flex-grow: inherit;
|
||||
flex-shrink: 0;
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
.simplebar-height-auto-observer {
|
||||
box-sizing: inherit;
|
||||
display: block;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 1000%;
|
||||
width: 1000%;
|
||||
min-height: 1px;
|
||||
min-width: 1px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.simplebar-track {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-simplebar].simplebar-dragging {
|
||||
pointer-events: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
[data-simplebar].simplebar-dragging .simplebar-content {
|
||||
pointer-events: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
[data-simplebar].simplebar-dragging .simplebar-track {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.simplebar-scrollbar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-height: 10px;
|
||||
}
|
||||
|
||||
.simplebar-scrollbar:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
background: black;
|
||||
border-radius: 7px;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s 0.5s linear;
|
||||
}
|
||||
|
||||
.simplebar-scrollbar.simplebar-visible:before {
|
||||
opacity: 0.5;
|
||||
transition-delay: 0s;
|
||||
transition-duration: 0s;
|
||||
}
|
||||
|
||||
.simplebar-track.simplebar-vertical {
|
||||
top: 0;
|
||||
width: 11px;
|
||||
}
|
||||
|
||||
.simplebar-scrollbar:before {
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
.simplebar-track.simplebar-horizontal {
|
||||
left: 0;
|
||||
height: 11px;
|
||||
}
|
||||
|
||||
.simplebar-track.simplebar-horizontal .simplebar-scrollbar {
|
||||
right: auto;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
min-height: 0;
|
||||
min-width: 10px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Rtl support */
|
||||
[data-simplebar-direction='rtl'] .simplebar-track.simplebar-vertical {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.simplebar-dummy-scrollbar-size {
|
||||
direction: rtl;
|
||||
position: fixed;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
height: 500px;
|
||||
width: 500px;
|
||||
overflow-y: hidden;
|
||||
overflow-x: scroll;
|
||||
-ms-overflow-style: scrollbar !important;
|
||||
}
|
||||
|
||||
.simplebar-dummy-scrollbar-size > div {
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.simplebar-hide-scrollbar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
visibility: hidden;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
2
backend/staticfiles/unfold/css/styles.css
Normal file
2
backend/staticfiles/unfold/css/styles.css
Normal file
File diff suppressed because one or more lines are too long
21
backend/staticfiles/unfold/filters/css/nouislider/LICENSE
Normal file
21
backend/staticfiles/unfold/filters/css/nouislider/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Léon Gersen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
1
backend/staticfiles/unfold/filters/css/nouislider/nouislider.min.css
vendored
Normal file
1
backend/staticfiles/unfold/filters/css/nouislider/nouislider.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.noUi-target,.noUi-target *{-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-ms-touch-action:none;touch-action:none;-ms-user-select:none;-moz-user-select:none;user-select:none;-moz-box-sizing:border-box;box-sizing:border-box}.noUi-target{position:relative}.noUi-base,.noUi-connects{width:100%;height:100%;position:relative;z-index:1}.noUi-connects{overflow:hidden;z-index:0}.noUi-connect,.noUi-origin{will-change:transform;position:absolute;z-index:1;top:0;right:0;height:100%;width:100%;-ms-transform-origin:0 0;-webkit-transform-origin:0 0;-webkit-transform-style:preserve-3d;transform-origin:0 0;transform-style:flat}.noUi-txt-dir-rtl.noUi-horizontal .noUi-origin{left:0;right:auto}.noUi-vertical .noUi-origin{top:-100%;width:0}.noUi-horizontal .noUi-origin{height:0}.noUi-handle{-webkit-backface-visibility:hidden;backface-visibility:hidden;position:absolute}.noUi-touch-area{height:100%;width:100%}.noUi-state-tap .noUi-connect,.noUi-state-tap .noUi-origin{-webkit-transition:transform .3s;transition:transform .3s}.noUi-state-drag *{cursor:inherit!important}.noUi-horizontal{height:18px}.noUi-horizontal .noUi-handle{width:34px;height:28px;right:-17px;top:-6px}.noUi-vertical{width:18px}.noUi-vertical .noUi-handle{width:28px;height:34px;right:-6px;bottom:-17px}.noUi-txt-dir-rtl.noUi-horizontal .noUi-handle{left:-17px;right:auto}.noUi-target{background:#FAFAFA;border-radius:4px;border:1px solid #D3D3D3;box-shadow:inset 0 1px 1px #F0F0F0,0 3px 6px -5px #BBB}.noUi-connects{border-radius:3px}.noUi-connect{background:#3FB8AF}.noUi-draggable{cursor:ew-resize}.noUi-vertical .noUi-draggable{cursor:ns-resize}.noUi-handle{border:1px solid #D9D9D9;border-radius:3px;background:#FFF;cursor:default;box-shadow:inset 0 0 1px #FFF,inset 0 1px 7px #EBEBEB,0 3px 6px -3px #BBB}.noUi-active{box-shadow:inset 0 0 1px #FFF,inset 0 1px 7px #DDD,0 3px 6px -3px #BBB}.noUi-handle:after,.noUi-handle:before{content:"";display:block;position:absolute;height:14px;width:1px;background:#E8E7E6;left:14px;top:6px}.noUi-handle:after{left:17px}.noUi-vertical .noUi-handle:after,.noUi-vertical .noUi-handle:before{width:14px;height:1px;left:6px;top:14px}.noUi-vertical .noUi-handle:after{top:17px}[disabled] .noUi-connect{background:#B8B8B8}[disabled] .noUi-handle,[disabled].noUi-handle,[disabled].noUi-target{cursor:not-allowed}.noUi-pips,.noUi-pips *{-moz-box-sizing:border-box;box-sizing:border-box}.noUi-pips{position:absolute;color:#999}.noUi-value{position:absolute;white-space:nowrap;text-align:center}.noUi-value-sub{color:#ccc;font-size:10px}.noUi-marker{position:absolute;background:#CCC}.noUi-marker-sub{background:#AAA}.noUi-marker-large{background:#AAA}.noUi-pips-horizontal{padding:10px 0;height:80px;top:100%;left:0;width:100%}.noUi-value-horizontal{-webkit-transform:translate(-50%,50%);transform:translate(-50%,50%)}.noUi-rtl .noUi-value-horizontal{-webkit-transform:translate(50%,50%);transform:translate(50%,50%)}.noUi-marker-horizontal.noUi-marker{margin-left:-1px;width:2px;height:5px}.noUi-marker-horizontal.noUi-marker-sub{height:10px}.noUi-marker-horizontal.noUi-marker-large{height:15px}.noUi-pips-vertical{padding:0 10px;height:100%;top:0;left:100%}.noUi-value-vertical{-webkit-transform:translate(0,-50%);transform:translate(0,-50%);padding-left:25px}.noUi-rtl .noUi-value-vertical{-webkit-transform:translate(0,50%);transform:translate(0,50%)}.noUi-marker-vertical.noUi-marker{width:5px;height:2px;margin-top:-1px}.noUi-marker-vertical.noUi-marker-sub{width:10px}.noUi-marker-vertical.noUi-marker-large{width:15px}.noUi-tooltip{display:block;position:absolute;border:1px solid #D9D9D9;border-radius:3px;background:#fff;color:#000;padding:5px;text-align:center;white-space:nowrap}.noUi-horizontal .noUi-tooltip{-webkit-transform:translate(-50%,0);transform:translate(-50%,0);left:50%;bottom:120%}.noUi-vertical .noUi-tooltip{-webkit-transform:translate(0,-50%);transform:translate(0,-50%);top:50%;right:120%}.noUi-horizontal .noUi-origin>.noUi-tooltip{-webkit-transform:translate(50%,0);transform:translate(50%,0);left:auto;bottom:10px}.noUi-vertical .noUi-origin>.noUi-tooltip{-webkit-transform:translate(0,-18px);transform:translate(0,-18px);top:auto;right:28px}
|
||||
408
backend/staticfiles/unfold/filters/js/DateTimeShortcuts.js
Normal file
408
backend/staticfiles/unfold/filters/js/DateTimeShortcuts.js
Normal file
@@ -0,0 +1,408 @@
|
||||
/*global Calendar, findPosX, findPosY, get_format, gettext, gettext_noop, interpolate, ngettext, quickElement*/
|
||||
// Inserts shortcut buttons after all of the following:
|
||||
// <input type="text" class="vDateField">
|
||||
// <input type="text" class="vTimeField">
|
||||
'use strict';
|
||||
{
|
||||
const DateTimeShortcuts = {
|
||||
calendars: [],
|
||||
calendarInputs: [],
|
||||
clockInputs: [],
|
||||
clockHours: {
|
||||
default_: [
|
||||
[gettext_noop('Now'), -1],
|
||||
[gettext_noop('Midnight'), 0],
|
||||
[gettext_noop('6 a.m.'), 6],
|
||||
[gettext_noop('Noon'), 12],
|
||||
[gettext_noop('6 p.m.'), 18]
|
||||
]
|
||||
},
|
||||
dismissClockFunc: [],
|
||||
dismissCalendarFunc: [],
|
||||
calendarDivName1: 'calendarbox', // name of calendar <div> that gets toggled
|
||||
calendarDivName2: 'calendarin', // name of <div> that contains calendar
|
||||
calendarLinkName: 'calendarlink', // name of the link that is used to toggle
|
||||
clockDivName: 'clockbox', // name of clock <div> that gets toggled
|
||||
clockLinkName: 'clocklink', // name of the link that is used to toggle
|
||||
shortCutsClass: 'datetimeshortcuts', // class of the clock and cal shortcuts
|
||||
timezoneWarningClass: 'timezonewarning', // class of the warning for timezone mismatch
|
||||
timezoneOffset: 0,
|
||||
init: function() {
|
||||
const serverOffset = document.body.dataset.adminUtcOffset;
|
||||
if (serverOffset) {
|
||||
const localOffset = new Date().getTimezoneOffset() * -60;
|
||||
DateTimeShortcuts.timezoneOffset = localOffset - serverOffset;
|
||||
}
|
||||
|
||||
for (const inp of document.getElementsByTagName('input')) {
|
||||
if (inp.type === 'text' && inp.classList.contains('vCustomTimeField')) {
|
||||
DateTimeShortcuts.addClock(inp);
|
||||
DateTimeShortcuts.addTimezoneWarning(inp);
|
||||
}
|
||||
else if (inp.type === 'text' && inp.classList.contains('vCustomDateField')) {
|
||||
DateTimeShortcuts.addCalendar(inp);
|
||||
DateTimeShortcuts.addTimezoneWarning(inp);
|
||||
}
|
||||
}
|
||||
},
|
||||
// Return the current time while accounting for the server timezone.
|
||||
now: function() {
|
||||
const serverOffset = document.body.dataset.adminUtcOffset;
|
||||
if (serverOffset) {
|
||||
const localNow = new Date();
|
||||
const localOffset = localNow.getTimezoneOffset() * -60;
|
||||
localNow.setTime(localNow.getTime() + 1000 * (serverOffset - localOffset));
|
||||
return localNow;
|
||||
} else {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
// Add a warning when the time zone in the browser and backend do not match.
|
||||
addTimezoneWarning: function(inp) {
|
||||
const warningClass = DateTimeShortcuts.timezoneWarningClass;
|
||||
let timezoneOffset = DateTimeShortcuts.timezoneOffset / 3600;
|
||||
|
||||
// Only warn if there is a time zone mismatch.
|
||||
if (!timezoneOffset) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if warning is already there.
|
||||
if (inp.parentNode.querySelectorAll('.' + warningClass).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let message;
|
||||
if (timezoneOffset > 0) {
|
||||
message = ngettext(
|
||||
'Note: You are %s hour ahead of server time.',
|
||||
'Note: You are %s hours ahead of server time.',
|
||||
timezoneOffset
|
||||
);
|
||||
}
|
||||
else {
|
||||
timezoneOffset *= -1;
|
||||
message = ngettext(
|
||||
'Note: You are %s hour behind server time.',
|
||||
'Note: You are %s hours behind server time.',
|
||||
timezoneOffset
|
||||
);
|
||||
}
|
||||
message = interpolate(message, [timezoneOffset]);
|
||||
|
||||
const warning = document.createElement('div');
|
||||
warning.classList.add('help', warningClass);
|
||||
warning.textContent = message;
|
||||
inp.parentNode.appendChild(warning);
|
||||
},
|
||||
// Add clock widget to a given field
|
||||
addClock: function(inp) {
|
||||
const num = DateTimeShortcuts.clockInputs.length;
|
||||
DateTimeShortcuts.clockInputs[num] = inp;
|
||||
DateTimeShortcuts.dismissClockFunc[num] = function() { DateTimeShortcuts.dismissClock(num); return true; };
|
||||
|
||||
// Shortcut links (clock icon and "Now" link)
|
||||
const shortcuts_span = document.createElement('span');
|
||||
shortcuts_span.className = DateTimeShortcuts.shortCutsClass;
|
||||
inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling);
|
||||
const now_link = document.createElement('a');
|
||||
now_link.href = "#";
|
||||
now_link.textContent = gettext('Now');
|
||||
now_link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
DateTimeShortcuts.handleClockQuicklink(num, -1);
|
||||
});
|
||||
const clock_link = document.createElement('a');
|
||||
clock_link.href = '#';
|
||||
clock_link.id = DateTimeShortcuts.clockLinkName + num;
|
||||
clock_link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
// avoid triggering the document click handler to dismiss the clock
|
||||
e.stopPropagation();
|
||||
DateTimeShortcuts.openClock(num);
|
||||
});
|
||||
|
||||
quickElement(
|
||||
'span', clock_link, '',
|
||||
'class', 'clock-icon',
|
||||
'title', gettext('Choose a Time')
|
||||
);
|
||||
shortcuts_span.appendChild(document.createTextNode('\u00A0'));
|
||||
shortcuts_span.appendChild(now_link);
|
||||
shortcuts_span.appendChild(document.createTextNode('\u00A0|\u00A0'));
|
||||
shortcuts_span.appendChild(clock_link);
|
||||
|
||||
// Create clock link div
|
||||
//
|
||||
// Markup looks like:
|
||||
// <div id="clockbox1" class="clockbox module">
|
||||
// <h2>Choose a time</h2>
|
||||
// <ul class="timelist">
|
||||
// <li><a href="#">Now</a></li>
|
||||
// <li><a href="#">Midnight</a></li>
|
||||
// <li><a href="#">6 a.m.</a></li>
|
||||
// <li><a href="#">Noon</a></li>
|
||||
// <li><a href="#">6 p.m.</a></li>
|
||||
// </ul>
|
||||
// <p class="calendar-cancel"><a href="#">Cancel</a></p>
|
||||
// </div>
|
||||
|
||||
const clock_box = document.createElement('div');
|
||||
clock_box.style.display = 'none';
|
||||
clock_box.style.position = 'absolute';
|
||||
clock_box.className = 'clockbox module';
|
||||
clock_box.id = DateTimeShortcuts.clockDivName + num;
|
||||
document.body.appendChild(clock_box);
|
||||
clock_box.addEventListener('click', function(e) { e.stopPropagation(); });
|
||||
|
||||
quickElement('h2', clock_box, gettext('Choose a time'));
|
||||
const time_list = quickElement('ul', clock_box);
|
||||
time_list.className = 'timelist';
|
||||
// The list of choices can be overridden in JavaScript like this:
|
||||
// DateTimeShortcuts.clockHours.name = [['3 a.m.', 3]];
|
||||
// where name is the name attribute of the <input>.
|
||||
const name = typeof DateTimeShortcuts.clockHours[inp.name] === 'undefined' ? 'default_' : inp.name;
|
||||
DateTimeShortcuts.clockHours[name].forEach(function(element) {
|
||||
const time_link = quickElement('a', quickElement('li', time_list), gettext(element[0]), 'href', '#');
|
||||
time_link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
DateTimeShortcuts.handleClockQuicklink(num, element[1]);
|
||||
});
|
||||
});
|
||||
|
||||
const cancel_p = quickElement('p', clock_box);
|
||||
cancel_p.className = 'calendar-cancel';
|
||||
const cancel_link = quickElement('a', cancel_p, gettext('Cancel'), 'href', '#');
|
||||
cancel_link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
DateTimeShortcuts.dismissClock(num);
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', function(event) {
|
||||
if (event.which === 27) {
|
||||
// ESC key closes popup
|
||||
DateTimeShortcuts.dismissClock(num);
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
},
|
||||
openClock: function(num) {
|
||||
const clock_box = document.getElementById(DateTimeShortcuts.clockDivName + num);
|
||||
const clock_link = document.getElementById(DateTimeShortcuts.clockLinkName + num);
|
||||
|
||||
// Recalculate the clockbox position
|
||||
// is it left-to-right or right-to-left layout ?
|
||||
if (window.getComputedStyle(document.body).direction !== 'rtl') {
|
||||
clock_box.style.left = findPosX(clock_link) + 17 + 'px';
|
||||
}
|
||||
else {
|
||||
// since style's width is in em, it'd be tough to calculate
|
||||
// px value of it. let's use an estimated px for now
|
||||
clock_box.style.left = findPosX(clock_link) - 110 + 'px';
|
||||
}
|
||||
clock_box.style.top = Math.max(0, findPosY(clock_link) - 30) + 'px';
|
||||
|
||||
// Show the clock box
|
||||
clock_box.style.display = 'block';
|
||||
document.addEventListener('click', DateTimeShortcuts.dismissClockFunc[num]);
|
||||
},
|
||||
dismissClock: function(num) {
|
||||
document.getElementById(DateTimeShortcuts.clockDivName + num).style.display = 'none';
|
||||
document.removeEventListener('click', DateTimeShortcuts.dismissClockFunc[num]);
|
||||
},
|
||||
handleClockQuicklink: function(num, val) {
|
||||
let d;
|
||||
if (val === -1) {
|
||||
d = DateTimeShortcuts.now();
|
||||
}
|
||||
else {
|
||||
d = new Date(1970, 1, 1, val, 0, 0, 0);
|
||||
}
|
||||
DateTimeShortcuts.clockInputs[num].value = d.strftime(get_format('TIME_INPUT_FORMATS')[0]);
|
||||
DateTimeShortcuts.clockInputs[num].focus();
|
||||
DateTimeShortcuts.dismissClock(num);
|
||||
},
|
||||
// Add calendar widget to a given field.
|
||||
addCalendar: function(inp) {
|
||||
const num = DateTimeShortcuts.calendars.length;
|
||||
|
||||
DateTimeShortcuts.calendarInputs[num] = inp;
|
||||
DateTimeShortcuts.dismissCalendarFunc[num] = function() { DateTimeShortcuts.dismissCalendar(num); return true; };
|
||||
|
||||
// Shortcut links (calendar icon and "Today" link)
|
||||
const shortcuts_span = document.createElement('span');
|
||||
shortcuts_span.className = DateTimeShortcuts.shortCutsClass;
|
||||
inp.parentNode.insertBefore(shortcuts_span, inp.nextSibling);
|
||||
const today_link = document.createElement('a');
|
||||
today_link.href = '#';
|
||||
today_link.appendChild(document.createTextNode(gettext('Today')));
|
||||
today_link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
DateTimeShortcuts.handleCalendarQuickLink(num, 0);
|
||||
});
|
||||
const cal_link = document.createElement('a');
|
||||
cal_link.href = '#';
|
||||
cal_link.id = DateTimeShortcuts.calendarLinkName + num;
|
||||
cal_link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
// avoid triggering the document click handler to dismiss the calendar
|
||||
e.stopPropagation();
|
||||
DateTimeShortcuts.openCalendar(num);
|
||||
});
|
||||
quickElement(
|
||||
'span', cal_link, '',
|
||||
'class', 'date-icon',
|
||||
'title', gettext('Choose a Date')
|
||||
);
|
||||
shortcuts_span.appendChild(document.createTextNode('\u00A0'));
|
||||
shortcuts_span.appendChild(today_link);
|
||||
shortcuts_span.appendChild(document.createTextNode('\u00A0|\u00A0'));
|
||||
shortcuts_span.appendChild(cal_link);
|
||||
|
||||
// Create calendarbox div.
|
||||
//
|
||||
// Markup looks like:
|
||||
//
|
||||
// <div id="calendarbox3" class="calendarbox module">
|
||||
// <h2>
|
||||
// <a href="#" class="link-previous">‹</a>
|
||||
// <a href="#" class="link-next">›</a> February 2003
|
||||
// </h2>
|
||||
// <div class="calendar" id="calendarin3">
|
||||
// <!-- (cal) -->
|
||||
// </div>
|
||||
// <div class="calendar-shortcuts">
|
||||
// <a href="#">Yesterday</a> | <a href="#">Today</a> | <a href="#">Tomorrow</a>
|
||||
// </div>
|
||||
// <p class="calendar-cancel"><a href="#">Cancel</a></p>
|
||||
// </div>
|
||||
const cal_box = document.createElement('div');
|
||||
cal_box.style.display = 'none';
|
||||
cal_box.style.position = 'absolute';
|
||||
cal_box.className = 'calendarbox module';
|
||||
cal_box.id = DateTimeShortcuts.calendarDivName1 + num;
|
||||
document.body.appendChild(cal_box);
|
||||
cal_box.addEventListener('click', function(e) { e.stopPropagation(); });
|
||||
|
||||
// next-prev links
|
||||
const cal_nav = quickElement('div', cal_box);
|
||||
const cal_nav_prev = quickElement('a', cal_nav, '<', 'href', '#');
|
||||
cal_nav_prev.className = 'calendarnav-previous';
|
||||
cal_nav_prev.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
DateTimeShortcuts.drawPrev(num);
|
||||
});
|
||||
|
||||
const cal_nav_next = quickElement('a', cal_nav, '>', 'href', '#');
|
||||
cal_nav_next.className = 'calendarnav-next';
|
||||
cal_nav_next.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
DateTimeShortcuts.drawNext(num);
|
||||
});
|
||||
|
||||
// main box
|
||||
const cal_main = quickElement('div', cal_box, '', 'id', DateTimeShortcuts.calendarDivName2 + num);
|
||||
cal_main.className = 'calendar';
|
||||
DateTimeShortcuts.calendars[num] = new Calendar(DateTimeShortcuts.calendarDivName2 + num, DateTimeShortcuts.handleCalendarCallback(num));
|
||||
DateTimeShortcuts.calendars[num].drawCurrent();
|
||||
|
||||
// calendar shortcuts
|
||||
const shortcuts = quickElement('div', cal_box);
|
||||
shortcuts.className = 'calendar-shortcuts';
|
||||
let day_link = quickElement('a', shortcuts, gettext('Yesterday'), 'href', '#');
|
||||
day_link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
DateTimeShortcuts.handleCalendarQuickLink(num, -1);
|
||||
});
|
||||
shortcuts.appendChild(document.createTextNode('\u00A0|\u00A0'));
|
||||
day_link = quickElement('a', shortcuts, gettext('Today'), 'href', '#');
|
||||
day_link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
DateTimeShortcuts.handleCalendarQuickLink(num, 0);
|
||||
});
|
||||
shortcuts.appendChild(document.createTextNode('\u00A0|\u00A0'));
|
||||
day_link = quickElement('a', shortcuts, gettext('Tomorrow'), 'href', '#');
|
||||
day_link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
DateTimeShortcuts.handleCalendarQuickLink(num, +1);
|
||||
});
|
||||
|
||||
// cancel bar
|
||||
const cancel_p = quickElement('p', cal_box);
|
||||
cancel_p.className = 'calendar-cancel';
|
||||
const cancel_link = quickElement('a', cancel_p, gettext('Cancel'), 'href', '#');
|
||||
cancel_link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
DateTimeShortcuts.dismissCalendar(num);
|
||||
});
|
||||
document.addEventListener('keyup', function(event) {
|
||||
if (event.which === 27) {
|
||||
// ESC key closes popup
|
||||
DateTimeShortcuts.dismissCalendar(num);
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
},
|
||||
openCalendar: function(num) {
|
||||
const cal_box = document.getElementById(DateTimeShortcuts.calendarDivName1 + num);
|
||||
const cal_link = document.getElementById(DateTimeShortcuts.calendarLinkName + num);
|
||||
const inp = DateTimeShortcuts.calendarInputs[num];
|
||||
|
||||
// Determine if the current value in the input has a valid date.
|
||||
// If so, draw the calendar with that date's year and month.
|
||||
if (inp.value) {
|
||||
const format = get_format('DATE_INPUT_FORMATS')[0];
|
||||
const selected = inp.value.strptime(format);
|
||||
const year = selected.getUTCFullYear();
|
||||
const month = selected.getUTCMonth() + 1;
|
||||
const re = /\d{4}/;
|
||||
if (re.test(year.toString()) && month >= 1 && month <= 12) {
|
||||
DateTimeShortcuts.calendars[num].drawDate(month, year, selected);
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate the clockbox position
|
||||
// is it left-to-right or right-to-left layout ?
|
||||
if (window.getComputedStyle(document.body).direction !== 'rtl') {
|
||||
cal_box.style.left = findPosX(cal_link) - 228 + 'px';
|
||||
}
|
||||
else {
|
||||
// since style's width is in em, it'd be tough to calculate
|
||||
// px value of it. let's use an estimated px for now
|
||||
cal_box.style.left = findPosX(cal_link) - 180 + 'px';
|
||||
}
|
||||
cal_box.style.top = Math.max(0, findPosY(cal_link) + 44) + 'px';
|
||||
|
||||
cal_box.style.display = 'block';
|
||||
document.addEventListener('click', DateTimeShortcuts.dismissCalendarFunc[num]);
|
||||
},
|
||||
dismissCalendar: function(num) {
|
||||
document.getElementById(DateTimeShortcuts.calendarDivName1 + num).style.display = 'none';
|
||||
document.removeEventListener('click', DateTimeShortcuts.dismissCalendarFunc[num]);
|
||||
},
|
||||
drawPrev: function(num) {
|
||||
DateTimeShortcuts.calendars[num].drawPreviousMonth();
|
||||
},
|
||||
drawNext: function(num) {
|
||||
DateTimeShortcuts.calendars[num].drawNextMonth();
|
||||
},
|
||||
handleCalendarCallback: function(num) {
|
||||
const format = get_format('DATE_INPUT_FORMATS')[0];
|
||||
return function(y, m, d) {
|
||||
DateTimeShortcuts.calendarInputs[num].value = new Date(y, m - 1, d).strftime(format);
|
||||
DateTimeShortcuts.calendarInputs[num].focus();
|
||||
document.getElementById(DateTimeShortcuts.calendarDivName1 + num).style.display = 'none';
|
||||
};
|
||||
},
|
||||
handleCalendarQuickLink: function(num, offset) {
|
||||
const d = DateTimeShortcuts.now();
|
||||
d.setDate(d.getDate() + offset);
|
||||
DateTimeShortcuts.calendarInputs[num].value = d.strftime(get_format('DATE_INPUT_FORMATS')[0]);
|
||||
DateTimeShortcuts.calendarInputs[num].focus();
|
||||
DateTimeShortcuts.dismissCalendar(num);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('load', DateTimeShortcuts.init);
|
||||
window.DateTimeShortcuts = DateTimeShortcuts;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
Array.from(
|
||||
document.getElementsByClassName("admin-numeric-filter-slider")
|
||||
).forEach(function (slider) {
|
||||
if (Array.from(slider.classList).includes("noUi-target")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fromInput = slider
|
||||
.closest(".admin-numeric-filter-wrapper")
|
||||
.querySelectorAll(".admin-numeric-filter-wrapper-group input")[0];
|
||||
|
||||
const toInput = slider
|
||||
.closest(".admin-numeric-filter-wrapper")
|
||||
.querySelectorAll(".admin-numeric-filter-wrapper-group input")[1];
|
||||
|
||||
noUiSlider.create(slider, {
|
||||
start: [parseFloat(fromInput.value), parseFloat(toInput.value)],
|
||||
step: parseFloat(slider.getAttribute("data-step")),
|
||||
connect: true,
|
||||
format: wNumb({
|
||||
decimals: parseFloat(slider.getAttribute("data-decimals")),
|
||||
}),
|
||||
range: {
|
||||
min: parseFloat(slider.getAttribute("data-min")),
|
||||
max: parseFloat(slider.getAttribute("data-max")),
|
||||
},
|
||||
});
|
||||
|
||||
/*************************************************************
|
||||
* Update slider when input values change
|
||||
*************************************************************/
|
||||
fromInput.addEventListener("keyup", function () {
|
||||
clearTimeout(this._sliderUpdateTimeout);
|
||||
this._sliderUpdateTimeout = setTimeout(() => {
|
||||
slider.noUiSlider.set([
|
||||
parseFloat(this.value),
|
||||
parseFloat(toInput.value),
|
||||
]);
|
||||
}, 500);
|
||||
});
|
||||
|
||||
toInput.addEventListener("keyup", function () {
|
||||
clearTimeout(this._sliderUpdateTimeout);
|
||||
this._sliderUpdateTimeout = setTimeout(() => {
|
||||
slider.noUiSlider.set([
|
||||
parseFloat(fromInput.value),
|
||||
parseFloat(this.value),
|
||||
]);
|
||||
}, 500);
|
||||
});
|
||||
|
||||
/*************************************************************
|
||||
* Updated inputs when slider is moved
|
||||
*************************************************************/
|
||||
slider.noUiSlider.on("update", function (values, handle) {
|
||||
const parent = this.target.closest(".admin-numeric-filter-wrapper");
|
||||
const from = parent.querySelectorAll(
|
||||
".admin-numeric-filter-wrapper-group input"
|
||||
)[0];
|
||||
const to = parent.querySelectorAll(
|
||||
".admin-numeric-filter-wrapper-group input"
|
||||
)[1];
|
||||
|
||||
from.value = values[0];
|
||||
to.value = values[1];
|
||||
});
|
||||
});
|
||||
});
|
||||
21
backend/staticfiles/unfold/filters/js/nouislider/LICENSE
Normal file
21
backend/staticfiles/unfold/filters/js/nouislider/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Léon Gersen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
1
backend/staticfiles/unfold/filters/js/nouislider/nouislider.min.js
vendored
Normal file
1
backend/staticfiles/unfold/filters/js/nouislider/nouislider.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
9
backend/staticfiles/unfold/filters/js/wnumb/LICENSE
Normal file
9
backend/staticfiles/unfold/filters/js/wnumb/LICENSE
Normal file
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Léon Gersen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
1
backend/staticfiles/unfold/filters/js/wnumb/wNumb.min.js
vendored
Normal file
1
backend/staticfiles/unfold/filters/js/wnumb/wNumb.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(e){"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?module.exports=e():window.wNumb=e()}(function(){"use strict";var o=["decimals","thousand","mark","prefix","suffix","encoder","decoder","negativeBefore","negative","edit","undo"];function w(e){return e.split("").reverse().join("")}function h(e,t){return e.substring(0,t.length)===t}function f(e,t,n){if((e[t]||e[n])&&e[t]===e[n])throw new Error(t)}function x(e){return"number"==typeof e&&isFinite(e)}function n(e,t,n,r,i,o,f,u,s,c,a,p){var d,l,h,g=p,v="",m="";return o&&(p=o(p)),!!x(p)&&(!1!==e&&0===parseFloat(p.toFixed(e))&&(p=0),p<0&&(d=!0,p=Math.abs(p)),!1!==e&&(p=function(e,t){return e=e.toString().split("e"),(+((e=(e=Math.round(+(e[0]+"e"+(e[1]?+e[1]+t:t)))).toString().split("e"))[0]+"e"+(e[1]?e[1]-t:-t))).toFixed(t)}(p,e)),-1!==(p=p.toString()).indexOf(".")?(h=(l=p.split("."))[0],n&&(v=n+l[1])):h=p,t&&(h=w((h=w(h).match(/.{1,3}/g)).join(w(t)))),d&&u&&(m+=u),r&&(m+=r),d&&s&&(m+=s),m+=h,m+=v,i&&(m+=i),c&&(m=c(m,g)),m)}function r(e,t,n,r,i,o,f,u,s,c,a,p){var d,l="";return a&&(p=a(p)),!(!p||"string"!=typeof p)&&(u&&h(p,u)&&(p=p.replace(u,""),d=!0),r&&h(p,r)&&(p=p.replace(r,"")),s&&h(p,s)&&(p=p.replace(s,""),d=!0),i&&function(e,t){return e.slice(-1*t.length)===t}(p,i)&&(p=p.slice(0,-1*i.length)),t&&(p=p.split(t).join("")),n&&(p=p.replace(n,".")),d&&(l+="-"),""!==(l=(l+=p).replace(/[^0-9\.\-.]/g,""))&&(l=Number(l),f&&(l=f(l)),!!x(l)&&l))}function i(e,t,n){var r,i=[];for(r=0;r<o.length;r+=1)i.push(e[o[r]]);return i.push(n),t.apply("",i)}return function e(t){if(!(this instanceof e))return new e(t);"object"==typeof t&&(t=function(e){var t,n,r,i={};for(void 0===e.suffix&&(e.suffix=e.postfix),t=0;t<o.length;t+=1)if(void 0===(r=e[n=o[t]]))"negative"!==n||i.negativeBefore?"mark"===n&&"."!==i.thousand?i[n]=".":i[n]=!1:i[n]="-";else if("decimals"===n){if(!(0<=r&&r<8))throw new Error(n);i[n]=r}else if("encoder"===n||"decoder"===n||"edit"===n||"undo"===n){if("function"!=typeof r)throw new Error(n);i[n]=r}else{if("string"!=typeof r)throw new Error(n);i[n]=r}return f(i,"mark","thousand"),f(i,"prefix","negative"),f(i,"prefix","negativeBefore"),i}(t),this.to=function(e){return i(t,n,e)},this.from=function(e){return i(t,r,e)})}});
|
||||
BIN
backend/staticfiles/unfold/fonts/inter/Inter-Bold.woff2
Normal file
BIN
backend/staticfiles/unfold/fonts/inter/Inter-Bold.woff2
Normal file
Binary file not shown.
BIN
backend/staticfiles/unfold/fonts/inter/Inter-Medium.woff2
Normal file
BIN
backend/staticfiles/unfold/fonts/inter/Inter-Medium.woff2
Normal file
Binary file not shown.
BIN
backend/staticfiles/unfold/fonts/inter/Inter-Regular.woff2
Normal file
BIN
backend/staticfiles/unfold/fonts/inter/Inter-Regular.woff2
Normal file
Binary file not shown.
BIN
backend/staticfiles/unfold/fonts/inter/Inter-SemiBold.woff2
Normal file
BIN
backend/staticfiles/unfold/fonts/inter/Inter-SemiBold.woff2
Normal file
Binary file not shown.
92
backend/staticfiles/unfold/fonts/inter/LICENSE
Normal file
92
backend/staticfiles/unfold/fonts/inter/LICENSE
Normal file
@@ -0,0 +1,92 @@
|
||||
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION AND CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
31
backend/staticfiles/unfold/fonts/inter/styles.css
Normal file
31
backend/staticfiles/unfold/fonts/inter/styles.css
Normal file
@@ -0,0 +1,31 @@
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(Inter-Regular.woff2) format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url(Inter-Medium.woff2) format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url(Inter-SemiBold.woff2) format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(Inter-Bold.woff2) format("woff2");
|
||||
}
|
||||
202
backend/staticfiles/unfold/fonts/material-symbols/LICENSE
Normal file
202
backend/staticfiles/unfold/fonts/material-symbols/LICENSE
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
@font-face {
|
||||
font-family: "Material Symbols Outlined";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("Material-Symbols-Outlined.woff2") format("woff2");
|
||||
}
|
||||
21
backend/staticfiles/unfold/js/alpine/LICENSE
Normal file
21
backend/staticfiles/unfold/js/alpine/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
# MIT License
|
||||
|
||||
Copyright © 2019-2021 Caleb Porzio and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
1
backend/staticfiles/unfold/js/alpine/alpine.anchor.js
Normal file
1
backend/staticfiles/unfold/js/alpine/alpine.anchor.js
Normal file
File diff suppressed because one or more lines are too long
5
backend/staticfiles/unfold/js/alpine/alpine.js
Normal file
5
backend/staticfiles/unfold/js/alpine/alpine.js
Normal file
File diff suppressed because one or more lines are too long
1
backend/staticfiles/unfold/js/alpine/alpine.persist.js
Normal file
1
backend/staticfiles/unfold/js/alpine/alpine.persist.js
Normal file
@@ -0,0 +1 @@
|
||||
(()=>{function d(t){let n=()=>{let r,a;try{a=localStorage}catch(i){console.error(i),console.warn("Alpine: $persist is using temporary storage since localStorage is unavailable.");let e=new Map;a={getItem:e.get.bind(e),setItem:e.set.bind(e)}}return t.interceptor((i,e,l,s,f)=>{let o=r||`_x_${s}`,u=g(o,a)?p(o,a):i;return l(u),t.effect(()=>{let c=e();m(o,c,a),l(c)}),u},i=>{i.as=e=>(r=e,i),i.using=e=>(a=e,i)})};Object.defineProperty(t,"$persist",{get:()=>n()}),t.magic("persist",n),t.persist=(r,{get:a,set:i},e=localStorage)=>{let l=g(r,e)?p(r,e):a();i(l),t.effect(()=>{let s=a();m(r,s,e),i(s)})}}function g(t,n){return n.getItem(t)!==null}function p(t,n){let r=n.getItem(t,n);if(r!==void 0)return JSON.parse(r)}function m(t,n,r){r.setItem(t,JSON.stringify(n))}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(d)});})();
|
||||
1
backend/staticfiles/unfold/js/alpine/alpine.resize.js
Normal file
1
backend/staticfiles/unfold/js/alpine/alpine.resize.js
Normal file
@@ -0,0 +1 @@
|
||||
(()=>{function u(e){e.directive("resize",e.skipDuringClone((t,{value:i,expression:n,modifiers:o},{evaluateLater:r,cleanup:h})=>{let f=r(n),s=(z,m)=>{f(()=>{},{scope:{$width:z,$height:m}})},v=o.includes("document")?b(s):a(t,s);h(()=>v())}))}function a(e,t){let i=new ResizeObserver(n=>{let[o,r]=c(n);t(o,r)});return i.observe(e),()=>i.disconnect()}var d,l=new Set;function b(e){return l.add(e),d||(d=new ResizeObserver(t=>{let[i,n]=c(t);l.forEach(o=>o(i,n))}),d.observe(document.documentElement)),()=>{l.delete(e)}}function c(e){let t,i;for(let n of e)t=n.borderBoxSize[0].inlineSize,i=n.borderBoxSize[0].blockSize;return[t,i]}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(u)});})();
|
||||
1
backend/staticfiles/unfold/js/alpine/alpine.sort.js
Normal file
1
backend/staticfiles/unfold/js/alpine/alpine.sort.js
Normal file
File diff suppressed because one or more lines are too long
687
backend/staticfiles/unfold/js/app.js
Normal file
687
backend/staticfiles/unfold/js/app.js
Normal file
@@ -0,0 +1,687 @@
|
||||
window.addEventListener("load", (e) => {
|
||||
fileInputUpdatePath();
|
||||
|
||||
dateTimeShortcutsOverlay();
|
||||
|
||||
renderCharts();
|
||||
|
||||
filterForm();
|
||||
|
||||
warnWithoutSaving();
|
||||
|
||||
tabNavigation();
|
||||
});
|
||||
|
||||
/*************************************************************
|
||||
* Move not visible tab items to dropdown
|
||||
*************************************************************/
|
||||
function tabNavigation() {
|
||||
const itemsDropdown = document.getElementById("tabs-dropdown");
|
||||
const itemsList = document.getElementById("tabs-items");
|
||||
const widths = [];
|
||||
|
||||
if (!itemsDropdown || !itemsList) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleTabNavigationResize();
|
||||
|
||||
window.addEventListener("resize", function () {
|
||||
handleTabNavigationResize();
|
||||
});
|
||||
|
||||
function handleTabNavigationResize() {
|
||||
const contentWidth = document.getElementById("content").offsetWidth;
|
||||
const tabsWidth = document.getElementById("tabs-wrapper").scrollWidth;
|
||||
const availableWidth =
|
||||
itemsList.parentElement.offsetWidth - itemsList.offsetWidth - 48;
|
||||
|
||||
if (tabsWidth > contentWidth) {
|
||||
const lastTabItem = itemsList ? itemsList.lastElementChild : null;
|
||||
|
||||
if (lastTabItem) {
|
||||
widths.push(lastTabItem.offsetWidth);
|
||||
itemsList.removeChild(lastTabItem);
|
||||
itemsDropdown.appendChild(lastTabItem);
|
||||
|
||||
// If there is still not enough space, move the last item to the dropdown again
|
||||
if (
|
||||
document.getElementById("content").offsetWidth <
|
||||
document.getElementById("tabs-wrapper").scrollWidth
|
||||
) {
|
||||
handleTabNavigationResize();
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
widths.length > 0 &&
|
||||
widths[widths.length - 1] < availableWidth
|
||||
) {
|
||||
const lastTabItem = itemsDropdown ? itemsDropdown.lastElementChild : null;
|
||||
|
||||
if (lastTabItem) {
|
||||
itemsDropdown.removeChild(lastTabItem);
|
||||
itemsList.appendChild(lastTabItem);
|
||||
widths.pop();
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide dropdown based on the number of items in dropdown
|
||||
if (itemsDropdown.childElementCount === 0) {
|
||||
itemsDropdown.parentElement.classList.add("hidden");
|
||||
} else {
|
||||
itemsDropdown.parentElement.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* Alpine.sort.js callback after sorting
|
||||
*************************************************************/
|
||||
const sortRecords = (e) => {
|
||||
const orderingField = e.from.dataset.orderingField;
|
||||
|
||||
const weightInputs = Array.from(
|
||||
e.from.querySelectorAll(
|
||||
`.has_original input[name$=-${orderingField}], td.field-${orderingField} input[name$=-${orderingField}]`
|
||||
)
|
||||
);
|
||||
|
||||
weightInputs.forEach((input, index) => {
|
||||
input.value = index;
|
||||
});
|
||||
};
|
||||
|
||||
/*************************************************************
|
||||
* Search form
|
||||
*************************************************************/
|
||||
function searchForm() {
|
||||
return {
|
||||
applyShortcut(event) {
|
||||
if (
|
||||
event.key === "/" &&
|
||||
document.activeElement.tagName.toLowerCase() !== "input" &&
|
||||
document.activeElement.tagName.toLowerCase() !== "textarea" &&
|
||||
!document.activeElement.isContentEditable
|
||||
) {
|
||||
event.preventDefault();
|
||||
this.$refs.searchInput.focus();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* Search dropdown
|
||||
*************************************************************/
|
||||
function searchDropdown() {
|
||||
return {
|
||||
openSearchResults: false,
|
||||
currentIndex: 0,
|
||||
applyShortcut(event) {
|
||||
if (
|
||||
event.key === "t" &&
|
||||
document.activeElement.tagName.toLowerCase() !== "input" &&
|
||||
document.activeElement.tagName.toLowerCase() !== "textarea" &&
|
||||
!document.activeElement.isContentEditable
|
||||
) {
|
||||
event.preventDefault();
|
||||
this.$refs.searchInput.focus();
|
||||
}
|
||||
},
|
||||
nextItem() {
|
||||
if (this.currentIndex < this.maxItem()) {
|
||||
this.currentIndex++;
|
||||
}
|
||||
},
|
||||
prevItem() {
|
||||
if (this.currentIndex > 1) {
|
||||
this.currentIndex--;
|
||||
}
|
||||
},
|
||||
maxItem() {
|
||||
return document.getElementById("search-results").querySelectorAll("li")
|
||||
.length;
|
||||
},
|
||||
selectItem() {
|
||||
const href = this.items[this.currentIndex - 1].querySelector("a").href;
|
||||
window.location = href;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* Search command
|
||||
*************************************************************/
|
||||
function searchCommand() {
|
||||
return {
|
||||
el: document.getElementById("command-results"),
|
||||
items: undefined,
|
||||
hasResults: false,
|
||||
openCommandResults: false,
|
||||
currentIndex: 0,
|
||||
totalItems: 0,
|
||||
commandHistory: JSON.parse(localStorage.getItem("commandHistory") || "[]"),
|
||||
handleOpen() {
|
||||
this.openCommandResults = true;
|
||||
this.toggleBodyOverflow();
|
||||
setTimeout(() => {
|
||||
this.$refs.searchInputCommand.focus();
|
||||
}, 20);
|
||||
|
||||
this.items = document.querySelectorAll("#command-history li");
|
||||
this.totalItems = this.items.length;
|
||||
},
|
||||
handleShortcut(event) {
|
||||
if (
|
||||
event.key === "k" &&
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
document.activeElement.tagName.toLowerCase() !== "input" &&
|
||||
document.activeElement.tagName.toLowerCase() !== "textarea" &&
|
||||
!document.activeElement.isContentEditable
|
||||
) {
|
||||
event.preventDefault();
|
||||
this.handleOpen();
|
||||
}
|
||||
},
|
||||
handleEscape() {
|
||||
if (this.$refs.searchInputCommand.value === "") {
|
||||
this.toggleBodyOverflow();
|
||||
this.openCommandResults = false;
|
||||
this.el.innerHTML = "";
|
||||
this.items = undefined;
|
||||
this.totalItems = 0;
|
||||
this.currentIndex = 0;
|
||||
} else {
|
||||
this.$refs.searchInputCommand.value = "";
|
||||
}
|
||||
},
|
||||
handleContentLoaded(event) {
|
||||
if (
|
||||
event.target.id !== "command-results" &&
|
||||
event.target.id !== "command-results-list"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commandResultsList = document.getElementById(
|
||||
"command-results-list"
|
||||
);
|
||||
if (commandResultsList) {
|
||||
this.items = commandResultsList.querySelectorAll("li");
|
||||
this.totalItems = this.items.length;
|
||||
} else {
|
||||
this.items = undefined;
|
||||
this.totalItems = 0;
|
||||
}
|
||||
|
||||
if (event.target.id === "command-results") {
|
||||
this.currentIndex = 0;
|
||||
|
||||
if (this.items) {
|
||||
this.totalItems = this.items.length;
|
||||
} else {
|
||||
this.totalItems = 0;
|
||||
}
|
||||
}
|
||||
|
||||
this.hasResults = this.totalItems > 0;
|
||||
|
||||
if (!this.hasResults) {
|
||||
this.items = document.querySelectorAll("#command-history li");
|
||||
}
|
||||
},
|
||||
handleOutsideClick() {
|
||||
this.$refs.searchInputCommand.value = "";
|
||||
this.openCommandResults = false;
|
||||
this.toggleBodyOverflow();
|
||||
},
|
||||
toggleBodyOverflow() {
|
||||
document
|
||||
.getElementsByTagName("body")[0]
|
||||
.classList.toggle("overflow-hidden");
|
||||
},
|
||||
scrollToActiveItem() {
|
||||
const item = this.items[this.currentIndex - 1];
|
||||
|
||||
if (item) {
|
||||
item.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
});
|
||||
}
|
||||
},
|
||||
nextItem() {
|
||||
if (this.currentIndex < this.totalItems) {
|
||||
this.currentIndex++;
|
||||
this.scrollToActiveItem();
|
||||
}
|
||||
},
|
||||
prevItem() {
|
||||
if (this.currentIndex > 1) {
|
||||
this.currentIndex--;
|
||||
this.scrollToActiveItem();
|
||||
}
|
||||
},
|
||||
selectItem(addHistory) {
|
||||
const link = this.items[this.currentIndex - 1].querySelector("a");
|
||||
const data = {
|
||||
title: link.dataset.title,
|
||||
description: link.dataset.description,
|
||||
link: link.href,
|
||||
favorite: false,
|
||||
};
|
||||
|
||||
if (addHistory) {
|
||||
this.addToHistory(data);
|
||||
}
|
||||
|
||||
window.location = link.href;
|
||||
},
|
||||
addToHistory(data) {
|
||||
let commandHistory = JSON.parse(
|
||||
localStorage.getItem("commandHistory") || "[]"
|
||||
);
|
||||
|
||||
for (const [index, item] of commandHistory.entries()) {
|
||||
if (item.link === data.link) {
|
||||
commandHistory.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
commandHistory.unshift(data);
|
||||
commandHistory = commandHistory.slice(0, 10);
|
||||
this.commandHistory = commandHistory;
|
||||
localStorage.setItem("commandHistory", JSON.stringify(commandHistory));
|
||||
},
|
||||
removeFromHistory(event, index) {
|
||||
event.preventDefault();
|
||||
|
||||
const commandHistory = JSON.parse(
|
||||
localStorage.getItem("commandHistory") || "[]"
|
||||
);
|
||||
commandHistory.splice(index, 1);
|
||||
this.commandHistory = commandHistory;
|
||||
localStorage.setItem("commandHistory", JSON.stringify(commandHistory));
|
||||
},
|
||||
toggleFavorite(event, index) {
|
||||
event.preventDefault();
|
||||
|
||||
const commandHistory = JSON.parse(
|
||||
localStorage.getItem("commandHistory") || "[]"
|
||||
);
|
||||
|
||||
commandHistory[index].favorite = !commandHistory[index].favorite;
|
||||
this.commandHistory = commandHistory.sort(
|
||||
(a, b) => Number(b.favorite) - Number(a.favorite)
|
||||
);
|
||||
localStorage.setItem("commandHistory", JSON.stringify(commandHistory));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/*************************************************************
|
||||
* Warn without saving
|
||||
*************************************************************/
|
||||
const warnWithoutSaving = () => {
|
||||
let formChanged = false;
|
||||
const form = document.querySelector("form.warn-unsaved-form");
|
||||
|
||||
const checkFormChanged = () => {
|
||||
const elements = document.querySelectorAll(
|
||||
"form.warn-unsaved-form input, form.warn-unsaved-form select, form.warn-unsaved-form textarea"
|
||||
);
|
||||
|
||||
for (const field of elements) {
|
||||
field.addEventListener("input", () => {
|
||||
formChanged = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
new MutationObserver((mutationsList, observer) => {
|
||||
checkFormChanged();
|
||||
}).observe(form, { attributes: true, childList: true, subtree: true });
|
||||
|
||||
checkFormChanged();
|
||||
|
||||
preventLeaving = (e) => {
|
||||
if (formChanged) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
form.addEventListener("submit", (e) => {
|
||||
window.removeEventListener("beforeunload", preventLeaving);
|
||||
});
|
||||
|
||||
window.addEventListener("beforeunload", preventLeaving);
|
||||
};
|
||||
|
||||
/*************************************************************
|
||||
* Filter form
|
||||
*************************************************************/
|
||||
const filterForm = () => {
|
||||
const filterForm = document.getElementById("filter-form");
|
||||
|
||||
if (!filterForm) {
|
||||
return;
|
||||
}
|
||||
|
||||
filterForm.addEventListener("formdata", (event) => {
|
||||
Array.from(event.formData.entries()).forEach(([key, value]) => {
|
||||
if (value === "") event.formData.delete(key);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/*************************************************************
|
||||
* Class watcher
|
||||
*************************************************************/
|
||||
const watchClassChanges = (selector, callback) => {
|
||||
const body = document.querySelector(selector);
|
||||
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (
|
||||
mutation.type === "attributes" &&
|
||||
mutation.attributeName === "class"
|
||||
) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(body, { attributes: true, attributeFilter: ["class"] });
|
||||
};
|
||||
|
||||
/*************************************************************
|
||||
* Calendar & clock
|
||||
*************************************************************/
|
||||
const dateTimeShortcutsOverlay = () => {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const mutationRecord of mutations) {
|
||||
const display = mutationRecord.target.style.display;
|
||||
const overlay = document.getElementById("modal-overlay");
|
||||
|
||||
if (display === "block") {
|
||||
overlay.style.display = "block";
|
||||
} else {
|
||||
overlay.style.display = "none";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const targets = document.querySelectorAll(".calendarbox, .clockbox");
|
||||
|
||||
for (const target of targets) {
|
||||
observer.observe(target, {
|
||||
attributes: true,
|
||||
attributeFilter: ["style"],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/*************************************************************
|
||||
* File upload path
|
||||
*************************************************************/
|
||||
const fileInputUpdatePath = () => {
|
||||
const checkInputChanged = () => {
|
||||
for (const input of document.querySelectorAll("input[type=file]")) {
|
||||
if (input.hasChangeListener) {
|
||||
continue;
|
||||
}
|
||||
|
||||
input.addEventListener("change", (e) => {
|
||||
const parts = e.target.value.split("\\");
|
||||
const placeholder =
|
||||
input.parentNode.parentNode.parentNode.querySelector(
|
||||
"input[type=text]"
|
||||
);
|
||||
placeholder.setAttribute("value", parts[parts.length - 1]);
|
||||
});
|
||||
|
||||
input.hasChangeListener = true;
|
||||
}
|
||||
};
|
||||
|
||||
new MutationObserver(() => {
|
||||
checkInputChanged();
|
||||
}).observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
checkInputChanged();
|
||||
};
|
||||
|
||||
/*************************************************************
|
||||
* Chart
|
||||
*************************************************************/
|
||||
const DEFAULT_CHART_OPTIONS = {
|
||||
animation: false,
|
||||
barPercentage: 1,
|
||||
base: 0,
|
||||
grouped: false,
|
||||
maxBarThickness: 4,
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
datasets: {
|
||||
bar: {
|
||||
borderRadius: 12,
|
||||
border: {
|
||||
width: 0,
|
||||
},
|
||||
borderSkipped: "middle",
|
||||
},
|
||||
line: {
|
||||
borderWidth: 2,
|
||||
pointBorderWidth: 0,
|
||||
pointStyle: false,
|
||||
},
|
||||
pie: {
|
||||
borderWidth: 0,
|
||||
},
|
||||
doughnut: {
|
||||
borderWidth: 0,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
align: "end",
|
||||
display: false,
|
||||
position: "top",
|
||||
labels: {
|
||||
boxHeight: 5,
|
||||
boxWidth: 5,
|
||||
color: "#9ca3af",
|
||||
pointStyle: "circle",
|
||||
usePointStyle: true,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: function (context) {
|
||||
if (["pie", "doughnut", "radar"].includes(context.chart.config.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
border: {
|
||||
dash: [5, 5],
|
||||
dashOffset: 2,
|
||||
width: 0,
|
||||
},
|
||||
ticks: {
|
||||
color: "#9ca3af",
|
||||
display: true,
|
||||
maxTicksLimit: function (context) {
|
||||
return context.chart.data.datasets.find(
|
||||
(dataset) => dataset.maxTicksXLimit
|
||||
)?.maxTicksXLimit;
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
display: true,
|
||||
tickWidth: 0,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
display: function (context) {
|
||||
if (["pie", "doughnut", "radar"].includes(context.chart.config.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
border: {
|
||||
dash: [5, 5],
|
||||
dashOffset: 5,
|
||||
width: 0,
|
||||
},
|
||||
ticks: {
|
||||
color: "#9ca3af",
|
||||
display: function (context) {
|
||||
return context.chart.data.datasets.some((dataset) => {
|
||||
return (
|
||||
dataset.hasOwnProperty("displayYAxis") && dataset.displayYAxis
|
||||
);
|
||||
});
|
||||
},
|
||||
callback: function (value) {
|
||||
const suffix = this.chart.data.datasets.find(
|
||||
(dataset) => dataset.suffixYAxis
|
||||
)?.suffixYAxis;
|
||||
if (suffix) {
|
||||
return `${value} ${suffix}`;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
lineWidth: (context) => {
|
||||
if (context.tick.value === 0) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
tickWidth: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const renderCharts = () => {
|
||||
const charts = [];
|
||||
|
||||
const changeDarkModeSettings = () => {
|
||||
const hasDarkClass = document
|
||||
.querySelector("html")
|
||||
.classList.contains("dark");
|
||||
|
||||
const baseColorDark = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--color-base-700")
|
||||
.trim();
|
||||
|
||||
const baseColorLight = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--color-base-300")
|
||||
.trim();
|
||||
|
||||
const borderColor = hasDarkClass ? baseColorDark : baseColorLight;
|
||||
|
||||
for (const chart of charts) {
|
||||
if (chart.options.scales.x) {
|
||||
chart.options.scales.x.grid.color = borderColor;
|
||||
}
|
||||
|
||||
if (chart.options.scales.y) {
|
||||
chart.options.scales.y.grid.color = borderColor;
|
||||
}
|
||||
|
||||
if (chart.options.scales.r) {
|
||||
chart.options.scales.r.grid.color = borderColor;
|
||||
}
|
||||
chart.update();
|
||||
}
|
||||
};
|
||||
|
||||
for (const chart of document.querySelectorAll(".chart")) {
|
||||
const ctx = chart.getContext("2d");
|
||||
const data = chart.dataset.value;
|
||||
const type = chart.dataset.type;
|
||||
const options = chart.dataset.options;
|
||||
|
||||
if (!data) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsedData = JSON.parse(chart.dataset.value);
|
||||
|
||||
for (const key in parsedData.datasets) {
|
||||
const dataset = parsedData.datasets[key];
|
||||
const processColor = (colorProp) => {
|
||||
if (Array.isArray(dataset?.[colorProp])) {
|
||||
for (const [index, prop] of dataset?.[colorProp].entries()) {
|
||||
if (prop.startsWith("var(")) {
|
||||
const cssVar = prop.match(/var\((.*?)\)/)[1];
|
||||
const color = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(cssVar)
|
||||
.trim();
|
||||
dataset[colorProp][index] = color;
|
||||
}
|
||||
}
|
||||
} else if (dataset?.[colorProp]?.startsWith("var(")) {
|
||||
const cssVar = dataset[colorProp].match(/var\((.*?)\)/)[1];
|
||||
const color = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(cssVar)
|
||||
.trim();
|
||||
dataset[colorProp] = color;
|
||||
}
|
||||
};
|
||||
|
||||
processColor("borderColor");
|
||||
processColor("backgroundColor");
|
||||
}
|
||||
|
||||
CHART_OPTIONS = { ...DEFAULT_CHART_OPTIONS };
|
||||
if (type === "radar") {
|
||||
CHART_OPTIONS.scales = {
|
||||
r: {
|
||||
ticks: {
|
||||
backdropColor: "transparent",
|
||||
},
|
||||
pointLabels: {
|
||||
color: "#9ca3af",
|
||||
font: {
|
||||
size: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
Chart.defaults.font.family = "Inter";
|
||||
Chart.defaults.font.size = 12;
|
||||
|
||||
charts.push(
|
||||
new Chart(ctx, {
|
||||
type: type || "bar",
|
||||
data: parsedData,
|
||||
options: options ? JSON.parse(options) : { ...CHART_OPTIONS },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
changeDarkModeSettings();
|
||||
|
||||
watchClassChanges("html", () => {
|
||||
changeDarkModeSettings();
|
||||
});
|
||||
};
|
||||
9
backend/staticfiles/unfold/js/chart/LICENSE
Normal file
9
backend/staticfiles/unfold/js/chart/LICENSE
Normal file
@@ -0,0 +1,9 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2024 Chart.js Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
1
backend/staticfiles/unfold/js/chart/chart.js
Normal file
1
backend/staticfiles/unfold/js/chart/chart.js
Normal file
File diff suppressed because one or more lines are too long
13
backend/staticfiles/unfold/js/htmx/LICENSE
Normal file
13
backend/staticfiles/unfold/js/htmx/LICENSE
Normal file
@@ -0,0 +1,13 @@
|
||||
Zero-Clause BSD
|
||||
=============
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for
|
||||
any purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
|
||||
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
|
||||
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
|
||||
DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
1
backend/staticfiles/unfold/js/htmx/htmx.js
Normal file
1
backend/staticfiles/unfold/js/htmx/htmx.js
Normal file
File diff suppressed because one or more lines are too long
45
backend/staticfiles/unfold/js/select2.init.js
Normal file
45
backend/staticfiles/unfold/js/select2.init.js
Normal file
@@ -0,0 +1,45 @@
|
||||
"use strict";
|
||||
{
|
||||
const $ = django.jQuery;
|
||||
|
||||
$.fn.djangoCustomSelect2 = function () {
|
||||
$.each(this, function (i, element) {
|
||||
if (element.id.match(/__prefix__/)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($(element).hasClass("select2-hidden-accessible")) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(element).select2();
|
||||
});
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
$.fn.djangoFilterSelect2 = function () {
|
||||
$.each(this, function (i, element) {
|
||||
$(element).select2({
|
||||
ajax: {
|
||||
data: (params) => {
|
||||
return {
|
||||
term: params.term,
|
||||
page: params.page,
|
||||
app_label: element.dataset.appLabel,
|
||||
model_name: element.dataset.modelName,
|
||||
field_name: element.dataset.fieldName,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
$(function () {
|
||||
$(".unfold-admin-autocomplete").djangoCustomSelect2();
|
||||
|
||||
$(".unfold-filter-autocomplete").djangoFilterSelect2();
|
||||
});
|
||||
}
|
||||
21
backend/staticfiles/unfold/js/simplebar/LICENSE
Normal file
21
backend/staticfiles/unfold/js/simplebar/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Jonathan Nicol
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
10
backend/staticfiles/unfold/js/simplebar/simplebar.js
Normal file
10
backend/staticfiles/unfold/js/simplebar/simplebar.js
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user