Fixed Side nav titles, Fixed ajax on bootstrap modals, fixed pagination all regressions

This commit is contained in:
johnnyq
2026-08-14 18:32:33 -04:00
parent 1c5803dc12
commit e2512b0f86
36 changed files with 282 additions and 134 deletions

View File

@@ -1,3 +1,36 @@
/**
* Re-run <script> elements that arrived via innerHTML, strictly in order.
*
* External scripts are awaited before the next one starts; inline scripts run
* synchronously. Order matters because a modal's own script is emitted before
* modal_footer.php's http.js / autocomplete.js / app.js, and depends on them.
*/
function runScriptsInOrder(scripts) {
return scripts.reduce(function (chain, old) {
return chain.then(function () {
return new Promise(function (resolve) {
const s = document.createElement('script');
for (const attr of old.attributes) {
s.setAttribute(attr.name, attr.value);
}
if (old.src) {
s.async = false;
s.onload = resolve;
s.onerror = function () {
console.error('ajax-modal: failed to load', old.src);
resolve();
};
old.replaceWith(s);
} else {
s.textContent = old.textContent;
old.replaceWith(s);
resolve();
}
});
});
}, Promise.resolve());
}
// Ajax Modal Load Script
document.addEventListener('click', function (e) {
const trigger = e.target.closest('.ajax-modal');
@@ -64,19 +97,17 @@ document.addEventListener('click', function (e) {
'</div>';
host.appendChild(wrapper);
// innerHTML does not execute <script> tags. The modal payload ends
// with modal_footer.php, which re-runs app.js to wire up Tom Select,
// IMask, flatpickr and friends - so re-inject them by hand.
wrapper.querySelectorAll('script').forEach(function (old) {
const s = document.createElement('script');
for (const attr of old.attributes) {
s.setAttribute(attr.name, attr.value);
}
s.textContent = old.textContent;
old.replaceWith(s);
});
bootstrap.Modal.getOrCreateInstance(wrapper).show();
// innerHTML does not execute <script> tags, so they have to be
// re-injected. They must also run IN ORDER: a modal payload loads
// its own script first and modal_footer.php's http.js / app.js
// after, and the modal script depends on helpers those define.
// A dynamically created <script src> is async by default and would
// run in completion order instead - jQuery's .append() loaded them
// sequentially, which is the behaviour reproduced here.
runScriptsInOrder(Array.from(wrapper.querySelectorAll('script')))
.then(function () {
bootstrap.Modal.getOrCreateInstance(wrapper).show();
});
wrapper.addEventListener('hidden.bs.modal', function () {
wrapper.remove();

View File

@@ -8,38 +8,20 @@
* 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.
* Run one initialiser in isolation.
*
* itflowInit() sets up eight independent libraries in sequence. Without this,
* a throw in any one of them aborts the whole function and every initialiser
* after it silently never runs - which is exactly the kind of failure that
* looks like "everything is broken" while the console shows one error from a
* library you were not looking at.
*/
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 itflowStep(name, fn) {
try {
fn();
} catch (e) {
console.error('itflow init [' + name + '] failed:', e);
}
}
function itflowBindOnce(name, type, selector, handler) {
@@ -90,8 +72,10 @@ function itflowInit() {
// 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);
itflowStep('tom-select', function () {
document.querySelectorAll('.select2').forEach(function (el) {
initTomSelect(el);
});
});
// Initialize TinyMCE
@@ -532,23 +516,29 @@ function itflowInit() {
// the lever, not a Bootstrap patch.
// Clipboard
var clipboard = new ClipboardJS('.clipboardjs');
itflowStep('clipboard', function () {
var clipboard = new ClipboardJS('.clipboardjs');
clipboard.on('success', function(e) {
flashTooltip(e.trigger, 'Copied!');
});
clipboard.on('success', function(e) {
flashTooltip(e.trigger, 'Copied!');
});
clipboard.on('error', function(e) {
flashTooltip(e.trigger, 'Failed!');
clipboard.on('error', function(e) {
flashTooltip(e.trigger, 'Failed!');
});
});
// Enable Popovers
document.querySelectorAll('[data-bs-toggle="popover"]').forEach(function (el) {
bootstrap.Popover.getOrCreateInstance(el);
itflowStep('popovers', function () {
document.querySelectorAll('[data-bs-toggle="popover"]').forEach(function (el) {
bootstrap.Popover.getOrCreateInstance(el);
});
});
// Data Tables
new DataTable('.dataTables');
itflowStep('datatables', function () {
new DataTable('.dataTables');
});
}
// modal_footer.php re-loads this file on every ajax modal open, so run now if

View File

@@ -1,6 +1,6 @@
function showOTPViaCredentialID(credential_id) {
// Send a GET request to ajax.php as ajax.php?get_totp_token_via_id=true&credential_id=ID
jQuery.get(
itflowGet(
"../ajax.php", {
get_totp_token_via_id: 'true',
credential_id: credential_id

96
js/http.js Normal file
View File

@@ -0,0 +1,96 @@
/**
* Drop-in replacements for itflowGet() and itflowPost().
*
* These keep jQuery's exact call shape - itflowGet(url, data, success, error) -
* so the ~28 existing call sites did not have to be restructured when jQuery
* was removed. Every one of them uses the plain callback form; none chained
* .done()/.fail() or passed a dataType, so nothing else is reproduced here.
*
* The success callback receives the raw response TEXT, matching jQuery's
* behaviour when the server does not send a JSON content type. Several call
* sites do their own JSON.parse(), so returning a parsed object here would
* break them.
*/
/** Serialise to PHP-style bracket params, the way jQuery did. */
function itflowSerialize(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 params.toString();
}
function itflowGet(url, data, success, error) {
const query = itflowSerialize(data);
const sep = url.indexOf('?') === -1 ? '?' : '&';
return fetch(query ? url + sep + query : url, {
method: 'GET',
credentials: 'same-origin'
})
.then(function (res) {
if (!res.ok) {
throw new Error('HTTP ' + res.status);
}
return res.text();
})
.then(function (text) {
if (typeof success === 'function') {
success(text);
}
return text;
})
.catch(function (err) {
if (typeof error === 'function') {
error(err);
} else {
console.error('itflowGet ' + url + ':', err);
}
});
}
function itflowPost(url, data, success, error) {
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
credentials: 'same-origin',
body: itflowSerialize(data)
})
.then(function (res) {
if (!res.ok) {
throw new Error('HTTP ' + res.status);
}
return res.text();
})
.then(function (text) {
if (typeof success === 'function') {
success(text);
}
return text;
})
.catch(function (err) {
if (typeof error === 'function') {
error(err);
} else {
console.error('itflowPost ' + url + ':', err);
}
});
}
/** Kept for the kanban and invoice item-order callers, which use this name. */
function itflowPostForm(url, data) {
return itflowPost(url, data);
}

View File

@@ -4,7 +4,7 @@
function keep_alive() {
//Send a GET request to keepalive.php as keepalive.php?keepalive
jQuery.get(
itflowGet(
"/keepalive.php",
{keepalive: 'true'},
function(data) {