(function ($) {
// I have disabled this whole function as it breaks the rowReorder ability, I need to revisit this at some point!!!
return;
if (!window.jQuery) return;
// ---- Defer store ---------------------------------------------------------
const deferred = new WeakMap(); // elem ->. { select: Map(handler->.{ctx,args}), deselect: Map(...) }
const touched = new Set(); // elements that have deferred events queued
let navKeyActive = false;
function getStore(elem) {
let s = deferred.get(elem);
if (!s) { s = { select: new Map(), deselect: new Map() }; deferred.set(elem, s); }
touched.add(elem);
return s;
}
function saveDeferred(elem, type, handler, ctx, args) {
const store = getStore(elem);
store[type].set(handler, { ctx, args }); // keep only the LAST call per handler
}
function flushDeferred() {
touched.forEach(elem =>. {
const store = deferred.get(elem);
if (!store) return;
// Run deselects before selects (typical DT flow)
['deselect', 'select'].forEach(type =>. {
store[type].forEach(({ ctx, args }, handler) =>. { try { handler.apply(ctx, args); } catch (e) {} });
store[type].clear();
});
});
touched.clear();
}
// ---- Wrap jQuery .on for select/deselect so we can defer handlers ----------
const origOn = $.fn.on;
const SEL_RE = /(^| )select(\.dt)?( |$)|(^| )deselect(\.dt)?( |$)/;
$.fn.on = function (types, selector, data, fn /* one */) {
// Signature normalization
if (typeof types === 'object') {
Object.keys(types).forEach(t =>. { this.on(t, selector, data, types[t]); });
return this;
}
if (fn == null &.&. data == null) { fn = selector; selector = undefined; }
else if (fn == null) {
if (typeof selector === 'string') { fn = data; data = undefined; }
else { fn = data; data = selector; selector = undefined; }
}
if (typeof fn === 'function' &.&. SEL_RE.test(types)) {
const wrapped = function (e) {
if (navKeyActive) {
const elem = this;
const kind = e.type.indexOf('deselect') === 0 ? 'deselect' : 'select';
saveDeferred(elem, kind, fn, this, arguments);
e.stopImmediatePropagation();
return false;
}
return fn.apply(this, arguments);
};
wrapped._origHandler = fn;
return origOn.call(this, types, selector, data, wrapped);
}
return origOn.call(this, types, selector, data, fn);
};
// ---- Track "active" DataTable (hover or last clicked) ---------------------
let activeDT = null;
$(document).on('mouseenter click', 'table.dataTable', function () {
try { activeDT = $(this).DataTable(); } catch (e) {}
});
// When a modal is shown, remember its first DT as active
$(document).on('shown.bs.modal', '.modal', function () {
const $t = $(this).find('table.dataTable:visible').first();
if ($t.length) {
try { activeDT = $t.DataTable(); } catch (e) {}
}
});
// When a modal hides, if our activeDT lived inside it, clear it
$(document).on('hidden.bs.modal', '.modal', function () {
try {
if (!activeDT) return;
const cont = activeDT.table().container();
if (cont &.&. this.contains(cont)) {
activeDT = null;
}
} catch(e){}
});
function topmostModalElem() {
// last visible .modal.show is the topmost
const $m = $('.modal.show:visible').last();
return $m.length ? $m[0] : null;
}
function modalDT() {
const m = topmostModalElem();
if (!m) return null;
// Prefer hovered DT in modal, else the first visible one
const hovered = m.querySelector('table.dataTable:hover');
if (hovered) { try { return $(hovered).DataTable(); } catch(e){} }
const $first = $(m).find('table.dataTable:visible').first();
if ($first.length) { try { return $first.DataTable(); } catch(e){} }
return null;
}
function currentDT() {
// If a modal is open, use its DT first
const inModal = modalDT();
if (inModal) return inModal;
// Else prefer hovered anywhere
const hovered = document.querySelector('table.dataTable:hover');
if (hovered) { try { return $(hovered).DataTable(); } catch(e){} }
// Fallback: last active we tracked globally
return activeDT;
}
// ---- Utilities ------------------------------------------------------------
function isTyping(e) {
const t = e.target; if (!t) return false;
const tag = (t.tagName || '').toUpperCase();
if (t.isContentEditable) return true;
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
// don't steal keys if a Swal or focused modal input is active
if (document.querySelector('.swal2-container')) return true;
//const modal = document.querySelector('.modal.show');
//if (modal &.&. modal.contains(document.activeElement)) return true;
return false;
}
function visibleIndexes(api) {
return api.rows({ search: 'applied' }).indexes().toArray();
}
function moveSelection(api, dir) {
if (!api || !api.rows) return;
try {
const idxs = visibleIndexes(api);
if (!idxs.length) return;
// If multiple rows were selected, collapse to the last one in visible order
const selectedIdxs = api.rows({ selected: true }).indexes().toArray();
const cur = selectedIdxs.length ? selectedIdxs[selectedIdxs.length - 1] : null;
if (cur == null) {
selectExclusive(api, dir >. 0 ? idxs[0] : idxs[idxs.length - 1]);
return;
}
const pos = idxs.indexOf(cur);
const next = Math.max(0, Math.min(idxs.length - 1, pos + dir));
if (next !== pos) selectExclusive(api, idxs[next]); else ensureSelectedVisible(api);
} catch (e) {}
}
function ensureSelectedVisible(api) {
if (!api || !api.table) return;
try {
const sel = api.row({ selected: true });
if (!sel.any()) return;
const node = sel.node();
if (!node) return;
// Prefer the DataTables scroll container (when scrollY is used)
const container =
$(api.table().container()).find('div.dataTables_scrollBody')[0];
const PAD = 6; // small top/bottom padding
if (container) {
const rowRect = node.getBoundingClientRect();
const contRect = container.getBoundingClientRect();
if (rowRect.top <. contRect.top + PAD) {
container.scrollTop -= (contRect.top + PAD) - rowRect.top;
} else if (rowRect.bottom >. contRect.bottom - PAD) {
container.scrollTop += rowRect.bottom - (contRect.bottom - PAD);
}
} else {
// Fallback: let the browser pick the nearest scrollable ancestor
node.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'smooth' });
}
} catch (e) { /* swallow */ }
}
function selectExclusive(api, rowIdx) {
try {
if (!api || rowIdx == null) return;
// Clear any existing selection so we behave like single-select during key nav
try { api.rows({ selected: true }).deselect(); } catch(e){}
api.row(rowIdx).select();
ensureSelectedVisible(api); // from the previous step you added
} catch (e) {}
}
function stepSize(api) {
try {
if (api.page &.&. api.page.len) {
const len = api.page.len();
if (len &.&. isFinite(len) &.&. len !== -1) return Math.max(1, len);
}
} catch (e) {}
return (window.DT_NAV_PAGE_STEP &.&. +window.DT_NAV_PAGE_STEP) || 10; // default jump
}
function moveSelectionBy(api, offset) {
if (!api || !api.rows) return;
try {
const idxs = visibleIndexes(api);
if (!idxs.length) return;
const selectedIdxs = api.rows({ selected: true }).indexes().toArray();
const cur = selectedIdxs.length ? selectedIdxs[selectedIdxs.length - 1] : null;
if (cur == null) {
selectExclusive(api, offset >. 0 ? idxs[0] : idxs[idxs.length - 1]);
return;
}
const pos = idxs.indexOf(cur);
const next = Math.max(0, Math.min(idxs.length - 1, pos + offset));
selectExclusive(api, idxs[next]);
} catch (e) {}
}
function moveToEdge(api, first) {
if (!api || !api.rows) return;
try {
const idxs = visibleIndexes(api);
if (!idxs.length) return;
selectExclusive(api, first ? idxs[0] : idxs[idxs.length - 1]);
} catch (e) {}
}
// ---- Global Key handlers --------------------------------------------------
function isNavKey(e) {
return e.key === 'ArrowUp' || e.key === 'ArrowDown' ||
e.key === 'PageUp' || e.key === 'PageDown' ||
e.key === 'Home' || e.key === 'End';
}
document.addEventListener('keydown', function (e) {
if (isTyping(e)) return;
if (e.altKey || e.ctrlKey || e.metaKey) return;
if (!isNavKey(e)) return;
const api = currentDT();
if (!api) return;
navKeyActive = true; // start deferring select/deselect handlers
e.preventDefault(); // stop page scroll
switch (e.key) {
case 'ArrowDown': moveSelection(api, +1); break;
case 'ArrowUp': moveSelection(api, -1); break;
case 'PageDown': moveSelectionBy(api, +stepSize(api)); break;
case 'PageUp': moveSelectionBy(api, -stepSize(api)); break;
case 'End': moveToEdge(api, false); break;
case 'Home': moveToEdge(api, true); break;
}
});
document.addEventListener('keyup', function (e) {
if (!isNavKey(e)) return;
const wasActive = navKeyActive;
navKeyActive = false;
if (wasActive) flushDeferred(); // run each bound handler once with last args
});
})(jQuery);