mirror of
https://github.com/itflow-org/itflow
synced 2026-09-02 04:45:15 +00:00
All Jquery removed replaced with stand JS and non jqery dependent libs
This commit is contained in:
120
js/app.js
120
js/app.js
@@ -1,13 +1,92 @@
|
||||
$(document).ready(function() {
|
||||
/**
|
||||
* Delegated listener that binds exactly once.
|
||||
*
|
||||
* modal_footer.php re-loads this file every time an ajax modal opens, which is
|
||||
* why the jQuery originals used a namespaced .off().on() - without it the
|
||||
* handlers stacked up and fired once per modal ever opened. The named flag on
|
||||
* window is the vanilla equivalent. `this` is the matched element, matching
|
||||
* jQuery's delegation contract so the handler bodies are unchanged.
|
||||
*/
|
||||
/**
|
||||
* $.post replacement. jQuery serialised nested arrays/objects into PHP-style
|
||||
* bracket params (positions[0][status_id]=...), which is what ajax.php parses,
|
||||
* so that encoding is reproduced here rather than sending JSON.
|
||||
*/
|
||||
function itflowPostForm(url, data) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
(function add(prefix, value) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(function (v, i) {
|
||||
add(prefix + '[' + i + ']', v);
|
||||
});
|
||||
} else if (value !== null && typeof value === 'object') {
|
||||
Object.keys(value).forEach(function (k) {
|
||||
add(prefix ? prefix + '[' + k + ']' : k, value[k]);
|
||||
});
|
||||
} else {
|
||||
params.append(prefix, value === true ? 'true' : String(value));
|
||||
}
|
||||
})('', data);
|
||||
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
credentials: 'same-origin',
|
||||
body: params.toString()
|
||||
}).then(function (res) {
|
||||
if (!res.ok) {
|
||||
throw new Error('HTTP ' + res.status);
|
||||
}
|
||||
return res.text();
|
||||
});
|
||||
}
|
||||
|
||||
function itflowBindOnce(name, type, selector, handler) {
|
||||
window.itflowBound = window.itflowBound || {};
|
||||
if (window.itflowBound[name]) {
|
||||
return;
|
||||
}
|
||||
window.itflowBound[name] = true;
|
||||
document.addEventListener(type, function (e) {
|
||||
const match = e.target.closest(selector);
|
||||
if (match) {
|
||||
handler.call(match, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function itflowInit() {
|
||||
// Prevents resubmit on forms
|
||||
if (window.history.replaceState) {
|
||||
window.history.replaceState(null, null, window.location.href);
|
||||
}
|
||||
|
||||
// Slide alert up after 4 secs
|
||||
$("#alert").fadeTo(5000, 500).slideUp(500, function() {
|
||||
$("#alert").slideUp(500);
|
||||
});
|
||||
// Fade the legacy #alert box out after 5s, then collapse it
|
||||
(function () {
|
||||
const alertEl = document.getElementById('alert');
|
||||
if (!alertEl) {
|
||||
return;
|
||||
}
|
||||
setTimeout(function () {
|
||||
alertEl.style.transition = 'opacity .5s linear';
|
||||
alertEl.style.opacity = '0';
|
||||
setTimeout(function () {
|
||||
alertEl.style.overflow = 'hidden';
|
||||
alertEl.style.transition = 'height .5s ease, margin .5s ease, padding .5s ease';
|
||||
alertEl.style.height = alertEl.offsetHeight + 'px';
|
||||
void alertEl.offsetHeight;
|
||||
alertEl.style.height = '0px';
|
||||
alertEl.style.marginTop = '0';
|
||||
alertEl.style.marginBottom = '0';
|
||||
alertEl.style.paddingTop = '0';
|
||||
alertEl.style.paddingBottom = '0';
|
||||
setTimeout(function () {
|
||||
alertEl.style.display = 'none';
|
||||
}, 500);
|
||||
}, 500);
|
||||
}, 5000);
|
||||
})();
|
||||
|
||||
// Initialize Tom Select (replaces Select2). Every instance is reachable
|
||||
// afterwards as element.tomselect, which is how the helpers below reach it.
|
||||
@@ -446,8 +525,11 @@ $(document).ready(function() {
|
||||
});
|
||||
});
|
||||
|
||||
// ClipboardJS fix for Bootstrap modals
|
||||
$.fn.modal.Constructor.prototype._enforceFocus = function() {};
|
||||
// Bootstrap 4 needed _enforceFocus patched out so ClipboardJS could reach
|
||||
// its textarea inside a modal. Bootstrap 5 registers no jQuery plugin, so
|
||||
// the old $.fn.modal line threw and killed everything below it. If copying
|
||||
// from inside a modal ever misbehaves, ClipboardJS's `container` option is
|
||||
// the lever, not a Bootstrap patch.
|
||||
|
||||
// Clipboard
|
||||
var clipboard = new ClipboardJS('.clipboardjs');
|
||||
@@ -461,15 +543,21 @@ $(document).ready(function() {
|
||||
});
|
||||
|
||||
// Enable Popovers
|
||||
$(function() {
|
||||
document.querySelectorAll('[data-bs-toggle="popover"]').forEach(function (el) {
|
||||
bootstrap.Popover.getOrCreateInstance(el);
|
||||
});
|
||||
document.querySelectorAll('[data-bs-toggle="popover"]').forEach(function (el) {
|
||||
bootstrap.Popover.getOrCreateInstance(el);
|
||||
});
|
||||
|
||||
// Data Tables
|
||||
new DataTable('.dataTables');
|
||||
});
|
||||
}
|
||||
|
||||
// modal_footer.php re-loads this file on every ajax modal open, so run now if
|
||||
// the document is already parsed, otherwise wait for it.
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', itflowInit);
|
||||
} else {
|
||||
itflowInit();
|
||||
}
|
||||
|
||||
/*
|
||||
* Calendar event modals - the All day switch shows or hides the time row.
|
||||
@@ -487,7 +575,7 @@ $(document).ready(function() {
|
||||
* namespaced .off() keeps this to a single handler - modal_footer.php re-loads this
|
||||
* file on every ajax modal open.
|
||||
*/
|
||||
$(document).off('change.itflowAllDay').on('change.itflowAllDay', '.event-all-day-toggle', function () {
|
||||
itflowBindOnce('itflowAllDay', 'change', '.event-all-day-toggle', function () {
|
||||
|
||||
const allDay = this.checked;
|
||||
const timeFields = document.getElementById(this.id.replace(/_all_day$/, '_time_fields'));
|
||||
@@ -511,7 +599,7 @@ $(document).off('change.itflowAllDay').on('change.itflowAllDay', '.event-all-day
|
||||
* Keep the end date at or after the start date, without shortening a longer span
|
||||
* the user has already chosen.
|
||||
*/
|
||||
$(document).off('change.itflowEventDate').on('change.itflowEventDate', '.event-start-date', function () {
|
||||
itflowBindOnce('itflowEventDate', 'change', '.event-start-date', function () {
|
||||
|
||||
const endField = document.getElementById(this.id.replace(/_start_date$/, '_end_date'));
|
||||
|
||||
@@ -529,7 +617,7 @@ $(document).off('change.itflowEventDate').on('change.itflowEventDate', '.event-s
|
||||
* A start late in the evening rolls the end onto the following day rather than
|
||||
* wrapping round to an end that precedes the start.
|
||||
*/
|
||||
$(document).off('change.itflowEventTime').on('change.itflowEventTime', '.event-start-time', function () {
|
||||
itflowBindOnce('itflowEventTime', 'change', '.event-start-time', function () {
|
||||
|
||||
const prefix = this.id.replace(/_start_time$/, '');
|
||||
const endField = document.getElementById(prefix + '_end_time');
|
||||
@@ -575,7 +663,7 @@ $(document).off('change.itflowEventTime').on('change.itflowEventTime', '.event-s
|
||||
* until the next page load.
|
||||
*/
|
||||
function flashTooltip(button, message) {
|
||||
const el = button instanceof Element ? button : $(button)[0];
|
||||
const el = button instanceof Element ? button : document.querySelector(button);
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
204
js/autocomplete.js
Normal file
204
js/autocomplete.js
Normal file
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Minimal autocomplete, replacing jQuery UI's.
|
||||
*
|
||||
* itflowAutocomplete(input, {
|
||||
* source: array of items, or fn(term) -> array
|
||||
* minLength: default 1
|
||||
* maxItems: cap on rendered results, default 50
|
||||
* match: fn(item, term) -> bool (default: substring over item.label)
|
||||
* render: fn(item) -> HTML string (default: escaped item.label)
|
||||
* onSelect: fn(item)
|
||||
* })
|
||||
*
|
||||
* Keyboard: up/down to move, Enter to pick, Escape to dismiss.
|
||||
* The menu is capped to the space actually available in the viewport and
|
||||
* scrolls internally, and flips above the input when there is more room there.
|
||||
*/
|
||||
function itflowEscapeHtml(value) {
|
||||
var div = document.createElement('div');
|
||||
div.textContent = value == null ? '' : String(value);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function itflowAutocomplete(input, options) {
|
||||
if (!input || input.itflowAutocomplete) {
|
||||
return;
|
||||
}
|
||||
var opts = options || {};
|
||||
var minLength = opts.minLength == null ? 1 : opts.minLength;
|
||||
var maxItems = opts.maxItems == null ? 50 : opts.maxItems;
|
||||
var GAP = 4; // breathing room against the input
|
||||
var MARGIN = 8; // never touch the viewport edge
|
||||
var MIN_HEIGHT = 120;
|
||||
var items = [];
|
||||
var active = -1;
|
||||
|
||||
var menu = document.createElement('div');
|
||||
menu.className = 'itflow-ac-menu dropdown-menu p-0';
|
||||
menu.setAttribute('role', 'listbox');
|
||||
document.body.appendChild(menu);
|
||||
|
||||
function defaultMatch(item, term) {
|
||||
return String(item.label || '').toLowerCase().indexOf(term) !== -1;
|
||||
}
|
||||
|
||||
function close() {
|
||||
menu.classList.remove('show');
|
||||
menu.innerHTML = '';
|
||||
active = -1;
|
||||
input.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
/**
|
||||
* Size and place the menu against whatever room the viewport actually has.
|
||||
* Without this a long product list runs off the bottom of the page.
|
||||
*/
|
||||
function position() {
|
||||
var r = input.getBoundingClientRect();
|
||||
var below = window.innerHeight - r.bottom - GAP - MARGIN;
|
||||
var above = r.top - GAP - MARGIN;
|
||||
var flip = below < MIN_HEIGHT && above > below;
|
||||
var room = Math.max(flip ? above : below, MIN_HEIGHT);
|
||||
|
||||
menu.style.position = 'fixed';
|
||||
menu.style.left = r.left + 'px';
|
||||
menu.style.minWidth = r.width + 'px';
|
||||
menu.style.maxWidth = Math.max(r.width, Math.min(520, window.innerWidth - (MARGIN * 2))) + 'px';
|
||||
menu.style.maxHeight = room + 'px';
|
||||
menu.style.overflowY = 'auto';
|
||||
menu.style.overflowX = 'hidden';
|
||||
menu.style.zIndex = '2000';
|
||||
|
||||
if (flip) {
|
||||
menu.style.top = '';
|
||||
menu.style.bottom = (window.innerHeight - r.top + GAP) + 'px';
|
||||
} else {
|
||||
menu.style.bottom = '';
|
||||
menu.style.top = (r.bottom + GAP) + 'px';
|
||||
}
|
||||
|
||||
// Keep it on screen horizontally too
|
||||
var width = menu.offsetWidth || r.width;
|
||||
if (r.left + width > window.innerWidth - MARGIN) {
|
||||
menu.style.left = Math.max(MARGIN, window.innerWidth - width - MARGIN) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function highlight(next) {
|
||||
var nodes = menu.querySelectorAll('.itflow-ac-item');
|
||||
if (!nodes.length) {
|
||||
return;
|
||||
}
|
||||
if (active >= 0 && nodes[active]) {
|
||||
nodes[active].classList.remove('active');
|
||||
}
|
||||
active = next < 0 ? nodes.length - 1 : (next >= nodes.length ? 0 : next);
|
||||
nodes[active].classList.add('active');
|
||||
nodes[active].scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
|
||||
function choose(index) {
|
||||
var item = items[index];
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
close();
|
||||
if (typeof opts.onSelect === 'function') {
|
||||
opts.onSelect(item);
|
||||
}
|
||||
}
|
||||
|
||||
function open(term) {
|
||||
var source = typeof opts.source === 'function' ? opts.source(term) : (opts.source || []);
|
||||
var matcher = typeof opts.match === 'function' ? opts.match : defaultMatch;
|
||||
var all = source.filter(function (item) {
|
||||
return matcher(item, term);
|
||||
});
|
||||
items = all.slice(0, maxItems);
|
||||
if (!items.length) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
menu.innerHTML = '';
|
||||
items.forEach(function (item, i) {
|
||||
var el = document.createElement('button');
|
||||
el.type = 'button';
|
||||
el.className = 'itflow-ac-item dropdown-item text-wrap';
|
||||
el.setAttribute('role', 'option');
|
||||
el.innerHTML = typeof opts.render === 'function'
|
||||
? opts.render(item)
|
||||
: itflowEscapeHtml(item.label);
|
||||
el.addEventListener('mousedown', function (e) {
|
||||
// mousedown, not click - blur would close the menu first
|
||||
e.preventDefault();
|
||||
choose(i);
|
||||
});
|
||||
menu.appendChild(el);
|
||||
});
|
||||
if (all.length > items.length) {
|
||||
var more = document.createElement('div');
|
||||
more.className = 'itflow-ac-more small text-muted px-3 py-2 border-top';
|
||||
more.textContent = 'Showing ' + items.length + ' of ' + all.length + ' - keep typing to narrow';
|
||||
menu.appendChild(more);
|
||||
}
|
||||
menu.classList.add('show');
|
||||
input.setAttribute('aria-expanded', 'true');
|
||||
active = -1;
|
||||
position();
|
||||
menu.scrollTop = 0;
|
||||
}
|
||||
|
||||
input.setAttribute('autocomplete', 'off');
|
||||
input.setAttribute('aria-autocomplete', 'list');
|
||||
|
||||
input.addEventListener('input', function () {
|
||||
var term = input.value.trim().toLowerCase();
|
||||
if (term.length < minLength) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
open(term);
|
||||
});
|
||||
|
||||
input.addEventListener('keydown', function (e) {
|
||||
if (!menu.classList.contains('show')) {
|
||||
return;
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
highlight(active + 1);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
highlight(active - 1);
|
||||
} else if (e.key === 'Enter') {
|
||||
if (active >= 0) {
|
||||
e.preventDefault();
|
||||
choose(active);
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('blur', function () {
|
||||
setTimeout(close, 150);
|
||||
});
|
||||
|
||||
function reposition() {
|
||||
if (menu.classList.contains('show')) {
|
||||
position();
|
||||
}
|
||||
}
|
||||
window.addEventListener('resize', reposition);
|
||||
// position: fixed does not follow the page, and these menus open inside
|
||||
// scrollable modal bodies - so track scroll on the way up the tree too
|
||||
window.addEventListener('scroll', reposition, true);
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.target !== input && !menu.contains(e.target)) {
|
||||
close();
|
||||
}
|
||||
});
|
||||
|
||||
input.itflowAutocomplete = { close: close, reposition: reposition };
|
||||
}
|
||||
Reference in New Issue
Block a user