unforld 1 not health yet

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-14 17:15:21 +00:00
parent ade055c971
commit cd2c84116b
43 changed files with 3326 additions and 1103 deletions

View File

@@ -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)

View File

@@ -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 = [

View File

@@ -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'

View File

@@ -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', '')

View File

@@ -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

View File

@@ -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 havent saved your changes to individual fields yet. Please click OK to save. Youll need to re-run the action.")
: gettext("You have selected an action, and you havent made any changes on individual fields. Youre 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 havent saved your changes to individual fields yet. Please click OK to save. Youll need to re-run the action."
)
: gettext(
"You have selected an action, and you havent made any changes on individual fields. Youre 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,
});
}
});
});
}

View File

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

View File

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

View 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.

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

File diff suppressed because one or more lines are too long

View 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.

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

View 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">&lsaquo;</a>
// <a href="#" class="link-next">&rsaquo;</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;
}

View File

@@ -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];
});
});
});

View 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.

File diff suppressed because one or more lines are too long

View 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.

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

View 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.

View 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");
}

View 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.

View File

@@ -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");
}

View 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.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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)});})();

View 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)});})();

File diff suppressed because one or more lines are too long

View 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();
});
};

View 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.

File diff suppressed because one or more lines are too long

View 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.

File diff suppressed because one or more lines are too long

View 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();
});
}

View 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.

File diff suppressed because one or more lines are too long