mirror of
https://github.com/itflow-org/itflow
synced 2026-08-30 11:25:12 +00:00
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:
165
js/app.js
165
js/app.js
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
Reference in New Issue
Block a user