├── .gitignore ├── README.md ├── clusterize-cluster.js ├── clusterize.js ├── dev ├── autoheight.vue ├── basic.vue ├── flex.vue ├── loading.vue ├── presetRowHeight.vue ├── webpack.config.coffee └── withOtherComponentInside.vue ├── karma.conf.coffee ├── package.json ├── src ├── clusterize-cluster.vue └── clusterize.vue └── test └── clusterize.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.sublime-project 3 | *.sublime-workspace 4 | npm-debug.log 5 | build 6 | dev/index.js 7 | static 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-clusterize 2 | 3 | An implementation of [Clusterize.js](https://nexts.github.io/Clusterize.js/) in [vue](http://vuejs.org/). 4 | 5 | Works similar to `v-for` but only takes enough data to fill the viewport 3 times. 6 | This data is then splitted into three clusters which will move and get filled with the right data on scrolling. 7 | 8 | ### [Demo](https://vue-comps.github.io/vue-clusterize/) 9 | 10 | # Disclaimer 11 | 12 | Only for [**webpack**](https://webpack.github.io/) workflows. 13 | 14 | **No jQuery dependency** 15 | 16 | # Install 17 | 18 | ```sh 19 | npm install --save-dev vue-clusterize 20 | ``` 21 | 22 | use version 0.2.0 before vue `1.0.24` 23 | 24 | ## Usage 25 | ```coffee 26 | # link the components up 27 | components: 28 | "clusterize": require("vue-clusterize") 29 | # or ES6 30 | import clusterize from "vue-clusterize" 31 | components: { 32 | "clusterize": clusterize 33 | } 34 | ``` 35 | ```html 36 | 37 | 38 |
{{data}}
39 | 40 |

loading...

41 |
42 | ``` 43 | For examples see [`dev/`](https://github.com/vue-comps/vue-clusterize/tree/master/dev). 44 | 45 | #### Available variables in template 46 | | Name | type | description | 47 | | ---:| --- | --- | 48 | | data | Object | a single datapiece (see `binding-name` in props) | 49 | | loading | Number | will be 0 when finished loading data (only with dynamic data) | 50 | | index | Number | index of the datapiece | 51 | | height | Number | the height of a single row | 52 | 53 | you can add your own variables with the `row-watchers` prop. 54 | 55 | example: 56 | ```html 57 | 58 |
{{data}} - index: {{index}}
59 |

loading...

60 |
61 | ``` 62 | 63 | #### Props 64 | | Name | type | default | description | 65 | | ---:| --- | ---| --- | 66 | | binding-name | String | "data" | name to access the data in your template | 67 | | height | Number | null | Height of the clusterize element | 68 | | auto-height | Boolean | false | If autoheight should be used (see below) | 69 | | manual-start | Boolean | false | rendering doesn't start on `ready` (call `start` on the component instance instead)| 70 | | data | Array | [] | static data to render | 71 | | scroll-top | Number | 0 | sets scrollTop | 72 | | scroll-left | Number | 0 | sets scrollLeft | 73 | | cluster-size-fac | Number | 1.5 | determines the cluster size relative to visible size | 74 | | row-height | Number | null | enforced row-height, will be determined at runtime when not set | 75 | | template | String | - | row template (defaults to slot content) | 76 | | style | Object | - | to pass trough style (vue object) | 77 | | row-watchers | Object | {height: {vm: this, prop:"rowHeight"}} | variables, will be available in template | 78 | | parent-vm | Object | this.$parent | where to resolve components in template | 79 | | flex | Boolean | false | allow multiple items per row. See [flex](#flex). | 80 | | flex-initial | Number | 20 | data pieces to take for calculation of row height (should fill several rows) | 81 | | flex-fac | Number | 1 | reduce to reduce items per row | 82 | 83 | ## Autoheight 84 | 85 | There are two ways clusterize can be used, either use a fixed height: 86 | ```html 87 | 88 | ``` 89 | 90 | Or use autoheight: 91 | ```html 92 | 93 | 94 |
95 | 96 | ``` 97 | In this case clusterize will always fill the nearest parent element with either `position:relative;` or `position:absolute;`. 98 | Keep in mind, that `padding` of the parent will be ignored. If you need a padding, use a wrapper `
`. 99 | 100 | ## Dynamic data 101 | 102 | The clusterize instance emits two events to get dynamic data: 103 | ```html 104 | 105 | ``` 106 | ```js 107 | methods: 108 | # For the first datapiece, first and last will be 0 109 | getData: function(first,last,cb) { 110 | # somehow get data 111 | cb(data) 112 | } 113 | getDataCount: function(cb) { 114 | cb(dataCount) 115 | } 116 | ``` 117 | 118 | To issue a manual redraw, call `redraw()` on the clusterize instance. 119 | 120 | If you want to enforce a scroll-to-top use the `scrollTop` prop. 121 | 122 | ## Flex 123 | 124 | When using the `flex` prop, the usage changes. You will now recieve a array of row items per row which you can use in a `v-for`: 125 | ```html 126 | 127 |
128 |
{{d}}
129 |
130 |
131 | ``` 132 | The row height, items per row and rows per cluster will be recalculated on resize of clusterize. 133 | 134 | # Development 135 | Clone repository. 136 | ```sh 137 | npm install 138 | npm run test 139 | ``` 140 | Browse to `http://localhost:8080/`. 141 | 142 | ## To-Do 143 | - use html5 history mode or document.store to save scroll position 144 | 145 | ## License 146 | Copyright (c) 2016 Paul Pflugradt 147 | Licensed under the MIT license. 148 | -------------------------------------------------------------------------------- /clusterize-cluster.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mixins: [require("vue-mixins/vue")], 3 | props: { 4 | "bindingName": { 5 | type: String, 6 | "default": "data" 7 | }, 8 | "loading": { 9 | type: Number, 10 | "default": 0 11 | }, 12 | "nr": { 13 | type: Number 14 | }, 15 | "index": { 16 | type: Number 17 | }, 18 | "height": { 19 | type: Number 20 | }, 21 | "data": { 22 | type: Array, 23 | "default": function() { 24 | return []; 25 | } 26 | }, 27 | "rowWatchers": { 28 | type: Object 29 | }, 30 | "parentVm": { 31 | type: Object 32 | } 33 | }, 34 | data: function() { 35 | return { 36 | isCluster: true, 37 | factory: null, 38 | Vue: null, 39 | end: null, 40 | frags: [] 41 | }; 42 | }, 43 | ready: function() { 44 | var key, ref, results, val; 45 | this.end = this.Vue.util.createAnchor('clusterize-cluster-end'); 46 | this.$el.appendChild(this.end); 47 | ref = this.rowWatchers; 48 | results = []; 49 | for (key in ref) { 50 | val = ref[key]; 51 | results.push(this.initRowWatchers(key, val)); 52 | } 53 | return results; 54 | }, 55 | methods: { 56 | createFrag: function(i) { 57 | var frag, key, parentScope, ref, scope, val; 58 | parentScope = this.parentVm; 59 | scope = Object.create(parentScope); 60 | scope.$refs = Object.create(parentScope.$refs); 61 | scope.$els = Object.create(parentScope.$els); 62 | scope.$parent = parentScope; 63 | scope.$forContext = this; 64 | this.Vue.util.defineReactive(scope, this.bindingName, this.data[i]); 65 | this.Vue.util.defineReactive(scope, "loading", this.loading); 66 | this.Vue.util.defineReactive(scope, "index", this.index + i); 67 | ref = this.rowWatchers; 68 | for (key in ref) { 69 | val = ref[key]; 70 | this.Vue.util.defineReactive(scope, key, val.vm[val.prop]); 71 | scope[key] = val.vm[val.prop]; 72 | } 73 | frag = this.factory.create(this, scope, this.$options._frag); 74 | frag.before(this.end); 75 | return this.frags[i] = frag; 76 | }, 77 | updateIndex: function() { 78 | var frag, i, j, len, ref, results; 79 | ref = this.frags; 80 | results = []; 81 | for (i = j = 0, len = ref.length; j < len; i = ++j) { 82 | frag = ref[i]; 83 | results.push(frag.scope.index = this.index + i); 84 | } 85 | return results; 86 | }, 87 | destroyFrag: function(i) { 88 | return this.frags[i].remove(); 89 | }, 90 | initRowWatchers: function(key, obj) { 91 | var self; 92 | self = this; 93 | return obj.vm.$watch(obj.prop, function(val) { 94 | var frag, j, len, ref, results; 95 | ref = self.frags; 96 | results = []; 97 | for (j = 0, len = ref.length; j < len; j++) { 98 | frag = ref[j]; 99 | results.push(frag.scope[key] = val); 100 | } 101 | return results; 102 | }); 103 | }, 104 | redraw: function() { 105 | var i, j, ref, results; 106 | if (this.frags.length > 0) { 107 | results = []; 108 | for (i = j = 0, ref = this.frags.length; 0 <= ref ? j < ref : j > ref; i = 0 <= ref ? ++j : --j) { 109 | this.destroyFrag(i); 110 | results.push(this.createFrag(i)); 111 | } 112 | return results; 113 | } 114 | } 115 | }, 116 | watch: { 117 | "index": "updateIndex", 118 | "factory": "redraw", 119 | "rowWatchers": function(newRW, oldRW) { 120 | var key, results, val; 121 | results = []; 122 | for (key in newRW) { 123 | val = newRW[key]; 124 | if (oldRW[key] == null) { 125 | results.push(this.initRowWatchers(key, val)); 126 | } else { 127 | results.push(void 0); 128 | } 129 | } 130 | return results; 131 | }, 132 | data: function(newData, oldData) { 133 | var diff, frag, i, index, j, k, l, len, ref, ref1, ref2, results; 134 | diff = newData.length - oldData.length; 135 | if (diff > 0) { 136 | for (i = j = 0, ref = diff; 0 <= ref ? j < ref : j > ref; i = 0 <= ref ? ++j : --j) { 137 | this.createFrag(oldData.length + i); 138 | } 139 | } else if (diff < 0) { 140 | for (i = k = ref1 = diff; ref1 <= 0 ? k < 0 : k > 0; i = ref1 <= 0 ? ++k : --k) { 141 | this.destroyFrag(oldData.length + i); 142 | } 143 | } 144 | ref2 = this.frags; 145 | results = []; 146 | for (index = l = 0, len = ref2.length; l < len; index = ++l) { 147 | frag = ref2[index]; 148 | results.push(frag.scope.data = newData[index]); 149 | } 150 | return results; 151 | }, 152 | loading: function(newLoading) { 153 | var frag, j, len, ref, results; 154 | ref = this.frags; 155 | results = []; 156 | for (j = 0, len = ref.length; j < len; j++) { 157 | frag = ref[j]; 158 | results.push(frag.scope.loading = newLoading); 159 | } 160 | return results; 161 | } 162 | } 163 | }; 164 | 165 | if (module.exports.__esModule) module.exports = module.exports.default 166 | ;(typeof module.exports === "function"? module.exports.options: module.exports).template = "
loading...
" 167 | -------------------------------------------------------------------------------- /clusterize.js: -------------------------------------------------------------------------------- 1 | var modulo = function(a, b) { return (+a % (b = +b) + b) % b; }; 2 | 3 | module.exports = { 4 | mixins: [require("vue-mixins/onElementResize"), require("vue-mixins/vue"), require("vue-mixins/fragToString")], 5 | components: { 6 | "clusterize-cluster": require("./clusterize-cluster") 7 | }, 8 | props: { 9 | "bindingName": { 10 | type: String, 11 | "default": "data" 12 | }, 13 | "height": { 14 | type: Number 15 | }, 16 | "autoHeight": { 17 | type: Boolean, 18 | "default": false 19 | }, 20 | "manualStart": { 21 | type: Boolean, 22 | "default": false 23 | }, 24 | "data": { 25 | type: Array 26 | }, 27 | "scrollTop": { 28 | type: Number, 29 | "default": 0 30 | }, 31 | "scrollLeft": { 32 | type: Number, 33 | "default": 0 34 | }, 35 | "clusterSizeFac": { 36 | type: Number, 37 | "default": 1.5 38 | }, 39 | "rowHeight": { 40 | type: Number 41 | }, 42 | "template": { 43 | type: String 44 | }, 45 | "style": { 46 | type: Object 47 | }, 48 | "rowWatchers": { 49 | type: Object, 50 | "default": function() { 51 | return { 52 | height: { 53 | vm: this, 54 | prop: "rowHeight" 55 | } 56 | }; 57 | } 58 | }, 59 | "parentVm": { 60 | type: Object, 61 | "default": function() { 62 | return this.$parent; 63 | } 64 | }, 65 | "flex": { 66 | type: Boolean, 67 | "default": false 68 | }, 69 | "flexInitial": { 70 | type: Number, 71 | "default": 20 72 | }, 73 | "flexFac": { 74 | type: Number, 75 | "default": 1 76 | } 77 | }, 78 | computed: { 79 | position: function() { 80 | if (this.autoHeight) { 81 | if (this.disposeResizeCb == null) { 82 | this.disposeResizeCb = this.onElementResize(this.$el, this.updateHeight); 83 | } 84 | return "absolute"; 85 | } else if (this.flex) { 86 | if (this.disposeResizeCb == null) { 87 | this.disposeResizeCb = this.onElementResize(this.$el, this.updateHeight); 88 | } 89 | } else { 90 | if (typeof this.disposeResizeCb === "function") { 91 | this.disposeResizeCb(); 92 | } 93 | } 94 | return null; 95 | }, 96 | computedStyle: function() { 97 | var key, ref, style, val; 98 | if (!this.state.started) { 99 | return null; 100 | } 101 | style = { 102 | height: this.height + 'px', 103 | position: this.position, 104 | top: this.autoHeight ? 0 : null, 105 | bottom: this.autoHeight ? 0 : null, 106 | left: this.autoHeight ? 0 : null, 107 | right: this.autoHeight ? 0 : null, 108 | overflow: "auto" 109 | }; 110 | if (this.style != null) { 111 | ref = this.style; 112 | for (key in ref) { 113 | val = ref[key]; 114 | style[key] = val; 115 | } 116 | } 117 | return style; 118 | } 119 | }, 120 | data: function() { 121 | return { 122 | clusters: [], 123 | firstRowHeight: null, 124 | lastRowHeight: null, 125 | rowCount: null, 126 | rowsCount: null, 127 | itemsPerRow: 1, 128 | clustersCount: null, 129 | clusterHeight: null, 130 | clusterSize: null, 131 | clustersBelow: 2, 132 | clusterVisible: 0, 133 | clusterVisibleLast: -1, 134 | offsetHeight: 0, 135 | itemWidth: 0, 136 | minHeight: null, 137 | lastScrollTop: this.scrollTop, 138 | lastScrollLeft: this.scrollLeft, 139 | state: { 140 | started: false, 141 | startFinished: false, 142 | loading: false 143 | } 144 | }; 145 | }, 146 | methods: { 147 | updateHeight: function() { 148 | var changedHeight, changedWidth, process; 149 | process = (function(_this) { 150 | return function() { 151 | var data, l, len, oldData, ref, tmp; 152 | if (_this.flex) { 153 | oldData = _this.clusters[0].data; 154 | tmp = []; 155 | ref = _this.clusters[0].data; 156 | for (l = 0, len = ref.length; l < len; l++) { 157 | data = ref[l]; 158 | tmp = tmp.concat(data); 159 | if (tmp.length >= _this.flexInitial) { 160 | break; 161 | } 162 | } 163 | _this.clusters[0].data = [tmp]; 164 | return _this.$nextTick(function() { 165 | _this.calcRowHeight(); 166 | return _this.processClusterChange(_this.$el.scrollTop, true); 167 | }); 168 | } else { 169 | _this.calcClusterSize(); 170 | return _this.processClusterChange(_this.$el.scrollTop, true); 171 | } 172 | }; 173 | })(this); 174 | if (this.state.startFinished && this.rowHeight > -1) { 175 | changedHeight = Math.abs(this.offsetHeight - this.$el.offsetHeight) / this.clusterHeight * this.clusterSizeFac > 0.2; 176 | if (this.flex) { 177 | changedWidth = this.$el.clientWidth - this.itemsPerRow * this.itemWidth; 178 | if (changedWidth > this.itemWidth || changedWidth < 1) { 179 | return process(); 180 | } 181 | } else if (changedHeight) { 182 | return process(); 183 | } 184 | } 185 | }, 186 | start: function(top) { 187 | var count; 188 | if (top == null) { 189 | top = this.$el.scrollTop; 190 | } 191 | this.state.started = true; 192 | this.processTemplate(); 193 | this.state.loading = true; 194 | if (this.data != null) { 195 | this.$watch("data", this.processData); 196 | } 197 | count = 0; 198 | if (this.flex) { 199 | count = this.flexInitial; 200 | } 201 | if (!this.rowHeight || this.flex) { 202 | return this.getData(0, count, (function(_this) { 203 | return function(data) { 204 | _this.getAndProcessDataCount(); 205 | _this.clusters[0].index = 0; 206 | if (_this.flex) { 207 | _this.clusters[0].data = [data]; 208 | } else { 209 | _this.clusters[0].data = data; 210 | } 211 | return _this.$nextTick(function() { 212 | _this.calcRowHeight(); 213 | _this.processScroll(top); 214 | return _this.state.startFinished = true; 215 | }); 216 | }; 217 | })(this)); 218 | } else { 219 | this.getAndProcessDataCount(); 220 | return this.$nextTick((function(_this) { 221 | return function() { 222 | _this.calcClusterSize(); 223 | _this.processScroll(top); 224 | return _this.state.startFinished = true; 225 | }; 226 | })(this)); 227 | } 228 | }, 229 | getData: function(first, last, cb) { 230 | if (this.data != null) { 231 | return cb(this.data.slice(first, +last + 1 || 9e9)); 232 | } else { 233 | return this.$emit("get-data", first, last, cb); 234 | } 235 | }, 236 | getAndProcessDataCount: function() { 237 | var getDataCount, processDataCount; 238 | getDataCount = (function(_this) { 239 | return function(cb) { 240 | if (_this.data != null) { 241 | return cb(_this.data.length); 242 | } else { 243 | return _this.$emit("get-data-count", cb); 244 | } 245 | }; 246 | })(this); 247 | processDataCount = (function(_this) { 248 | return function(count) { 249 | if (count > 0) { 250 | _this.dataCount = count; 251 | _this.clustersCount = Math.ceil(_this.dataCount / _this.itemsPerRow / _this.clusterSize); 252 | return _this.updateLastRowHeight(); 253 | } 254 | }; 255 | })(this); 256 | return getDataCount(processDataCount); 257 | }, 258 | calcRowHeight: function() { 259 | var child, el, height, i, items, itemsPerRow, itemsPerRowLast, j, k, l, lastTop, maxHeights, rect, ref, row, style, width; 260 | if (this.flex) { 261 | maxHeights = [0]; 262 | el = this.clusters[0].$el; 263 | lastTop = Number.MIN_VALUE; 264 | itemsPerRow = []; 265 | itemsPerRowLast = 0; 266 | row = el.children[1]; 267 | items = row.children.length - 1; 268 | width = 0; 269 | k = 0; 270 | for (i = l = 1, ref = items; 1 <= ref ? l <= ref : l >= ref; i = 1 <= ref ? ++l : --l) { 271 | child = row.children[i]; 272 | if (!child) { 273 | return; 274 | } 275 | rect = child.getBoundingClientRect(); 276 | style = window.getComputedStyle(child); 277 | height = rect.height + parseInt(style.marginTop, 10) + parseInt(style.marginBottom, 10); 278 | width += rect.width; 279 | if (rect.top > lastTop + maxHeights[k] * 1 / 3 && i > 1) { 280 | j = i - 1; 281 | k++; 282 | itemsPerRow.push(j - itemsPerRowLast); 283 | itemsPerRowLast = j; 284 | lastTop = rect.top; 285 | maxHeights.push(height); 286 | } else { 287 | if (lastTop < rect.top) { 288 | lastTop = rect.top; 289 | } 290 | if (maxHeights[maxHeights.length - 1] < height) { 291 | maxHeights[maxHeights.length - 1] = height; 292 | } 293 | } 294 | } 295 | itemsPerRow.shift(); 296 | maxHeights.shift(); 297 | if (itemsPerRow.length > 0) { 298 | this.itemsPerRow = Math.floor(itemsPerRow.reduce(function(a, b) { 299 | return a + b; 300 | }) / itemsPerRow.length * this.flexFac); 301 | } else { 302 | this.itemsPerRow = items; 303 | } 304 | if (this.itemsPerRow === 0) { 305 | this.itemsPerRow = 1; 306 | } 307 | this.itemWidth = width / items; 308 | if (maxHeights.length > 0) { 309 | this.rowHeight = maxHeights.reduce(function(a, b) { 310 | return a + b; 311 | }) / maxHeights.length; 312 | } else { 313 | this.rowHeight = height; 314 | } 315 | } else { 316 | this.rowHeight = this.clusters[0].$el.children[1].getBoundingClientRect().height; 317 | } 318 | return this.calcClusterSize(); 319 | }, 320 | calcClusterSize: function() { 321 | var cluster, l, len, ref, results; 322 | this.offsetHeight = this.$el.offsetHeight; 323 | this.clusterSize = Math.ceil(this.$el.offsetHeight / this.rowHeight * this.clusterSizeFac) * this.itemsPerRow; 324 | if (this.dataCount) { 325 | this.clustersCount = Math.ceil(this.dataCount / this.itemsPerRow / this.clusterSize); 326 | if (this.clustersCount < 3) { 327 | this.clustersCount = 3; 328 | } 329 | this.updateLastRowHeight(); 330 | } 331 | this.clusterHeight = this.rowHeight * this.clusterSize / this.itemsPerRow; 332 | ref = this.clusters; 333 | results = []; 334 | for (l = 0, len = ref.length; l < len; l++) { 335 | cluster = ref[l]; 336 | results.push(cluster.height = this.clusterHeight); 337 | } 338 | return results; 339 | }, 340 | updateLastRowHeight: function() { 341 | var newHeight; 342 | if (this.dataCount && this.clusterSize) { 343 | newHeight = (this.dataCount - (this.clusterVisible + this.clustersBelow + 1) * this.clusterSize) * this.rowHeight / this.itemsPerRow; 344 | if (newHeight > 0) { 345 | return this.lastRowHeight = newHeight; 346 | } else { 347 | return this.lastRowHeight = 0; 348 | } 349 | } 350 | }, 351 | processScroll: function(top) { 352 | this.clusterVisible = Math.floor(top / this.clusterHeight + 0.5); 353 | if (this.clusterVisibleLast !== this.clusterVisible) { 354 | this.processClusterChange(top); 355 | return this.clusterVisibleLast = this.clusterVisible; 356 | } 357 | }, 358 | processClusterChange: function(top, repaint) { 359 | var absI, absIs, down, l, len, m, n, position, ref, ref1, relI, results, results1; 360 | if (top == null) { 361 | top = this.$el.scrollTop; 362 | } 363 | if (repaint == null) { 364 | repaint = false; 365 | } 366 | down = this.clusterVisibleLast < this.clusterVisible; 367 | if (this.clusterVisible === 0) { 368 | this.clustersBelow = 2; 369 | } else if (this.clusterVisible === this.clustersCount - 1) { 370 | this.clustersBelow = 0; 371 | } else { 372 | this.clustersBelow = 1; 373 | } 374 | position = this.clusterVisible + this.clustersBelow; 375 | if (down) { 376 | absIs = (function() { 377 | results = []; 378 | for (var l = ref = position - 2; ref <= position ? l <= position : l >= position; ref <= position ? l++ : l--){ results.push(l); } 379 | return results; 380 | }).apply(this); 381 | } else { 382 | absIs = (function() { 383 | results1 = []; 384 | for (var m = position, ref1 = position - 2; position <= ref1 ? m <= ref1 : m >= ref1; position <= ref1 ? m++ : m--){ results1.push(m); } 385 | return results1; 386 | }).apply(this); 387 | } 388 | for (n = 0, len = absIs.length; n < len; n++) { 389 | absI = absIs[n]; 390 | relI = absI % 3; 391 | if (this.clusters[relI].nr !== absI || repaint) { 392 | if (down) { 393 | this.clusters[relI].$before(this.$els.lastRow); 394 | } else { 395 | this.clusters[relI].$after(this.$els.firstRow); 396 | } 397 | this.clusters[relI].nr = absI; 398 | this.clusters[relI].index = absI * this.clusterSize; 399 | this.fillClusterWithData(this.clusters[relI], absI * this.clusterSize, (absI + 1) * this.clusterSize - 1); 400 | } 401 | } 402 | this.updateFirstRowHeight(); 403 | return this.updateLastRowHeight(); 404 | }, 405 | fillClusterWithData: function(cluster, first, last) { 406 | var loading; 407 | if (this.state.loading) { 408 | this.state.loading = false; 409 | this.$emit("clusterize-loaded"); 410 | } 411 | cluster.loading += 1; 412 | loading = cluster.loading; 413 | this.$emit("cluster-loading", cluster.nr); 414 | return this.getData(first, last, (function(_this) { 415 | return function(data) { 416 | var currentData, d, data2, i, l, len; 417 | if (cluster.loading === loading) { 418 | if (data.length !== _this.clusterSize) { 419 | cluster.height = data.length * _this.rowHeight / _this.itemsPerRow; 420 | } else { 421 | cluster.height = _this.clusterHeight; 422 | } 423 | if (_this.flex) { 424 | data2 = []; 425 | currentData = []; 426 | for (i = l = 0, len = data.length; l < len; i = ++l) { 427 | d = data[i]; 428 | if (modulo(i, _this.itemsPerRow) === 0) { 429 | currentData = []; 430 | data2.push(currentData); 431 | } 432 | currentData.push(d); 433 | } 434 | cluster.data = data2; 435 | } else { 436 | cluster.data = data; 437 | } 438 | cluster.loading = 0; 439 | return _this.$emit("cluster-loaded", cluster.nr); 440 | } 441 | }; 442 | })(this)); 443 | }, 444 | updateFirstRowHeight: function() { 445 | var newHeight; 446 | newHeight = (this.clusterVisible - (2 - this.clustersBelow)) * this.clusterHeight; 447 | if (newHeight > 0) { 448 | return this.firstRowHeight = newHeight; 449 | } else { 450 | return this.firstRowHeight = 0; 451 | } 452 | }, 453 | onScroll: function(e) { 454 | var top; 455 | top = this.$el.scrollTop; 456 | this.$emit("scroll-y", top); 457 | this.$emit("scroll-x", this.$el.scrollLeft); 458 | if (this.lastScrollTop !== top) { 459 | this.lastScrollTop = top; 460 | return this.processScroll(top); 461 | } 462 | }, 463 | processData: function(newData, oldData) { 464 | if (newData !== oldData) { 465 | return this.redraw(); 466 | } 467 | }, 468 | redraw: function() { 469 | this.getAndProcessDataCount(); 470 | return this.processClusterChange(this.$el.scrollTop, true); 471 | }, 472 | processTemplate: function() { 473 | var cluster, factory, l, len, ref, results; 474 | if (this.state.started) { 475 | if (!this.template) { 476 | this.template = this.fragToString(this._slotContents["default"]); 477 | } 478 | factory = new this.Vue.FragmentFactory(this.parentVm, this.template); 479 | ref = this.clusters; 480 | results = []; 481 | for (l = 0, len = ref.length; l < len; l++) { 482 | cluster = ref[l]; 483 | results.push(cluster.factory = factory); 484 | } 485 | return results; 486 | } 487 | } 488 | }, 489 | ready: function() { 490 | var child, l, len, ref; 491 | ref = this.$children; 492 | for (l = 0, len = ref.length; l < len; l++) { 493 | child = ref[l]; 494 | if (child.isCluster) { 495 | this.clusters.push(child); 496 | } 497 | } 498 | if (!this.manualStart) { 499 | return this.start(); 500 | } 501 | }, 502 | watch: { 503 | "height": "updateHeight", 504 | "scrollTop": function(val) { 505 | if (val !== this.$el.scrollTop) { 506 | this.$el.scrollTop = val; 507 | return this.processScroll(val); 508 | } 509 | }, 510 | "template": "processTemplate", 511 | "rowWatchers": function(val) { 512 | if (val.height == null) { 513 | val.height = { 514 | vm: this, 515 | prop: "rowHeight" 516 | }; 517 | } 518 | return val; 519 | } 520 | } 521 | }; 522 | 523 | if (module.exports.__esModule) module.exports = module.exports.default 524 | ;(typeof module.exports === "function"? module.exports.options: module.exports).template = "
" 525 | -------------------------------------------------------------------------------- /dev/autoheight.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 32 | 33 | 41 | -------------------------------------------------------------------------------- /dev/basic.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 16 | 24 | -------------------------------------------------------------------------------- /dev/flex.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 60 | 61 | 69 | -------------------------------------------------------------------------------- /dev/loading.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | 25 | 37 | -------------------------------------------------------------------------------- /dev/presetRowHeight.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | 16 | 24 | -------------------------------------------------------------------------------- /dev/webpack.config.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | module: 3 | loaders: [ 4 | { test: /\.vue$/, loader: "vue-loader"} 5 | { test: /\.html$/, loader: "html"} 6 | { test: /\.css$/, loader: "style-loader!css-loader" } 7 | ] 8 | resolve: 9 | extensions: ["",".js",".vue",".coffee"] 10 | -------------------------------------------------------------------------------- /dev/withOtherComponentInside.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | 18 | 30 | -------------------------------------------------------------------------------- /karma.conf.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (config) -> 2 | config.set 3 | preprocessors: 4 | "**/*.coffee": ["webpack",'sourcemap'] 5 | webpack: 6 | devtool: 'inline-source-map' 7 | resolve: 8 | extensions: ["",".js",".coffee",".vue"] 9 | module: 10 | loaders: [ 11 | { test: /\.coffee$/, loader: "coffee-loader" } 12 | { test: /\.vue$/, loader: "vue-loader" } 13 | { test: /\.html$/, loader: "html"} 14 | { test: /\.css$/, loader: "style-loader!css-loader" } 15 | ] 16 | webpackMiddleware: 17 | noInfo: true 18 | files: ["test/*.coffee"] 19 | frameworks: ["mocha","chai-dom","chai-spies","chai","vue-component"] 20 | plugins: [ 21 | require("karma-chai") 22 | require("karma-chai-dom") 23 | require("karma-chrome-launcher") 24 | require("karma-firefox-launcher") 25 | require("karma-mocha") 26 | require("karma-webpack") 27 | require("karma-sourcemap-loader") 28 | require("karma-spec-reporter") 29 | require("karma-chai-spies") 30 | require("karma-vue-component") 31 | ] 32 | browsers: ["Chrome","Firefox"] 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-clusterize", 3 | "description": "clusterize - done in vue", 4 | "version": "0.5.1", 5 | "homepage": "https://github.com/paulpflug", 6 | "author": { 7 | "name": "Paul Pflugradt", 8 | "email": "paul.pflugradt@gmail.com" 9 | }, 10 | "license": "MIT", 11 | "main": "clusterize.js", 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/paulpflug/vue-clusterize" 15 | }, 16 | "engines": { 17 | "node": "*" 18 | }, 19 | "dependencies": { 20 | "vue-mixins": "^0.2.10" 21 | }, 22 | "devDependencies": { 23 | "chai": "^3.5.0", 24 | "chai-spies": "^0.7.1", 25 | "coffee-loader": "^0.7.2", 26 | "coffee-script": "^1.10.0", 27 | "css-loader": "^0.23.1", 28 | "gh-pages": "^0.11.0", 29 | "karma": "^0.13.22", 30 | "karma-chai": "^0.1.0", 31 | "karma-chai-dom": "^1.1.0", 32 | "karma-chai-spies": "^0.1.4", 33 | "karma-chrome-launcher": "^1.0.1", 34 | "karma-firefox-launcher": "^1.0.0", 35 | "karma-mocha": "^1.0.1", 36 | "karma-sourcemap-loader": "^0.3.7", 37 | "karma-spec-reporter": "0.0.26", 38 | "karma-vue-component": "^0.1.0", 39 | "karma-webpack": "^1.7.0", 40 | "mocha": "^2.5.3", 41 | "pug": "^2.0.0-beta3", 42 | "template-html-loader": "0.0.3", 43 | "vue": "^1.0.25", 44 | "vue-compiler": "^0.3.0", 45 | "vue-comps-tooltip": "^0.2.0", 46 | "vue-dev-server": "^0.2.10", 47 | "vue-html-loader": "^1.2.2", 48 | "vue-loader": "^8.5.2", 49 | "webpack": "^1.13.1" 50 | }, 51 | "keywords": [ 52 | "clusterize", 53 | "vue", 54 | "invisible pagination" 55 | ], 56 | "readmeFilename": "README.md", 57 | "scripts": { 58 | "build": "NODE_ENV=production vue-compiler --out . src/*.vue", 59 | "dev": "vue-dev-server", 60 | "watch": "karma start --browsers Chrome --auto-watch --reporters spec", 61 | "test": "karma start --single-run", 62 | "preversion": "npm test", 63 | "version": "npm run build && git add .", 64 | "postversion": "git push && git push --tags && npm publish", 65 | "ghpages": "vue-dev-server --static static/ && gh-pages -d static" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/clusterize-cluster.vue: -------------------------------------------------------------------------------- 1 | // out: .. 2 | 13 | 14 | 109 | -------------------------------------------------------------------------------- /src/clusterize.vue: -------------------------------------------------------------------------------- 1 | // out: .. 2 | 17 | 18 | 375 | -------------------------------------------------------------------------------- /test/clusterize.coffee: -------------------------------------------------------------------------------- 1 | env = null 2 | cl = null 3 | clel = null 4 | overallHeight = null 5 | describe "clusterize", -> 6 | 7 | 8 | describe "basic env", -> 9 | 10 | before -> 11 | env = loadComp(require("../dev/basic.vue")) 12 | cl = env.$refs.clusterize 13 | 14 | after -> 15 | unloadComp(env) 16 | 17 | it "should render clusterize", -> 18 | should.exist(cl) 19 | should.exist(cl.$el) 20 | clel = cl.$el 21 | 22 | it "should have class clusterize", -> 23 | clel.should.have.class "clusterize" 24 | 25 | it "should have element clusterize-first-row and last-row", -> 26 | clel.should.contain "div.clusterize-first-row" 27 | clel.should.contain "div.clusterize-last-row" 28 | 29 | 30 | it "should contain three clusters", -> 31 | clel.querySelectorAll("div.clusterize-cluster").should.have.length(3) 32 | 33 | it "should emit event clusterize-loaded", (done) -> 34 | cl.$once "clusterize-loaded", done 35 | 36 | 37 | describe "after loaded", -> 38 | 39 | 40 | describe "clusterize-first-row", -> 41 | 42 | it "should have a height of 0", -> 43 | clfrel = clel.querySelector "div.clusterize-first-row" 44 | clfrel.should.exist 45 | clfrel.should.have.attr("style","height: 0px;") 46 | 47 | 48 | describe "clusterize-last-row", -> 49 | 50 | it "should have a height", -> 51 | cllrel = clel.querySelector "div.clusterize-last-row" 52 | cllrel.should.exist 53 | overallHeight = cl.rowHeight * 10000 54 | cllrel.should.have.attr("style","height: #{overallHeight-3*cl.clusterSize*cl.rowHeight}px;") 55 | 56 | 57 | describe "the clusters", -> 58 | clsels = null 59 | 60 | it "should have a height", -> 61 | clsels = clel.querySelectorAll("div.clusterize-cluster") 62 | for clusterel in clsels 63 | clusterel.should.have.attr("style").match(new RegExp("height: #{cl.clusterSize*cl.rowHeight}px;")) 64 | 65 | it "should have cl.clusterSize rows", -> 66 | for clusterel in clsels 67 | clusterel.querySelectorAll("div.clusterize-row").should.have.length(cl.clusterSize) 68 | 69 | it "should have the right data", -> 70 | i = 1 71 | for clusterel in clsels 72 | rowels = clusterel.querySelectorAll("div.clusterize-row") 73 | for row in rowels 74 | row.should.have.text "#{i} - index: #{i-1}" 75 | i++ 76 | 77 | 78 | 79 | 80 | describe "scrolling", -> 81 | 82 | it "should scroll up 3/2 clustersize without change", -> 83 | cl.scrollTop = Math.floor(3/2*cl.clusterHeight-1) 84 | clel.querySelector("div.clusterize-first-row").should.have.attr("style","height: 0px;") 85 | i = 1 86 | for clusterel in clel.querySelectorAll("div.clusterize-cluster") 87 | rowels = clusterel.querySelectorAll("div.clusterize-row") 88 | for row in rowels 89 | row.should.have.text "#{i} - index: #{i-1}" 90 | i++ 91 | 92 | it "should transit at scroll top 918", (done) -> 93 | cl.$once "cluster-loading", (nr) -> 94 | nr.should.equal 3 95 | cl.scrollTop = Math.floor(3/2*cl.clusterHeight) 96 | cl.$nextTick -> 97 | clel.querySelector("div.clusterize-first-row").should.have.attr("style","height: #{cl.clusterSize*cl.rowHeight}px;") 98 | clel.querySelector("div.clusterize-last-row").should.have.attr("style","height: #{overallHeight-4*cl.clusterSize*cl.rowHeight}px;") 99 | i = cl.clusterSize+1 100 | for clusterel in clel.querySelectorAll("div.clusterize-cluster") 101 | rowels = clusterel.querySelectorAll("div.clusterize-row") 102 | for row in rowels 103 | row.should.have.text "#{i} - index: #{i-1}" 104 | i++ 105 | cl.scrollTop = 0 106 | cl.$nextTick done 107 | 108 | 109 | describe "size change", -> 110 | 111 | it "should change clustersize on size change", (done) -> 112 | cl.height = 200 113 | cl.$nextTick -> 114 | clsels = clel.querySelectorAll("div.clusterize-cluster") 115 | clel.querySelector("div.clusterize-last-row").should.have.attr("style") 116 | .match(new RegExp("height: #{overallHeight-3*cl.clusterSize*cl.rowHeight}px;")) 117 | i = 1 118 | for clusterel in clsels 119 | clusterel.should.have.attr("style").match(new RegExp("height: #{cl.clusterSize*cl.rowHeight}px;")) 120 | rowels = clusterel.querySelectorAll("div.clusterize-row") 121 | rowels.should.have.length(cl.clusterSize) 122 | for row in rowels 123 | row.should.have.text "#{i} - index: #{i-1}" 124 | i++ 125 | done() 126 | 127 | describe "loading env", -> 128 | 129 | before -> 130 | env = loadComp(require("../dev/loading.vue")) 131 | cl = env.$refs.clusterize 132 | should.exist(cl) 133 | should.exist(cl.$el) 134 | clel = cl.$el 135 | 136 | after -> 137 | unloadComp(env) 138 | 139 | it "should dispatch event clusterize-loaded", (done) -> 140 | cl.$once "clusterize-loaded", done 141 | 142 | it "should be loading", -> 143 | clsels = clel.querySelectorAll("div.clusterize-cluster") 144 | for clusterel in clsels 145 | clusterel.should.contain "div.clusterize-cluster-loading>p[slot='loading']" 146 | clusterel.should.have.attr("style").match(new RegExp("height: #{cl.clusterSize*cl.rowHeight}px;")) 147 | 148 | it "should contain data once loaded", (done) -> 149 | clsels = clel.querySelectorAll("div.clusterize-cluster") 150 | j = 0 151 | cl.$on "cluster-loaded", (nr) -> 152 | clel.querySelector("div.clusterize-last-row").should.have.attr("style").match(new RegExp("height: #{overallHeight-3*cl.clusterSize*cl.rowHeight}px;")) 153 | env.$nextTick -> 154 | i = 1+nr*cl.clusterSize 155 | rowels = clsels[nr].querySelectorAll("div.clusterize-row") 156 | rowels.should.have.length(cl.clusterSize) 157 | for row in rowels 158 | row.should.have.text "#{i}" 159 | i++ 160 | j++ 161 | done() if j == 3 162 | 163 | it "should have a hidden loading item afterwards", -> 164 | clsels = clel.querySelectorAll("div.clusterize-cluster") 165 | for clusterel in clsels 166 | lel = clusterel.querySelector("div.clusterize-cluster-loading[style='display: none;']") 167 | lel.should.exit 168 | lel.should.contain "p[slot='loading']" 169 | lel.should.have.text "loading" 170 | --------------------------------------------------------------------------------