Update Font, Migrated away from JQUERY dependent libs: toastr to bootstrap5 toasts, daterangepicker to flatpickr, select2 to Tom Select, InputMask to IMask

This commit is contained in:
johnnyq
2026-08-14 17:08:40 -04:00
parent 0fe6d51bdb
commit dc6b191eed
377 changed files with 1783 additions and 3769 deletions

165
js/app.js
View File

@@ -9,9 +9,10 @@ $(document).ready(function() {
$("#alert").slideUp(500);
});
// Initialize Select2 Elements
$('.select2').select2({
theme: 'bootstrap-5',
// Initialize Tom Select (replaces Select2). Every instance is reachable
// afterwards as element.tomselect, which is how the helpers below reach it.
document.querySelectorAll('.select2').forEach(function (el) {
initTomSelect(el);
});
// Initialize TinyMCE
@@ -385,11 +386,45 @@ $(document).ready(function() {
// DateTime
document.querySelectorAll('.datetimepicker').forEach(function (el) {
new tempusDominus.TempusDominus(el);
if (el._flatpickr) {
return;
}
flatpickr(el, {
enableTime: true,
time_24hr: true,
dateFormat: 'Y-m-d H:i',
allowInput: true
});
});
// Data Input Mask
$('[data-mask]').inputmask();
// IMask replaces jquery.inputmask. Only two aliases were ever used.
document.querySelectorAll('[data-mask]').forEach(function (el) {
if (el.dataset.imaskReady) {
return;
}
el.dataset.imaskReady = '1';
var spec = el.getAttribute('data-inputmask') || '';
if (spec.indexOf('ip') !== -1) {
IMask(el, {
mask: 'a.b.c.d',
blocks: {
a: { mask: IMask.MaskedRange, from: 0, to: 255 },
b: { mask: IMask.MaskedRange, from: 0, to: 255 },
c: { mask: IMask.MaskedRange, from: 0, to: 255 },
d: { mask: IMask.MaskedRange, from: 0, to: 255 }
},
lazy: true
});
} else if (spec.indexOf('mac') !== -1) {
IMask(el, {
mask: 'HH:HH:HH:HH:HH:HH',
definitions: { H: /[0-9a-fA-F]/ },
prepare: function (s) { return s.toUpperCase(); },
lazy: true
});
}
});
// Password reveal. Replaces Show-Hide-Passwords-Bootstrap-4, which has no
// Bootstrap 5 release. Same data-toggle="password" contract as before.
@@ -556,3 +591,123 @@ function flashTooltip(button, message) {
tip.dispose();
}, 1000);
}
/**
* Tom Select integration.
*
* Replaces Select2. Tom Select is vanilla JS and exposes its instance on the
* element as `el.tomselect`, so nothing here needs jQuery.
*
* Select2 concepts and their equivalents, for anyone reading this later:
* $(el).select2({tags:true}) -> create: true
* $(el).val(null).trigger('change') -> el.tomselect.clear()
* $(el).trigger('change.select2') -> el.tomselect.sync() (options replaced)
* $(el).on('select2:select', fn) -> el.tomselect.on('change', fn)
*/
function initTomSelect(el, options) {
if (!el || el.tomselect) {
return el ? el.tomselect : null;
}
var settings = Object.assign({
create: false,
allowEmptyOption: true,
plugins: el.multiple ? ['remove_button'] : [],
placeholder: el.getAttribute('data-placeholder') || undefined
}, options || {});
return new TomSelect(el, settings);
}
/** Re-read the <option> list after it has been replaced server-side. */
function refreshTomSelect(el) {
if (el && el.tomselect) {
el.tomselect.sync();
}
}
/** Clear a selection (single or multiple) without firing a server round-trip. */
function clearTomSelect(el) {
if (el && el.tomselect) {
el.tomselect.clear(true);
}
}
/**
* Set a select's value from code. A plain el.value = x (or jQuery .val()) does
* not repaint a Tom Select widget - the underlying <select> changes but the
* visible control does not. Falls back to a native change event when the
* element was never enhanced.
*/
function setTomSelectValue(el, value) {
if (!el) {
return;
}
if (el.tomselect) {
el.tomselect.setValue(value);
return;
}
el.value = value;
el.dispatchEvent(new Event('change', { bubbles: true }));
}
/**
* Show a Bootstrap toast from JavaScript.
*
* The PHP side renders its own toast markup in includes/inc_alert_feedback.php
* so that user-supplied text never reaches a JS string literal. This helper is
* for toasts raised by client-side code, where the caller controls the text.
*
* Replaces toastr.success() / .error() / .warning() / .info().
*/
function itflowToast(message, type) {
var styles = {
success: 'text-bg-success',
info: 'text-bg-info',
warning: 'text-bg-warning',
alert: 'text-bg-warning',
error: 'text-bg-danger',
danger: 'text-bg-danger'
};
var style = styles[type] || styles.success;
var darkText = type === 'warning' || type === 'alert' || type === 'info';
var container = document.querySelector('.toast-container.itflow-toast-js');
if (!container) {
container = document.createElement('div');
container.className = 'toast-container itflow-toast-js position-fixed top-0 start-50 translate-middle-x p-3';
container.style.zIndex = '1090';
document.body.appendChild(container);
}
var toast = document.createElement('div');
toast.className = 'toast fade align-items-center ' + style + ' border-0';
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
var flex = document.createElement('div');
flex.className = 'd-flex';
var body = document.createElement('div');
body.className = 'toast-body';
body.textContent = message;
var close = document.createElement('button');
close.type = 'button';
close.className = 'btn-close ' + (darkText ? '' : 'btn-close-white') + ' me-2 m-auto';
close.setAttribute('data-bs-dismiss', 'toast');
close.setAttribute('aria-label', 'Close');
flex.appendChild(body);
flex.appendChild(close);
toast.appendChild(flex);
container.appendChild(toast);
toast.addEventListener('hidden.bs.toast', function () {
toast.remove();
});
bootstrap.Toast.getOrCreateInstance(toast, {
animation: true,
autohide: true,
delay: 5000
}).show();
}

View File

@@ -1,108 +1,183 @@
(function ($, window) {
"use strict";
/**
* Date range filter.
*
* Rebuilt on Flatpickr, replacing daterangepicker (jQuery) and moment.
*
* Flatpickr has no preset-ranges feature, so the nine presets daterangepicker
* gave us for free are built here as a panel injected into the calendar via
* the onReady hook. Behaviour is unchanged: picking a preset writes its canned
* value and clears dtf/dtt; picking a custom range writes 'custom' plus both
* dates; either way the form auto-submits.
*
* Date maths is plain Date - weeks start Monday, matching the old isoWeek.
*/
(function () {
'use strict';
$(function () {
var $input = $('#dateFilter');
if (!$input.length) return; // nothing to initialize
var ALL_TIME_START = '1970-01-01';
var ALL_TIME_END = '2099-12-31';
var $canned = $('#canned_date');
var $dtf = $('#dtf');
var $dtt = $('#dtt');
// Default to "All Time" if nothing provided
var hasValues =
($dtf.val() && $dtt.val()) ||
($canned.val() && $canned.val() !== '');
if (!hasValues) {
$canned.val('alltime');
$dtf.val('1970-01-01');
$dtt.val('2099-12-31');
function ymd(d) {
var m = String(d.getMonth() + 1).padStart(2, '0');
var day = String(d.getDate()).padStart(2, '0');
return d.getFullYear() + '-' + m + '-' + day;
}
var initialStart = moment($dtf.val(), "YYYY-MM-DD");
var initialEnd = moment($dtt.val(), "YYYY-MM-DD");
function setDisplay(start, end, label) {
// Special display for All Time
if (
label === 'All Time' ||
(start.format('YYYY-MM-DD') === '1970-01-01' &&
end.format('YYYY-MM-DD') === '2099-12-31')
) {
$input.val('All Time');
} else {
$input.val(start.format('YYYY-MM-DD') + " — " + end.format('YYYY-MM-DD'));
}
function parseYmd(s) {
var parts = String(s || '').split('-');
if (parts.length !== 3) {
return null;
}
var d = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
return isNaN(d.getTime()) ? null : d;
}
var cannedMap = {
"Today": "today",
"Yesterday": "yesterday",
"This Week": "thisweek",
"Last Week": "lastweek",
"This Month": "thismonth",
"Last Month": "lastmonth",
"This Year": "thisyear",
"Last Year": "lastyear",
"All Time": "alltime"
};
function addDays(d, n) {
var c = new Date(d.getTime());
c.setDate(c.getDate() + n);
return c;
}
$input.daterangepicker({
startDate: initialStart,
endDate: initialEnd,
autoUpdateInput: true,
opens: 'left',
locale: {
format: 'YYYY-MM-DD',
firstDay: 1
},
ranges: {
'Today': [moment(), moment()],
'Yesterday': [moment().subtract(1, 'day'), moment().subtract(1, 'day')],
'This Week': [moment().startOf('isoWeek'), moment()],
'Last Week': [
moment().subtract(1, 'week').startOf('isoWeek'),
moment().subtract(1, 'week').endOf('isoWeek')
],
'This Month': [moment().startOf('month'), moment()],
'Last Month': [
moment().subtract(1, 'month').startOf('month'),
moment().subtract(1, 'month').endOf('month')
],
'This Year': [moment().startOf('year'), moment()],
'Last Year': [
moment().subtract(1, 'year').startOf('year'),
moment().subtract(1, 'year').endOf('year')
],
'All Time': [
moment('1970-01-01', 'YYYY-MM-DD'),
moment('2099-12-31', 'YYYY-MM-DD')
]
}
}, setDisplay);
/** Monday-based start of week, matching moment's isoWeek. */
function startOfWeek(d) {
var c = new Date(d.getTime());
var dow = (c.getDay() + 6) % 7;
return addDays(c, -dow);
}
// Show initial label
setDisplay(initialStart, initialEnd);
function buildRanges() {
var now = new Date();
var lastWeekStart = addDays(startOfWeek(now), -7);
$input.on('apply.daterangepicker', function (ev, picker) {
var label = picker.chosenLabel || '';
var canned = cannedMap[label];
return [
{ label: 'Today', canned: 'today', start: now, end: now },
{ label: 'Yesterday', canned: 'yesterday',
start: addDays(now, -1), end: addDays(now, -1) },
{ label: 'This Week', canned: 'thisweek',
start: startOfWeek(now), end: now },
{ label: 'Last Week', canned: 'lastweek',
start: lastWeekStart, end: addDays(lastWeekStart, 6) },
{ label: 'This Month', canned: 'thismonth',
start: new Date(now.getFullYear(), now.getMonth(), 1), end: now },
{ label: 'Last Month', canned: 'lastmonth',
start: new Date(now.getFullYear(), now.getMonth() - 1, 1),
end: new Date(now.getFullYear(), now.getMonth(), 0) },
{ label: 'This Year', canned: 'thisyear',
start: new Date(now.getFullYear(), 0, 1), end: now },
{ label: 'Last Year', canned: 'lastyear',
start: new Date(now.getFullYear() - 1, 0, 1),
end: new Date(now.getFullYear() - 1, 11, 31) },
{ label: 'All Time', canned: 'alltime',
start: parseYmd(ALL_TIME_START), end: parseYmd(ALL_TIME_END) }
];
}
if (canned) {
$canned.val(canned);
$dtf.val('');
$dtt.val('');
} else {
$canned.val('custom');
$dtf.val(picker.startDate.format('YYYY-MM-DD'));
$dtt.val(picker.endDate.format('YYYY-MM-DD'));
}
document.addEventListener('DOMContentLoaded', function () {
var input = document.getElementById('dateFilter');
if (!input || input._flatpickr) {
return;
}
setDisplay(picker.startDate, picker.endDate, label);
var canned = document.getElementById('canned_date');
var dtf = document.getElementById('dtf');
var dtt = document.getElementById('dtt');
if (!canned || !dtf || !dtt) {
return;
}
// Auto-submit form
this.form.submit();
// Default to All Time when the page arrives with nothing set
if (!((dtf.value && dtt.value) || canned.value)) {
canned.value = 'alltime';
dtf.value = ALL_TIME_START;
dtt.value = ALL_TIME_END;
}
var ranges = buildRanges();
var submitting = false;
var presetClicked = false;
function setDisplay(start, end, label) {
if (label === 'All Time' ||
(ymd(start) === ALL_TIME_START && ymd(end) === ALL_TIME_END)) {
input.value = 'All Time';
} else {
input.value = ymd(start) + ' \u2014 ' + ymd(end);
}
}
function apply(start, end, range) {
if (submitting) {
return;
}
submitting = true;
if (range) {
canned.value = range.canned;
dtf.value = '';
dtt.value = '';
} else {
canned.value = 'custom';
dtf.value = ymd(start);
dtt.value = ymd(end);
}
setDisplay(start, end, range ? range.label : undefined);
if (input.form) {
input.form.submit();
}
}
var initialStart = parseYmd(dtf.value) || parseYmd(ALL_TIME_START);
var initialEnd = parseYmd(dtt.value) || parseYmd(ALL_TIME_END);
var initialKey = ymd(initialStart) + '|' + ymd(initialEnd);
flatpickr(input, {
mode: 'range',
dateFormat: 'Y-m-d',
defaultDate: [initialStart, initialEnd],
locale: { firstDayOfWeek: 1 },
allowInput: false,
onReady: function (selectedDates, dateStr, instance) {
// The preset panel daterangepicker used to provide
var panel = document.createElement('div');
panel.className = 'itflow-fp-ranges';
ranges.forEach(function (r) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'itflow-fp-range';
btn.textContent = r.label;
btn.addEventListener('click', function () {
// close() fires onClose synchronously - flag it first
// or onClose applies the stale range as a custom one
presetClicked = true;
instance.close();
apply(r.start, r.end, r);
});
panel.appendChild(btn);
});
instance.calendarContainer.classList.add('itflow-fp-with-ranges');
instance.calendarContainer.appendChild(panel);
},
onClose: function (selectedDates, dateStr, instance) {
// A preset closed the calendar itself; its handler applies.
if (presetClicked) {
presetClicked = false;
return;
}
// Abandoned half-selection - put the box back how it was.
if (selectedDates.length !== 2) {
instance.setDate([initialStart, initialEnd], false);
setDisplay(initialStart, initialEnd);
return;
}
// Dismissed without actually changing the range - daterangepicker
// only applied on its Apply button, so don't resubmit here.
if (ymd(selectedDates[0]) + '|' + ymd(selectedDates[1]) === initialKey) {
setDisplay(initialStart, initialEnd);
return;
}
apply(selectedDates[0], selectedDates[1], null);
}
});
setDisplay(initialStart, initialEnd);
});
});
})(jQuery, window);
})();

View File

@@ -2,6 +2,6 @@ $(document).ready(function(){
// Add class to tables
$('div.prettyContent table').addClass('table');
// Add img-responsive class to img tags
// Add img-fluid class to img tags
$('div.prettyContent img').addClass('img-fluid');
});