UNPKG

12.6 kBJavaScriptView Raw
1/*! Clusterize.js - v0.17.2 - 2016-10-07
2* http://NeXTs.github.com/Clusterize.js/
3* Copyright (c) 2015 Denis Lukov; Licensed GPLv3 */
4
5;(function(name, definition) {
6 if (typeof module != 'undefined') module.exports = definition();
7 else if (typeof define == 'function' && typeof define.amd == 'object') define(definition);
8 else this[name] = definition();
9}('Clusterize', function() {
10 "use strict"
11
12 // detect ie9 and lower
13 // https://gist.github.com/padolsey/527683#comment-786682
14 var ie = (function(){
15 for( var v = 3,
16 el = document.createElement('b'),
17 all = el.all || [];
18 el.innerHTML = '<!--[if gt IE ' + (++v) + ']><i><![endif]-->',
19 all[0];
20 ){}
21 return v > 4 ? v : document.documentMode;
22 }()),
23 is_mac = navigator.platform.toLowerCase().indexOf('mac') + 1;
24 var Clusterize = function(data) {
25 if( ! (this instanceof Clusterize))
26 return new Clusterize(data);
27 var self = this;
28
29 var defaults = {
30 rows_in_block: 50,
31 blocks_in_cluster: 4,
32 tag: null,
33 show_no_data_row: true,
34 no_data_class: 'clusterize-no-data',
35 no_data_text: 'No data',
36 keep_parity: true,
37 callbacks: {}
38 }
39
40 // public parameters
41 self.options = {};
42 var options = ['rows_in_block', 'blocks_in_cluster', 'show_no_data_row', 'no_data_class', 'no_data_text', 'keep_parity', 'tag', 'callbacks'];
43 for(var i = 0, option; option = options[i]; i++) {
44 self.options[option] = typeof data[option] != 'undefined' && data[option] != null
45 ? data[option]
46 : defaults[option];
47 }
48
49 var elems = ['scroll', 'content'];
50 for(var i = 0, elem; elem = elems[i]; i++) {
51 self[elem + '_elem'] = data[elem + 'Id']
52 ? document.getElementById(data[elem + 'Id'])
53 : data[elem + 'Elem'];
54 if( ! self[elem + '_elem'])
55 throw new Error("Error! Could not find " + elem + " element");
56 }
57
58 // tabindex forces the browser to keep focus on the scrolling list, fixes #11
59 if( ! self.content_elem.hasAttribute('tabindex'))
60 self.content_elem.setAttribute('tabindex', 0);
61
62 // private parameters
63 var rows = isArray(data.rows)
64 ? data.rows
65 : self.fetchMarkup(),
66 cache = {data: '', bottom: 0},
67 scroll_top = self.scroll_elem.scrollTop;
68
69 // append initial data
70 self.insertToDOM(rows, cache);
71
72 // restore the scroll position
73 self.scroll_elem.scrollTop = scroll_top;
74
75 // adding scroll handler
76 var last_cluster = false,
77 scroll_debounce = 0,
78 pointer_events_set = false,
79 scrollEv = function() {
80 // fixes scrolling issue on Mac #3
81 if (is_mac) {
82 if( ! pointer_events_set) self.content_elem.style.pointerEvents = 'none';
83 pointer_events_set = true;
84 clearTimeout(scroll_debounce);
85 scroll_debounce = setTimeout(function () {
86 self.content_elem.style.pointerEvents = 'auto';
87 pointer_events_set = false;
88 }, 50);
89 }
90 if (last_cluster != (last_cluster = self.getClusterNum()))
91 self.insertToDOM(rows, cache);
92 if (self.options.callbacks.scrollingProgress)
93 self.options.callbacks.scrollingProgress(self.getScrollProgress());
94 },
95 resize_debounce = 0,
96 resizeEv = function() {
97 clearTimeout(resize_debounce);
98 resize_debounce = setTimeout(self.refresh, 100);
99 }
100 on('scroll', self.scroll_elem, scrollEv);
101 on('resize', window, resizeEv);
102
103 // public methods
104 self.destroy = function(clean) {
105 off('scroll', self.scroll_elem, scrollEv);
106 off('resize', window, resizeEv);
107 self.html((clean ? self.generateEmptyRow() : rows).join(''));
108 }
109 self.refresh = function(force) {
110 if(self.getRowsHeight(rows) || force) self.update(rows);
111 }
112 self.update = function(new_rows) {
113 rows = isArray(new_rows)
114 ? new_rows
115 : [];
116 var scroll_top = self.scroll_elem.scrollTop;
117 // fixes #39
118 if(rows.length * self.options.item_height < scroll_top) {
119 self.scroll_elem.scrollTop = 0;
120 last_cluster = 0;
121 }
122 self.insertToDOM(rows, cache);
123 self.scroll_elem.scrollTop = scroll_top;
124 }
125 self.clear = function() {
126 self.update([]);
127 }
128 self.getRowsAmount = function() {
129 return rows.length;
130 }
131 self.getScrollProgress = function() {
132 return this.options.scroll_top / (rows.length * this.options.item_height) * 100 || 0;
133 }
134
135 var add = function(where, _new_rows) {
136 var new_rows = isArray(_new_rows)
137 ? _new_rows
138 : [];
139 if( ! new_rows.length) return;
140 rows = where == 'append'
141 ? rows.concat(new_rows)
142 : new_rows.concat(rows);
143 self.insertToDOM(rows, cache);
144 }
145 self.append = function(rows) {
146 add('append', rows);
147 }
148 self.prepend = function(rows) {
149 add('prepend', rows);
150 }
151 }
152
153 Clusterize.prototype = {
154 constructor: Clusterize,
155 // fetch existing markup
156 fetchMarkup: function() {
157 var rows = [], rows_nodes = this.getChildNodes(this.content_elem);
158 while (rows_nodes.length) {
159 rows.push(rows_nodes.shift().outerHTML);
160 }
161 return rows;
162 },
163 // get tag name, content tag name, tag height, calc cluster height
164 exploreEnvironment: function(rows) {
165 var opts = this.options;
166 opts.content_tag = this.content_elem.tagName.toLowerCase();
167 if( ! rows.length) return;
168 if(ie && ie <= 9 && ! opts.tag) opts.tag = rows[0].match(/<([^>\s/]*)/)[1].toLowerCase();
169 if(this.content_elem.children.length <= 1) this.html(rows[0] + rows[0] + rows[0]);
170 if( ! opts.tag) opts.tag = this.content_elem.children[0].tagName.toLowerCase();
171 this.getRowsHeight(rows);
172 },
173 getRowsHeight: function(rows) {
174 var opts = this.options,
175 prev_item_height = opts.item_height;
176 opts.cluster_height = 0
177 if( ! rows.length) return;
178 var nodes = this.content_elem.children;
179 var node = nodes[Math.floor(nodes.length / 2)];
180 opts.item_height = node.offsetHeight;
181 // consider table's border-spacing
182 if(opts.tag == 'tr' && getStyle('borderCollapse', this.content_elem) != 'collapse')
183 opts.item_height += parseInt(getStyle('borderSpacing', this.content_elem), 10) || 0;
184 // consider margins (and margins collapsing)
185 if(opts.tag != 'tr') {
186 var marginTop = parseInt(getStyle('marginTop', node), 10) || 0;
187 var marginBottom = parseInt(getStyle('marginBottom', node), 10) || 0;
188 opts.item_height += Math.max(marginTop, marginBottom);
189 }
190 opts.block_height = opts.item_height * opts.rows_in_block;
191 opts.rows_in_cluster = opts.blocks_in_cluster * opts.rows_in_block;
192 opts.cluster_height = opts.blocks_in_cluster * opts.block_height;
193 return prev_item_height != opts.item_height;
194 },
195 // get current cluster number
196 getClusterNum: function () {
197 this.options.scroll_top = this.scroll_elem.scrollTop;
198 return Math.floor(this.options.scroll_top / (this.options.cluster_height - this.options.block_height)) || 0;
199 },
200 // generate empty row if no data provided
201 generateEmptyRow: function() {
202 var opts = this.options;
203 if( ! opts.tag || ! opts.show_no_data_row) return [];
204 var empty_row = document.createElement(opts.tag),
205 no_data_content = document.createTextNode(opts.no_data_text), td;
206 empty_row.className = opts.no_data_class;
207 if(opts.tag == 'tr') {
208 td = document.createElement('td');
209 // fixes #53
210 td.colSpan = 100;
211 td.appendChild(no_data_content);
212 }
213 empty_row.appendChild(td || no_data_content);
214 return [empty_row.outerHTML];
215 },
216 // generate cluster for current scroll position
217 generate: function (rows, cluster_num) {
218 var opts = this.options,
219 rows_len = rows.length;
220 if (rows_len < opts.rows_in_block) {
221 return {
222 top_offset: 0,
223 bottom_offset: 0,
224 rows_above: 0,
225 rows: rows_len ? rows : this.generateEmptyRow()
226 }
227 }
228 var items_start = Math.max((opts.rows_in_cluster - opts.rows_in_block) * cluster_num, 0),
229 items_end = items_start + opts.rows_in_cluster,
230 top_offset = Math.max(items_start * opts.item_height, 0),
231 bottom_offset = Math.max((rows_len - items_end) * opts.item_height, 0),
232 this_cluster_rows = [],
233 rows_above = items_start;
234 if(top_offset < 1) {
235 rows_above++;
236 }
237 for (var i = items_start; i < items_end; i++) {
238 rows[i] && this_cluster_rows.push(rows[i]);
239 }
240 return {
241 top_offset: top_offset,
242 bottom_offset: bottom_offset,
243 rows_above: rows_above,
244 rows: this_cluster_rows
245 }
246 },
247 renderExtraTag: function(class_name, height) {
248 var tag = document.createElement(this.options.tag),
249 clusterize_prefix = 'clusterize-';
250 tag.className = [clusterize_prefix + 'extra-row', clusterize_prefix + class_name].join(' ');
251 height && (tag.style.height = height + 'px');
252 return tag.outerHTML;
253 },
254 // if necessary verify data changed and insert to DOM
255 insertToDOM: function(rows, cache) {
256 // explore row's height
257 if( ! this.options.cluster_height) {
258 this.exploreEnvironment(rows);
259 }
260 var data = this.generate(rows, this.getClusterNum()),
261 this_cluster_rows = data.rows.join(''),
262 this_cluster_content_changed = this.checkChanges('data', this_cluster_rows, cache),
263 only_bottom_offset_changed = this.checkChanges('bottom', data.bottom_offset, cache),
264 callbacks = this.options.callbacks,
265 layout = [];
266
267 if(this_cluster_content_changed) {
268 if(data.top_offset) {
269 this.options.keep_parity && layout.push(this.renderExtraTag('keep-parity'));
270 layout.push(this.renderExtraTag('top-space', data.top_offset));
271 }
272 layout.push(this_cluster_rows);
273 data.bottom_offset && layout.push(this.renderExtraTag('bottom-space', data.bottom_offset));
274 callbacks.clusterWillChange && callbacks.clusterWillChange();
275 this.html(layout.join(''));
276 this.options.content_tag == 'ol' && this.content_elem.setAttribute('start', data.rows_above);
277 callbacks.clusterChanged && callbacks.clusterChanged();
278 } else if(only_bottom_offset_changed) {
279 this.content_elem.lastChild.style.height = data.bottom_offset + 'px';
280 }
281 },
282 // unfortunately ie <= 9 does not allow to use innerHTML for table elements, so make a workaround
283 html: function(data) {
284 var content_elem = this.content_elem;
285 if(ie && ie <= 9 && this.options.tag == 'tr') {
286 var div = document.createElement('div'), last;
287 div.innerHTML = '<table><tbody>' + data + '</tbody></table>';
288 while((last = content_elem.lastChild)) {
289 content_elem.removeChild(last);
290 }
291 var rows_nodes = this.getChildNodes(div.firstChild.firstChild);
292 while (rows_nodes.length) {
293 content_elem.appendChild(rows_nodes.shift());
294 }
295 } else {
296 content_elem.innerHTML = data;
297 }
298 },
299 getChildNodes: function(tag) {
300 var child_nodes = tag.children, nodes = [];
301 for (var i = 0, ii = child_nodes.length; i < ii; i++) {
302 nodes.push(child_nodes[i]);
303 }
304 return nodes;
305 },
306 checkChanges: function(type, value, cache) {
307 var changed = value != cache[type];
308 cache[type] = value;
309 return changed;
310 }
311 }
312
313 // support functions
314 function on(evt, element, fnc) {
315 return element.addEventListener ? element.addEventListener(evt, fnc, false) : element.attachEvent("on" + evt, fnc);
316 }
317 function off(evt, element, fnc) {
318 return element.removeEventListener ? element.removeEventListener(evt, fnc, false) : element.detachEvent("on" + evt, fnc);
319 }
320 function isArray(arr) {
321 return Object.prototype.toString.call(arr) === '[object Array]';
322 }
323 function getStyle(prop, elem) {
324 return window.getComputedStyle ? window.getComputedStyle(elem)[prop] : elem.currentStyle[prop];
325 }
326
327 return Clusterize;
328}));
\No newline at end of file