UNPKG

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