mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-10-05 12:05:38 +00:00
* build: move frontend assets to Bootstrap 5.3 and Tom Select * feat: migrate templates and scripts to Bootstrap 5, dropping jQuery Templates - Rename Bootstrap 4 classes and data attributes (data-bs-*, ms-/me-/ps-/pe-, text-start/end, form-group -> mb-3, input-group wrappers removed, badges, custom controls -> form-check/form-select, media -> d-flex, btn-close, ...) - Dark mode now uses Bootstrap 5.3 colour modes (data-bs-theme on <html>), set from <head> to avoid a flash of the light theme - Remove the Bootstrap 4 /bootstrap/ demo page - Contextual table classes (table-success etc.) still work as plain backgrounds for the rigboard key JavaScript - Remove jQuery: src.js/interaction.js and every inline template script are rewritten in plain JS on top of small helpers (helpers.js, bundled with Bootstrap into jpop.js so inline scripts can use them) - Replace bootstrap-select/ajax-bootstrap-select with Tom Select. The remote search now sends the `q` parameter and reads `text`, which is what the API actually uses (the old picker sent `term` and read `label`) - HTML fragments are fetched with an X-Requested-With header so views keep rendering their modal variants, and fetched scripts are re-executed - Remove the Bootstrap 4 plugin script includes * fix: stop rigboard header buttons stretching to full width under Bootstrap 5 * fix: restore dark-mode brand colours and contextual table colours under Bootstrap 5 * fix: brighten dark-mode links and make coloured card outlines visible * fix: calendar layout and grid borders, navbar search button width, guard missing modal links * fix: readable text in dark-mode Tom Select inputs and placeholders, restore form-control column widths * build: split the Dockerfile into cached stages with prod and dev targets * deploy: make compose.yml development-only with live reload, and document it * fix: keep badge text colour for links inside badges * fix: restore table-bordered grid lines in dark mode * fix: include Bootstrap's progress styles and keep progress labels readable
139 lines
4.6 KiB
JavaScript
139 lines
4.6 KiB
JavaScript
// Helpers used by inline template scripts, so this is loaded in <head> (bundled with Bootstrap as jpop.js)
|
|
// Run fn once the DOM is ready
|
|
function ready(fn) {
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', fn);
|
|
} else {
|
|
fn();
|
|
}
|
|
}
|
|
|
|
// Delegated event handler: on(document, 'click', '.selector', function (e) { this === matched element })
|
|
function on(root, type, selector, handler) {
|
|
root.addEventListener(type, function (e) {
|
|
var target = e.target instanceof Element ? e.target.closest(selector) : null;
|
|
if (target && root.contains(target)) {
|
|
handler.call(target, e);
|
|
}
|
|
});
|
|
}
|
|
|
|
// fetch() for endpoints that return HTML fragments: the X-Requested-With header makes the server
|
|
// render the ajax (modal) variant of a page, as jQuery's .load()/.post() used to
|
|
function ajaxFetch(url, options) {
|
|
options = Object.assign({credentials: 'same-origin'}, options);
|
|
options.headers = Object.assign({'X-Requested-With': 'XMLHttpRequest'}, options.headers);
|
|
return fetch(url, options);
|
|
}
|
|
|
|
// Replace the contents of an element with an HTML string, executing any <script>s it contains
|
|
function setHtml(element, html) {
|
|
element.innerHTML = html;
|
|
element.querySelectorAll('script').forEach(function (old) {
|
|
var script = document.createElement('script');
|
|
Array.from(old.attributes).forEach(function (attr) { script.setAttribute(attr.name, attr.value); });
|
|
script.text = old.text;
|
|
old.replaceWith(script);
|
|
});
|
|
initTooltips(element);
|
|
initPopovers(element);
|
|
if (typeof initSelects === 'function') {
|
|
initSelects(element);
|
|
}
|
|
}
|
|
|
|
function initTooltips(root) {
|
|
(root || document).querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function (el) {
|
|
bootstrap.Tooltip.getOrCreateInstance(el);
|
|
});
|
|
}
|
|
|
|
// Popovers may contain <ins>/<del> (used by the activity feed's diffs)
|
|
function popoverOptions() {
|
|
var allowList = bootstrap.Tooltip.Default.allowList;
|
|
allowList.ins = [];
|
|
allowList.del = [];
|
|
return {allowList: allowList};
|
|
}
|
|
|
|
function initPopovers(root) {
|
|
(root || document).querySelectorAll('[data-bs-toggle="popover"]').forEach(function (el) {
|
|
bootstrap.Popover.getOrCreateInstance(el, popoverOptions());
|
|
});
|
|
}
|
|
|
|
// Load a URL into the shared modal and show it
|
|
function loadModal(url) {
|
|
var modal = document.getElementById('modal');
|
|
return ajaxFetch(url)
|
|
.then(function (response) { return response.text(); })
|
|
.then(function (html) {
|
|
setHtml(modal, html);
|
|
showModal(modal);
|
|
});
|
|
}
|
|
|
|
function showModal(selectorOrElement) {
|
|
var el = typeof selectorOrElement === 'string' ? document.querySelector(selectorOrElement) : selectorOrElement;
|
|
bootstrap.Modal.getOrCreateInstance(el).show();
|
|
}
|
|
|
|
function hideModal(selectorOrElement) {
|
|
var el = typeof selectorOrElement === 'string' ? document.querySelector(selectorOrElement) : selectorOrElement;
|
|
var modal = bootstrap.Modal.getInstance(el);
|
|
if (modal) {
|
|
modal.hide();
|
|
}
|
|
}
|
|
|
|
// Animated show/hide for form sections (replacing jQuery's slideDown/slideUp)
|
|
function resetSlide(el) {
|
|
window.clearTimeout(el._slideTimer);
|
|
['height', 'overflow', 'transition', 'opacity'].forEach(function (p) { el.style.removeProperty(p); });
|
|
}
|
|
|
|
function slideUp(el, duration) {
|
|
resetSlide(el);
|
|
if (duration === 0 || isHidden(el)) {
|
|
el.style.display = 'none';
|
|
return;
|
|
}
|
|
el.style.overflow = 'hidden';
|
|
el.style.height = el.offsetHeight + 'px';
|
|
el.offsetHeight; // force reflow
|
|
el.style.transition = 'height ' + duration + 'ms ease, opacity ' + duration + 'ms ease';
|
|
el.style.height = '0';
|
|
el.style.opacity = '0';
|
|
el._slideTimer = window.setTimeout(function () {
|
|
resetSlide(el);
|
|
el.style.display = 'none';
|
|
}, duration);
|
|
}
|
|
|
|
function slideDown(el, duration) {
|
|
resetSlide(el);
|
|
el.hidden = false;
|
|
el.style.removeProperty('display');
|
|
if (isHidden(el)) {
|
|
el.style.display = 'block'; // not hidden by an inline style, so fall back to a block element
|
|
}
|
|
if (duration === 0) {
|
|
return;
|
|
}
|
|
var height = el.scrollHeight;
|
|
el.style.overflow = 'hidden';
|
|
el.style.height = '0';
|
|
el.style.opacity = '0';
|
|
el.offsetHeight; // force reflow
|
|
el.style.transition = 'height ' + duration + 'ms ease, opacity ' + duration + 'ms ease';
|
|
el.style.height = height + 'px';
|
|
el.style.opacity = '1';
|
|
el._slideTimer = window.setTimeout(function () {
|
|
resetSlide(el);
|
|
}, duration);
|
|
}
|
|
|
|
function isHidden(el) {
|
|
return getComputedStyle(el).display === 'none';
|
|
}
|