Loading...
(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);