├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── dist └── vue-collection-cluster.js ├── example ├── dist │ └── build.js ├── index.html ├── src │ ├── App.vue │ └── main.js └── webpack.config.js ├── package-lock.json ├── package.json ├── src └── vue-collection-cluster.vue └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env" 4 | ] 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Andrej Adamcik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-collection-cluster 2 | 3 | A vue component for displaying large data sets. Render 100000+ rows/columns easily with great performance. 4 | 5 | 6 | [![npm](https://img.shields.io/npm/v/vue-collection-cluster.svg) 7 | ![npm](https://img.shields.io/npm/dm/vue-collection-cluster.svg)](https://www.npmjs.com/package/vue-collection-cluster) 8 | [![vue2](https://img.shields.io/badge/vue-2.x-brightgreen.svg)](https://vuejs.org/) 9 | [![license](https://img.shields.io/npm/l/express.svg)]() 10 | 11 | ## Demo 12 | 13 | - [Basic](https://jsfiddle.net/adamcikado/jv694b8c/) 14 | - [Multiple columns](https://jsfiddle.net/adamcikado/eknhg2du/) 15 | - [Automatic/dynamic height](https://jsfiddle.net/adamcikado/v9grvxvq/) 16 | - [Multiple slots](https://jsfiddle.net/adamcikado/q6c7xteu/) 17 | 18 | ## Installation 19 | 20 | ```bash 21 | npm install --save vue-collection-cluster 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```html 27 | 33 | ``` 34 | 35 | ```javascript 36 | import CollectionCluster from 'vue-collection-cluster'; 37 | 38 | export default { 39 | components: {CollectionCluster}, 40 | data() { 41 | return { 42 | collection: { 43 | 44 | }, 45 | items: [{ 46 | type: 'header', 47 | title: 'List', 48 | }, { 49 | type: 'letter', 50 | value: 'A', 51 | }] 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | 58 | ### Scoped slots 59 | 60 | Each item in the list must have type, by the type a correct slot is rendered for item. 61 | 62 | ```html 63 | 64 |
69 | {{ item.title }} 70 |
71 | 72 |
77 | {{ item.value }} 78 |
79 |
80 | ``` 81 | 82 | ### Height Types 83 | 84 | ### static 85 | Each slot must have size set in the css, which must be equal to size set in `itemHeight` option. 86 | 87 | ### dynamic 88 | Each item in the list must have property (`heightField`) with the exact height of the slot for that item. 89 | 90 | ### automatic 91 | Size of slot is automaticaly calculated when rendered and set to `heightField` property of the item. 92 | 93 | For dynamic/automatic types, the `itemHeight` option is used as estimate. It's **strongly recommended** to use it. 94 | 95 | ## Options 96 | 97 | ### items 98 | Type: `Array`, Required 99 | 100 | List of items to display. 101 | 102 | ### columns 103 | Type: `Number`, Default: `1` 104 | 105 | Number of columns per row. 106 | 107 | ### itemHeight 108 | Type: `Number`, Default: `100` 109 | 110 | Height of the row. 111 | 112 | ### typeField 113 | Type: `String`, Default: `type` 114 | 115 | Item property's name for type. 116 | 117 | ### heightField 118 | Type: `String`, Default: `height` 119 | 120 | Item property's name for height. 121 | 122 | ### heightType 123 | Type: `String`, Default: `static`, Options: `static`, `dynamic`, `automatic` 124 | 125 | ### inset 126 | Type: `Object`, Default: `{top: 0, bottom: 0}` 127 | 128 | Inset from top and bottom of the list. 129 | 130 | ### scrollPastEnd 131 | Type: `Number`, Default: `0` 132 | 133 | Renders space at the end of the list of size `height` * `scrollPastEnd`. 134 | 135 | `0.5` = 50% of height 136 | 137 | ### buffer 138 | Type: `Number`, Default: `200` 139 | 140 | Pixels to pre-render around visible area. 141 | 142 | ### threshold 143 | Type: `Number`, Default: `50` 144 | 145 | Threshold for `scrollToTop` & `scrollToBottom` events. 146 | 147 | ### autoResize 148 | Type: `Boolean`, Default: `true` 149 | 150 | Sets whether the list should auto resize and render items when window resizes. 151 | 152 | 153 | ## Events 154 | 155 | ### cellsChange 156 | Emitted when visible/rendered cells change. There is one argument with list of cells. 157 | 158 | ### scrollToTop 159 | 160 | ### scrollToBottom 161 | 162 | ## Methods 163 | 164 | ### isAtTop() 165 | Return: `Boolean` 166 | 167 | Is list at the top? 168 | 169 | ### isAtBottom() 170 | Return: `Boolean` 171 | 172 | Is list at the bottom? 173 | 174 | ### scrollTo(index, position) 175 | index: `Int`, position: `default|top|bottom|topInset|bottomInset'` 176 | 177 | Scrolls to specified index at position. 178 | 179 | ### scrollToBottom() 180 | Scrolls to bottom of list. 181 | 182 | ### resizeItem(index) 183 | index: `Int` 184 | 185 | Should be called whenever item with dynamic height did change height. 186 | 187 | 188 | --- 189 | 190 | ## Example 191 | 192 | ```html 193 | 212 | 213 | 234 | ``` 235 | 236 | `Letter.vue` source: 237 | ```html 238 | 241 | 242 | 247 | ``` 248 | 249 | `Name.vue` source: 250 | ```html 251 | 254 | 255 | 260 | ``` 261 | 262 | --- 263 | 264 | ## License 265 | 266 | [MIT](http://opensource.org/licenses/MIT) 267 | 268 | Copyright (c) 2018 Andrej Adamcik -------------------------------------------------------------------------------- /dist/vue-collection-cluster.js: -------------------------------------------------------------------------------- 1 | !function(t,i){"object"==typeof exports&&"object"==typeof module?module.exports=i():"function"==typeof define&&define.amd?define("VueCollectionCluster",[],i):"object"==typeof exports?exports.VueCollectionCluster=i():t.VueCollectionCluster=i()}("undefined"!=typeof self?self:this,function(){return function(t){function i(s){if(e[s])return e[s].exports;var h=e[s]={i:s,l:!1,exports:{}};return t[s].call(h.exports,h,h.exports,i),h.l=!0,h.exports}var e={};return i.m=t,i.c=e,i.d=function(t,e,s){i.o(t,e)||Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get:s})},i.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return i.d(e,"a",e),e},i.o=function(t,i){return Object.prototype.hasOwnProperty.call(t,i)},i.p="",i(i.s=1)}([function(t,i,e){"use strict";function s(t,i){if(!(t instanceof i))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(i,"__esModule",{value:!0});var h={static:"static",dynamic:"dynamic",automatic:"automatic"},n={default:"default",top:"top",bottom:"bottom",topInset:"topInset",bottomInset:"bottomInset"},r=function t(i,e){s(this,t),this.item=i,this.offset=e};i.default={props:{items:{type:Array,required:!0},columns:{type:Number,default:1},itemWidth:{type:Number,default:null},itemHeight:{type:Number,default:100},typeField:{type:String,default:"type"},heightField:{type:String,default:"height"},heightType:{type:String,default:h.static},inset:{type:Object,default:function(){return{top:0,bottom:0}}},scrollPastEnd:{type:Number,default:0},buffer:{type:Number,default:200},threshold:{type:Number,default:50},autoResize:{type:Boolean,default:!0},prerender:{type:Number,default:0}},data:function(){return{visibleCells:[],totalHeight:0,startHeight:0,endHeight:0}},computed:{length:function(){return this.items.length},scrollPastEndSize:function(){var t=this.scrollPastEnd*this.height;return t-=this.inset.top+this.inset.bottom+this.itemHeight,t<0?0:t}},watch:{items:function(){this.verifyCells(),this.updateVisibleCells()},columns:function(){this.updateVisibleCells()},autoResize:function(){this.autoResize?window.addEventListener("resize",this.onResize):window.removeEventListener("resize",this.onResize)}},mounted:function(){this.scrollTop=0,this.currentStart=0,this.currentEnd=0,this.startHeight=0,this.endHeight=0,this.heightInvalidAfter=0,this.heightUpdateCounter=0,this.accumulatorProp=Symbol(),this.height=this.$el.clientHeight,this.scrollHeight=0,this.updateVisibleCells(),this.autoResize&&window.addEventListener("resize",this.onResize)},beforeDestroy:function(){this.autoResize&&window.removeEventListener("resize",this.onResize)},methods:{onScroll:function(t){this.scrollTop=this.$el.scrollTop,this.$emit("scroll",t),this.updateVisibleCells()},onResize:function(){this.height=this.$el.clientHeight,this.updateVisibleCells()},invalidateHeightAfter:function(t){t<0&&(t=0),tthis.length&&(e=this.length);var s=Math.ceil(i/this.itemHeight)*this.columns;return s>this.length&&(s=this.length),{start:e,end:s}}var n=Math.floor(t/this.itemHeight);for(n>this.heightInvalidAfter&&(n=this.heightInvalidAfter);n>0&&(void 0===this.items[n][this.accumulatorProp]||this.items[n][this.accumulatorProp]>t);)n--;for(var r=0===n?0:this.items[n][this.accumulatorProp]||0;;){var o=this.items[n],l=o[this.heightField]||this.itemHeight;if(o[this.accumulatorProp]=r,(r+=l)>t||n>=this.length-1)break;n++}for(var a=n;athis.heightInvalidAfter&&(this.heightInvalidAfter=a),a++,{start:n,end:a}},updateVisibleCells:function(){var t=this.getStartEnd(),i=t.start,e=t.end,s=void 0,n=void 0,r=ithis.currentStart?e:this.currentStart;for(s=o;si){var a=[];for(s=i;sthis.currentEnd?i:this.currentEnd;if(u=this.length&&this.invalidateHeightAfter(this.length-1),i&&(this.heightType===h.automatic&&this.scheduleUpdateHeights(),this.$emit("cellsChange",this.visibleCells))},isAtTop:function(){return this.scrollTop-this.inset.top=0;i--){var e=this.visibleCells[i],s=this.$el.children[1+i],h=s.clientHeight;e.item[this.heightField]!==h&&(e.item[this.heightField]=h,t=!0,this.invalidateHeightAfter(this.currentStart+i))}t&&this.updateVisibleCells()},resizeItem:function(t){if(t<0||t>=this.length)throw new Error("Invalid index.");this.heightType===h.dynamic?(this.invalidateHeightAfter(t),this.updateVisibleCells()):this.heightType===h.automatic&&t>=this.currentStart&&t=this.length)throw new Error("Invalid index.");if(this.heightType===h.static)return t*this.itemHeight;var i=Math.min(t,this.heightInvalidAfter);return(this.items[i][this.accumulatorProp]||0)+(t-i)*this.itemHeight},scrollToPosition:function(t){this.$el.scrollTop=t,this.scrollTop=this.$el.scrollTop,this.updateVisibleCells()},storeScrollPosition:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.currentStart;if(t<0||t>=this.length)throw new Error("Invalid index.");return new r(this.items[t],this.getItemPosition(t)-this.scrollTop)},restoreScrollPosition:function(t){var i=this;if(!(t instanceof r))throw new Error("Invalid position.");var e=this.items.indexOf(t.item);if(-1!==e){var s=this.getItemPosition(e)-t.offset,h=Math.max(0,this.scrollHeight-this.height);s<0&&(s=0),s>h&&(s=h),this.scrollTop!==s&&(this.scrollToPosition(s),this.$nextTick(function(){i.restoreScrollPosition(t)}))}},scrollTo:function(t,i){var e=this;if(t<0||t>=this.length)throw new Error("Invalid index.");i=i||n.default;var s=this.getItemPosition(t),r=this.heightType===h.static?this.itemHeight:this.items[t][this.heightField]||this.itemHeight,o=r>this.height,l=s+r,a=void 0;switch(i!==n.topInset&&i!==n.bottomInset?(s+=this.inset.top,l+=this.inset.top):l+=this.inset.top+this.inset.bottom,i){case n.default:l>this.scrollTop+this.height&&!o?(a=Math.max(0,l-this.height),i=n.bottom):(sthis.scrollTop)&&(a=s,i=n.top);break;case n.top:case n.topInset:a=Math.max(0,Math.min(s,this.scrollHeight-this.height));break;case n.bottom:case n.bottomInset:a=Math.max(0,Math.min(l-this.height,this.scrollHeight-this.height))}void 0!==a&&a!==this.scrollTop&&(this.scrollToPosition(a),this.$nextTick(function(){e.scrollTo(t,i)}))},scrollToBottom:function(){var t=this,i=this.scrollHeight-this.height;this.scrollTop===i||this.scrollHeight 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | vue-collection-cluster 16 | 17 | 42 | 43 | 44 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /example/src/App.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | -------------------------------------------------------------------------------- /example/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import App from './App.vue'; 4 | 5 | new Vue({ 6 | render: h => h(App) 7 | }).$mount('.app'); -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | 4 | module.exports = { 5 | entry: path.resolve(__dirname, './src/main.js'), 6 | output: { 7 | path: path.resolve(__dirname, './dist'), 8 | publicPath: '/dist/', 9 | filename: 'build.js' 10 | }, 11 | resolve: { 12 | extensions: ['.js', '.vue'] 13 | }, 14 | module: { 15 | loaders: [{ 16 | test: /\.vue$/, 17 | loader: 'vue-loader', 18 | options: { 19 | hmr: false, 20 | preserveWhitespace: false, 21 | optimizeSSR: false, 22 | hotReload: false, 23 | loaders: { 24 | js: { 25 | loader: 'babel-loader', 26 | options: { 27 | highlightCode: false, 28 | } 29 | }, 30 | }, 31 | } 32 | }, { 33 | test: /\.js$/, 34 | loader: 'babel-loader', 35 | exclude: /node_modules/ 36 | }] 37 | }, 38 | devServer: { 39 | historyApiFallback: true, 40 | port: 9000, 41 | noInfo: true 42 | }, 43 | devtool: '#inline-source-map' 44 | } 45 | 46 | if (process.env.NODE_ENV === 'production') { 47 | module.exports.devtool = '#source-map' 48 | // http://vue-loader.vuejs.org/en/workflow/production.html 49 | module.exports.plugins = (module.exports.plugins || []).concat([ 50 | new webpack.DefinePlugin({ 51 | 'process.env': { 52 | NODE_ENV: '"production"' 53 | } 54 | }), 55 | new webpack.optimize.UglifyJsPlugin({ 56 | compress: { 57 | warnings: false 58 | } 59 | }) 60 | ]) 61 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-collection-cluster", 3 | "version": "1.1.0", 4 | "description": "A vue component for displaying large data sets easily with great performance.", 5 | "main": "dist/vue-collection-cluster.js", 6 | "files": [ 7 | "dist/*" 8 | ], 9 | "scripts": { 10 | "build": "webpack", 11 | "prepublish": "npm run build", 12 | "dev": "webpack --watch --hide-modules --config ./example/webpack.config.js" 13 | }, 14 | "keywords": [ 15 | "vue", 16 | "component", 17 | "big-data", 18 | "big-list", 19 | "scroll-list", 20 | "virtual-list", 21 | "collection", 22 | "uitableview" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/adamcikado/vue-collection-cluster.git" 27 | }, 28 | "homepage": "https://github.com/adamcikado/vue-collection-cluster#readme", 29 | "author": "adamcikado", 30 | "license": "MIT", 31 | "devDependencies": { 32 | "babel-core": "^6.26.0", 33 | "babel-loader": "^7.1.2", 34 | "babel-preset-env": "^1.6.1", 35 | "uglify-es": "^3.3.9", 36 | "vue": "^2.5.13", 37 | "vue-loader": "^14.0.3", 38 | "vue-template-compiler": "^2.5.13", 39 | "webpack": "^3.10.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/vue-collection-cluster.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 534 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | 4 | module.exports = { 5 | devtool: 'cheap-module-source-map', 6 | entry: './src/vue-collection-cluster.vue', 7 | output: { 8 | path: path.resolve(__dirname, './dist/'), 9 | filename: 'vue-collection-cluster.js', 10 | library: 'VueCollectionCluster', 11 | libraryTarget: 'umd', 12 | umdNamedDefine: true, 13 | }, 14 | resolve: { 15 | extensions: ['.js', '.vue'], 16 | alias: { 17 | 'vue$': 'vue/dist/vue.common.js', 18 | } 19 | }, 20 | module: { 21 | loaders: [{ 22 | test: /\.js$/, 23 | loader: 'babel-loader', 24 | include: __dirname, 25 | exclude: /node_modules/ 26 | }, 27 | { 28 | test: /\.vue$/, 29 | loader: 'vue-loader', 30 | include: __dirname, 31 | exclude: /node_modules/, 32 | } 33 | ] 34 | }, 35 | plugins: [ 36 | new webpack.LoaderOptionsPlugin({ 37 | minimize: true 38 | }), 39 | new webpack.optimize.UglifyJsPlugin({ 40 | beautify: false, 41 | comments: false, 42 | compress: { 43 | warnings: false 44 | } 45 | }), 46 | new webpack.DefinePlugin({ 47 | 'process.env': { 48 | NODE_ENV: '"production"' 49 | } 50 | }), 51 | ] 52 | } --------------------------------------------------------------------------------