relux-runtime 0.6.0

Internal: runtime for Relux. No semver guarantees.
(function () {
  function ready(fn) {
    if (document.readyState !== "loading") fn();
    else document.addEventListener("DOMContentLoaded", fn);
  }

  ready(function () {
    var input = document.querySelector('input[data-search-input]');
    if (!input) return;
    var counter = document.querySelector('.search-input .count');
    var kbdBadge = document.querySelector('.search-input .kbd');
    var isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
    if (kbdBadge) kbdBadge.textContent = isMac ? "\u2318S" : "Ctrl+S";

    var groups = Array.prototype.slice.call(document.querySelectorAll('.group'));
    var groupHeaders = Array.prototype.slice.call(document.querySelectorAll('.group-header'));
    var rows = Array.prototype.slice.call(document.querySelectorAll('.row'));

    rows.forEach(function (row) {
      var nameSpan = row.querySelector('.test .name');
      if (nameSpan) row.dataset.originalName = nameSpan.textContent;
    });

    var currentIndex = -1;
    var currentHits = [];

    function escapeRegex(s) {
      return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    }

    function clearHighlights() {
      rows.forEach(function (row) {
        var nameSpan = row.querySelector('.test .name');
        if (nameSpan && row.dataset.originalName !== undefined) {
          nameSpan.textContent = row.dataset.originalName;
        }
      });
    }

    function rebuildHits(query) {
      clearHighlights();
      if (query.length === 0) {
        rows.forEach(function (row) { row.hidden = false; });
        groups.forEach(function (g) { g.hidden = false; });
        groupHeaders.forEach(function (h) {
          h.hidden = false;
          var label = h.dataset.groupHeader;
          var count = document.querySelectorAll('.group[data-group="' + label + '"] .row').length;
          h.innerHTML = '\u2014 ' + label + ' <span class="count">(' + count + ')</span>';
        });
        if (counter) counter.textContent = "";
        return [];
      }

      var insensitive = query === query.toLowerCase();
      var flags = insensitive ? "gi" : "g";
      var re = new RegExp(escapeRegex(query), flags);

      var hits = [];
      var perGroupVisible = {};
      var perGroupTotal = {};
      rows.forEach(function (row) {
        var groupEl = row.closest('.group');
        if (!groupEl) return;
        var groupLabel = groupEl.dataset.group;
        if (!(groupLabel in perGroupTotal)) perGroupTotal[groupLabel] = 0;
        if (!(groupLabel in perGroupVisible)) perGroupVisible[groupLabel] = 0;
        perGroupTotal[groupLabel]++;

        var name = row.dataset.originalName || "";
        re.lastIndex = 0;
        var matches = [];
        for (var m = re.exec(name); m !== null; m = re.exec(name)) {
          matches.push({ start: m.index, end: m.index + m[0].length });
          if (m.index === re.lastIndex) re.lastIndex++;
        }
        if (matches.length === 0) {
          row.hidden = true;
          return;
        }
        row.hidden = false;
        perGroupVisible[groupLabel]++;

        var nameSpan = row.querySelector('.test .name');
        if (!nameSpan) return;
        var frag = document.createDocumentFragment();
        var pos = 0;
        matches.forEach(function (mt) {
          if (mt.start > pos) frag.appendChild(document.createTextNode(name.slice(pos, mt.start)));
          var mark = document.createElement('mark');
          mark.className = 'search-hit';
          mark.textContent = name.slice(mt.start, mt.end);
          frag.appendChild(mark);
          hits.push({ row: row, mark: mark });
          pos = mt.end;
        });
        if (pos < name.length) frag.appendChild(document.createTextNode(name.slice(pos)));
        nameSpan.textContent = '';
        nameSpan.appendChild(frag);
      });

      groupHeaders.forEach(function (h) {
        var label = h.dataset.groupHeader;
        var visible = perGroupVisible[label] || 0;
        var total = perGroupTotal[label] || 0;
        var groupEl = document.querySelector('.group[data-group="' + label + '"]');
        if (visible === 0) {
          h.hidden = true;
          if (groupEl) groupEl.hidden = true;
        } else {
          h.hidden = false;
          if (groupEl) groupEl.hidden = false;
          h.innerHTML = '\u2014 ' + label + ' <span class="count">(' + visible + ' / ' + total + ')</span>';
        }
      });

      if (counter) counter.textContent = hits.length + " / " + rows.length;
      return hits;
    }

    function markCurrent(index) {
      currentHits.forEach(function (h) { h.mark.classList.remove('search-hit-current'); });
      if (index < 0 || index >= currentHits.length) return;
      var hit = currentHits[index];
      hit.mark.classList.add('search-hit-current');
      var rect = hit.row.getBoundingClientRect();
      var top = window.scrollY + rect.top + rect.height / 2 - window.innerHeight / 2;
      var max = document.documentElement.scrollHeight - window.innerHeight;
      window.scrollTo(0, Math.max(0, Math.min(max, top)));
    }

    function recompute() {
      currentHits = rebuildHits(input.value);
      currentIndex = currentHits.length > 0 ? 0 : -1;
      markCurrent(currentIndex);
    }

    input.addEventListener('input', recompute);
    input.addEventListener('keydown', function (event) {
      if (event.key === 'Enter') {
        event.preventDefault();
        if (currentHits.length === 0) return;
        var delta = event.shiftKey ? -1 : 1;
        currentIndex = (currentIndex + delta + currentHits.length) % currentHits.length;
        markCurrent(currentIndex);
      } else if (event.key === 'Escape') {
        event.preventDefault();
        if (input.value.length > 0) {
          input.value = '';
          recompute();
        } else {
          input.blur();
        }
      }
    });

    document.addEventListener('keydown', function (event) {
      if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === 's') {
        event.preventDefault();
        input.focus();
        input.select();
      }
    });
  });
})();