vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

commit f8d1cfad2adee491aec703c05e4a5b91d082928a
parent 81741647f3e8eb980da2853f853a67902bf9aa11
Author: Daniel García <dani-garcia@users.noreply.github.com>
Date:   Thu, 16 Sep 2021 21:36:25 +0200

Merge branch 'admin-interface' of https://github.com/BlackDex/vaultwarden into BlackDex-admin-interface

Diffstat:
M.pre-commit-config.yaml | 1+
Msrc/static/scripts/bootstrap-native.js | 101+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Msrc/static/scripts/bootstrap.css | 883+++++++++++++++++++++++--------------------------------------------------------
Msrc/static/scripts/datatables.css | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/static/scripts/datatables.js | 810++++++++++++++++++++++++++++++++++---------------------------------------------
Msrc/static/templates/admin/settings.hbs | 15+++++----------
6 files changed, 764 insertions(+), 1156 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml @@ -7,6 +7,7 @@ repos: - id: check-json - id: check-toml - id: end-of-file-fixer + exclude: "(.*js$|.*css$)" - id: check-case-conflict - id: check-merge-conflict - id: detect-private-key diff --git a/src/static/scripts/bootstrap-native.js b/src/static/scripts/bootstrap-native.js @@ -1,5 +1,5 @@ /*! - * Native JavaScript for Bootstrap v4.0.4 (https://thednp.github.io/bootstrap.native/) + * Native JavaScript for Bootstrap v4.0.6 (https://thednp.github.io/bootstrap.native/) * Copyright 2015-2021 © dnp_theme * Licensed under MIT (https://github.com/thednp/bootstrap.native/blob/master/LICENSE) */ @@ -1467,19 +1467,26 @@ } } - const modalOpenClass = 'modal-open'; const modalBackdropClass = 'modal-backdrop'; + const offcanvasBackdropClass = 'offcanvas-backdrop'; const modalActiveSelector = `.modal.${showClass}`; const offcanvasActiveSelector = `.offcanvas.${showClass}`; - const overlay = document.createElement('div'); - overlay.setAttribute('class', `${modalBackdropClass}`); function getCurrentOpen() { return queryElement(`${modalActiveSelector},${offcanvasActiveSelector}`); } - function appendOverlay(hasFade) { + function toggleOverlayType(isModal) { + const targetClass = isModal ? modalBackdropClass : offcanvasBackdropClass; + [modalBackdropClass, offcanvasBackdropClass].forEach((c) => { + removeClass(overlay, c); + }); + addClass(overlay, targetClass); + } + + function appendOverlay(hasFade, isModal) { + toggleOverlayType(isModal); document.body.appendChild(overlay); if (hasFade) addClass(overlay, fadeClass); } @@ -1499,7 +1506,6 @@ if (!currentOpen) { removeClass(overlay, fadeClass); - removeClass(bd, modalOpenClass); bd.removeChild(overlay); resetScrollbar(); } @@ -1518,7 +1524,6 @@ const modalString = 'modal'; const modalComponent = 'Modal'; const modalSelector = `.${modalString}`; - // const modalActiveSelector = `.${modalString}.${showClass}`; const modalToggleSelector = `[${dataBsToggle}="${modalString}"]`; const modalDismissSelector = `[${dataBsDismiss}="${modalString}"]`; const modalStaticClass = `${modalString}-static`; @@ -1567,8 +1572,11 @@ } function afterModalHide(self) { - const { triggers } = self; - removeOverlay(); + const { triggers, options } = self; + if (!getCurrentOpen()) { + if (options.backdrop) removeOverlay(); + resetScrollbar(); + } self.element.style.paddingRight = ''; self.isAnimating = false; @@ -1594,9 +1602,8 @@ element.style.display = 'block'; setModalScrollbar(self); - if (!queryElement(modalActiveSelector)) { + if (!getCurrentOpen()) { document.body.style.overflow = 'hidden'; - addClass(document.body, modalOpenClass); } addClass(element, showClass); @@ -1609,16 +1616,15 @@ function beforeModalHide(self, force) { const { - element, relatedTarget, hasFade, + element, options, relatedTarget, hasFade, } = self; - const currentOpen = getCurrentOpen(); element.style.display = ''; // force can also be the transitionEvent object, we wanna make sure it's not // call is not forced and overlay is visible - if (!force && hasFade && hasClass(overlay, showClass) - && !currentOpen) { // AND no modal is visible + if (options.backdrop && !force && hasFade && hasClass(overlay, showClass) + && !getCurrentOpen()) { // AND no modal is visible hideOverlay(); emulateTransitionEnd(overlay, () => afterModalHide(self)); } else { @@ -1666,7 +1672,8 @@ if (self.isAnimating) return; - const { isStatic, modalDialog } = self; + const { options, isStatic, modalDialog } = self; + const { backdrop } = options; const { target } = e; const selectedText = document.getSelection().toString().length; const targetInsideDialog = modalDialog.contains(target); @@ -1676,7 +1683,7 @@ addClass(element, modalStaticClass); self.isAnimating = true; emulateTransitionEnd(modalDialog, () => staticTransitionEnd(self)); - } else if (dismiss || (!selectedText && !isStatic && !targetInsideDialog)) { + } else if (dismiss || (!selectedText && !isStatic && !targetInsideDialog && backdrop)) { self.relatedTarget = dismiss || null; self.hide(); e.preventDefault(); @@ -1734,8 +1741,9 @@ show() { const self = this; const { - element, isAnimating, hasFade, relatedTarget, + element, options, isAnimating, hasFade, relatedTarget, } = self; + const { backdrop } = options; let overlayDelay = 0; if (hasClass(element, showClass) && !isAnimating) return; @@ -1744,8 +1752,6 @@ element.dispatchEvent(showModalEvent); if (showModalEvent.defaultPrevented) return; - self.isAnimating = true; - // we elegantly hide any opened modal/offcanvas const currentOpen = getCurrentOpen(); if (currentOpen && currentOpen !== element) { @@ -1755,18 +1761,24 @@ that.hide(); } - if (!queryElement(`.${modalBackdropClass}`)) { - appendOverlay(hasFade); - } - overlayDelay = getElementTransitionDuration(overlay); + self.isAnimating = true; - if (!hasClass(overlay, showClass)) { - showOverlay(); - } + if (backdrop) { + if (!currentOpen && !hasClass(overlay, showClass)) { + appendOverlay(hasFade, 1); + } else { + toggleOverlayType(1); + } + overlayDelay = getElementTransitionDuration(overlay); - if (!currentOpen) { + if (!hasClass(overlay, showClass)) showOverlay(); setTimeout(() => beforeModalShow(self), overlayDelay); - } else beforeModalShow(self); + } else { + beforeModalShow(self); + if (currentOpen && hasClass(overlay, showClass)) { + hideOverlay(); + } + } } hide(force) { @@ -1863,7 +1875,6 @@ const { element, options } = self; if (!options.scroll) { - addClass(document.body, modalOpenClass); document.body.style.overflow = 'hidden'; setOffCanvasScrollbar(self); } @@ -1909,15 +1920,15 @@ const self = element[offcanvasComponent]; if (!self) return; - const { options, open, triggers } = self; + const { options, triggers } = self; const { target } = e; const trigger = target.closest(offcanvasToggleSelector); if (trigger && trigger.tagName === 'A') e.preventDefault(); - if (open && ((!element.contains(target) && options.backdrop + if ((!element.contains(target) && options.backdrop && (!trigger || (trigger && !triggers.includes(trigger)))) - || offCanvasDismiss.contains(target))) { + || offCanvasDismiss.contains(target)) { self.relatedTarget = target === offCanvasDismiss ? offCanvasDismiss : null; self.hide(); } @@ -1965,7 +1976,6 @@ element.removeAttribute(ariaModal); element.removeAttribute('role'); element.style.visibility = ''; - self.open = false; self.isAnimating = false; if (triggers.length) { @@ -1979,7 +1989,6 @@ if (options.backdrop) removeOverlay(); if (!options.scroll) { resetScrollbar(); - removeClass(document.body, modalOpenClass); } } @@ -2005,7 +2014,6 @@ .filter((btn) => getTargetElement(btn) === element); // additional instance property - self.open = false; self.isAnimating = false; self.scrollbarWidth = measureScrollbar(); @@ -2017,7 +2025,8 @@ // ======================== toggle() { const self = this; - return self.open ? self.hide() : self.show(); + if (hasClass(self.element, showClass)) self.hide(); + else self.show(); } show() { @@ -2027,7 +2036,7 @@ } = self; let overlayDelay = 0; - if (self.open || isAnimating) return; + if (hasClass(element, showClass) || isAnimating) return; showOffcanvasEvent.relatedTarget = relatedTarget || null; element.dispatchEvent(showOffcanvasEvent); @@ -2043,12 +2052,13 @@ that.hide(); } - self.open = true; self.isAnimating = true; if (options.backdrop) { - if (!queryElement(`.${modalBackdropClass}`)) { + if (!currentOpen) { appendOverlay(1); + } else { + toggleOverlayType(); } overlayDelay = getElementTransitionDuration(overlay); @@ -2056,14 +2066,19 @@ if (!hasClass(overlay, showClass)) showOverlay(); setTimeout(() => beforeOffcanvasShow(self), overlayDelay); - } else beforeOffcanvasShow(self); + } else { + beforeOffcanvasShow(self); + if (currentOpen && hasClass(overlay, showClass)) { + hideOverlay(); + } + } } hide(force) { const self = this; const { element, isAnimating, relatedTarget } = self; - if (!self.open || isAnimating) return; + if (!hasClass(element, showClass) || isAnimating) return; hideOffcanvasEvent.relatedTarget = relatedTarget || null; element.dispatchEvent(hideOffcanvasEvent); @@ -3483,7 +3498,7 @@ constructor: Tooltip, }; - var version = "4.0.4"; + var version = "4.0.6"; // import { alertInit } from '../components/alert-native.js'; // import { buttonInit } from '../components/button-native.js'; diff --git a/src/static/scripts/bootstrap.css b/src/static/scripts/bootstrap.css @@ -1,6 +1,6 @@ @charset "UTF-8"; /*! - * Bootstrap v5.1.0 (https://getbootstrap.com/) + * Bootstrap v5.0.2 (https://getbootstrap.com/) * Copyright 2011-2021 The Bootstrap Authors * Copyright 2011-2021 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) @@ -19,15 +19,6 @@ --bs-white: #fff; --bs-gray: #6c757d; --bs-gray-dark: #343a40; - --bs-gray-100: #f8f9fa; - --bs-gray-200: #e9ecef; - --bs-gray-300: #dee2e6; - --bs-gray-400: #ced4da; - --bs-gray-500: #adb5bd; - --bs-gray-600: #6c757d; - --bs-gray-700: #495057; - --bs-gray-800: #343a40; - --bs-gray-900: #212529; --bs-primary: #0d6efd; --bs-secondary: #6c757d; --bs-success: #198754; @@ -36,26 +27,9 @@ --bs-danger: #dc3545; --bs-light: #f8f9fa; --bs-dark: #212529; - --bs-primary-rgb: 13, 110, 253; - --bs-secondary-rgb: 108, 117, 125; - --bs-success-rgb: 25, 135, 84; - --bs-info-rgb: 13, 202, 240; - --bs-warning-rgb: 255, 193, 7; - --bs-danger-rgb: 220, 53, 69; - --bs-light-rgb: 248, 249, 250; - --bs-dark-rgb: 33, 37, 41; - --bs-white-rgb: 255, 255, 255; - --bs-black-rgb: 0, 0, 0; - --bs-body-rgb: 33, 37, 41; --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); - --bs-body-font-family: var(--bs-font-sans-serif); - --bs-body-font-size: 1rem; - --bs-body-font-weight: 400; - --bs-body-line-height: 1.5; - --bs-body-color: #212529; - --bs-body-bg: #fff; } *, @@ -72,13 +46,12 @@ body { margin: 0; - font-family: var(--bs-body-font-family); - font-size: var(--bs-body-font-size); - font-weight: var(--bs-body-font-weight); - line-height: var(--bs-body-line-height); - color: var(--bs-body-color); - text-align: var(--bs-body-text-align); - background-color: var(--bs-body-bg); + font-family: var(--bs-font-sans-serif); + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #212529; + background-color: #fff; -webkit-text-size-adjust: 100%; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } @@ -712,6 +685,206 @@ progress { width: 16.6666666667%; } +@media (min-width: 576px) { + .col-sm { + flex: 1 0 0%; + } + + .row-cols-sm-auto > * { + flex: 0 0 auto; + width: auto; + } + + .row-cols-sm-1 > * { + flex: 0 0 auto; + width: 100%; + } + + .row-cols-sm-2 > * { + flex: 0 0 auto; + width: 50%; + } + + .row-cols-sm-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .row-cols-sm-4 > * { + flex: 0 0 auto; + width: 25%; + } + + .row-cols-sm-5 > * { + flex: 0 0 auto; + width: 20%; + } + + .row-cols-sm-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } +} +@media (min-width: 768px) { + .col-md { + flex: 1 0 0%; + } + + .row-cols-md-auto > * { + flex: 0 0 auto; + width: auto; + } + + .row-cols-md-1 > * { + flex: 0 0 auto; + width: 100%; + } + + .row-cols-md-2 > * { + flex: 0 0 auto; + width: 50%; + } + + .row-cols-md-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .row-cols-md-4 > * { + flex: 0 0 auto; + width: 25%; + } + + .row-cols-md-5 > * { + flex: 0 0 auto; + width: 20%; + } + + .row-cols-md-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } +} +@media (min-width: 992px) { + .col-lg { + flex: 1 0 0%; + } + + .row-cols-lg-auto > * { + flex: 0 0 auto; + width: auto; + } + + .row-cols-lg-1 > * { + flex: 0 0 auto; + width: 100%; + } + + .row-cols-lg-2 > * { + flex: 0 0 auto; + width: 50%; + } + + .row-cols-lg-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .row-cols-lg-4 > * { + flex: 0 0 auto; + width: 25%; + } + + .row-cols-lg-5 > * { + flex: 0 0 auto; + width: 20%; + } + + .row-cols-lg-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } +} +@media (min-width: 1200px) { + .col-xl { + flex: 1 0 0%; + } + + .row-cols-xl-auto > * { + flex: 0 0 auto; + width: auto; + } + + .row-cols-xl-1 > * { + flex: 0 0 auto; + width: 100%; + } + + .row-cols-xl-2 > * { + flex: 0 0 auto; + width: 50%; + } + + .row-cols-xl-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .row-cols-xl-4 > * { + flex: 0 0 auto; + width: 25%; + } + + .row-cols-xl-5 > * { + flex: 0 0 auto; + width: 20%; + } + + .row-cols-xl-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } +} +@media (min-width: 1400px) { + .col-xxl { + flex: 1 0 0%; + } + + .row-cols-xxl-auto > * { + flex: 0 0 auto; + width: auto; + } + + .row-cols-xxl-1 > * { + flex: 0 0 auto; + width: 100%; + } + + .row-cols-xxl-2 > * { + flex: 0 0 auto; + width: 50%; + } + + .row-cols-xxl-3 > * { + flex: 0 0 auto; + width: 33.3333333333%; + } + + .row-cols-xxl-4 > * { + flex: 0 0 auto; + width: 25%; + } + + .row-cols-xxl-5 > * { + flex: 0 0 auto; + width: 20%; + } + + .row-cols-xxl-6 > * { + flex: 0 0 auto; + width: 16.6666666667%; + } +} .col-auto { flex: 0 0 auto; width: auto; @@ -882,45 +1055,6 @@ progress { } @media (min-width: 576px) { - .col-sm { - flex: 1 0 0%; - } - - .row-cols-sm-auto > * { - flex: 0 0 auto; - width: auto; - } - - .row-cols-sm-1 > * { - flex: 0 0 auto; - width: 100%; - } - - .row-cols-sm-2 > * { - flex: 0 0 auto; - width: 50%; - } - - .row-cols-sm-3 > * { - flex: 0 0 auto; - width: 33.3333333333%; - } - - .row-cols-sm-4 > * { - flex: 0 0 auto; - width: 25%; - } - - .row-cols-sm-5 > * { - flex: 0 0 auto; - width: 20%; - } - - .row-cols-sm-6 > * { - flex: 0 0 auto; - width: 16.6666666667%; - } - .col-sm-auto { flex: 0 0 auto; width: auto; @@ -1095,45 +1229,6 @@ progress { } } @media (min-width: 768px) { - .col-md { - flex: 1 0 0%; - } - - .row-cols-md-auto > * { - flex: 0 0 auto; - width: auto; - } - - .row-cols-md-1 > * { - flex: 0 0 auto; - width: 100%; - } - - .row-cols-md-2 > * { - flex: 0 0 auto; - width: 50%; - } - - .row-cols-md-3 > * { - flex: 0 0 auto; - width: 33.3333333333%; - } - - .row-cols-md-4 > * { - flex: 0 0 auto; - width: 25%; - } - - .row-cols-md-5 > * { - flex: 0 0 auto; - width: 20%; - } - - .row-cols-md-6 > * { - flex: 0 0 auto; - width: 16.6666666667%; - } - .col-md-auto { flex: 0 0 auto; width: auto; @@ -1299,54 +1394,15 @@ progress { .g-md-5, .gx-md-5 { - --bs-gutter-x: 3rem; - } - - .g-md-5, -.gy-md-5 { - --bs-gutter-y: 3rem; - } -} -@media (min-width: 992px) { - .col-lg { - flex: 1 0 0%; - } - - .row-cols-lg-auto > * { - flex: 0 0 auto; - width: auto; - } - - .row-cols-lg-1 > * { - flex: 0 0 auto; - width: 100%; - } - - .row-cols-lg-2 > * { - flex: 0 0 auto; - width: 50%; - } - - .row-cols-lg-3 > * { - flex: 0 0 auto; - width: 33.3333333333%; - } - - .row-cols-lg-4 > * { - flex: 0 0 auto; - width: 25%; - } - - .row-cols-lg-5 > * { - flex: 0 0 auto; - width: 20%; + --bs-gutter-x: 3rem; } - .row-cols-lg-6 > * { - flex: 0 0 auto; - width: 16.6666666667%; + .g-md-5, +.gy-md-5 { + --bs-gutter-y: 3rem; } - +} +@media (min-width: 992px) { .col-lg-auto { flex: 0 0 auto; width: auto; @@ -1521,45 +1577,6 @@ progress { } } @media (min-width: 1200px) { - .col-xl { - flex: 1 0 0%; - } - - .row-cols-xl-auto > * { - flex: 0 0 auto; - width: auto; - } - - .row-cols-xl-1 > * { - flex: 0 0 auto; - width: 100%; - } - - .row-cols-xl-2 > * { - flex: 0 0 auto; - width: 50%; - } - - .row-cols-xl-3 > * { - flex: 0 0 auto; - width: 33.3333333333%; - } - - .row-cols-xl-4 > * { - flex: 0 0 auto; - width: 25%; - } - - .row-cols-xl-5 > * { - flex: 0 0 auto; - width: 20%; - } - - .row-cols-xl-6 > * { - flex: 0 0 auto; - width: 16.6666666667%; - } - .col-xl-auto { flex: 0 0 auto; width: auto; @@ -1734,45 +1751,6 @@ progress { } } @media (min-width: 1400px) { - .col-xxl { - flex: 1 0 0%; - } - - .row-cols-xxl-auto > * { - flex: 0 0 auto; - width: auto; - } - - .row-cols-xxl-1 > * { - flex: 0 0 auto; - width: 100%; - } - - .row-cols-xxl-2 > * { - flex: 0 0 auto; - width: 50%; - } - - .row-cols-xxl-3 > * { - flex: 0 0 auto; - width: 33.3333333333%; - } - - .row-cols-xxl-4 > * { - flex: 0 0 auto; - width: 25%; - } - - .row-cols-xxl-5 > * { - flex: 0 0 auto; - width: 20%; - } - - .row-cols-xxl-6 > * { - flex: 0 0 auto; - width: 16.6666666667%; - } - .col-xxl-auto { flex: 0 0 auto; width: auto; @@ -2288,7 +2266,7 @@ progress { } .form-control-sm { - min-height: calc(1.5em + 0.5rem + 2px); + min-height: calc(1.5em + (0.5rem + 2px)); padding: 0.25rem 0.5rem; font-size: 0.875rem; border-radius: 0.2rem; @@ -2307,7 +2285,7 @@ progress { } .form-control-lg { - min-height: calc(1.5em + 1rem + 2px); + min-height: calc(1.5em + (1rem + 2px)); padding: 0.5rem 1rem; font-size: 1.25rem; border-radius: 0.3rem; @@ -2326,17 +2304,17 @@ progress { } textarea.form-control { - min-height: calc(1.5em + 0.75rem + 2px); + min-height: calc(1.5em + (0.75rem + 2px)); } textarea.form-control-sm { - min-height: calc(1.5em + 0.5rem + 2px); + min-height: calc(1.5em + (0.5rem + 2px)); } textarea.form-control-lg { - min-height: calc(1.5em + 1rem + 2px); + min-height: calc(1.5em + (1rem + 2px)); } .form-control-color { - width: 3rem; + max-width: 3rem; height: auto; padding: 0.375rem; } @@ -3452,16 +3430,6 @@ textarea.form-control-lg { transition: none; } } -.collapsing.collapse-horizontal { - width: 0; - height: auto; - transition: width 0.35s ease; -} -@media (prefers-reduced-motion: reduce) { - .collapsing.collapse-horizontal { - transition: none; - } -} .dropup, .dropend, @@ -4079,33 +4047,6 @@ textarea.form-control-lg { .navbar-expand-sm .navbar-toggler { display: none; } - .navbar-expand-sm .offcanvas-header { - display: none; - } - .navbar-expand-sm .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; - } - .navbar-expand-sm .offcanvas-top, -.navbar-expand-sm .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; - } - .navbar-expand-sm .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - } } @media (min-width: 768px) { .navbar-expand-md { @@ -4132,33 +4073,6 @@ textarea.form-control-lg { .navbar-expand-md .navbar-toggler { display: none; } - .navbar-expand-md .offcanvas-header { - display: none; - } - .navbar-expand-md .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; - } - .navbar-expand-md .offcanvas-top, -.navbar-expand-md .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; - } - .navbar-expand-md .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - } } @media (min-width: 992px) { .navbar-expand-lg { @@ -4185,33 +4099,6 @@ textarea.form-control-lg { .navbar-expand-lg .navbar-toggler { display: none; } - .navbar-expand-lg .offcanvas-header { - display: none; - } - .navbar-expand-lg .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; - } - .navbar-expand-lg .offcanvas-top, -.navbar-expand-lg .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; - } - .navbar-expand-lg .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - } } @media (min-width: 1200px) { .navbar-expand-xl { @@ -4238,33 +4125,6 @@ textarea.form-control-lg { .navbar-expand-xl .navbar-toggler { display: none; } - .navbar-expand-xl .offcanvas-header { - display: none; - } - .navbar-expand-xl .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; - } - .navbar-expand-xl .offcanvas-top, -.navbar-expand-xl .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; - } - .navbar-expand-xl .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - } } @media (min-width: 1400px) { .navbar-expand-xxl { @@ -4291,33 +4151,6 @@ textarea.form-control-lg { .navbar-expand-xxl .navbar-toggler { display: none; } - .navbar-expand-xxl .offcanvas-header { - display: none; - } - .navbar-expand-xxl .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; - } - .navbar-expand-xxl .offcanvas-top, -.navbar-expand-xxl .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; - } - .navbar-expand-xxl .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - } } .navbar-expand { flex-wrap: nowrap; @@ -4343,33 +4176,6 @@ textarea.form-control-lg { .navbar-expand .navbar-toggler { display: none; } -.navbar-expand .offcanvas-header { - display: none; -} -.navbar-expand .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; -} -.navbar-expand .offcanvas-top, -.navbar-expand .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; -} -.navbar-expand .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; -} .navbar-light .navbar-brand { color: rgba(0, 0, 0, 0.9); @@ -4493,6 +4299,9 @@ textarea.form-control-lg { margin-bottom: 0; } +.card-link:hover { + text-decoration: none; +} .card-link + .card-link { margin-left: 1rem; } @@ -5368,10 +5177,10 @@ textarea.form-control-lg { box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); border-radius: 0.25rem; } -.toast.showing { +.toast:not(.showing):not(.show) { opacity: 0; } -.toast:not(.show) { +.toast.hide { display: none; } @@ -5411,7 +5220,7 @@ textarea.form-control-lg { position: fixed; top: 0; left: 0; - z-index: 1055; + z-index: 1060; display: none; width: 100%; height: 100%; @@ -5476,7 +5285,7 @@ textarea.form-control-lg { position: fixed; top: 0; left: 0; - z-index: 1050; + z-index: 1040; width: 100vw; height: 100vh; background-color: #000; @@ -6201,7 +6010,7 @@ textarea.form-control-lg { .offcanvas { position: fixed; bottom: 0; - z-index: 1045; + z-index: 1050; display: flex; flex-direction: column; max-width: 100%; @@ -6217,22 +6026,6 @@ textarea.form-control-lg { } } -.offcanvas-backdrop { - position: fixed; - top: 0; - left: 0; - z-index: 1040; - width: 100vw; - height: 100vh; - background-color: #000; -} -.offcanvas-backdrop.fade { - opacity: 0; -} -.offcanvas-backdrop.show { - opacity: 0.5; -} - .offcanvas-header { display: flex; align-items: center; @@ -6296,69 +6089,6 @@ textarea.form-control-lg { transform: none; } -.placeholder { - display: inline-block; - min-height: 1em; - vertical-align: middle; - cursor: wait; - background-color: currentColor; - opacity: 0.5; -} -.placeholder.btn::before { - display: inline-block; - content: ""; -} - -.placeholder-xs { - min-height: 0.6em; -} - -.placeholder-sm { - min-height: 0.8em; -} - -.placeholder-lg { - min-height: 1.2em; -} - -.placeholder-glow .placeholder { - -webkit-animation: placeholder-glow 2s ease-in-out infinite; - animation: placeholder-glow 2s ease-in-out infinite; -} - -@-webkit-keyframes placeholder-glow { - 50% { - opacity: 0.2; - } -} - -@keyframes placeholder-glow { - 50% { - opacity: 0.2; - } -} -.placeholder-wave { - -webkit-mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%); - mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%); - -webkit-mask-size: 200% 100%; - mask-size: 200% 100%; - -webkit-animation: placeholder-wave 2s linear infinite; - animation: placeholder-wave 2s linear infinite; -} - -@-webkit-keyframes placeholder-wave { - 100% { - -webkit-mask-position: -200% 0%; - mask-position: -200% 0%; - } -} - -@keyframes placeholder-wave { - 100% { - -webkit-mask-position: -200% 0%; - mask-position: -200% 0%; - } -} .clearfix::after { display: block; clear: both; @@ -6517,20 +6247,6 @@ textarea.form-control-lg { z-index: 1020; } } -.hstack { - display: flex; - flex-direction: row; - align-items: center; - align-self: stretch; -} - -.vstack { - display: flex; - flex: 1 1 auto; - flex-direction: column; - align-self: stretch; -} - .visually-hidden, .visually-hidden-focusable:not(:focus):not(:focus-within) { position: absolute !important; @@ -6560,15 +6276,6 @@ textarea.form-control-lg { white-space: nowrap; } -.vr { - display: inline-block; - align-self: stretch; - width: 1px; - min-height: 1em; - background-color: currentColor; - opacity: 0.25; -} - .align-baseline { vertical-align: baseline !important; } @@ -6605,26 +6312,6 @@ textarea.form-control-lg { float: none !important; } -.opacity-0 { - opacity: 0 !important; -} - -.opacity-25 { - opacity: 0.25 !important; -} - -.opacity-50 { - opacity: 0.5 !important; -} - -.opacity-75 { - opacity: 0.75 !important; -} - -.opacity-100 { - opacity: 1 !important; -} - .overflow-auto { overflow: auto !important; } @@ -7648,176 +7335,105 @@ textarea.form-control-lg { /* rtl:end:remove */ .text-primary { - --bs-text-opacity: 1; - color: rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important; + color: #0d6efd !important; } .text-secondary { - --bs-text-opacity: 1; - color: rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important; + color: #6c757d !important; } .text-success { - --bs-text-opacity: 1; - color: rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important; + color: #198754 !important; } .text-info { - --bs-text-opacity: 1; - color: rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important; + color: #0dcaf0 !important; } .text-warning { - --bs-text-opacity: 1; - color: rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important; + color: #ffc107 !important; } .text-danger { - --bs-text-opacity: 1; - color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important; + color: #dc3545 !important; } .text-light { - --bs-text-opacity: 1; - color: rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important; + color: #f8f9fa !important; } .text-dark { - --bs-text-opacity: 1; - color: rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important; -} - -.text-black { - --bs-text-opacity: 1; - color: rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important; + color: #212529 !important; } .text-white { - --bs-text-opacity: 1; - color: rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important; + color: #fff !important; } .text-body { - --bs-text-opacity: 1; - color: rgba(var(--bs-body-rgb), var(--bs-text-opacity)) !important; + color: #212529 !important; } .text-muted { - --bs-text-opacity: 1; color: #6c757d !important; } .text-black-50 { - --bs-text-opacity: 1; color: rgba(0, 0, 0, 0.5) !important; } .text-white-50 { - --bs-text-opacity: 1; color: rgba(255, 255, 255, 0.5) !important; } .text-reset { - --bs-text-opacity: 1; color: inherit !important; } -.text-opacity-25 { - --bs-text-opacity: 0.25; -} - -.text-opacity-50 { - --bs-text-opacity: 0.5; -} - -.text-opacity-75 { - --bs-text-opacity: 0.75; -} - -.text-opacity-100 { - --bs-text-opacity: 1; -} - .bg-primary { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important; + background-color: #0d6efd !important; } .bg-secondary { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important; + background-color: #6c757d !important; } .bg-success { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important; + background-color: #198754 !important; } .bg-info { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important; + background-color: #0dcaf0 !important; } .bg-warning { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important; + background-color: #ffc107 !important; } .bg-danger { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important; + background-color: #dc3545 !important; } .bg-light { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important; + background-color: #f8f9fa !important; } .bg-dark { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important; + background-color: #212529 !important; } -.bg-black { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important; +.bg-body { + background-color: #fff !important; } .bg-white { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-body { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-body-rgb), var(--bs-bg-opacity)) !important; + background-color: #fff !important; } .bg-transparent { - --bs-bg-opacity: 1; background-color: transparent !important; } -.bg-opacity-10 { - --bs-bg-opacity: 0.1; -} - -.bg-opacity-25 { - --bs-bg-opacity: 0.25; -} - -.bg-opacity-50 { - --bs-bg-opacity: 0.5; -} - -.bg-opacity-75 { - --bs-bg-opacity: 0.75; -} - -.bg-opacity-100 { - --bs-bg-opacity: 1; -} - .bg-gradient { background-image: var(--bs-gradient) !important; } @@ -11218,4 +10834,4 @@ textarea.form-control-lg { } } -/*# sourceMappingURL=bootstrap.css.map */ +/*# sourceMappingURL=bootstrap.css.map */ +\ No newline at end of file diff --git a/src/static/scripts/datatables.css b/src/static/scripts/datatables.css @@ -4,13 +4,94 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-1.10.25 + * https://datatables.net/download/#bs5/dt-1.11.2 * * Included libraries: - * DataTables 1.10.25 + * DataTables 1.11.2 */ @charset "UTF-8"; +td.dt-control { + background: url("https://www.datatables.net/examples/resources/details_open.png") no-repeat center center; + cursor: pointer; +} + +tr.dt-hasChild td.dt-control { + background: url("https://www.datatables.net/examples/resources/details_close.png") no-repeat center center; +} + +table.dataTable th.dt-left, +table.dataTable td.dt-left { + text-align: left; +} +table.dataTable th.dt-center, +table.dataTable td.dt-center, +table.dataTable td.dataTables_empty { + text-align: center; +} +table.dataTable th.dt-right, +table.dataTable td.dt-right { + text-align: right; +} +table.dataTable th.dt-justify, +table.dataTable td.dt-justify { + text-align: justify; +} +table.dataTable th.dt-nowrap, +table.dataTable td.dt-nowrap { + white-space: nowrap; +} +table.dataTable thead th.dt-head-left, +table.dataTable thead td.dt-head-left, +table.dataTable tfoot th.dt-head-left, +table.dataTable tfoot td.dt-head-left { + text-align: left; +} +table.dataTable thead th.dt-head-center, +table.dataTable thead td.dt-head-center, +table.dataTable tfoot th.dt-head-center, +table.dataTable tfoot td.dt-head-center { + text-align: center; +} +table.dataTable thead th.dt-head-right, +table.dataTable thead td.dt-head-right, +table.dataTable tfoot th.dt-head-right, +table.dataTable tfoot td.dt-head-right { + text-align: right; +} +table.dataTable thead th.dt-head-justify, +table.dataTable thead td.dt-head-justify, +table.dataTable tfoot th.dt-head-justify, +table.dataTable tfoot td.dt-head-justify { + text-align: justify; +} +table.dataTable thead th.dt-head-nowrap, +table.dataTable thead td.dt-head-nowrap, +table.dataTable tfoot th.dt-head-nowrap, +table.dataTable tfoot td.dt-head-nowrap { + white-space: nowrap; +} +table.dataTable tbody th.dt-body-left, +table.dataTable tbody td.dt-body-left { + text-align: left; +} +table.dataTable tbody th.dt-body-center, +table.dataTable tbody td.dt-body-center { + text-align: center; +} +table.dataTable tbody th.dt-body-right, +table.dataTable tbody td.dt-body-right { + text-align: right; +} +table.dataTable tbody th.dt-body-justify, +table.dataTable tbody td.dt-body-justify { + text-align: justify; +} +table.dataTable tbody th.dt-body-nowrap, +table.dataTable tbody td.dt-body-nowrap { + white-space: nowrap; +} + /*! Bootstrap 5 integration for DataTables * * ©2020 SpryMedia Ltd, all rights reserved. @@ -143,21 +224,21 @@ div.dataTables_scrollHead table.dataTable { margin-bottom: 0 !important; } -div.dataTables_scrollBody table { +div.dataTables_scrollBody > table { border-top: none; margin-top: 0 !important; margin-bottom: 0 !important; } -div.dataTables_scrollBody table thead .sorting:before, -div.dataTables_scrollBody table thead .sorting_asc:before, -div.dataTables_scrollBody table thead .sorting_desc:before, -div.dataTables_scrollBody table thead .sorting:after, -div.dataTables_scrollBody table thead .sorting_asc:after, -div.dataTables_scrollBody table thead .sorting_desc:after { +div.dataTables_scrollBody > table > thead .sorting:before, +div.dataTables_scrollBody > table > thead .sorting_asc:before, +div.dataTables_scrollBody > table > thead .sorting_desc:before, +div.dataTables_scrollBody > table > thead .sorting:after, +div.dataTables_scrollBody > table > thead .sorting_asc:after, +div.dataTables_scrollBody > table > thead .sorting_desc:after { display: none; } -div.dataTables_scrollBody table tbody tr:first-child th, -div.dataTables_scrollBody table tbody tr:first-child td { +div.dataTables_scrollBody > table > tbody tr:first-child th, +div.dataTables_scrollBody > table > tbody tr:first-child td { border-top: none; } @@ -235,4 +316,11 @@ div.table-responsive > div.dataTables_wrapper > div.row > div[class^=col-]:last- padding-right: 0; } +table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) { + --bs-table-accent-bg: transparent; +} +table.dataTable.table-striped > tbody > tr.odd { + --bs-table-accent-bg: var(--bs-table-striped-bg); +} + diff --git a/src/static/scripts/datatables.js b/src/static/scripts/datatables.js @@ -4,20 +4,20 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-1.10.25 + * https://datatables.net/download/#bs5/dt-1.11.2 * * Included libraries: - * DataTables 1.10.25 + * DataTables 1.11.2 */ -/*! DataTables 1.10.25 +/*! DataTables 1.11.2 * ©2008-2021 SpryMedia Ltd - datatables.net/license */ /** * @summary DataTables * @description Paginate, search and order HTML tables - * @version 1.10.25 + * @version 1.11.2 * @file jquery.dataTables.js * @author SpryMedia Ltd * @contact www.datatables.net @@ -65,7 +65,7 @@ } else { // Browser - factory( jQuery, window, document ); + window.DataTable = factory( jQuery, window, document ); } } (function( $, window, document, undefined ) { @@ -103,8 +103,17 @@ * } ); * } ); */ - var DataTable = function ( options ) + var DataTable = function ( selector, options ) { + // When creating with `new`, create a new DataTable, returning the API instance + if (this instanceof DataTable) { + return $(selector).DataTable(options); + } + else { + // Argument switching + options = selector; + } + /** * Perform a jQuery selector action on the table's TR elements (from the tbody) and * return the resulting jQuery object. @@ -1097,8 +1106,8 @@ dataType: 'json', url: oLanguage.sUrl, success: function ( json ) { - _fnLanguageCompat( json ); _fnCamelToHungarian( defaults.oLanguage, json ); + _fnLanguageCompat( json ); $.extend( true, oLanguage, json ); _fnCallbackFire( oSettings, null, 'i18n', [oSettings]); @@ -1313,10 +1322,11 @@ }; /* Must be done after everything which can be overridden by the state saving! */ + _fnCallbackReg( oSettings, 'aoDrawCallback', _fnSaveState, 'state_save' ); + if ( oInit.bStateSave ) { features.bStateSave = true; - _fnCallbackReg( oSettings, 'aoDrawCallback', _fnSaveState, 'state_save' ); _fnLoadState( oSettings, oInit, loadedInit ); } else { @@ -1687,6 +1697,227 @@ */ escapeRegex: function ( val ) { return val.replace( _re_escape_regex, '\\$1' ); + }, + + /** + * Create a function that will write to a nested object or array + * @param {*} source JSON notation string + * @returns Write function + */ + set: function ( source ) { + if ( $.isPlainObject( source ) ) { + /* Unlike get, only the underscore (global) option is used for for + * setting data since we don't know the type here. This is why an object + * option is not documented for `mData` (which is read/write), but it is + * for `mRender` which is read only. + */ + return DataTable.util.set( source._ ); + } + else if ( source === null ) { + // Nothing to do when the data source is null + return function () {}; + } + else if ( typeof source === 'function' ) { + return function (data, val, meta) { + source( data, 'set', val, meta ); + }; + } + else if ( typeof source === 'string' && (source.indexOf('.') !== -1 || + source.indexOf('[') !== -1 || source.indexOf('(') !== -1) ) + { + // Like the get, we need to get data from a nested object + var setData = function (data, val, src) { + var a = _fnSplitObjNotation( src ), b; + var aLast = a[a.length-1]; + var arrayNotation, funcNotation, o, innerSrc; + + for ( var i=0, iLen=a.length-1 ; i<iLen ; i++ ) { + // Protect against prototype pollution + if (a[i] === '__proto__' || a[i] === 'constructor') { + throw new Error('Cannot set prototype values'); + } + + // Check if we are dealing with an array notation request + arrayNotation = a[i].match(__reArray); + funcNotation = a[i].match(__reFn); + + if ( arrayNotation ) { + a[i] = a[i].replace(__reArray, ''); + data[ a[i] ] = []; + + // Get the remainder of the nested object to set so we can recurse + b = a.slice(); + b.splice( 0, i+1 ); + innerSrc = b.join('.'); + + // Traverse each entry in the array setting the properties requested + if ( Array.isArray( val ) ) { + for ( var j=0, jLen=val.length ; j<jLen ; j++ ) { + o = {}; + setData( o, val[j], innerSrc ); + data[ a[i] ].push( o ); + } + } + else { + // We've been asked to save data to an array, but it + // isn't array data to be saved. Best that can be done + // is to just save the value. + data[ a[i] ] = val; + } + + // The inner call to setData has already traversed through the remainder + // of the source and has set the data, thus we can exit here + return; + } + else if ( funcNotation ) { + // Function call + a[i] = a[i].replace(__reFn, ''); + data = data[ a[i] ]( val ); + } + + // If the nested object doesn't currently exist - since we are + // trying to set the value - create it + if ( data[ a[i] ] === null || data[ a[i] ] === undefined ) { + data[ a[i] ] = {}; + } + data = data[ a[i] ]; + } + + // Last item in the input - i.e, the actual set + if ( aLast.match(__reFn ) ) { + // Function call + data = data[ aLast.replace(__reFn, '') ]( val ); + } + else { + // If array notation is used, we just want to strip it and use the property name + // and assign the value. If it isn't used, then we get the result we want anyway + data[ aLast.replace(__reArray, '') ] = val; + } + }; + + return function (data, val) { // meta is also passed in, but not used + return setData( data, val, source ); + }; + } + else { + // Array or flat object mapping + return function (data, val) { // meta is also passed in, but not used + data[source] = val; + }; + } + }, + + /** + * Create a function that will read nested objects from arrays, based on JSON notation + * @param {*} source JSON notation string + * @returns Value read + */ + get: function ( source ) { + if ( $.isPlainObject( source ) ) { + // Build an object of get functions, and wrap them in a single call + var o = {}; + $.each( source, function (key, val) { + if ( val ) { + o[key] = DataTable.util.get( val ); + } + } ); + + return function (data, type, row, meta) { + var t = o[type] || o._; + return t !== undefined ? + t(data, type, row, meta) : + data; + }; + } + else if ( source === null ) { + // Give an empty string for rendering / sorting etc + return function (data) { // type, row and meta also passed, but not used + return data; + }; + } + else if ( typeof source === 'function' ) { + return function (data, type, row, meta) { + return source( data, type, row, meta ); + }; + } + else if ( typeof source === 'string' && (source.indexOf('.') !== -1 || + source.indexOf('[') !== -1 || source.indexOf('(') !== -1) ) + { + /* If there is a . in the source string then the data source is in a + * nested object so we loop over the data for each level to get the next + * level down. On each loop we test for undefined, and if found immediately + * return. This allows entire objects to be missing and sDefaultContent to + * be used if defined, rather than throwing an error + */ + var fetchData = function (data, type, src) { + var arrayNotation, funcNotation, out, innerSrc; + + if ( src !== "" ) { + var a = _fnSplitObjNotation( src ); + + for ( var i=0, iLen=a.length ; i<iLen ; i++ ) { + // Check if we are dealing with special notation + arrayNotation = a[i].match(__reArray); + funcNotation = a[i].match(__reFn); + + if ( arrayNotation ) { + // Array notation + a[i] = a[i].replace(__reArray, ''); + + // Condition allows simply [] to be passed in + if ( a[i] !== "" ) { + data = data[ a[i] ]; + } + out = []; + + // Get the remainder of the nested object to get + a.splice( 0, i+1 ); + innerSrc = a.join('.'); + + // Traverse each entry in the array getting the properties requested + if ( Array.isArray( data ) ) { + for ( var j=0, jLen=data.length ; j<jLen ; j++ ) { + out.push( fetchData( data[j], type, innerSrc ) ); + } + } + + // If a string is given in between the array notation indicators, that + // is used to join the strings together, otherwise an array is returned + var join = arrayNotation[0].substring(1, arrayNotation[0].length-1); + data = (join==="") ? out : out.join(join); + + // The inner call to fetchData has already traversed through the remainder + // of the source requested, so we exit from the loop + break; + } + else if ( funcNotation ) { + // Function call + a[i] = a[i].replace(__reFn, ''); + data = data[ a[i] ](); + continue; + } + + if ( data === null || data[ a[i] ] === undefined ) { + return undefined; + } + + data = data[ a[i] ]; + } + } + + return data; + }; + + return function (data, type) { // row and meta also passed, but not used + return fetchData( data, type, source ); + }; + } + else { + // Array or flat object mapping + return function (data, type) { // row and meta also passed, but not used + return data[source]; + }; + } } }; @@ -2201,7 +2432,7 @@ /** - * Covert the index of a visible column to the index in the data array (take account + * Convert the index of a visible column to the index in the data array (take account * of hidden columns) * @param {object} oSettings dataTables settings object * @param {int} iMatch Visible column index to lookup @@ -2219,7 +2450,7 @@ /** - * Covert the index of an index in the data array and convert it to the visible + * Convert the index of an index in the data array and convert it to the visible * column index (take account of hidden columns) * @param {int} iMatch Column index to lookup * @param {object} oSettings dataTables settings object @@ -2533,12 +2764,19 @@ * @param {object} settings dataTables settings object * @param {int} rowIdx aoData row id * @param {int} colIdx Column index - * @param {string} type data get type ('display', 'type' 'filter' 'sort') + * @param {string} type data get type ('display', 'type' 'filter|search' 'sort|order') * @returns {*} Cell data * @memberof DataTable#oApi */ function _fnGetCellData( settings, rowIdx, colIdx, type ) { + if (type === 'search') { + type = 'filter'; + } + else if (type === 'order') { + type = 'sort'; + } + var draw = settings.iDraw; var col = settings.aoColumns[colIdx]; var rowData = settings.aoData[rowIdx]._aData; @@ -2622,122 +2860,7 @@ * @returns {function} Data get function * @memberof DataTable#oApi */ - function _fnGetObjectDataFn( mSource ) - { - if ( $.isPlainObject( mSource ) ) - { - /* Build an object of get functions, and wrap them in a single call */ - var o = {}; - $.each( mSource, function (key, val) { - if ( val ) { - o[key] = _fnGetObjectDataFn( val ); - } - } ); - - return function (data, type, row, meta) { - var t = o[type] || o._; - return t !== undefined ? - t(data, type, row, meta) : - data; - }; - } - else if ( mSource === null ) - { - /* Give an empty string for rendering / sorting etc */ - return function (data) { // type, row and meta also passed, but not used - return data; - }; - } - else if ( typeof mSource === 'function' ) - { - return function (data, type, row, meta) { - return mSource( data, type, row, meta ); - }; - } - else if ( typeof mSource === 'string' && (mSource.indexOf('.') !== -1 || - mSource.indexOf('[') !== -1 || mSource.indexOf('(') !== -1) ) - { - /* If there is a . in the source string then the data source is in a - * nested object so we loop over the data for each level to get the next - * level down. On each loop we test for undefined, and if found immediately - * return. This allows entire objects to be missing and sDefaultContent to - * be used if defined, rather than throwing an error - */ - var fetchData = function (data, type, src) { - var arrayNotation, funcNotation, out, innerSrc; - - if ( src !== "" ) - { - var a = _fnSplitObjNotation( src ); - - for ( var i=0, iLen=a.length ; i<iLen ; i++ ) - { - // Check if we are dealing with special notation - arrayNotation = a[i].match(__reArray); - funcNotation = a[i].match(__reFn); - - if ( arrayNotation ) - { - // Array notation - a[i] = a[i].replace(__reArray, ''); - - // Condition allows simply [] to be passed in - if ( a[i] !== "" ) { - data = data[ a[i] ]; - } - out = []; - - // Get the remainder of the nested object to get - a.splice( 0, i+1 ); - innerSrc = a.join('.'); - - // Traverse each entry in the array getting the properties requested - if ( Array.isArray( data ) ) { - for ( var j=0, jLen=data.length ; j<jLen ; j++ ) { - out.push( fetchData( data[j], type, innerSrc ) ); - } - } - - // If a string is given in between the array notation indicators, that - // is used to join the strings together, otherwise an array is returned - var join = arrayNotation[0].substring(1, arrayNotation[0].length-1); - data = (join==="") ? out : out.join(join); - - // The inner call to fetchData has already traversed through the remainder - // of the source requested, so we exit from the loop - break; - } - else if ( funcNotation ) - { - // Function call - a[i] = a[i].replace(__reFn, ''); - data = data[ a[i] ](); - continue; - } - - if ( data === null || data[ a[i] ] === undefined ) - { - return undefined; - } - data = data[ a[i] ]; - } - } - - return data; - }; - - return function (data, type) { // row and meta also passed, but not used - return fetchData( data, type, mSource ); - }; - } - else - { - /* Array or flat object mapping */ - return function (data, type) { // row and meta also passed, but not used - return data[mSource]; - }; - } - } + var _fnGetObjectDataFn = DataTable.util.get; /** @@ -2747,122 +2870,7 @@ * @returns {function} Data set function * @memberof DataTable#oApi */ - function _fnSetObjectDataFn( mSource ) - { - if ( $.isPlainObject( mSource ) ) - { - /* Unlike get, only the underscore (global) option is used for for - * setting data since we don't know the type here. This is why an object - * option is not documented for `mData` (which is read/write), but it is - * for `mRender` which is read only. - */ - return _fnSetObjectDataFn( mSource._ ); - } - else if ( mSource === null ) - { - /* Nothing to do when the data source is null */ - return function () {}; - } - else if ( typeof mSource === 'function' ) - { - return function (data, val, meta) { - mSource( data, 'set', val, meta ); - }; - } - else if ( typeof mSource === 'string' && (mSource.indexOf('.') !== -1 || - mSource.indexOf('[') !== -1 || mSource.indexOf('(') !== -1) ) - { - /* Like the get, we need to get data from a nested object */ - var setData = function (data, val, src) { - var a = _fnSplitObjNotation( src ), b; - var aLast = a[a.length-1]; - var arrayNotation, funcNotation, o, innerSrc; - - for ( var i=0, iLen=a.length-1 ; i<iLen ; i++ ) - { - // Protect against prototype pollution - if (a[i] === '__proto__' || a[i] === 'constructor') { - throw new Error('Cannot set prototype values'); - } - - // Check if we are dealing with an array notation request - arrayNotation = a[i].match(__reArray); - funcNotation = a[i].match(__reFn); - - if ( arrayNotation ) - { - a[i] = a[i].replace(__reArray, ''); - data[ a[i] ] = []; - - // Get the remainder of the nested object to set so we can recurse - b = a.slice(); - b.splice( 0, i+1 ); - innerSrc = b.join('.'); - - // Traverse each entry in the array setting the properties requested - if ( Array.isArray( val ) ) - { - for ( var j=0, jLen=val.length ; j<jLen ; j++ ) - { - o = {}; - setData( o, val[j], innerSrc ); - data[ a[i] ].push( o ); - } - } - else - { - // We've been asked to save data to an array, but it - // isn't array data to be saved. Best that can be done - // is to just save the value. - data[ a[i] ] = val; - } - - // The inner call to setData has already traversed through the remainder - // of the source and has set the data, thus we can exit here - return; - } - else if ( funcNotation ) - { - // Function call - a[i] = a[i].replace(__reFn, ''); - data = data[ a[i] ]( val ); - } - - // If the nested object doesn't currently exist - since we are - // trying to set the value - create it - if ( data[ a[i] ] === null || data[ a[i] ] === undefined ) - { - data[ a[i] ] = {}; - } - data = data[ a[i] ]; - } - - // Last item in the input - i.e, the actual set - if ( aLast.match(__reFn ) ) - { - // Function call - data = data[ aLast.replace(__reFn, '') ]( val ); - } - else - { - // If array notation is used, we just want to strip it and use the property name - // and assign the value. If it isn't used, then we get the result we want anyway - data[ aLast.replace(__reArray, '') ] = val; - } - }; - - return function (data, val) { // meta is also passed in, but not used - return setData( data, val, mSource ); - }; - } - else - { - /* Array or flat object mapping */ - return function (data, val) { // meta is also passed in, but not used - data[mSource] = val; - }; - } - } + var _fnSetObjectDataFn = DataTable.util.set; /** @@ -3291,9 +3299,6 @@ if ( createHeader ) { _fnDetectHeader( oSettings.aoHeader, thead ); } - - /* ARIA role for the rows */ - $(thead).children('tr').attr('role', 'row'); /* Deal with the footer - add classes if required */ $(thead).children('tr').children('th, td').addClass( classes.sHeaderTH ); @@ -3912,6 +3917,22 @@ var ajax = oSettings.ajax; var instance = oSettings.oInstance; var callback = function ( json ) { + var status = oSettings.jqXhr + ? oSettings.jqXhr.status + : null; + + if ( json === null || (typeof status === 'number' && status == 204 ) ) { + json = {}; + _fnAjaxDataSrc( oSettings, json, [] ); + } + + var error = json.error || json.sError; + if ( error ) { + _fnLog( oSettings, 0, error ); + } + + oSettings.json = json; + _fnCallbackFire( oSettings, null, 'xhr', [oSettings, json, oSettings.jqXHR] ); fn( json ); }; @@ -3936,15 +3957,7 @@ var baseAjax = { "data": data, - "success": function (json) { - var error = json.error || json.sError; - if ( error ) { - _fnLog( oSettings, 0, error ); - } - - oSettings.json = json; - callback( json ); - }, + "success": callback, "dataType": "json", "cache": false, "type": oSettings.sServerMethod, @@ -4166,6 +4179,11 @@ settings.iDraw = draw * 1; } + // No data in returned object, so rather than an array, we show an empty table + if ( ! data ) { + data = []; + } + _fnClearTable( settings ); settings._iRecordsTotal = parseInt(recordsTotal, 10); settings._iRecordsDisplay = parseInt(recordsFiltered, 10); @@ -4193,21 +4211,26 @@ * @param {object} json Data source object / array from the server * @return {array} Array of data to use */ - function _fnAjaxDataSrc ( oSettings, json ) - { + function _fnAjaxDataSrc ( oSettings, json, write ) + { var dataSrc = $.isPlainObject( oSettings.ajax ) && oSettings.ajax.dataSrc !== undefined ? oSettings.ajax.dataSrc : oSettings.sAjaxDataProp; // Compatibility with 1.9-. - // Compatibility with 1.9-. In order to read from aaData, check if the - // default has been changed, if not, check for aaData - if ( dataSrc === 'data' ) { - return json.aaData || json[dataSrc]; + if ( ! write ) { + if ( dataSrc === 'data' ) { + // If the default, then we still want to support the old style, and safely ignore + // it if possible + return json.aaData || json[dataSrc]; + } + + return dataSrc !== "" ? + _fnGetObjectDataFn( dataSrc )( json ) : + json; } - return dataSrc !== "" ? - _fnGetObjectDataFn( dataSrc )( json ) : - json; + // set + _fnSetObjectDataFn( dataSrc )( json, write ); } /** @@ -4236,18 +4259,21 @@ } ) .append( $('<label/>' ).append( str ) ); - var searchFn = function() { + var searchFn = function(event) { /* Update all other filter input elements for the new display */ var n = features.f; var val = !this.value ? "" : this.value; // mental IE8 fix :-( - + if(previousSearch.return && event.key !== "Enter") { + return; + } /* Now do the filter */ if ( val != previousSearch.sSearch ) { _fnFilterComplete( settings, { "sSearch": val, "bRegex": previousSearch.bRegex, "bSmart": previousSearch.bSmart , - "bCaseInsensitive": previousSearch.bCaseInsensitive + "bCaseInsensitive": previousSearch.bCaseInsensitive, + "return": previousSearch.return } ); // Need to redraw, without resorting @@ -4276,7 +4302,7 @@ // on the clear icon (Edge bug 17584515). This is safe in other browsers as `searchFn` // checks the value to see if it has changed. In other browsers it won't have. setTimeout( function () { - searchFn.call(jqFilter[0]); + searchFn.call(jqFilter[0], e); }, 10); } ) .on( 'keypress.DT', function(e) { @@ -4322,6 +4348,7 @@ oPrevSearch.bRegex = oFilter.bRegex; oPrevSearch.bSmart = oFilter.bSmart; oPrevSearch.bCaseInsensitive = oFilter.bCaseInsensitive; + oPrevSearch.return = oFilter.return; }; var fnRegex = function ( o ) { // Backwards compatibility with the bEscapeRegex option @@ -4336,7 +4363,7 @@ if ( _fnDataSource( oSettings ) != 'ssp' ) { /* Global filter */ - _fnFilter( oSettings, oInput.sSearch, iForce, fnRegex(oInput), oInput.bSmart, oInput.bCaseInsensitive ); + _fnFilter( oSettings, oInput.sSearch, iForce, fnRegex(oInput), oInput.bSmart, oInput.bCaseInsensitive, oInput.return ); fnSaveFilter( oInput ); /* Now do the individual column filter */ @@ -4399,7 +4426,7 @@ * @param {int} iColumn column to filter * @param {bool} bRegex treat search string as a regular expression or not * @param {bool} bSmart use smart filtering or not - * @param {bool} bCaseInsensitive Do case insenstive matching or not + * @param {bool} bCaseInsensitive Do case insensitive matching or not * @memberof DataTable#oApi */ function _fnFilterColumn ( settings, searchStr, colIdx, regex, smart, caseInsensitive ) @@ -4432,7 +4459,7 @@ * @param {int} force optional - force a research of the master array (1) or not (undefined or 0) * @param {bool} regex treat as a regular expression or not * @param {bool} smart perform smart filtering or not - * @param {bool} caseInsensitive Do case insenstive matching or not + * @param {bool} caseInsensitive Do case insensitive matching or not * @memberof DataTable#oApi */ function _fnFilter( settings, input, force, regex, smart, caseInsensitive ) @@ -5093,9 +5120,6 @@ { var table = $(settings.nTable); - // Add the ARIA grid role to the table - table.attr( 'role', 'grid' ); - // Scrolling from here on in var scroll = settings.oScroll; @@ -5383,17 +5407,17 @@ // Read all widths in next pass _fnApplyToChildren( function(nSizer) { + var style = window.getComputedStyle ? + window.getComputedStyle(nSizer).width : + _fnStringToCss( $(nSizer).width() ); + headerContent.push( nSizer.innerHTML ); - headerWidths.push( _fnStringToCss( $(nSizer).css('width') ) ); + headerWidths.push( style ); }, headerSrcEls ); // Apply all widths in final pass _fnApplyToChildren( function(nToSize, i) { - // Only apply widths to the DataTables detected header cells - this - // prevents complex headers from having contradictory sizes applied - if ( $.inArray( nToSize, dtHeaderCells ) !== -1 ) { - nToSize.style.width = headerWidths[i]; - } + nToSize.style.width = headerWidths[i]; }, headerTrgEls ); $(headerSrcEls).height(0); @@ -6350,11 +6374,6 @@ */ function _fnSaveState ( settings ) { - if ( !settings.oFeatures.bStateSave || settings.bDestroying ) - { - return; - } - /* Store the interesting variables */ var state = { time: +new Date(), @@ -6370,10 +6389,13 @@ } ) }; - _fnCallbackFire( settings, "aoStateSaveParams", 'stateSaveParams', [settings, state] ); - settings.oSavedState = state; - settings.fnStateSaveCallback.call( settings.oInstance, settings, state ); + _fnCallbackFire( settings, "aoStateSaveParams", 'stateSaveParams', [settings, state] ); + + if ( settings.oFeatures.bStateSave && !settings.bDestroying ) + { + settings.fnStateSaveCallback.call( settings.oInstance, settings, state ); + } } @@ -7872,7 +7894,7 @@ _range( 0, displayMaster.length ); } else if ( page == 'current' ) { - // Current page implies that order=current and fitler=applied, since it is + // Current page implies that order=current and filter=applied, since it is // fairly senseless otherwise, regardless of what order and search actually // are for ( i=settings._iDisplayStart, ien=settings.fnDisplayEnd() ; i<ien ; i++ ) { @@ -8253,6 +8275,24 @@ } ); + $(document).on('plugin-init.dt', function (e, context) { + var api = new _Api( context ); + api.on( 'stateSaveParams', function ( e, settings, data ) { + var indexes = api.rows().iterator( 'row', function ( settings, idx ) { + return settings.aoData[idx]._detailsShow ? idx : undefined; + }); + + data.childRows = api.rows( indexes ).ids( true ).toArray(); + }) + + var loaded = api.state.loaded(); + + if ( loaded && loaded.childRows ) { + api.rows( loaded.childRows ).every( function () { + _fnCallbackFire( context, null, 'requestChild', [ this ] ) + }) + } + }) var __details_add = function ( ctx, row, data, klass ) { @@ -8311,6 +8351,8 @@ row._detailsShow = undefined; row._details = undefined; + $( row.nTr ).removeClass( 'dt-hasChild' ); + _fnSaveState( ctx[0] ); } } }; @@ -8327,12 +8369,17 @@ if ( show ) { row._details.insertAfter( row.nTr ); + $( row.nTr ).addClass( 'dt-hasChild' ); } else { row._details.detach(); + $( row.nTr ).removeClass( 'dt-hasChild' ); } + _fnCallbackFire( ctx[0], null, 'childRow', [ show, api.row( api[0] ) ] ) + __details_events( ctx[0] ); + _fnSaveState( ctx[0] ); } } }; @@ -9543,7 +9590,7 @@ * @type string * @default Version number */ - DataTable.version = "1.10.25"; + DataTable.version = "1.11.2"; /** * Private data store, containing all of the settings objects that are @@ -9603,7 +9650,15 @@ * @type boolean * @default true */ - "bSmart": true + "bSmart": true, + + /** + * Flag to indicate if DataTables should only trigger a search when + * the return key is pressed. + * @type boolean + * @default false + */ + "return": false }; @@ -12499,7 +12554,7 @@ * "data": function ( source, type, val ) { * if (type === 'set') { * source.price = val; - * // Store the computed dislay and filter values for efficiency + * // Store the computed display and filter values for efficiency * source.price_display = val=="" ? "" : "$"+numberFormat(val); * source.price_filter = val=="" ? "" : "$"+numberFormat(val)+" "+val; * return; @@ -13048,7 +13103,7 @@ * Delay the creation of TR and TD elements until they are actually * needed by a driven page draw. This can give a significant speed * increase for Ajax source and Javascript source data, but makes no - * difference at all fro DOM and server-side processing tables. + * difference at all for DOM and server-side processing tables. * Note that this parameter will be set by the initialisation routine. To * set a default use {@link DataTable.defaults}. * @type boolean @@ -13960,7 +14015,7 @@ * * @type string */ - build:"bs5/dt-1.10.25", + build:"bs5/dt-1.11.2", /** @@ -14406,7 +14461,7 @@ // // Depreciated - // The following properties are retained for backwards compatiblity only. + // The following properties are retained for backwards compatibility only. // The should not be used in new projects and will be removed in a future // version // @@ -15226,170 +15281,7 @@ $.fn.DataTable[ prop ] = val; } ); - - // Information about events fired by DataTables - for documentation. - /** - * Draw event, fired whenever the table is redrawn on the page, at the same - * point as fnDrawCallback. This may be useful for binding events or - * performing calculations when the table is altered at all. - * @name DataTable#draw.dt - * @event - * @param {event} e jQuery event object - * @param {object} o DataTables settings object {@link DataTable.models.oSettings} - */ - - /** - * Search event, fired when the searching applied to the table (using the - * built-in global search, or column filters) is altered. - * @name DataTable#search.dt - * @event - * @param {event} e jQuery event object - * @param {object} o DataTables settings object {@link DataTable.models.oSettings} - */ - - /** - * Page change event, fired when the paging of the table is altered. - * @name DataTable#page.dt - * @event - * @param {event} e jQuery event object - * @param {object} o DataTables settings object {@link DataTable.models.oSettings} - */ - - /** - * Order event, fired when the ordering applied to the table is altered. - * @name DataTable#order.dt - * @event - * @param {event} e jQuery event object - * @param {object} o DataTables settings object {@link DataTable.models.oSettings} - */ - - /** - * DataTables initialisation complete event, fired when the table is fully - * drawn, including Ajax data loaded, if Ajax data is required. - * @name DataTable#init.dt - * @event - * @param {event} e jQuery event object - * @param {object} oSettings DataTables settings object - * @param {object} json The JSON object request from the server - only - * present if client-side Ajax sourced data is used</li></ol> - */ - - /** - * State save event, fired when the table has changed state a new state save - * is required. This event allows modification of the state saving object - * prior to actually doing the save, including addition or other state - * properties (for plug-ins) or modification of a DataTables core property. - * @name DataTable#stateSaveParams.dt - * @event - * @param {event} e jQuery event object - * @param {object} oSettings DataTables settings object - * @param {object} json The state information to be saved - */ - - /** - * State load event, fired when the table is loading state from the stored - * data, but prior to the settings object being modified by the saved state - * - allowing modification of the saved state is required or loading of - * state for a plug-in. - * @name DataTable#stateLoadParams.dt - * @event - * @param {event} e jQuery event object - * @param {object} oSettings DataTables settings object - * @param {object} json The saved state information - */ - - /** - * State loaded event, fired when state has been loaded from stored data and - * the settings object has been modified by the loaded data. - * @name DataTable#stateLoaded.dt - * @event - * @param {event} e jQuery event object - * @param {object} oSettings DataTables settings object - * @param {object} json The saved state information - */ - - /** - * Processing event, fired when DataTables is doing some kind of processing - * (be it, order, search or anything else). It can be used to indicate to - * the end user that there is something happening, or that something has - * finished. - * @name DataTable#processing.dt - * @event - * @param {event} e jQuery event object - * @param {object} oSettings DataTables settings object - * @param {boolean} bShow Flag for if DataTables is doing processing or not - */ - - /** - * Ajax (XHR) event, fired whenever an Ajax request is completed from a - * request to made to the server for new data. This event is called before - * DataTables processed the returned data, so it can also be used to pre- - * process the data returned from the server, if needed. - * - * Note that this trigger is called in `fnServerData`, if you override - * `fnServerData` and which to use this event, you need to trigger it in you - * success function. - * @name DataTable#xhr.dt - * @event - * @param {event} e jQuery event object - * @param {object} o DataTables settings object {@link DataTable.models.oSettings} - * @param {object} json JSON returned from the server - * - * @example - * // Use a custom property returned from the server in another DOM element - * $('#table').dataTable().on('xhr.dt', function (e, settings, json) { - * $('#status').html( json.status ); - * } ); - * - * @example - * // Pre-process the data returned from the server - * $('#table').dataTable().on('xhr.dt', function (e, settings, json) { - * for ( var i=0, ien=json.aaData.length ; i<ien ; i++ ) { - * json.aaData[i].sum = json.aaData[i].one + json.aaData[i].two; - * } - * // Note no return - manipulate the data directly in the JSON object. - * } ); - */ - - /** - * Destroy event, fired when the DataTable is destroyed by calling fnDestroy - * or passing the bDestroy:true parameter in the initialisation object. This - * can be used to remove bound events, added DOM nodes, etc. - * @name DataTable#destroy.dt - * @event - * @param {event} e jQuery event object - * @param {object} o DataTables settings object {@link DataTable.models.oSettings} - */ - - /** - * Page length change event, fired when number of records to show on each - * page (the length) is changed. - * @name DataTable#length.dt - * @event - * @param {event} e jQuery event object - * @param {object} o DataTables settings object {@link DataTable.models.oSettings} - * @param {integer} len New length - */ - - /** - * Column sizing has changed. - * @name DataTable#column-sizing.dt - * @event - * @param {event} e jQuery event object - * @param {object} o DataTables settings object {@link DataTable.models.oSettings} - */ - - /** - * Column visibility has changed. - * @name DataTable#column-visibility.dt - * @event - * @param {event} e jQuery event object - * @param {object} o DataTables settings object {@link DataTable.models.oSettings} - * @param {int} column Column index - * @param {bool} vis `false` if column now hidden, or `true` if visible - */ - - return $.fn.dataTable; + return DataTable; })); diff --git a/src/static/templates/admin/settings.hbs b/src/static/templates/admin/settings.hbs @@ -12,9 +12,7 @@ {{#each config}} {{#if groupdoc}} <div class="card bg-light mb-3"> - <div class="card-header" role="button" data-bs-toggle="collapse" data-bs-target="#g_{{group}}"> - <button type="button" class="btn btn-link text-decoration-none collapsed" data-bs-toggle="collapse" data-bs-target="#g_{{group}}">{{groupdoc}}</button> - </div> + <button id="b_{{group}}" type="button" class="card-header text-start btn btn-link text-decoration-none" aria-expanded="false" aria-controls="g_{{group}}" data-bs-toggle="collapse" data-bs-target="#g_{{group}}">{{groupdoc}}</button> <div id="g_{{group}}" class="card-body collapse"> {{#each elements}} {{#if editable}} @@ -61,10 +59,8 @@ {{/each}} <div class="card bg-light mb-3"> - <div class="card-header" role="button" data-bs-toggle="collapse" data-bs-target="#g_readonly"> - <button type="button" class="btn btn-link text-decoration-none collapsed" data-bs-toggle="collapse" data-bs-target="#g_readonly">Read-Only Config</button> - </div> - + <button id="b_readonly" type="button" class="card-header text-start btn btn-link text-decoration-none" aria-expanded="false" aria-controls="g_readonly" + data-bs-toggle="collapse" data-bs-target="#g_readonly">Read-Only Config</button> <div id="g_readonly" class="card-body collapse"> <div class="small mb-3"> NOTE: These options can't be modified in the editor because they would require the server @@ -109,9 +105,8 @@ {{#if can_backup}} <div class="card bg-light mb-3"> - <div class="card-header" role="button" data-bs-toggle="collapse" data-bs-target="#g_database"> - <button type="button" class="btn btn-link text-decoration-none collapsed" data-bs-toggle="collapse" data-bs-target="#g_database">Backup Database</button> - </div> + <button id="b_database" type="button" class="card-header text-start btn btn-link text-decoration-none" aria-expanded="false" aria-controls="g_database" + data-bs-toggle="collapse" data-bs-target="#g_database">Backup Database</button> <div id="g_database" class="card-body collapse"> <div class="small mb-3"> WARNING: This function only creates a backup copy of the SQLite database.