├── .babelrc ├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── babel.config.js ├── demo ├── multiple.gif ├── single-height.gif └── single.gif ├── examples ├── App.vue ├── Item.vue ├── item-factory.js ├── main.js ├── pages │ ├── index.vue │ ├── known-single-for.vue │ ├── known-single-prop.vue │ ├── normal.vue │ ├── unknown-multiple.vue │ ├── unknown-single.vue │ └── vue-virtual-scroll-list.vue └── router.js ├── jest.config.js ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── src └── render.js ├── tests └── unit │ ├── .eslintrc.js │ └── example.spec.js ├── vue.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | "@babel/plugin-transform-runtime", 7 | "@babel/plugin-transform-modules-commonjs" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ['plugin:vue/recommended'], 7 | rules: { 8 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 9 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 10 | }, 11 | parserOptions: { 12 | parser: 'babel-eslint' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-present, Yuxi (Evan) You 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-flow-render 2 | 3 | 一个 vue 的列表惰性渲染容器组件 4 | 5 | ## How it works 6 | 7 | #### 单列定高 8 | 9 | 10 | #### 单列不定高 11 | 12 | 13 | #### 多列不定高(waterfall) 14 | 15 | 16 | ## Download 17 | ```shell 18 | yarn add vue-flow-render 19 | or 20 | npm install vue-flow-render 21 | ``` 22 | 23 | ## Usage 24 | ```javascript 25 | import VueFlowRender from 'vue-flow-render' 26 | ``` 27 | 28 | ## Props 29 | | key | value | description | required | validator | 30 | | ------ | ------ | ------ | ------ | --- | 31 | | remain | Number | 列表里保留的 item 的 DOM 个数 | Y | >= 0 | 32 | | total | Number | item 的总数 | Y | >= 0 | 33 | | column | Number | 列表的列数,默认是1列,多列为瀑布流 | N | >= 1 | 34 | | height | Number | 每个 item 的高度,如果为不定高度的组件,则不填 | N | >= 0 | 35 | | item | VueComponent | 如果 item 为单一固定高度的,则可以把 item 组件传过来 | N | - | 36 | | getter | Function | 如果传了 item 的组件,则 getter 方法用来获取 item 的属性,调用 getter 方法的传参是 index | N | - | 37 | 38 | 39 | > PS:如果 item 的高度为不固定的,必须在 item 的 style 上设置高度,单位为 px,如: 40 | 41 | 1. 普通用法 42 | ```Vue 43 | 48 | 53 | 54 | ``` 55 | 56 | 2. item 用法 57 | ```vue 58 | 68 | 69 | 91 | ``` 92 | 93 | ## Public methods 94 | > 通过 ref 来拿到组件,然后调用组件的方法 95 | 1. `scroll(scrollEvt.target.offsetTop)` 96 | 组件不会自己滚动,需要在外层容器滚动的时候将`evt.target.offsetTop`传递到 scroll 函数里的第二个参数是 isUp(是否向上滑动,默认可不传) 97 | 98 | 2. `setOffset()` 99 | 如果容器的上面存在动态高度的元素,那么当其高度变化后,调用`setOffset`函数 100 | 101 | 3. `setWrap(el)` 102 | 如果使用`better-scroll`,那么你要把 render 的 wrap 设置为`better-scroll`的父容器。默认为组件外层 overflow:hidden 的第一个元素 103 | 104 | 4. `getRect(index)` 105 | 使用这个组件后,浏览器自带的`Ctrl + F`搜索就无法正常使用,请自行实现搜索功能,然后通过该方法获取到指定元素的 rect,再让容器滚动到指定位置 106 | 107 | 5. `clear()` 108 | 刷新页面的时候,调用该方法清空缓存 109 | 110 | ## Contributions 111 | Welcome to improve this vue component with any issue, pull request or code review! 112 | 113 | ## License 114 | [MIT](https://github.com/falstack/vue-flow-render/blob/master/LICENSE) 115 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@vue/app', 5 | { 6 | useBuiltIns: false 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /demo/multiple.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falstack/vue-flow-render/1f322e374bc55dd270c610f626688a3bb37c19b5/demo/multiple.gif -------------------------------------------------------------------------------- /demo/single-height.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falstack/vue-flow-render/1f322e374bc55dd270c610f626688a3bb37c19b5/demo/single-height.gif -------------------------------------------------------------------------------- /demo/single.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falstack/vue-flow-render/1f322e374bc55dd270c610f626688a3bb37c19b5/demo/single.gif -------------------------------------------------------------------------------- /examples/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /examples/Item.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | -------------------------------------------------------------------------------- /examples/item-factory.js: -------------------------------------------------------------------------------- 1 | export default new class { 2 | constructor() { 3 | this.lastIndex = 0 4 | } 5 | 6 | get(count) { 7 | var items = [], 8 | i 9 | for (i = 0; i < count; i++) { 10 | const width = 100 + ~~(Math.random() * 50) 11 | const height = 50 + ~~(Math.random() * 150) 12 | items[i] = { 13 | index: this.lastIndex++, 14 | id: Math.random().toString(36), 15 | style: { 16 | color: this.getRandomColor(), 17 | image: `http://lorempixel.com/${width * 2}/${height * 2}/cats/` 18 | }, 19 | width, 20 | height 21 | } 22 | } 23 | return items 24 | } 25 | 26 | getRandomColor() { 27 | var colors = [ 28 | 'rgba(21,174,103,.5)', 29 | 'rgba(245,163,59,.5)', 30 | 'rgba(255,230,135,.5)', 31 | 'rgba(194,217,78,.5)', 32 | 'rgba(195,123,177,.5)', 33 | 'rgba(125,205,244,.5)' 34 | ] 35 | return colors[~~(Math.random() * colors.length)] 36 | } 37 | }() 38 | -------------------------------------------------------------------------------- /examples/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import Render from '../src/render.js' 5 | import ItemFactory from './item-factory' 6 | 7 | Vue.config.productionTip = false 8 | Vue.component('v-render', Render) 9 | Vue.prototype.$factory = ItemFactory 10 | 11 | new Vue({ 12 | el: '#app', 13 | router, 14 | render: h => h(App) 15 | }) 16 | 17 | -------------------------------------------------------------------------------- /examples/pages/index.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 26 | 27 | 42 | -------------------------------------------------------------------------------- /examples/pages/known-single-for.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 65 | 66 | 94 | -------------------------------------------------------------------------------- /examples/pages/known-single-prop.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 57 | 58 | 86 | -------------------------------------------------------------------------------- /examples/pages/normal.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 34 | 35 | 56 | -------------------------------------------------------------------------------- /examples/pages/unknown-multiple.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 90 | 91 | 129 | -------------------------------------------------------------------------------- /examples/pages/unknown-single.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 64 | 65 | 82 | -------------------------------------------------------------------------------- /examples/pages/vue-virtual-scroll-list.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 40 | 41 | 74 | -------------------------------------------------------------------------------- /examples/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | Vue.use(VueRouter) 5 | 6 | export default new VueRouter({ 7 | mode: 'history', 8 | routes: [ 9 | { 10 | path: '/', 11 | name: 'index', 12 | component: () => import('./pages/index') 13 | }, 14 | { 15 | path: '/known-single-for', 16 | name: 'known-single-for', 17 | component: () => import('./pages/known-single-for') 18 | }, 19 | { 20 | path: '/known-single-prop', 21 | name: 'known-single-prop', 22 | component: () => import('./pages/known-single-prop') 23 | }, 24 | { 25 | path: '/unknown-single', 26 | name: 'unknown-single', 27 | component: () => import('./pages/unknown-single') 28 | }, 29 | { 30 | path: '/unknown-multiple', 31 | name: 'unknown-multiple', 32 | component: () => import('./pages/unknown-multiple') 33 | }, 34 | { 35 | path: '/vue-virtual-scroll-list', 36 | name: 'vue-virtual-scroll-list', 37 | component: () => import('./pages/vue-virtual-scroll-list') 38 | }, 39 | { 40 | path: '/normal-list', 41 | name: 'normal-list', 42 | component: () => import('./pages/normal.vue') 43 | } 44 | ] 45 | }) 46 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest' 8 | }, 9 | transformIgnorePatterns: ['/node_modules/'], 10 | moduleNameMapper: { 11 | '^@/(.*)$': '/src/$1' 12 | }, 13 | snapshotSerializers: ['jest-serializer-vue'], 14 | testMatch: [ 15 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 16 | ], 17 | testURL: 'http://localhost/' 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-flow-render", 3 | "version": "1.0.5", 4 | "main": "dist/vue-flow-render.umd.min.js", 5 | "files": [ 6 | "dist" 7 | ], 8 | "keywords": [ 9 | "waterfall", 10 | "vue", 11 | "virtual-list", 12 | "big-list", 13 | "pinterest", 14 | "layout" 15 | ], 16 | "author": "falstack ", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/falstack/vue-flow-render/issues" 20 | }, 21 | "homepage": "https://github.com/falstack/vue-flow-render", 22 | "scripts": { 23 | "dev": "vue-cli-service serve", 24 | "build": "vue-cli-service build --target lib --name vue-flow-render src/render.js", 25 | "lint": "vue-cli-service lint", 26 | "test:unit": "vue-cli-service test:unit" 27 | }, 28 | "dependencies": { 29 | "vue": "^2.6.11" 30 | }, 31 | "devDependencies": { 32 | "@vue/cli-plugin-babel": "^4.1.2", 33 | "@vue/cli-plugin-eslint": "^4.2.3", 34 | "@vue/cli-plugin-unit-jest": "^4.2.3", 35 | "@vue/cli-service": "^4.2.3", 36 | "@vue/eslint-config-prettier": "^6.0.0", 37 | "@vue/test-utils": "1.0.0-beta.32", 38 | "babel-core": "7.0.0-bridge.0", 39 | "babel-eslint": "^10.1.0", 40 | "babel-jest": "^25.1.0", 41 | "eslint": "^6.8.0", 42 | "eslint-plugin-vue": "^6.1.1", 43 | "node-sass": "^4.13.1", 44 | "postcss-px-to-viewport": "^1.1.1", 45 | "sass-loader": "^8.0.2", 46 | "vue-router": "^3.1.6", 47 | "vue-template-compiler": "^2.6.11", 48 | "vue-virtual-scroll-list": "^1.4.6" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/falstack/vue-flow-render/1f322e374bc55dd270c610f626688a3bb37c19b5/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-flow-render 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/render.js: -------------------------------------------------------------------------------- 1 | const getWrapParent = dom => { 2 | let el = dom 3 | while ( 4 | el && 5 | el.tagName !== 'HTML' && 6 | el.nodeType === 1 7 | ) { 8 | const overflowY = window.getComputedStyle(el).overflowY 9 | const overflow = window.getComputedStyle(el).overflow 10 | if (overflowY === 'hidden' || overflow === 'hidden') { 11 | return el 12 | } 13 | el = el.parentNode 14 | } 15 | return dom.parentNode 16 | } 17 | 18 | export default { 19 | name: 'VueFlowRender', 20 | props: { 21 | column: { 22 | type: Number, 23 | default: 1, 24 | validator: (val) => val >= 1 25 | }, 26 | height: { 27 | type: Number, 28 | default: 0, 29 | validator: (val) => val >= 0 30 | }, 31 | remain: { 32 | type: Number, 33 | required: true 34 | }, 35 | total: { 36 | type: Number, 37 | required: true, 38 | default: 0 39 | }, 40 | item: { 41 | type: Object, 42 | default: null 43 | }, 44 | getter: { 45 | type: Function, 46 | default: () => {} 47 | } 48 | }, 49 | data() { 50 | return { 51 | wrapHeight: 0, 52 | offsetTop: 0, 53 | lastScrollTop: 0, 54 | start: 0, 55 | flowHeight: 0, 56 | paddingTop: 0, 57 | cache: {} 58 | } 59 | }, 60 | computed: { 61 | isSameHeight() { 62 | return this.height !== 0 63 | }, 64 | isSingleColumn() { 65 | return this.column === 1 66 | } 67 | }, 68 | watch: { 69 | total(newVal, oldVal) { 70 | if (!newVal) { 71 | this.clear() 72 | } else if (newVal < oldVal) { 73 | this.clear() 74 | this._computeRenderHeight(this.$slots.default, 0) 75 | } else { 76 | this._computeRenderHeight(this.isSameHeight ? undefined : this.$slots.default.slice(oldVal, newVal), oldVal) 77 | } 78 | this.scroll(this.lastScrollTop, false) 79 | } 80 | }, 81 | mounted() { 82 | this.setOffset() 83 | this.setWrap() 84 | this._computeRenderHeight(this.$slots.default, 0) 85 | }, 86 | methods: { 87 | setOffset() { 88 | this.offsetTop = this.$el.offsetTop 89 | }, 90 | setWrap(el) { 91 | this.wrapHeight = (el || getWrapParent(this.$el)).clientHeight 92 | }, 93 | getRect(index) { 94 | return this.cache[index] 95 | }, 96 | scroll(offset, up) { 97 | const isUp = up === undefined ? offset < this.lastScrollTop : up 98 | this.lastScrollTop = offset 99 | const { cache, start, remain, total } = this 100 | /** 101 | * 元素比较少,还不需要懒加载 102 | */ 103 | if (remain >= total) { 104 | return 105 | } 106 | /** 107 | * 如果在顶部,则直接修正 108 | */ 109 | const scrollTop = offset - this.offsetTop 110 | if (scrollTop <= 0) { 111 | this.paddingTop = 0 112 | this.start = 0 113 | return 114 | } 115 | /** 116 | * 如果触底了,则直接修正 117 | */ 118 | const scrollBottom = scrollTop + this.wrapHeight 119 | if (scrollBottom >= cache[total - 1].bottom) { 120 | this.start = total - remain 121 | this.paddingTop = cache[total - remain].top 122 | return 123 | } 124 | 125 | const { isSameHeight, column, height } = this 126 | /** 127 | * 向上修正 128 | */ 129 | const adjustUp = () => { 130 | const detectRect = cache[start] 131 | const deltaHeight = detectRect.top - scrollTop 132 | /** 133 | * 如果当前列表的第一个元素的顶部在视口的上方,则不用修正 134 | */ 135 | if (deltaHeight <= 0) { 136 | return 137 | } 138 | if (isSameHeight) { 139 | /** 140 | * 如果元素是等高的,直接根据高度差算出需要修正的距离 141 | */ 142 | const decreaseCount = Math.abs(Math.ceil(deltaHeight / height / column)) 143 | const index = Math.max((start - decreaseCount - remain / 2) | 0, 0) 144 | this.start = index 145 | this.paddingTop = cache[index].top 146 | } else { 147 | /** 148 | * 如果元素不等高 149 | * 从当前列表的上一个元素开始,到第 0 个元素结束 150 | * 寻找第一个顶部在视口边界的元素 151 | */ 152 | for (let i = start - 1; i >= 0; i--) { 153 | if (cache[i].top <= scrollTop) { 154 | const index = Math.max((i - remain / 2) | 0, 0) 155 | this.paddingTop = cache[index].top 156 | this.start = index 157 | break 158 | } 159 | } 160 | } 161 | } 162 | /** 163 | * 向下修正 164 | */ 165 | const adjustDown = () => { 166 | const detectRect = cache[start + remain - 1] 167 | const deltaHeight = detectRect.bottom - scrollBottom 168 | /** 169 | * 如果当前列表的最后一个元素的底部在视口的下方,则不用修正 170 | */ 171 | if (deltaHeight >= 0) { 172 | return 173 | } 174 | if (isSameHeight) { 175 | /** 176 | * 如果元素是等高的,直接根据高度差算出需要修正的距离 177 | */ 178 | const increaseCount = Math.abs(Math.floor(deltaHeight / height / column)) 179 | const index = Math.min((start + increaseCount + remain / 2) | 0, total - remain) 180 | this.start = index 181 | this.paddingTop = cache[index].top 182 | } else { 183 | /** 184 | * 如果元素不等高 185 | * 从当前列表的最后一个元素的下一个元素开始,到最后一个元素结束 186 | * 寻找第一个底部在视口边界的元素 187 | */ 188 | for (let i = start + remain; i < total; i++) { 189 | if (cache[i].bottom >= scrollBottom) { 190 | const index = Math.min((i + 1 - remain / 2) | 0, total - remain) 191 | this.paddingTop = cache[index].top 192 | this.start = index 193 | break 194 | } 195 | } 196 | } 197 | } 198 | isUp ? adjustUp() : adjustDown() 199 | }, 200 | clear() { 201 | this.flowHeight = 0 202 | this.paddingTop = 0 203 | this.start = 0 204 | this.cache = {} 205 | }, 206 | _computeRenderHeight(items, offset) { 207 | const { total, column, cache } = this 208 | if (!total) { 209 | return 210 | } 211 | if (this.isSameHeight) { 212 | const height = this.height 213 | const end = items ? items.length + offset : total 214 | if (this.isSingleColumn) { 215 | for (let i = offset; i < end; i++) { 216 | const top = height * Math.floor(i / column) 217 | cache[i] = { 218 | top, 219 | height, 220 | bottom: height + top 221 | } 222 | } 223 | this.flowHeight = height * Math.ceil(total / column) 224 | } else { 225 | for (let i = offset; i < end; i++) { 226 | const top = height * i 227 | cache[i] = { 228 | top, 229 | height, 230 | bottom: height + top 231 | } 232 | } 233 | this.flowHeight = height * total 234 | } 235 | } else if (this.isSingleColumn) { 236 | let beforeHeight = offset ? cache[offset - 1].bottom : 0 237 | items.forEach((item, index) => { 238 | const hgt = parseInt(item.data.style.height, 10) 239 | cache[index + offset] = { 240 | top: beforeHeight, 241 | bottom: hgt + beforeHeight, 242 | height: hgt 243 | } 244 | beforeHeight += hgt 245 | }) 246 | this.flowHeight = beforeHeight 247 | } else { 248 | let offsets 249 | if (offset) { 250 | offsets = [] 251 | for (let i = offset - column, end = offset - 1; i <= end; i++) { 252 | offsets.push(cache[i].bottom) 253 | } 254 | } else { 255 | offsets = new Array(column).fill(0) 256 | } 257 | items.forEach((item, index) => { 258 | const beforeHeight = Math.min(...offsets) 259 | const hgt = parseInt(item.data.style.height, 10) 260 | cache[index + offset] = { 261 | top: beforeHeight, 262 | bottom: hgt + beforeHeight, 263 | height: hgt 264 | } 265 | offsets[offsets.indexOf(beforeHeight)] += hgt 266 | }) 267 | this.flowHeight = Math.max(...offsets) 268 | } 269 | }, 270 | _filter(h) { 271 | const { remain, total, start, item } = this 272 | const end = remain >= total ? total : start + remain 273 | 274 | if (item) { 275 | const result = [] 276 | for (let i = start; i < end; i++) { 277 | result.push(h(item, this.getter(i))) 278 | } 279 | return result 280 | } 281 | 282 | if (!this.$slots.default) { 283 | return [] 284 | } 285 | return this.$slots.default.slice(start, end) 286 | } 287 | }, 288 | render(h) { 289 | return h( 290 | 'div', 291 | { 292 | style: { 293 | position: 'relative', 294 | boxSizing: 'border-box', 295 | willChange: 'padding-top', 296 | paddingTop: `${this.paddingTop}px`, 297 | height: `${this.flowHeight}px` 298 | }, 299 | class: 'vue-flow-render' 300 | }, 301 | this._filter(h) 302 | ) 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/unit/example.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import HelloWorld from '@/components/HelloWorld.vue' 3 | 4 | describe('HelloWorld.vue', () => { 5 | it('renders props.msg when passed', () => { 6 | const msg = 'new message' 7 | const wrapper = shallowMount(HelloWorld, { 8 | propsData: { msg } 9 | }) 10 | expect(wrapper.text()).toMatch(msg) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const npmCfg = require('./package.json') 3 | 4 | const banner = [ 5 | npmCfg.name + ' v' + npmCfg.version, 6 | '(c) ' + new Date().getFullYear() + ' ' + npmCfg.author, 7 | npmCfg.homepage 8 | ].join('\n') 9 | 10 | module.exports = { 11 | productionSourceMap: false, 12 | pages: { 13 | index: { 14 | entry: 'examples/main.js', 15 | template: 'public/index.html', 16 | filename: 'index.html' 17 | } 18 | }, 19 | configureWebpack: { 20 | plugins: [new webpack.BannerPlugin(banner)] 21 | } 22 | } 23 | --------------------------------------------------------------------------------