Files
PyRIGS/pipeline/source_assets/js/helpers.js
Joe Banks a29384a314 Bootstrap 5 & remove jQuery (#661)
* 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
2026-10-02 16:28:11 +01:00

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