[
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 Denis Lukov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# Clusterize.js\n[![Clusterize.js on NPM](https://img.shields.io/npm/v/clusterize.js.svg)](https://www.npmjs.com/package/clusterize.js) \n[![Package Quality](http://npm.packagequality.com/shield/clusterize.js.svg)](http://packagequality.com/#?package=clusterize.js)\n[![Gzip Size](http://img.badgesize.io/https://cdn.jsdelivr.net/npm/clusterize.js/clusterize.min.js?compression=gzip)](https://cdn.jsdelivr.net/npm/clusterize.js/clusterize.min.js)\n[![Install Size](https://packagephobia.now.sh/badge?p=clusterize.js)](https://packagephobia.now.sh/result?p=clusterize.js)\n[![Download Count](https://img.shields.io/npm/dt/clusterize.js.svg)](https://www.npmjs.com/package/clusterize.js)\n[![Join the chat at https://gitter.im/NeXTs/Clusterize.js](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/NeXTs/Clusterize.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)\n\n> Tiny vanilla JS plugin to display large data sets easily\n\n[Demo, usage, etc…](https://clusterize.js.org/)\n\n[![example](http://nexts.github.io/Clusterize.js/img/table_example.gif)](https://clusterize.js.org/)\n"
  },
  {
    "path": "clusterize.css",
    "content": "/* max-height - the only parameter in this file that needs to be edited.\n * Change it to suit your needs. The rest is recommended to leave as is.\n */\n.clusterize-scroll{\n  max-height: 200px;\n  overflow: auto;\n}\n\n/**\n * Avoid vertical margins for extra tags\n * Necessary for correct calculations when rows have nonzero vertical margins\n */\n.clusterize-extra-row{\n  margin-top: 0 !important;\n  margin-bottom: 0 !important;\n}\n\n/* By default extra tag .clusterize-keep-parity added to keep parity of rows.\n * Useful when used :nth-child(even/odd)\n */\n.clusterize-extra-row.clusterize-keep-parity{\n  display: none;\n}\n\n/* During initialization clusterize adds tabindex to force the browser to keep focus\n * on the scrolling list, see issue #11\n * Outline removes default browser's borders for focused elements.\n */\n.clusterize-content{\n  outline: 0;\n  counter-reset: clusterize-counter;\n}\n\n/* Centering message that appears when no data provided\n */\n.clusterize-no-data td{\n  text-align: center;\n}"
  },
  {
    "path": "clusterize.js",
    "content": "/* Clusterize.js - v1.0.0 - 2023-01-22\n http://NeXTs.github.com/Clusterize.js/\n Copyright (c) 2015 Denis Lukov; Licensed MIT */\n\n;(function(name, definition) {\n    if (typeof module != 'undefined') module.exports = definition();\n    else if (typeof define == 'function' && typeof define.amd == 'object') define(definition);\n    else this[name] = definition();\n}('Clusterize', function() {\n  \"use strict\"\n\n  // detect ie9 and lower\n  // https://gist.github.com/padolsey/527683#comment-786682\n  var ie = (function(){\n    for( var v = 3,\n             el = document.createElement('b'),\n             all = el.all || [];\n         el.innerHTML = '<!--[if gt IE ' + (++v) + ']><i><![endif]-->',\n         all[0];\n       ){}\n    return v > 4 ? v : document.documentMode;\n  }()),\n  is_mac = navigator.platform.toLowerCase().indexOf('mac') + 1;\n  var Clusterize = function(data) {\n    if( ! (this instanceof Clusterize))\n      return new Clusterize(data);\n    var self = this;\n\n    var defaults = {\n      rows_in_block: 50,\n      blocks_in_cluster: 4,\n      tag: null,\n      show_no_data_row: true,\n      no_data_class: 'clusterize-no-data',\n      no_data_text: 'No data',\n      keep_parity: true,\n      callbacks: {}\n    }\n\n    // public parameters\n    self.options = {};\n    var options = ['rows_in_block', 'blocks_in_cluster', 'show_no_data_row', 'no_data_class', 'no_data_text', 'keep_parity', 'tag', 'callbacks'];\n    for(var i = 0, option; option = options[i]; i++) {\n      self.options[option] = typeof data[option] != 'undefined' && data[option] != null\n        ? data[option]\n        : defaults[option];\n    }\n\n    var elems = ['scroll', 'content'];\n    for(var i = 0, elem; elem = elems[i]; i++) {\n      self[elem + '_elem'] = data[elem + 'Id']\n        ? document.getElementById(data[elem + 'Id'])\n        : data[elem + 'Elem'];\n      if( ! self[elem + '_elem'])\n        throw new Error(\"Error! Could not find \" + elem + \" element\");\n    }\n\n    // tabindex forces the browser to keep focus on the scrolling list, fixes #11\n    if( ! self.content_elem.hasAttribute('tabindex'))\n      self.content_elem.setAttribute('tabindex', 0);\n\n    // private parameters\n    var rows = isArray(data.rows)\n        ? data.rows\n        : self.fetchMarkup(),\n      cache = {},\n      scroll_top = self.scroll_elem.scrollTop;\n\n    // append initial data\n    self.insertToDOM(rows, cache);\n\n    // restore the scroll position\n    self.scroll_elem.scrollTop = scroll_top;\n\n    // adding scroll handler\n    var last_cluster = false,\n    scroll_debounce = 0,\n    pointer_events_set = false,\n    scrollEv = function() {\n      // fixes scrolling issue on Mac #3\n      if (is_mac) {\n          if( ! pointer_events_set) self.content_elem.style.pointerEvents = 'none';\n          pointer_events_set = true;\n          clearTimeout(scroll_debounce);\n          scroll_debounce = setTimeout(function () {\n              self.content_elem.style.pointerEvents = 'auto';\n              pointer_events_set = false;\n          }, 50);\n      }\n      if (last_cluster != (last_cluster = self.getClusterNum(rows)))\n        self.insertToDOM(rows, cache);\n      if (self.options.callbacks.scrollingProgress)\n        self.options.callbacks.scrollingProgress(self.getScrollProgress());\n    },\n    resize_debounce = 0,\n    resizeEv = function() {\n      clearTimeout(resize_debounce);\n      resize_debounce = setTimeout(self.refresh, 100);\n    }\n    on('scroll', self.scroll_elem, scrollEv);\n    on('resize', window, resizeEv);\n\n    // public methods\n    self.destroy = function(clean) {\n      off('scroll', self.scroll_elem, scrollEv);\n      off('resize', window, resizeEv);\n      self.html((clean ? self.generateEmptyRow() : rows).join(''));\n    }\n    self.refresh = function(force) {\n      if(self.getRowsHeight(rows) || force) self.update(rows);\n    }\n    self.update = function(new_rows) {\n      rows = isArray(new_rows)\n        ? new_rows\n        : [];\n      var scroll_top = self.scroll_elem.scrollTop;\n      // fixes #39\n      if(rows.length * self.options.item_height < scroll_top) {\n        self.scroll_elem.scrollTop = 0;\n        last_cluster = 0;\n      }\n      self.insertToDOM(rows, cache);\n      self.scroll_elem.scrollTop = scroll_top;\n    }\n    self.clear = function() {\n      self.update([]);\n    }\n    self.getRowsAmount = function() {\n      return rows.length;\n    }\n    self.getScrollProgress = function() {\n      return this.options.scroll_top / (rows.length * this.options.item_height) * 100 || 0;\n    }\n\n    var add = function(where, _new_rows) {\n      var new_rows = isArray(_new_rows)\n        ? _new_rows\n        : [];\n      if( ! new_rows.length) return;\n      rows = where == 'append'\n        ? rows.concat(new_rows)\n        : new_rows.concat(rows);\n      self.insertToDOM(rows, cache);\n    }\n    self.append = function(rows) {\n      add('append', rows);\n    }\n    self.prepend = function(rows) {\n      add('prepend', rows);\n    }\n  }\n\n  Clusterize.prototype = {\n    constructor: Clusterize,\n    // fetch existing markup\n    fetchMarkup: function() {\n      var rows = [], rows_nodes = this.getChildNodes(this.content_elem);\n      while (rows_nodes.length) {\n        rows.push(rows_nodes.shift().outerHTML);\n      }\n      return rows;\n    },\n    // get tag name, content tag name, tag height, calc cluster height\n    exploreEnvironment: function(rows, cache) {\n      var opts = this.options;\n      opts.content_tag = this.content_elem.tagName.toLowerCase();\n      if( ! rows.length) return;\n      if(ie && ie <= 9 && ! opts.tag) opts.tag = rows[0].match(/<([^>\\s/]*)/)[1].toLowerCase();\n      if(this.content_elem.children.length <= 1) cache.data = this.html(rows[0] + rows[0] + rows[0]);\n      if( ! opts.tag) opts.tag = this.content_elem.children[0].tagName.toLowerCase();\n      this.getRowsHeight(rows);\n    },\n    getRowsHeight: function(rows) {\n      var opts = this.options,\n        prev_item_height = opts.item_height;\n      opts.cluster_height = 0;\n      if( ! rows.length) return;\n      var nodes = this.content_elem.children;\n      if( ! nodes.length) return;\n      var node = nodes[Math.floor(nodes.length / 2)];\n      opts.item_height = node.offsetHeight;\n      // consider table's border-spacing\n      if(opts.tag == 'tr' && getStyle('borderCollapse', this.content_elem) != 'collapse')\n        opts.item_height += parseInt(getStyle('borderSpacing', this.content_elem), 10) || 0;\n      // consider margins (and margins collapsing)\n      if(opts.tag != 'tr') {\n        var marginTop = parseInt(getStyle('marginTop', node), 10) || 0;\n        var marginBottom = parseInt(getStyle('marginBottom', node), 10) || 0;\n        opts.item_height += Math.max(marginTop, marginBottom);\n      }\n      opts.block_height = opts.item_height * opts.rows_in_block;\n      opts.rows_in_cluster = opts.blocks_in_cluster * opts.rows_in_block;\n      opts.cluster_height = opts.blocks_in_cluster * opts.block_height;\n      return prev_item_height != opts.item_height;\n    },\n    // get current cluster number\n    getClusterNum: function (rows) {\n      var opts = this.options;\n      opts.scroll_top = this.scroll_elem.scrollTop;\n      var cluster_divider = opts.cluster_height - opts.block_height;\n      var current_cluster = Math.floor(opts.scroll_top / cluster_divider);\n      var max_cluster = Math.floor((rows.length * opts.item_height) / cluster_divider);\n      return Math.min(current_cluster, max_cluster);\n    },\n    // generate empty row if no data provided\n    generateEmptyRow: function() {\n      var opts = this.options;\n      if( ! opts.tag || ! opts.show_no_data_row) return [];\n      var empty_row = document.createElement(opts.tag),\n        no_data_content = document.createTextNode(opts.no_data_text), td;\n      empty_row.className = opts.no_data_class;\n      if(opts.tag == 'tr') {\n        td = document.createElement('td');\n        // fixes #53\n        td.colSpan = 100;\n        td.appendChild(no_data_content);\n      }\n      empty_row.appendChild(td || no_data_content);\n      return [empty_row.outerHTML];\n    },\n    // generate cluster for current scroll position\n    generate: function (rows) {\n      var opts = this.options,\n        rows_len = rows.length;\n      if (rows_len < opts.rows_in_block) {\n        return {\n          top_offset: 0,\n          bottom_offset: 0,\n          rows_above: 0,\n          rows: rows_len ? rows : this.generateEmptyRow()\n        }\n      }\n      var items_start = Math.max((opts.rows_in_cluster - opts.rows_in_block) * this.getClusterNum(rows), 0),\n        items_end = items_start + opts.rows_in_cluster,\n        top_offset = Math.max(items_start * opts.item_height, 0),\n        bottom_offset = Math.max((rows_len - items_end) * opts.item_height, 0),\n        this_cluster_rows = [],\n        rows_above = items_start;\n      if(top_offset < 1) {\n        rows_above++;\n      }\n      for (var i = items_start; i < items_end; i++) {\n        rows[i] && this_cluster_rows.push(rows[i]);\n      }\n      return {\n        top_offset: top_offset,\n        bottom_offset: bottom_offset,\n        rows_above: rows_above,\n        rows: this_cluster_rows\n      }\n    },\n    renderExtraTag: function(class_name, height) {\n      var tag = document.createElement(this.options.tag),\n        clusterize_prefix = 'clusterize-';\n      tag.className = [clusterize_prefix + 'extra-row', clusterize_prefix + class_name].join(' ');\n      height && (tag.style.height = height + 'px');\n      return tag.outerHTML;\n    },\n    // if necessary verify data changed and insert to DOM\n    insertToDOM: function(rows, cache) {\n      // explore row's height\n      if( ! this.options.cluster_height) {\n        this.exploreEnvironment(rows, cache);\n      }\n      var data = this.generate(rows),\n        this_cluster_rows = data.rows.join(''),\n        this_cluster_content_changed = this.checkChanges('data', this_cluster_rows, cache),\n        top_offset_changed = this.checkChanges('top', data.top_offset, cache),\n        only_bottom_offset_changed = this.checkChanges('bottom', data.bottom_offset, cache),\n        callbacks = this.options.callbacks,\n        layout = [];\n\n      if(this_cluster_content_changed || top_offset_changed) {\n        if(data.top_offset) {\n          this.options.keep_parity && layout.push(this.renderExtraTag('keep-parity'));\n          layout.push(this.renderExtraTag('top-space', data.top_offset));\n        }\n        layout.push(this_cluster_rows);\n        data.bottom_offset && layout.push(this.renderExtraTag('bottom-space', data.bottom_offset));\n        callbacks.clusterWillChange && callbacks.clusterWillChange();\n        this.html(layout.join(''));\n        this.options.content_tag == 'ol' && this.content_elem.setAttribute('start', data.rows_above);\n        this.content_elem.style['counter-increment'] = 'clusterize-counter ' + (data.rows_above-1);\n        callbacks.clusterChanged && callbacks.clusterChanged();\n      } else if(only_bottom_offset_changed) {\n        this.content_elem.lastChild.style.height = data.bottom_offset + 'px';\n      }\n    },\n    // unfortunately ie <= 9 does not allow to use innerHTML for table elements, so make a workaround\n    html: function(data) {\n      var content_elem = this.content_elem;\n      if(ie && ie <= 9 && this.options.tag == 'tr') {\n        var div = document.createElement('div'), last;\n        div.innerHTML = '<table><tbody>' + data + '</tbody></table>';\n        while((last = content_elem.lastChild)) {\n          content_elem.removeChild(last);\n        }\n        var rows_nodes = this.getChildNodes(div.firstChild.firstChild);\n        while (rows_nodes.length) {\n          content_elem.appendChild(rows_nodes.shift());\n        }\n      } else {\n        content_elem.innerHTML = data;\n      }\n    },\n    getChildNodes: function(tag) {\n        var child_nodes = tag.children, nodes = [];\n        for (var i = 0, ii = child_nodes.length; i < ii; i++) {\n            nodes.push(child_nodes[i]);\n        }\n        return nodes;\n    },\n    checkChanges: function(type, value, cache) {\n      var changed = value != cache[type];\n      cache[type] = value;\n      return changed;\n    }\n  }\n\n  // support functions\n  function on(evt, element, fnc) {\n    return element.addEventListener ? element.addEventListener(evt, fnc, false) : element.attachEvent(\"on\" + evt, fnc);\n  }\n  function off(evt, element, fnc) {\n    return element.removeEventListener ? element.removeEventListener(evt, fnc, false) : element.detachEvent(\"on\" + evt, fnc);\n  }\n  function isArray(arr) {\n    return Object.prototype.toString.call(arr) === '[object Array]';\n  }\n  function getStyle(prop, elem) {\n    return window.getComputedStyle ? window.getComputedStyle(elem)[prop] : elem.currentStyle[prop];\n  }\n\n  return Clusterize;\n}));"
  },
  {
    "path": "externs.js",
    "content": "/**\n * @fileoverview Closure Compiler externs for Clusterize.js 0.16.0.\n * @see https://developers.google.com/closure/compiler/docs/api-tutorial3\n * @externs\n */\n\nvar Clusterize = {\n    \"scroll_elem\": {},\n    \"content_elem\": {},\n    \"tag\": {},\n    \"rows_in_block\": {},\n    \"blocks_in_cluster\": {},\n    \"show_no_data_row\": {},\n    \"no_data_text\": {},\n    \"no_data_class\": {},\n    \"keep_parity\": {},\n    \"callbacks\": {\n        \"clusterWillChange\": function() {},\n        \"clusterChanged\": function() {},\n        \"scrollingProgress\": function() {}\n    },\n    \"update\": function() {},\n    \"append\": function() {},\n    \"prepend\": function() {},\n    \"refresh\": function() {},\n    \"getRowsAmount\": function() {},\n    \"getScrollProgress\": function() {},\n    \"clear\": function() {},\n    \"destroy\": function() {}\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"clusterize.js\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Tiny vanilla JS plugin to display large data sets easily\",\n  \"main\": \"clusterize.js\",\n  \"style\": \"clusterize.css\",\n  \"scripts\": {\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git@github.com:NeXTs/Clusterize.js.git\"\n  },\n  \"keywords\": [\n    \"large\",\n    \"vanillajs\",\n    \"table\",\n    \"grid\",\n    \"list\",\n    \"scroll\",\n    \"cluster\"\n  ],\n  \"author\": \"Denis Lukov\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/NeXTs/Clusterize.js/issues\"\n  },\n  \"homepage\": \"https://github.com/NeXTs/Clusterize.js\"\n}"
  }
]