├── .babelrc ├── docs ├── favicon.png ├── css │ ├── fixed-size.840ec49f.css │ ├── page-mode.bb1117ff.css │ ├── dynamic-size.9013d8ce.css │ ├── horizontal.50e26ffa.css │ ├── infinite-loading.4b238c18.css │ ├── keep-state.2736386a.css │ ├── chat-room.9f37805a.css │ └── app.12df65ee.css ├── highlight │ └── theme.css ├── index.html ├── js │ ├── fixed-size.1b9763e9.js │ ├── horizontal.ad2c5dae.js │ ├── keep-state.08df8235.js │ ├── page-mode.7fc3339d.js │ ├── dynamic-size.3003e8a2.js │ ├── infinite-loading.ce8d4372.js │ └── chat-room.c08970e6.js └── milligram.css ├── .gitignore ├── example ├── babel.config.js ├── public │ ├── favicon.png │ ├── index.html │ └── highlight │ │ └── theme.css ├── readme.md ├── src │ ├── common │ │ ├── gen-unique-id.js │ │ ├── const.js │ │ ├── ua.js │ │ ├── user.js │ │ ├── get-code-url.js │ │ └── sentences.js │ ├── mixins │ │ └── index.js │ ├── views │ │ ├── fixed-size │ │ │ ├── Item.vue │ │ │ ├── Code.vue │ │ │ └── Main.vue │ │ ├── horizontal │ │ │ ├── Code.vue │ │ │ ├── Item.vue │ │ │ └── Main.vue │ │ ├── dynamic-size │ │ │ ├── Item.vue │ │ │ ├── Code.vue │ │ │ └── Main.vue │ │ ├── page-mode │ │ │ ├── Item.vue │ │ │ └── Main.vue │ │ ├── infinite-loading │ │ │ ├── Item.vue │ │ │ ├── Code.vue │ │ │ └── Main.vue │ │ ├── chat-room │ │ │ ├── Toolbar.vue │ │ │ ├── util.js │ │ │ ├── Item.vue │ │ │ ├── Editor.vue │ │ │ └── Main.vue │ │ ├── keep-state │ │ │ ├── Code.vue │ │ │ ├── Item.vue │ │ │ └── Main.vue │ │ └── home │ │ │ └── Main.vue │ ├── components │ │ ├── Introduction.vue │ │ ├── CodeHighLight.vue │ │ ├── Corner.vue │ │ └── Tab.vue │ ├── main.js │ ├── router │ │ └── index.js │ └── App.vue ├── vue.config.js └── package.json ├── .gitattributes ├── .travis.yml ├── .github ├── ISSUE_TEMPLATE │ ├── question-or-discuss.md │ ├── feature_request.md │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── npm-publish.yml │ ├── command-rebase.yml │ ├── node.yml │ └── command-compile.yml ├── test ├── util.js ├── item.vue ├── scroll.test.js ├── offset.test.js ├── start.test.js ├── horizontal.test.js ├── extra-props2.test.js ├── extra-props.test.js ├── slot.test.js ├── base.test.js └── element.test.js ├── scripts ├── rollup.banner.js ├── rollup.development.js └── rollup.production.js ├── .eslintrc.json ├── jest.config.js ├── LICENSE ├── package.json └── src ├── item.js ├── props.js ├── virtual.js └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env"] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skjnldsv/vue-virtual-scroll-list/master/docs/favicon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | dist/*.map 4 | docs/js/*.map 5 | dev 6 | issues 7 | node_modules 8 | coverage 9 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /example/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skjnldsv/vue-virtual-scroll-list/master/example/public/favicon.png -------------------------------------------------------------------------------- /example/readme.md: -------------------------------------------------------------------------------- 1 | ## example folder 2 | 3 | This is a independent project create by `vue-cli` for serving online example, output dir is `../docs`. 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-detectable=true 2 | *.css linguist-detectable=false 3 | *.vue linguist-detectable=false 4 | *.html linguist-detectable=false 5 | -------------------------------------------------------------------------------- /example/src/common/gen-unique-id.js: -------------------------------------------------------------------------------- 1 | // gen a simple unique id. 2 | export default (prefix) => { 3 | return `${prefix}$${Math.random().toString(16).substr(9)}` 4 | } 5 | -------------------------------------------------------------------------------- /example/src/common/const.js: -------------------------------------------------------------------------------- 1 | // config and constants. 2 | 3 | export const TAB_TYPE = { 4 | VIEW: 1, 5 | CODE: 2 6 | } 7 | 8 | export const DEFAULT_TAB = TAB_TYPE.VIEW 9 | -------------------------------------------------------------------------------- /example/src/common/ua.js: -------------------------------------------------------------------------------- 1 | const ua = navigator.userAgent 2 | const Android = !!ua.match(/Android/i) 3 | const iOS = !!ua.match(/iPhone|iPad|iPod/i) 4 | export const isMobile = Android || iOS 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | script: 5 | - npm run lint 6 | branches: 7 | only: 8 | - master 9 | after_success: 10 | # - bash <(curl -s https://codecov.io/bash) 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question-or-discuss.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question or discuss 3 | about: Ask your questions or topics you want to discuss. 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **What kind of this PR?** (check at least one) 2 | 3 | - [ ] Bugfix 4 | - [ ] Feature 5 | - [ ] Code style update 6 | - [ ] Build-related changes 7 | - [ ] Other, please describe: 8 | 9 | **Other information:** 10 | -------------------------------------------------------------------------------- /example/vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | // https://cli.vuejs.org/config/#global-cli-config 4 | module.exports = { 5 | publicPath: './', 6 | 7 | outputDir: path.resolve(__dirname, '../docs'), 8 | 9 | productionSourceMap: false 10 | } 11 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | export function getDatas (counts) { 2 | const data = [] 3 | for (let index = 0; index < counts; index++) { 4 | data.push({ 5 | id: String(index), 6 | text: Math.random().toString(16).substr(8) 7 | }) 8 | } 9 | return data 10 | } 11 | -------------------------------------------------------------------------------- /docs/css/fixed-size.840ec49f.css: -------------------------------------------------------------------------------- 1 | .item-inner span[data-v-e8597b74]:first-child{margin-right:1em}.list{width:100%;height:500px;border:2px solid;border-radius:3px;overflow-y:auto;border-color:#696969}.list .list-item-fixed{display:flex;align-items:center;padding:0 1em;height:60px;border-bottom:1px solid;border-color:#d3d3d3} -------------------------------------------------------------------------------- /scripts/rollup.banner.js: -------------------------------------------------------------------------------- 1 | const packageJson = require('../package.json') 2 | const version = packageJson.version 3 | const homepage = packageJson.homepage 4 | 5 | export default ` 6 | /*! 7 | * vue-virtual-scroll-list v${version} 8 | * open source under the MIT license 9 | * ${homepage} 10 | */ 11 | ` 12 | -------------------------------------------------------------------------------- /example/src/common/user.js: -------------------------------------------------------------------------------- 1 | import { Random } from './mock' 2 | 3 | const getRandomAvatar = () => { 4 | return `https://avatars1.githubusercontent.com/u/${Random.integer(10000, 99999)}` 5 | } 6 | 7 | export default () => { 8 | return { 9 | name: Random.name(), 10 | avatar: getRandomAvatar() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["standard"], 3 | "env": { 4 | "browser": true, 5 | "amd": true 6 | }, 7 | "globals": { 8 | "it": "readonly", 9 | "expect": "readonly", 10 | "describe": "readonly" 11 | }, 12 | "parserOptions": { 13 | "ecmaVersion": 2018, 14 | "sourceType": "module" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/src/common/get-code-url.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | const hashValue = (location.hash || '').substr(2) 3 | if (hashValue) { 4 | return `https://github.com/tangbc/vue-virtual-scroll-list/tree/master/example/src/views/${hashValue}/Main.vue` 5 | } else { 6 | return `https://github.com/tangbc/vue-virtual-scroll-list` 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this component. 4 | title: '' 5 | labels: suggestion 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the feature request 11 | 12 | A clear and concise description of what you want to happen. Or a clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | -------------------------------------------------------------------------------- /scripts/rollup.development.js: -------------------------------------------------------------------------------- 1 | import ProductionConfig from './rollup.production' 2 | 3 | // development mode just rewrite from production config. 4 | export default Object.assign({}, ProductionConfig, { 5 | output: { 6 | ...ProductionConfig.output, 7 | file: './dev/index.js', 8 | sourcemap: true, 9 | banner: '/* eslint-disable */' // disable eslint when bundle with docs. 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /docs/css/page-mode.bb1117ff.css: -------------------------------------------------------------------------------- 1 | .item-inner .head[data-v-1df8c792]{font-weight:500}.item-inner span[data-v-1df8c792]:first-child{margin-right:1em}.item-inner .desc[data-v-1df8c792]{padding-top:.5em;text-align:justify}.list-page{width:100%;border:2px solid;border-radius:3px;overflow-y:auto;border-color:#696969}.list-page .list-item-page{display:flex;align-items:center;padding:1em;border-bottom:1px solid;border-color:#d3d3d3}.bottom{padding:2em 0} -------------------------------------------------------------------------------- /docs/css/dynamic-size.9013d8ce.css: -------------------------------------------------------------------------------- 1 | .item-inner .head[data-v-5f14b5b8]{font-weight:500}.item-inner span[data-v-5f14b5b8]:first-child{margin-right:1em}.item-inner .desc[data-v-5f14b5b8]{padding-top:.5em;text-align:justify}.list-dynamic{width:100%;height:500px;border:2px solid;border-radius:3px;overflow-y:auto;border-color:#696969}.list-dynamic .list-item-dynamic{display:flex;align-items:center;padding:1em;border-bottom:1px solid;border-color:#d3d3d3} -------------------------------------------------------------------------------- /scripts/rollup.production.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import bannerString from './rollup.banner' 3 | 4 | export default { 5 | external: ['vue'], 6 | input: './src/index.js', 7 | output: { 8 | format: 'umd', 9 | file: './dist/index.js', 10 | name: 'VirtualList', 11 | sourcemap: false, 12 | globals: { 13 | vue: 'Vue', 14 | }, 15 | banner: bannerString.replace(/\n/, '') 16 | }, 17 | plugins: [ 18 | babel() 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /docs/css/horizontal.50e26ffa.css: -------------------------------------------------------------------------------- 1 | .item-inner[data-v-3248155a]{display:flex;align-items:center;flex-direction:column;padding:2em 0}.item-inner .index[data-v-3248155a]{width:100%;text-align:center}.item-inner .size[data-v-3248155a]{text-align:right;color:#a9a9a9;font-size:16px}.list-horizontal{width:100%;border:2px solid;border-radius:3px;overflow-x:auto;border-color:#696969;display:flex}.list-horizontal .wrapper{display:flex;flex-direction:row}.list-horizontal .list-item-horizontal{border-right:1px solid #dfdfdf} -------------------------------------------------------------------------------- /example/src/mixins/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | methods: { 3 | dispatch(componentName, eventName, ...rest) { 4 | let parent = this.$parent || this.$root 5 | let name = parent.$options.name 6 | 7 | while (parent && (!name || name !== componentName)) { 8 | parent = parent.$parent 9 | if (parent) { 10 | name = parent.$options.name 11 | } 12 | } 13 | 14 | if (parent) { 15 | parent.$emit.apply(parent, [eventName].concat(rest)) 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/src/views/fixed-size/Item.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | 24 | -------------------------------------------------------------------------------- /example/src/components/Introduction.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | 18 | 29 | -------------------------------------------------------------------------------- /test/item.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 29 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transformIgnorePatterns: [ 3 | '/node_modules' 4 | ], 5 | collectCoverage: true, 6 | coverageDirectory: '/coverage', 7 | coverageReporters: [ 8 | 'lcov', 9 | 'html', 10 | 'text-summary', 11 | ], 12 | collectCoverageFrom: [ 13 | '/src/*.js' 14 | ], 15 | moduleFileExtensions: [ 16 | 'js', 17 | 'vue' 18 | ], 19 | transform: { 20 | '.*\\.(vue)$': 'vue-jest', 21 | '^.+\\.js$': '/node_modules/babel-jest' 22 | }, 23 | // testRegex: 'scroll.test.js?$' 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report to help improve. 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe 11 | A clear and concise description of what the bug is. 12 | 13 | ## To Reproduce 14 | Steps to reproduce the behavior: 15 | 1. Click on '....' 16 | 2. Scroll down to '....' 17 | 3. See error 18 | 19 | ## Reproduce demo 20 | Providing a reproduce demo of this bug, that will help me solve the problem more quickly. You can fork by this online demo: https://codesandbox.io/s/live-demo-virtual-list-e1ww1 21 | 22 | ## Other 23 | - Version [e.g. 2.0] 24 | - Browser [e.g. chrome, safari] 25 | -------------------------------------------------------------------------------- /example/src/views/horizontal/Code.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 33 | 34 | -------------------------------------------------------------------------------- /example/src/views/dynamic-size/Item.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | 27 | -------------------------------------------------------------------------------- /example/src/views/page-mode/Item.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | 27 | 41 | -------------------------------------------------------------------------------- /example/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | 5 | import GithubButton from 'vue-github-button' 6 | import VirtualList from '../../src/index' 7 | import Introduction from './components/Introduction' 8 | import CodeHighLight from './components/CodeHighLight' 9 | import Corner from './components/Corner' 10 | import Tab from './components/Tab' 11 | 12 | Vue.component('virtual-list', VirtualList) 13 | Vue.component(Introduction.name, Introduction) 14 | Vue.component(CodeHighLight.name, CodeHighLight) 15 | Vue.component(Corner.name, Corner) 16 | Vue.component(Tab.name, Tab) 17 | Vue.component('github-button', GithubButton) 18 | 19 | Vue.config.devtools = false 20 | Vue.config.productionTip = false 21 | 22 | new Vue({ 23 | router, 24 | render: h => h(App) 25 | }).$mount('#app') 26 | -------------------------------------------------------------------------------- /example/src/views/horizontal/Item.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | 24 | -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | vue-virtual-scroll-list 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /example/src/views/fixed-size/Code.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 50 | 51 | -------------------------------------------------------------------------------- /example/src/views/infinite-loading/Item.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | 27 | -------------------------------------------------------------------------------- /example/src/views/dynamic-size/Code.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 50 | 51 | -------------------------------------------------------------------------------- /example/src/views/infinite-loading/Code.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 48 | 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 tangbc 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. -------------------------------------------------------------------------------- /example/src/components/CodeHighLight.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 39 | 40 | 49 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "npm run serve", 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "core-js": "^3.6.4", 13 | "vue-github-button": "^1.1.2", 14 | "vue-router": "^3.1.6" 15 | }, 16 | "devDependencies": { 17 | "@vue/cli-plugin-babel": "~4.3.0", 18 | "@vue/cli-plugin-eslint": "~4.3.0", 19 | "@vue/cli-plugin-router": "^4.3.0", 20 | "@vue/cli-service": "~4.3.0", 21 | "babel-eslint": "^10.1.0", 22 | "eslint": "^6.7.2", 23 | "eslint-plugin-vue": "^6.2.2", 24 | "less": "^3.11.1", 25 | "less-loader": "^5.0.0", 26 | "raw-loader": "^4.0.0", 27 | "vue-template-compiler": "^2.6.11" 28 | }, 29 | "eslintConfig": { 30 | "root": true, 31 | "env": { 32 | "node": true 33 | }, 34 | "extends": [ 35 | "plugin:vue/essential", 36 | "eslint:recommended" 37 | ], 38 | "parserOptions": { 39 | "parser": "babel-eslint" 40 | }, 41 | "rules": {} 42 | }, 43 | "browserslist": [ 44 | "> 1%", 45 | "last 2 versions", 46 | "not dead" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /docs/css/infinite-loading.4b238c18.css: -------------------------------------------------------------------------------- 1 | .item-inner .head[data-v-b5a02d2e]{font-weight:500}.item-inner .index[data-v-b5a02d2e]{margin-right:1em}.item-inner .name[data-v-b5a02d2e]{margin-left:1em}.item-inner .desc[data-v-b5a02d2e]{padding-top:.5em;text-align:justify}.result{margin-bottom:1em}.list-infinite{width:100%;height:500px;border:2px solid;border-radius:3px;overflow-y:auto;border-color:#696969;position:relative}.list-infinite .list-item-infinite{display:flex;align-items:center;padding:1em;border-bottom:1px solid;border-color:#d3d3d3}.list-infinite .loader-wrapper{padding:1em}.list-infinite .loader{font-size:10px;margin:0 auto;text-indent:-9999em;width:30px;height:30px;border-radius:50%;background:#fff;background:linear-gradient(90deg,#9b4dca 10%,hsla(0,0%,100%,0) 42%);position:relative;-webkit-animation:load3 1.4s linear infinite;animation:load3 1.4s linear infinite;transform:translateZ(0)}.list-infinite .loader:before{width:50%;height:50%;background:#9b4dca;border-radius:100% 0 0 0;position:absolute;top:0;left:0;content:""}.list-infinite .loader:after{background:#fff;width:75%;height:75%;border-radius:50%;content:"";margin:auto;position:absolute;top:0;left:0;bottom:0;right:0}@-webkit-keyframes load3{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@keyframes load3{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}} -------------------------------------------------------------------------------- /example/src/views/chat-room/Toolbar.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 39 | 40 | 56 | -------------------------------------------------------------------------------- /example/src/views/keep-state/Code.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 52 | 53 | 58 | 59 | -------------------------------------------------------------------------------- /test/scroll.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import VirtualList from '../src/index' 3 | import Item from './item.vue' 4 | import { getDatas } from './util' 5 | import Vue from 'vue' 6 | 7 | describe('scroll', () => { 8 | const Instance = mount({ 9 | name: 'test', 10 | components: { 11 | 'virtual-list': VirtualList 12 | }, 13 | template: ` 14 |
15 | 20 |
21 | `, 22 | data () { 23 | return { 24 | items: getDatas(1000), 25 | item: Item 26 | } 27 | } 28 | }) 29 | 30 | it('check mount', () => { 31 | expect(Instance.name()).toBe('test') 32 | expect(Instance.is('div')).toBe(true) 33 | expect(Instance.isVueInstance()).toBe(true) 34 | expect(Instance.find('.my-list').exists()).toBe(true) 35 | }) 36 | 37 | // @TODO 38 | it('check scroll behavior', () => { 39 | const myList = Instance.find('.my-list') 40 | const vslVm = myList.vm 41 | const rootEl = vslVm.$el 42 | 43 | rootEl.scrollTop = 2000 44 | myList.trigger('scroll') 45 | 46 | Vue.nextTick(() => { 47 | // console.log(vslVm.$data.range.start) 48 | }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/offset.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import VirtualList from '../src/index' 3 | import Item from './item.vue' 4 | import { getDatas } from './util' 5 | import Vue from 'vue' 6 | 7 | describe('offset', () => { 8 | const Instance = mount({ 9 | name: 'test', 10 | components: { 11 | 'virtual-list': VirtualList 12 | }, 13 | template: ` 14 |
15 | 21 |
22 | `, 23 | data () { 24 | return { 25 | items: getDatas(1000), 26 | item: Item, 27 | offset: 0 28 | } 29 | } 30 | }) 31 | 32 | it('check mount', () => { 33 | expect(Instance.name()).toBe('test') 34 | expect(Instance.is('div')).toBe(true) 35 | expect(Instance.isVueInstance()).toBe(true) 36 | expect(Instance.find('.my-list').exists()).toBe(true) 37 | }) 38 | 39 | // @TODO 40 | it('check offset and data reactive', () => { 41 | const vmData = Instance.vm.$data 42 | const vslVm = Instance.find('.my-list').vm 43 | expect(vslVm.virtual.offset).toBe(0) 44 | 45 | vmData.offset = 100 46 | // Vue.nextTick(() => { 47 | // expect(vslVm.virtual.offset).toBe(100) 48 | // }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/start.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import VirtualList from '../src/index' 3 | import Item from './item.vue' 4 | import { getDatas } from './util' 5 | import Vue from 'vue' 6 | 7 | describe('start', () => { 8 | const Instance = mount({ 9 | name: 'test', 10 | components: { 11 | 'virtual-list': VirtualList 12 | }, 13 | template: ` 14 |
15 | 21 |
22 | `, 23 | data () { 24 | return { 25 | items: getDatas(1000), 26 | item: Item, 27 | start: 0 28 | } 29 | } 30 | }) 31 | 32 | it('check mount', () => { 33 | expect(Instance.name()).toBe('test') 34 | expect(Instance.is('div')).toBe(true) 35 | expect(Instance.isVueInstance()).toBe(true) 36 | expect(Instance.find('.my-list').exists()).toBe(true) 37 | }) 38 | 39 | // @TODO 40 | it('check start and data reactive', () => { 41 | const vmData = Instance.vm.$data 42 | const vslVm = Instance.find('.my-list').vm 43 | expect(vslVm.virtual.range.start).toBe(0) 44 | 45 | vmData.start = 100 46 | // Vue.nextTick(() => { 47 | // expect(vslVm.virtual.range.start).toBe(100) 48 | // }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /docs/css/keep-state.2736386a.css: -------------------------------------------------------------------------------- 1 | .item-inner[data-v-f0c75490]{position:relative;display:flex;align-items:center}.item-inner .index[data-v-f0c75490]{margin-right:1em}.item-inner .name[data-v-f0c75490]{margin-left:1em;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.item-inner .checkbox[data-v-f0c75490]{text-align:center;width:30px;height:30px;border:none;outline:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;margin:0;background-color:#fff;background-repeat:no-repeat;background-position:0;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='-10 -18 100 135'%3E%3Ccircle cx='50' cy='50' r='50' fill='none' stroke='%23ededed' stroke-width='3'/%3E%3C/svg%3E")}.item-inner .checkbox[data-v-f0c75490]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='-10 -18 100 135'%3E%3Ccircle cx='50' cy='50' r='50' fill='none' stroke='%23c28ce2' stroke-width='3'/%3E%3Cpath fill='%239b4dca' d='M72 25L42 71 27 56l-4 4 20 20 34-52z'/%3E%3C/svg%3E")}.title[data-v-5618352e]{margin:3em 0 2em 0}.selects{margin-bottom:1em;font-size:14px}.list-keep{width:100%;height:500px;border:2px solid;border-radius:3px;overflow-y:auto;border-color:#696969}.list-keep .list-item-keep{display:flex;align-items:center;padding:0 1em;height:60px;border-bottom:1px solid;border-color:#d3d3d3} -------------------------------------------------------------------------------- /test/horizontal.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import VirtualList from '../src/index' 3 | import Item from './item.vue' 4 | import { getDatas } from './util' 5 | 6 | describe('horizontal', () => { 7 | const Instance = mount({ 8 | name: 'test', 9 | components: { 10 | 'virtual-list': VirtualList 11 | }, 12 | template: ` 13 |
14 | 21 |
22 | `, 23 | data () { 24 | return { 25 | items: getDatas(1000), 26 | item: Item 27 | } 28 | } 29 | }) 30 | 31 | it('check mount', () => { 32 | expect(Instance.name()).toBe('test') 33 | expect(Instance.is('div')).toBe(true) 34 | expect(Instance.isVueInstance()).toBe(true) 35 | expect(Instance.find('.my-list').exists()).toBe(true) 36 | }) 37 | 38 | // @TODO scrollHeight scrollWidth is both 0 39 | it('check scroll direction', () => { 40 | const vslVm = Instance.find('.my-list').vm 41 | const rootEl = vslVm.$el 42 | // const wrapperEl = rootEl.querySelector('[role="group"]') 43 | 44 | expect(rootEl.scrollHeight === rootEl.clientHeight).toBe(true) 45 | // expect(rootEl.scrollWidth > rootEl.clientWidth).toBe(true) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /docs/highlight/theme.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | github.com style (c) Vasily Polovnyov 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | color: #333; 12 | background: #f8f8f8; 13 | } 14 | 15 | .hljs-comment, 16 | .hljs-quote { 17 | color: #998; 18 | font-style: italic; 19 | } 20 | 21 | .hljs-keyword, 22 | .hljs-selector-tag, 23 | .hljs-subst { 24 | color: #333; 25 | font-weight: bold; 26 | } 27 | 28 | .hljs-number, 29 | .hljs-literal, 30 | .hljs-variable, 31 | .hljs-template-variable, 32 | .hljs-tag .hljs-attr { 33 | color: #008080; 34 | } 35 | 36 | .hljs-string, 37 | .hljs-doctag { 38 | color: #d14; 39 | } 40 | 41 | .hljs-title, 42 | .hljs-section, 43 | .hljs-selector-id { 44 | color: #900; 45 | font-weight: bold; 46 | } 47 | 48 | .hljs-subst { 49 | font-weight: normal; 50 | } 51 | 52 | .hljs-type, 53 | .hljs-class .hljs-title { 54 | color: #458; 55 | font-weight: bold; 56 | } 57 | 58 | .hljs-tag, 59 | .hljs-name, 60 | .hljs-attribute { 61 | color: #000080; 62 | font-weight: normal; 63 | } 64 | 65 | .hljs-regexp, 66 | .hljs-link { 67 | color: #009926; 68 | } 69 | 70 | .hljs-symbol, 71 | .hljs-bullet { 72 | color: #990073; 73 | } 74 | 75 | .hljs-built_in, 76 | .hljs-builtin-name { 77 | color: #0086b3; 78 | } 79 | 80 | .hljs-meta { 81 | color: #999; 82 | font-weight: bold; 83 | } 84 | 85 | .hljs-deletion { 86 | background: #fdd; 87 | } 88 | 89 | .hljs-addition { 90 | background: #dfd; 91 | } 92 | 93 | .hljs-emphasis { 94 | font-style: italic; 95 | } 96 | 97 | .hljs-strong { 98 | font-weight: bold; 99 | } -------------------------------------------------------------------------------- /example/public/highlight/theme.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | github.com style (c) Vasily Polovnyov 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | color: #333; 12 | background: #f8f8f8; 13 | } 14 | 15 | .hljs-comment, 16 | .hljs-quote { 17 | color: #998; 18 | font-style: italic; 19 | } 20 | 21 | .hljs-keyword, 22 | .hljs-selector-tag, 23 | .hljs-subst { 24 | color: #333; 25 | font-weight: bold; 26 | } 27 | 28 | .hljs-number, 29 | .hljs-literal, 30 | .hljs-variable, 31 | .hljs-template-variable, 32 | .hljs-tag .hljs-attr { 33 | color: #008080; 34 | } 35 | 36 | .hljs-string, 37 | .hljs-doctag { 38 | color: #d14; 39 | } 40 | 41 | .hljs-title, 42 | .hljs-section, 43 | .hljs-selector-id { 44 | color: #900; 45 | font-weight: bold; 46 | } 47 | 48 | .hljs-subst { 49 | font-weight: normal; 50 | } 51 | 52 | .hljs-type, 53 | .hljs-class .hljs-title { 54 | color: #458; 55 | font-weight: bold; 56 | } 57 | 58 | .hljs-tag, 59 | .hljs-name, 60 | .hljs-attribute { 61 | color: #000080; 62 | font-weight: normal; 63 | } 64 | 65 | .hljs-regexp, 66 | .hljs-link { 67 | color: #009926; 68 | } 69 | 70 | .hljs-symbol, 71 | .hljs-bullet { 72 | color: #990073; 73 | } 74 | 75 | .hljs-built_in, 76 | .hljs-builtin-name { 77 | color: #0086b3; 78 | } 79 | 80 | .hljs-meta { 81 | color: #999; 82 | font-weight: bold; 83 | } 84 | 85 | .hljs-deletion { 86 | background: #fdd; 87 | } 88 | 89 | .hljs-addition { 90 | background: #dfd; 91 | } 92 | 93 | .hljs-emphasis { 94 | font-style: italic; 95 | } 96 | 97 | .hljs-strong { 98 | font-weight: bold; 99 | } -------------------------------------------------------------------------------- /example/src/views/chat-room/util.js: -------------------------------------------------------------------------------- 1 | import { Random } from '../../common/mock' 2 | import getSentences from '../../common/sentences' 3 | import getUser from '../../common/user' 4 | 5 | let sidCounter = 0 6 | const maxCounts = 500 7 | 8 | export function genSid () { 9 | return `sid-${sidCounter++}` 10 | } 11 | 12 | export function genBody () { 13 | return { 14 | user: {}, 15 | sid: '', 16 | content: '', 17 | images: [], 18 | isCreator: false 19 | } 20 | } 21 | 22 | export function getMessages (numbers, delay) { 23 | return new Promise((resolve) => { 24 | if (sidCounter >= maxCounts) { 25 | resolve([]) 26 | return 27 | } 28 | 29 | setTimeout(() => { 30 | const messages = [] 31 | while (numbers--) { 32 | let body = genBody() 33 | body.user = getUser() 34 | body.content = getSentences() 35 | body.sid = genSid() 36 | messages.push(body) 37 | } 38 | 39 | resolve(messages) 40 | }, delay ? Random.pick([300, 500, 800]) : 0) 41 | }) 42 | } 43 | 44 | export function getSids (messages) { 45 | return messages.map((message) => message.sid) 46 | } 47 | 48 | export const LOAD_TYPES = { 49 | EMPTY: 'EMPTY', 50 | PAGES: 'PAGES', 51 | FEW: 'FEW' 52 | } 53 | 54 | export function setLoadType (type) { 55 | try { 56 | sessionStorage.setItem('LOAD_TYPES', type) 57 | } catch (e) { 58 | console.error(e) 59 | } 60 | } 61 | 62 | export function getLoadType () { 63 | try { 64 | return sessionStorage.getItem('LOAD_TYPES') || LOAD_TYPES.PAGES 65 | } catch (e) { 66 | console.error(e) 67 | return LOAD_TYPES.EMPTY 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | vue-virtual-scroll-list
-------------------------------------------------------------------------------- /example/src/components/Corner.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 27 | 28 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | 6 | name: Publish 7 | 8 | on: 9 | release: 10 | types: [published] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | publish: 17 | runs-on: ubuntu-latest 18 | 19 | name: Build and publish to npm 20 | steps: 21 | - name: Check actor permission level 22 | uses: skjnldsv/check-actor-permission@e591dbfe838300c007028e1219ca82cc26e8d7c5 # v2.1 23 | with: 24 | require: admin 25 | 26 | - name: Checkout 27 | uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 28 | 29 | - name: Read package.json node and npm engines version 30 | uses: skjnldsv/read-package-engines-version-actions@1bdcee71fa343c46b18dc6aceffb4cd1e35209c6 # v1.2 31 | id: versions 32 | with: 33 | fallbackNode: '^16' 34 | fallbackNpm: '^7' 35 | 36 | - name: Set up node ${{ steps.versions.outputs.nodeVersion }} 37 | uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # v3 38 | with: 39 | node-version: ${{ steps.versions.outputs.nodeVersion }} 40 | 41 | - name: Set up npm ${{ steps.versions.outputs.npmVersion }} 42 | run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}" 43 | 44 | - name: Install dependencies & build 45 | run: | 46 | npm ci 47 | npm run build --if-present 48 | 49 | - name: Publish 50 | run: | 51 | npm config set //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN 52 | npm publish 53 | env: 54 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 55 | -------------------------------------------------------------------------------- /.github/workflows/command-rebase.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | 6 | name: Rebase command 7 | 8 | on: 9 | issue_comment: 10 | types: created 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | rebase: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: none 20 | 21 | # On pull requests and if the comment starts with `/rebase` 22 | if: github.event.issue.pull_request != '' && startsWith(github.event.comment.body, '/rebase') 23 | 24 | steps: 25 | - name: Add reaction on start 26 | uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # v2.1.0 27 | with: 28 | token: ${{ secrets.COMMAND_BOT_PAT }} 29 | repository: ${{ github.event.repository.full_name }} 30 | comment-id: ${{ github.event.comment.id }} 31 | reaction-type: "+1" 32 | 33 | - name: Checkout the latest code 34 | uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 35 | with: 36 | fetch-depth: 0 37 | token: ${{ secrets.COMMAND_BOT_PAT }} 38 | 39 | - name: Automatic Rebase 40 | uses: cirrus-actions/rebase@6e572f08c244e2f04f9beb85a943eb618218714d # 1.7 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.COMMAND_BOT_PAT }} 43 | 44 | - name: Add reaction on failure 45 | uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # v2.1.0 46 | if: failure() 47 | with: 48 | token: ${{ secrets.COMMAND_BOT_PAT }} 49 | repository: ${{ github.event.repository.full_name }} 50 | comment-id: ${{ github.event.comment.id }} 51 | reaction-type: "-1" 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-virtual-scroll-list", 3 | "version": "2.3.5", 4 | "description": "A vue component support big amount data list with high scroll performance.", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "src", 8 | "dist" 9 | ], 10 | "scripts": { 11 | "lint": "eslint --fix src --ext .js", 12 | "dev:docs": "cd example && npm run dev", 13 | "build:docs": "cd example && npm run build && cp ../dist/index.js ../docs", 14 | "build": "npm run lint && rollup --config ./scripts/rollup.production.js", 15 | "dev": "rollup --config ./scripts/rollup.development.js --watch", 16 | "test": "rm -rf ./coverage && jest" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/tangbc/vue-virtual-scroll-list.git" 21 | }, 22 | "keywords": [ 23 | "vue", 24 | "big-data", 25 | "big-list", 26 | "scroll-list", 27 | "virtual-list" 28 | ], 29 | "author": "tangbc", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/tangbc/vue-virtual-scroll-list/issues" 33 | }, 34 | "homepage": "https://github.com/tangbc/vue-virtual-scroll-list#readme", 35 | "dependencies": {}, 36 | "devDependencies": { 37 | "@babel/core": "^7.8.7", 38 | "@babel/preset-env": "^7.8.7", 39 | "@vue/test-utils": "^1.0.0-beta.33", 40 | "babel-core": "^7.0.0-bridge.0", 41 | "babel-jest": "^25.5.1", 42 | "eslint": "^6.8.0", 43 | "eslint-config-standard": "^14.1.1", 44 | "eslint-loader": "^4.0.0", 45 | "eslint-plugin-import": "^2.20.2", 46 | "eslint-plugin-node": "^11.1.0", 47 | "eslint-plugin-promise": "^4.2.1", 48 | "eslint-plugin-standard": "^4.0.1", 49 | "eslint-plugin-vue": "^6.2.2", 50 | "jest": "^25.5.1", 51 | "rollup": "^2.1.0", 52 | "rollup-plugin-babel": "^4.4.0", 53 | "vue": "^2.6.11", 54 | "vue-jest": "^3.0.5", 55 | "vue-template-compiler": "^2.6.11" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /example/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Index from '../views/home/Main.vue' 4 | 5 | Vue.use(VueRouter) 6 | 7 | const routes = [ 8 | { 9 | path: '/', 10 | name: 'home', 11 | component: Index 12 | }, 13 | { 14 | path: '/fixed-size', 15 | name: 'fixed-size', 16 | component: () => import(/* webpackChunkName: "fixed-size" */ '../views/fixed-size/Main.vue') 17 | }, 18 | { 19 | path: '/dynamic-size', 20 | name: 'dynamic-size', 21 | component: () => import(/* webpackChunkName: "dynamic-size" */ '../views/dynamic-size/Main.vue') 22 | }, 23 | { 24 | path: '/horizontal', 25 | name: 'horizontal', 26 | component: () => import(/* webpackChunkName: "horizontal" */ '../views/horizontal/Main.vue') 27 | }, 28 | { 29 | path: '/infinite-loading', 30 | name: 'infinite-loading', 31 | component: () => import(/* webpackChunkName: "infinite-loading" */ '../views/infinite-loading/Main.vue') 32 | }, 33 | { 34 | path: '/keep-state', 35 | name: 'keep-state', 36 | component: () => import(/* webpackChunkName: "keep-state" */ '../views/keep-state/Main.vue') 37 | }, 38 | { 39 | path: '/chat-room', 40 | name: 'chat-room', 41 | component: () => import(/* webpackChunkName: "chat-room" */ '../views/chat-room/Main.vue') 42 | }, 43 | { 44 | path: '/page-mode', 45 | name: 'page-mode', 46 | component: () => import(/* webpackChunkName: "page-mode" */ '../views/page-mode/Main.vue') 47 | } 48 | ] 49 | 50 | // just for development, if you want to run this project in your local 51 | // please copy a any example and rename it as dev in example/src/views folder 52 | if (process.env.NODE_ENV === 'development') { 53 | routes.push({ 54 | path: '/dev', 55 | name: 'dev', 56 | component: () => import(/* webpackChunkName: "dev" */ '../views/dev/Main.vue') 57 | }) 58 | } 59 | 60 | const router = new VueRouter({ 61 | routes 62 | }) 63 | 64 | export default router 65 | -------------------------------------------------------------------------------- /example/src/views/fixed-size/Main.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 69 | 70 | 89 | -------------------------------------------------------------------------------- /test/extra-props2.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import VirtualList from '../src/index' 3 | import Item from './item.vue' 4 | import { getDatas } from './util' 5 | import Vue from 'vue' 6 | 7 | describe('extra-props inline', () => { 8 | const Instance = mount({ 9 | name: 'test', 10 | components: { 11 | 'virtual-list': VirtualList 12 | }, 13 | template: ` 14 |
15 | 21 |
22 | `, 23 | data () { 24 | return { 25 | items: getDatas(1000), 26 | item: Item, 27 | otherProp: 'abc' 28 | } 29 | } 30 | }) 31 | 32 | it('check mount', () => { 33 | expect(Instance.name()).toBe('test') 34 | expect(Instance.is('div')).toBe(true) 35 | expect(Instance.isVueInstance()).toBe(true) 36 | expect(Instance.find('.my-list').exists()).toBe(true) 37 | }) 38 | 39 | it('check extra props render and data reactive', () => { 40 | const vmData = Instance.vm.$data 41 | const vslVm = Instance.find('.my-list').vm 42 | const rootEl = vslVm.$el 43 | const wrapperEl = rootEl.firstElementChild 44 | 45 | const checkProps = (otherProp) => { 46 | // items render content 47 | for (let i = 0; i < wrapperEl.childNodes.length; i++) { 48 | const itemEl = wrapperEl.childNodes[i] 49 | const itemInnerEl = itemEl.firstElementChild 50 | expect(itemInnerEl.className).toBe('inner') 51 | expect(itemInnerEl.querySelector('.index').textContent).toBe(`${i}`) 52 | expect(itemInnerEl.querySelector('.source').textContent).toBe(vmData.items[i].text) 53 | expect(itemInnerEl.querySelector('.other').textContent).toBe(otherProp) 54 | } 55 | } 56 | 57 | checkProps(vmData.otherProp) 58 | 59 | vmData.otherProp = 'xyz' 60 | Vue.nextTick(() => { 61 | checkProps('xyz') 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /example/src/views/keep-state/Item.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 37 | 38 | -------------------------------------------------------------------------------- /example/src/views/page-mode/Main.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 70 | 71 | 91 | -------------------------------------------------------------------------------- /test/extra-props.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import VirtualList from '../src/index' 3 | import Item from './item.vue' 4 | import { getDatas } from './util' 5 | import Vue from 'vue' 6 | 7 | describe('extra-props', () => { 8 | const Instance = mount({ 9 | name: 'test', 10 | components: { 11 | 'virtual-list': VirtualList 12 | }, 13 | template: ` 14 |
15 | 21 |
22 | `, 23 | data () { 24 | return { 25 | items: getDatas(1000), 26 | item: Item, 27 | extraProps: { 28 | otherProp: '123' 29 | } 30 | } 31 | } 32 | }) 33 | 34 | it('check mount', () => { 35 | expect(Instance.name()).toBe('test') 36 | expect(Instance.is('div')).toBe(true) 37 | expect(Instance.isVueInstance()).toBe(true) 38 | expect(Instance.find('.my-list').exists()).toBe(true) 39 | }) 40 | 41 | it('check extra props render and data reactive', () => { 42 | const vmData = Instance.vm.$data 43 | const vslVm = Instance.find('.my-list').vm 44 | const rootEl = vslVm.$el 45 | const wrapperEl = rootEl.firstElementChild 46 | 47 | const checkProps = (otherProp) => { 48 | // items render content 49 | for (let i = 0; i < wrapperEl.childNodes.length; i++) { 50 | const itemEl = wrapperEl.childNodes[i] 51 | const itemInnerEl = itemEl.firstElementChild 52 | expect(itemInnerEl.className).toBe('inner') 53 | expect(itemInnerEl.querySelector('.index').textContent).toBe(`${i}`) 54 | expect(itemInnerEl.querySelector('.source').textContent).toBe(vmData.items[i].text) 55 | expect(itemInnerEl.querySelector('.other').textContent).toBe(otherProp) 56 | } 57 | } 58 | 59 | checkProps(vmData.extraProps.otherProp) 60 | 61 | vmData.extraProps.otherProp = '789' 62 | Vue.nextTick(() => { 63 | checkProps('789') 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /test/slot.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import VirtualList from '../src/index' 3 | import Item from './item.vue' 4 | import { getDatas } from './util' 5 | 6 | describe('slot', () => { 7 | const Instance = mount({ 8 | name: 'test', 9 | components: { 10 | 'virtual-list': VirtualList 11 | }, 12 | template: ` 13 |
14 | 23 |
Header
24 |
Footer
25 |
26 |
27 | `, 28 | data () { 29 | return { 30 | items: getDatas(1000), 31 | item: Item 32 | } 33 | } 34 | }) 35 | 36 | it('check mount', () => { 37 | expect(Instance.name()).toBe('test') 38 | expect(Instance.is('div')).toBe(true) 39 | expect(Instance.isVueInstance()).toBe(true) 40 | expect(Instance.find('.my-list').exists()).toBe(true) 41 | }) 42 | 43 | it('check slot build', () => { 44 | const vslVm = Instance.find('.my-list').vm 45 | const rootEl = vslVm.$el 46 | const wrapperEl = rootEl.querySelector('[role="group"]') 47 | const headerEl = rootEl.querySelector('[role="header"]') 48 | const footerEl = rootEl.querySelector('[role="footer"]') 49 | 50 | // wrapper shoud be in middle between header and footer 51 | expect(wrapperEl.previousElementSibling).toBe(headerEl) 52 | expect(wrapperEl.nextElementSibling).toBe(footerEl) 53 | 54 | expect(!!headerEl).toBe(true) 55 | expect(!!footerEl).toBe(true) 56 | 57 | expect(headerEl.className).toBe('head1') 58 | expect(headerEl.tagName.toLowerCase()).toBe('section') 59 | expect(headerEl.firstElementChild.textContent).toBe('Header') 60 | 61 | expect(footerEl.className).toBe('foot1') 62 | expect(footerEl.tagName.toLowerCase()).toBe('article') 63 | expect(footerEl.firstElementChild.textContent).toBe('Footer') 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /.github/workflows/node.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | 6 | name: Node 7 | 8 | on: 9 | pull_request: 10 | paths: 11 | - '.github/workflows/**' 12 | - 'src/**' 13 | - 'appinfo/info.xml' 14 | - 'package.json' 15 | - 'package-lock.json' 16 | - 'tsconfig.json' 17 | - '**.js' 18 | - '**.ts' 19 | - '**.vue' 20 | push: 21 | branches: 22 | - main 23 | - master 24 | - stable* 25 | 26 | permissions: 27 | contents: read 28 | 29 | concurrency: 30 | group: node-${{ github.head_ref || github.run_id }} 31 | cancel-in-progress: true 32 | 33 | jobs: 34 | build: 35 | runs-on: ubuntu-latest 36 | 37 | name: node 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 41 | 42 | - name: Read package.json node and npm engines version 43 | uses: skjnldsv/read-package-engines-version-actions@1bdcee71fa343c46b18dc6aceffb4cd1e35209c6 # v1.2 44 | id: versions 45 | with: 46 | fallbackNode: '^16' 47 | fallbackNpm: '^7' 48 | 49 | - name: Set up node ${{ steps.versions.outputs.nodeVersion }} 50 | uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # v3 51 | with: 52 | node-version: ${{ steps.versions.outputs.nodeVersion }} 53 | 54 | - name: Set up npm ${{ steps.versions.outputs.npmVersion }} 55 | run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}" 56 | 57 | - name: Install dependencies & build 58 | run: | 59 | npm ci 60 | npm run build --if-present 61 | 62 | - name: Check webpack build changes 63 | run: | 64 | bash -c "[[ ! \"`git status --porcelain `\" ]] || (echo 'Please recompile and commit the assets, see the section \"Show changes on failure\" for details' && exit 1)" 65 | 66 | - name: Show changes on failure 67 | if: failure() 68 | run: | 69 | git status 70 | git --no-pager diff 71 | exit 1 # make it red to grab attention 72 | -------------------------------------------------------------------------------- /example/src/views/horizontal/Main.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 71 | 72 | 91 | -------------------------------------------------------------------------------- /example/src/views/dynamic-size/Main.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 75 | 76 | 94 | -------------------------------------------------------------------------------- /src/item.js: -------------------------------------------------------------------------------- 1 | /** 2 | * item and slot component both use similar wrapper 3 | * we need to know their size change at any time 4 | */ 5 | 6 | import Vue from 'vue' 7 | import { ItemProps, SlotProps } from './props' 8 | 9 | const Wrapper = { 10 | created () { 11 | this.shapeKey = this.horizontal ? 'offsetWidth' : 'offsetHeight' 12 | }, 13 | 14 | mounted () { 15 | if (typeof ResizeObserver !== 'undefined') { 16 | this.resizeObserver = new ResizeObserver(() => { 17 | this.dispatchSizeChange() 18 | }) 19 | this.resizeObserver.observe(this.$el) 20 | } 21 | }, 22 | 23 | // since componet will be reused, so disptach when updated 24 | updated () { 25 | // this.dispatchSizeChange() 26 | this.resizeObserver.observe(this.$el) 27 | }, 28 | 29 | beforeDestroy () { 30 | if (this.resizeObserver) { 31 | this.resizeObserver.disconnect() 32 | this.resizeObserver = null 33 | } 34 | }, 35 | 36 | methods: { 37 | getCurrentSize () { 38 | return this.$el ? this.$el[this.shapeKey] : 0 39 | }, 40 | 41 | // tell parent current size identify by unqiue key 42 | dispatchSizeChange () { 43 | this.$parent.$emit(this.event, this.uniqueKey, this.getCurrentSize(), this.hasInitial) 44 | } 45 | } 46 | } 47 | 48 | // wrapping for item 49 | export const Item = Vue.component('virtual-list-item', { 50 | mixins: [Wrapper], 51 | 52 | props: ItemProps, 53 | 54 | render (h) { 55 | const { tag, component, extraProps = {}, index, source, scopedSlots = {}, uniqueKey, slotComponent, tableMode } = this 56 | const props = { 57 | ...extraProps, 58 | source, 59 | index 60 | } 61 | 62 | return h(tag, { 63 | key: uniqueKey, 64 | attrs: { 65 | role: tableMode ? null : 'listitem' 66 | } 67 | }, [slotComponent ? slotComponent({ item: source, index: index, scope: props }) : h(component, { 68 | props, 69 | scopedSlots: scopedSlots 70 | })]) 71 | } 72 | }) 73 | 74 | // wrapping for slot 75 | export const Slot = Vue.component('virtual-list-slot', { 76 | mixins: [Wrapper], 77 | 78 | props: SlotProps, 79 | 80 | render (h) { 81 | const { tag, uniqueKey } = this 82 | 83 | return h(tag, { 84 | key: uniqueKey, 85 | attrs: { 86 | role: uniqueKey 87 | } 88 | }, this.$slots.default) 89 | } 90 | }) 91 | -------------------------------------------------------------------------------- /test/base.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import VirtualList from '../src/index' 3 | import { VirtualProps } from '../src/props' 4 | import Item from './item.vue' 5 | import { getDatas } from './util' 6 | 7 | describe('base', () => { 8 | const Instance = mount({ 9 | name: 'test', 10 | components: { 11 | 'virtual-list': VirtualList 12 | }, 13 | template: ` 14 |
15 | 20 |
21 | `, 22 | data () { 23 | return { 24 | items: getDatas(1000), 25 | item: Item 26 | } 27 | } 28 | }) 29 | 30 | it('check mount', () => { 31 | expect(Instance.name()).toBe('test') 32 | expect(Instance.is('div')).toBe(true) 33 | expect(Instance.isVueInstance()).toBe(true) 34 | expect(Instance.find('.my-list').exists()).toBe(true) 35 | }) 36 | 37 | it('check list build by default', () => { 38 | const vmData = Instance.vm.$data 39 | const vslVm = Instance.find('.my-list').vm 40 | const rootEl = vslVm.$el 41 | expect(rootEl.tagName.toLowerCase()).toBe(VirtualProps.rootTag.default) 42 | 43 | const wrapperEl = rootEl.firstElementChild 44 | 45 | // wrapper element and padding style 46 | expect(wrapperEl.getAttribute('role')).toBe('group') 47 | expect(!!rootEl.style.padding).toBe(false) 48 | expect(!!wrapperEl.style.padding).toBe(true) 49 | expect(wrapperEl.tagName.toLowerCase()).toBe(VirtualProps.wrapTag.default) 50 | 51 | // render number keeps by default 52 | expect(wrapperEl.childNodes.length).toBe(VirtualProps.keeps.default) 53 | 54 | // items render content 55 | for (let i = 0; i < wrapperEl.childNodes.length; i++) { 56 | const itemEl = wrapperEl.childNodes[i] 57 | expect(itemEl.className).toBe('') 58 | expect(itemEl.tagName.toLowerCase()).toBe(VirtualProps.itemTag.default) 59 | 60 | // item inner render (see ./item.vue) 61 | const itemInnerEl = itemEl.firstElementChild 62 | expect(itemInnerEl.className).toBe('inner') 63 | expect(itemInnerEl.querySelector('.index').textContent).toBe(`${i}`) 64 | expect(itemInnerEl.querySelector('.source').textContent).toBe(vmData.items[i].text) 65 | } 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /docs/js/fixed-size.1b9763e9.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["fixed-size"],{"19e7":function(e,t,n){},3188:function(e,t,n){},c227:function(e,t,n){"use strict";n.r(t);var i=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"example"},[n("github-corner"),n("introduction",{attrs:{description:"The size of each item is equal."}}),n("div",{staticClass:"example-content"},[n("tab",{on:{"tab-change":e.onTabChange}}),n("div",{directives:[{name:"show",rawName:"v-show",value:e.isShowView,expression:"isShowView"}]},[n("virtual-list",{staticClass:"list scroll-touch",attrs:{"data-key":"id","data-sources":e.items,"data-component":e.itemComponent,"estimate-size":50,"item-class":"list-item-fixed"}})],1),n("codeblock",{directives:[{name:"show",rawName:"v-show",value:!e.isShowView,expression:"!isShowView"}]})],1)],1)},s=[],a=(n("b0c0"),function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"item-inner"},[n("span",[e._v("# "+e._s(e.source.index))]),n("span",[e._v(e._s(e.source.name))])])}),o=[],c={name:"fix-size-item",props:{source:{type:Object,default:function(){return{}}}}},r=c,l=(n("d16c"),n("2877")),u=Object(l["a"])(r,a,o,!1,null,"e8597b74",null),d=u.exports,m=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",[n("code-high-light",{attrs:{type:"html",code:e.html}}),n("code-high-light",{attrs:{type:"js",code:e.js}})],1)},h=[],f='\n\n',p="\nimport Item from './Item'\nconst items = [\n {\n id: 'unique-id-xxx',\n ...\n },\n ....\n]\n\nexport default {\n ...\n data () {\n return {\n items: items,\n itemComponent: Item,\n }\n }\n ...\n}\n",w={name:"fix-size-code",data:function(){return{html:f,js:p}}},v=w,b=Object(l["a"])(v,m,h,!1,null,null,null),x=b.exports,g=n("adf9"),C=n("c57d"),_=n("b95e"),j=1e4,S=[],V=j;while(V--){var z=j-V;S.push({index:z,name:g["Random"].name(),id:Object(C["a"])(z)})}var k={name:"fix-size",components:{codeblock:x},data:function(){return{total:j.toLocaleString(),items:S,itemComponent:d,isShowView:_["a"]===_["b"].VIEW}},methods:{onTabChange:function(e){this.isShowView=e===_["b"].VIEW}}},y=k,E=(n("e3d2"),Object(l["a"])(y,i,s,!1,null,null,null));t["default"]=E.exports},c57d:function(e,t,n){"use strict";n("99af"),n("d3b7"),n("25f0");t["a"]=function(e){return"".concat(e,"$").concat(Math.random().toString(16).substr(9))}},d16c:function(e,t,n){"use strict";var i=n("19e7"),s=n.n(i);s.a},e3d2:function(e,t,n){"use strict";var i=n("3188"),s=n.n(i);s.a}}]); -------------------------------------------------------------------------------- /example/src/views/chat-room/Item.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 35 | 36 | -------------------------------------------------------------------------------- /docs/js/horizontal.ad2c5dae.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["horizontal"],{a0c6:function(t,e,i){"use strict";var a=i("baec"),n=i.n(a);n.a},a2ab:function(t,e,i){"use strict";i.r(e);var a=function(){var t=this,e=t.$createElement,i=t._self._c||e;return i("div",{staticClass:"example"},[i("github-corner"),i("introduction",{attrs:{description:"Set direction as horizontal, and also can use wrap-class, item-class to help you layout items in horizontal."}}),i("div",{staticClass:"example-content"},[i("tab",{on:{"tab-change":t.onTabChange}}),i("div",{directives:[{name:"show",rawName:"v-show",value:t.isShowView,expression:"isShowView"}]},[i("virtual-list",{staticClass:"list-horizontal scroll-touch",attrs:{"data-key":"id","data-sources":t.items,"data-component":t.itemComponent,"estimate-size":110,direction:"horizontal","wrap-class":"wrapper","item-class":"list-item-horizontal"}})],1),i("codeblock",{directives:[{name:"show",rawName:"v-show",value:!t.isShowView,expression:"!isShowView"}]})],1)],1)},n=[],o=function(){var t=this,e=t.$createElement,i=t._self._c||e;return i("div",{staticClass:"item-inner",style:{width:t.source.size+"px"}},[i("div",{staticClass:"index"},[t._v("# "+t._s(t.source.index))]),i("div",{staticClass:"size"},[t._v(t._s(t.source.size))])])},s=[],c={name:"horizontal-item",props:{source:{type:Object,default:function(){return{}}}}},r=c,l=(i("a0c6"),i("2877")),d=Object(l["a"])(r,o,s,!1,null,"3248155a",null),u=d.exports,h=function(){var t=this,e=t.$createElement,i=t._self._c||e;return i("div",[t.html?i("code-high-light",{attrs:{type:"html",code:t.html}}):t._e(),t.js?i("code-high-light",{attrs:{type:"js",code:t.js}}):t._e()],1)},m=[],p='\n\n',w="",v={name:"horizontal-code",data:function(){return{html:p,js:w}}},f=v,b=Object(l["a"])(f,h,m,!1,null,null,null),z=b.exports,_=i("adf9"),x=i("c57d"),C=i("b95e"),g=1e4,j=[60,80,100,150,180],k=[],y=g;while(y--){var S=g-y;k.push({index:S,id:Object(x["a"])(S),size:_["Random"].pick(j)})}var V={name:"horizontal",components:{codeblock:z},data:function(){return{items:k,itemComponent:u,isShowView:C["a"]===C["b"].VIEW}},methods:{onTabChange:function(t){this.isShowView=t===C["b"].VIEW}}},E=V,O=(i("f597"),Object(l["a"])(E,a,n,!1,null,null,null));e["default"]=O.exports},baec:function(t,e,i){},c57d:function(t,e,i){"use strict";i("99af"),i("d3b7"),i("25f0");e["a"]=function(t){return"".concat(t,"$").concat(Math.random().toString(16).substr(9))}},ca79:function(t,e,i){},f597:function(t,e,i){"use strict";var a=i("ca79"),n=i.n(a);n.a}}]); -------------------------------------------------------------------------------- /test/element.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import VirtualList from '../src/index' 3 | import { VirtualProps } from '../src/props' 4 | import Item from './item.vue' 5 | import { getDatas } from './util' 6 | 7 | describe('element', () => { 8 | const Instance = mount({ 9 | name: 'test', 10 | components: { 11 | 'virtual-list': VirtualList 12 | }, 13 | template: ` 14 |
15 | 26 |
27 | `, 28 | data () { 29 | return { 30 | items: getDatas(1000), 31 | item: Item 32 | } 33 | }, 34 | methods: { 35 | addItemClass (index) { 36 | return 'extra-item-' + index 37 | } 38 | } 39 | }) 40 | 41 | it('check mount', () => { 42 | expect(Instance.name()).toBe('test') 43 | expect(Instance.is('div')).toBe(true) 44 | expect(Instance.isVueInstance()).toBe(true) 45 | expect(Instance.find('.my-list').exists()).toBe(true) 46 | }) 47 | 48 | it('check element tag and class', () => { 49 | const vmData = Instance.vm.$data 50 | const vslVm = Instance.find('.my-list').vm 51 | const rootEl = vslVm.$el 52 | expect(rootEl.tagName.toLowerCase()).toBe('article') 53 | 54 | const wrapperEl = rootEl.firstElementChild 55 | 56 | // wrapper element and padding style 57 | expect(wrapperEl.getAttribute('role')).toBe('group') 58 | expect(!!rootEl.style.padding).toBe(false) 59 | expect(!!wrapperEl.style.padding).toBe(true) 60 | expect(wrapperEl.className).toBe('wrap-class-aaa') 61 | expect(wrapperEl.tagName.toLowerCase()).toBe('section') 62 | 63 | // render number keeps by default 64 | expect(wrapperEl.childNodes.length).toBe(VirtualProps.keeps.default) 65 | 66 | // items render content 67 | for (let i = 0; i < wrapperEl.childNodes.length; i++) { 68 | const itemEl = wrapperEl.childNodes[i] 69 | expect(itemEl.className).toBe(`item-class-bbb extra-item-${i}`) 70 | expect(itemEl.tagName.toLowerCase()).toBe('p') 71 | 72 | // item inner render (see ./item.vue) 73 | const itemInnerEl = itemEl.firstElementChild 74 | expect(itemInnerEl.className).toBe('inner') 75 | expect(itemInnerEl.querySelector('.index').textContent).toBe(`${i}`) 76 | expect(itemInnerEl.querySelector('.source').textContent).toBe(vmData.items[i].text) 77 | } 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /example/src/views/keep-state/Main.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 91 | 92 | 115 | -------------------------------------------------------------------------------- /src/props.js: -------------------------------------------------------------------------------- 1 | /** 2 | * props declaration for default, item and slot component 3 | */ 4 | 5 | export const VirtualProps = { 6 | dataKey: { 7 | type: [String, Function], 8 | required: true 9 | }, 10 | dataSources: { 11 | type: Array, 12 | required: true 13 | }, 14 | dataComponent: { 15 | type: [Object, Function], 16 | required: true 17 | }, 18 | 19 | keeps: { 20 | type: Number, 21 | default: 30 22 | }, 23 | extraProps: { 24 | type: Object 25 | }, 26 | estimateSize: { 27 | type: Number, 28 | default: 50 29 | }, 30 | 31 | tableMode: { 32 | type: Boolean, 33 | default: false 34 | }, 35 | 36 | direction: { 37 | type: String, 38 | default: 'vertical' // the other value is horizontal 39 | }, 40 | start: { 41 | type: Number, 42 | default: 0 43 | }, 44 | offset: { 45 | type: Number, 46 | default: 0 47 | }, 48 | topThreshold: { 49 | type: Number, 50 | default: 0 51 | }, 52 | bottomThreshold: { 53 | type: Number, 54 | default: 0 55 | }, 56 | pageMode: { 57 | type: Boolean, 58 | default: false 59 | }, 60 | rootTag: { 61 | type: String, 62 | default: 'div' 63 | }, 64 | wrapTag: { 65 | type: String, 66 | default: 'div' 67 | }, 68 | wrapClass: { 69 | type: String, 70 | default: '' 71 | }, 72 | wrapStyle: { 73 | type: Object 74 | }, 75 | itemTag: { 76 | type: String, 77 | default: 'div' 78 | }, 79 | itemClass: { 80 | type: String, 81 | default: '' 82 | }, 83 | itemClassAdd: { 84 | type: Function 85 | }, 86 | itemStyle: { 87 | type: Object 88 | }, 89 | headerTag: { 90 | type: String, 91 | default: 'div' 92 | }, 93 | headerClass: { 94 | type: String, 95 | default: '' 96 | }, 97 | headerStyle: { 98 | type: Object 99 | }, 100 | footerTag: { 101 | type: String, 102 | default: 'div' 103 | }, 104 | footerClass: { 105 | type: String, 106 | default: '' 107 | }, 108 | footerStyle: { 109 | type: Object 110 | }, 111 | itemScopedSlots: { 112 | type: Object 113 | } 114 | } 115 | 116 | export const ItemProps = { 117 | index: { 118 | type: Number 119 | }, 120 | event: { 121 | type: String 122 | }, 123 | tag: { 124 | type: String 125 | }, 126 | horizontal: { 127 | type: Boolean 128 | }, 129 | source: { 130 | type: Object 131 | }, 132 | component: { 133 | type: [Object, Function] 134 | }, 135 | slotComponent: { 136 | type: Function 137 | }, 138 | uniqueKey: { 139 | type: [String, Number] 140 | }, 141 | extraProps: { 142 | type: Object 143 | }, 144 | scopedSlots: { 145 | type: Object 146 | }, 147 | tableMode: { 148 | type: Boolean, 149 | default: false 150 | } 151 | } 152 | 153 | export const SlotProps = { 154 | event: { 155 | type: String 156 | }, 157 | uniqueKey: { 158 | type: String 159 | }, 160 | tag: { 161 | type: String 162 | }, 163 | horizontal: { 164 | type: Boolean 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /example/src/views/home/Main.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 54 | 55 | 87 | -------------------------------------------------------------------------------- /example/src/App.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | -------------------------------------------------------------------------------- /example/src/views/chat-room/Editor.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 108 | 109 | 156 | -------------------------------------------------------------------------------- /example/src/components/Tab.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 51 | 52 | 98 | -------------------------------------------------------------------------------- /docs/js/keep-state.08df8235.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["keep-state"],{"0786":function(e,t,n){"use strict";n.r(t);var a=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"example"},[n("github-corner"),n("introduction",{attrs:{description:"Maintaining item component inner state is a trouble here, recommend to use only props data."}}),n("div",{staticClass:"example-content"},[n("tab",{on:{"tab-change":e.onTabChange}}),n("div",{directives:[{name:"show",rawName:"v-show",value:e.isShowView,expression:"isShowView"}],staticClass:"selects"},[e._v(e._s(e.selectNames))]),n("div",{directives:[{name:"show",rawName:"v-show",value:e.isShowView,expression:"isShowView"}]},[n("virtual-list",{staticClass:"list-keep scroll-touch",attrs:{"data-key":"id","data-sources":e.items,"data-component":e.itemComponent,"estimate-size":60,"item-class":"list-item-keep"}})],1),n("codeblock",{directives:[{name:"show",rawName:"v-show",value:!e.isShowView,expression:"!isShowView"}]})],1)],1)},i=[],s=(n("4de4"),n("7db0"),n("d81d"),n("b0c0"),function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"item-inner"},[n("span",{staticClass:"index"},[e._v("# "+e._s(e.source.index))]),n("input",{staticClass:"checkbox",attrs:{type:"checkbox"},domProps:{checked:e.source.checked},on:{change:e.onChange}}),n("span",{staticClass:"name",on:{click:e.onClickName}},[e._v(e._s(e.source.name))])])}),c=[],o=(n("99af"),{methods:{dispatch:function(e,t){var n=this.$parent||this.$root,a=n.$options.name;while(n&&(!a||a!==e))n=n.$parent,n&&(a=n.$options.name);if(n){for(var i=arguments.length,s=new Array(i>2?i-2:0),c=2;c1?arguments[1]:void 0)}}),s(o)},b2e6:function(e,t,n){"use strict";var a=n("4fc0"),i=n.n(a);i.a},c57d:function(e,t,n){"use strict";n("99af"),n("d3b7"),n("25f0");t["a"]=function(e){return"".concat(e,"$").concat(Math.random().toString(16).substr(9))}},cb52:function(e,t,n){"use strict";var a=n("64d5"),i=n.n(a);i.a}}]); -------------------------------------------------------------------------------- /example/src/views/infinite-loading/Main.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 102 | 103 | 181 | -------------------------------------------------------------------------------- /.github/workflows/command-compile.yml: -------------------------------------------------------------------------------- 1 | name: Compile Command 2 | on: 3 | issue_comment: 4 | types: [created] 5 | 6 | jobs: 7 | init: 8 | runs-on: ubuntu-latest 9 | 10 | # On pull requests and if the comment starts with `/compile` 11 | if: github.event.issue.pull_request != '' && startsWith(github.event.comment.body, '/compile') 12 | 13 | outputs: 14 | git_path: ${{ steps.git-path.outputs.path }} 15 | arg1: ${{ steps.command.outputs.arg1 }} 16 | arg2: ${{ steps.command.outputs.arg2 }} 17 | head_ref: ${{ steps.comment-branch.outputs.head_ref }} 18 | 19 | steps: 20 | - name: Check actor permission 21 | uses: skjnldsv/check-actor-permission@e591dbfe838300c007028e1219ca82cc26e8d7c5 # v2 22 | with: 23 | require: write 24 | 25 | - name: Add reaction on start 26 | uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # v2.1.0 27 | with: 28 | token: ${{ secrets.COMMAND_BOT_PAT }} 29 | repository: ${{ github.event.repository.full_name }} 30 | comment-id: ${{ github.event.comment.id }} 31 | reaction-type: "+1" 32 | 33 | - name: Parse command 34 | uses: skjnldsv/parse-command-comment@7cef1df370a99dfd5bf896d50121390c96785db8 # v2 35 | id: command 36 | 37 | # Init path depending on which command is run 38 | - name: Init path 39 | id: git-path 40 | run: | 41 | if ${{ startsWith(steps.command.outputs.arg1, '/') }}; then 42 | echo "path=${{ github.workspace }}${{steps.command.outputs.arg1}}" >> $GITHUB_OUTPUT 43 | else 44 | echo "path=${{ github.workspace }}${{steps.command.outputs.arg2}}" >> $GITHUB_OUTPUT 45 | fi 46 | 47 | - name: Init branch 48 | uses: xt0rted/pull-request-comment-branch@653a7d5ca8bd91d3c5cb83286063314d0b063b8e # v1 49 | id: comment-branch 50 | 51 | process: 52 | runs-on: ubuntu-latest 53 | needs: init 54 | 55 | steps: 56 | - name: Checkout ${{ needs.init.outputs.head_ref }} 57 | uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 58 | with: 59 | token: ${{ secrets.COMMAND_BOT_PAT }} 60 | fetch-depth: 0 61 | ref: ${{ needs.init.outputs.head_ref }} 62 | 63 | - name: Setup git 64 | run: | 65 | git config --local user.email "nextcloud-command@users.noreply.github.com" 66 | git config --local user.name "nextcloud-command" 67 | 68 | - name: Read package.json node and npm engines version 69 | uses: skjnldsv/read-package-engines-version-actions@1bdcee71fa343c46b18dc6aceffb4cd1e35209c6 # v1.2 70 | id: package-engines-versions 71 | with: 72 | fallbackNode: '^16' 73 | fallbackNpm: '^7' 74 | 75 | - name: Set up node ${{ steps.package-engines-versions.outputs.nodeVersion }} 76 | uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 # v3 77 | with: 78 | node-version: ${{ steps.package-engines-versions.outputs.nodeVersion }} 79 | cache: npm 80 | 81 | - name: Set up npm ${{ steps.package-engines-versions.outputs.npmVersion }} 82 | run: npm i -g npm@"${{ steps.package-engines-versions.outputs.npmVersion }}" 83 | 84 | - name: Install dependencies & build 85 | run: | 86 | npm ci 87 | npm run build --if-present 88 | 89 | - name: Commit and push default 90 | if: ${{ needs.init.outputs.arg1 != 'fixup' && needs.init.outputs.arg1 != 'amend' }} 91 | run: | 92 | git add ${{ needs.init.outputs.git_path }} 93 | git commit --signoff -m 'chore(assets): Recompile assets' 94 | git push origin ${{ needs.init.outputs.head_ref }} 95 | 96 | - name: Commit and push fixup 97 | if: ${{ needs.init.outputs.arg1 == 'fixup' }} 98 | run: | 99 | git add ${{ needs.init.outputs.git_path }} 100 | git commit --fixup=HEAD --signoff 101 | git push origin ${{ needs.init.outputs.head_ref }} 102 | 103 | - name: Commit and push amend 104 | if: ${{ needs.init.outputs.arg1 == 'amend' }} 105 | run: | 106 | git add ${{ needs.init.outputs.git_path }} 107 | git commit --amend --no-edit --signoff 108 | git push --force origin ${{ needs.init.outputs.head_ref }} 109 | 110 | - name: Add reaction on failure 111 | uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # v2.1.0 112 | if: failure() 113 | with: 114 | token: ${{ secrets.COMMAND_BOT_PAT }} 115 | repository: ${{ github.event.repository.full_name }} 116 | comment-id: ${{ github.event.comment.id }} 117 | reaction-type: "-1" 118 | -------------------------------------------------------------------------------- /docs/css/chat-room.9f37805a.css: -------------------------------------------------------------------------------- 1 | .item[data-v-5b53987b]{display:flex}.item .avatar[data-v-5b53987b]{width:40px;height:40px;border-radius:50%;background:rgba(255,192,203,.2)}@media (max-width:640px){.item .avatar[data-v-5b53987b]{width:30px;height:30px}}.item .avatar img[data-v-5b53987b]{display:block;width:100%;height:100%;border-radius:50%}.item .body[data-v-5b53987b]{flex:1;padding-left:1em;font-size:16px;max-width:560px;word-break:break-word}@media (max-width:640px){.item .body[data-v-5b53987b]{font-size:14px;max-width:unset}}.item .body .name[data-v-5b53987b]{padding-bottom:.2em;font-size:12px}.item .body .content[data-v-5b53987b]{position:relative;color:#000;background-color:#f0f8ff;border-radius:15px;padding:.5em 1em}@media (max-width:640px){.item .body .content[data-v-5b53987b]{padding:.5em}}.item .body .content[data-v-5b53987b]:after{content:"";position:absolute;right:100%;top:10px;width:14px;height:14px;border-width:0;border-style:solid;border-color:transparent;border-bottom-width:10px;border-bottom-color:currentColor;border-radius:0 0 0 32px;color:#f0f8ff}.item.creator[data-v-5b53987b]{transform:rotateX(180deg);direction:rtl;align-items:flex-end}.item.creator .avatar[data-v-5b53987b]{transform:rotateX(180deg)}.item.creator .body[data-v-5b53987b]{transform:rotate(180deg)}.item.creator .text[data-v-5b53987b]{transform:rotateY(180deg);direction:ltr}.editor[data-v-0655a508]{position:relative;font-size:16px}.rich[data-v-0655a508]{border:2px solid;border-color:#696969;border-top:none;width:100%;padding:1em 4em 1em 1em;outline:none;cursor:text;background-color:#fffaf0}.rich[data-v-0655a508]:empty:before{color:#a9a9a9;content:attr(placehoder)}.send[data-v-0655a508]{position:absolute;right:20px;top:15px;width:33px;height:30px;cursor:pointer;background-size:cover;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg class='icon' viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cpath d='M0 561.6l366.4 100.8 400-356.8-304 401.6 363.2 107.2 198.4-768zm460.8 416l126.4-192L460.8 760z' fill='%239b4dca'/%3E%3C/svg%3E")}.send[data-v-0655a508]:active{opacity:.7}.send.disabled[data-v-0655a508]{opacity:.3;pointer-events:none}.author-avatar[data-v-0655a508]{display:none}.item[data-v-0655a508]{padding:0}.line[data-v-0655a508]{font-size:12px;margin:0 1em}@media (max-width:640px){.toolbar[data-v-53de24ea]{width:100%;white-space:nowrap;overflow-x:auto}}.toolbar .item[data-v-53de24ea]{padding:0}.toolbar .line[data-v-53de24ea]{margin:0 1em;font-size:12px}.main{margin-top:1em}.empty,.stream{position:relative;width:100%;height:500px;border:2px solid;border-bottom:none;overflow-y:auto;border-color:#696969;display:flex;flex-direction:column-reverse}@media (max-width:640px){.empty,.stream{height:430px}}.empty.overflow,.stream.overflow{flex-direction:column}.empty .stream-item,.stream .stream-item{display:flex;align-items:center;padding:1em}@media (max-width:640px){.empty .stream-item,.stream .stream-item{padding:.5em}}.empty .stream-item.creator,.stream .stream-item.creator{flex-direction:row-reverse}.empty .wrapper{position:absolute;left:0;top:0;bottom:0;right:0;display:flex;justify-content:center;align-items:center;flex-direction:column;color:#bfbfbf}.empty .icon{width:70px;height:70px;background-size:cover;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg class='icon' viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cpath d='M915.91 428.28l40.909 6.786 1.773-10.49 8.709-12.937-103.65-69.574-7.776-5.22-7.838-2.919-295.426-109.994-6.927-2.58-6.962 2.309L191.15 338.875l-8.805 2.148-5.705 4.046-123.09 87.058 9.834 13.854 4.493 11.24 39.739-16.354-31.881 44.224 96.369 54.05.107 199.535-2.35 4.283 2.353 1.311.001 2.71h4.86l281.959 157.194 7.696 4.26 2.344 1.277 2.635 1.726 9.419-3.08 8.748-3.42 388.7-159.572.032-9.443V546.374l92.49-35.76-55.189-82.334zM561.218 260.058l245.843 91.539-245.843 92.116V260.058zm-440.216 215.07l40.684-56.453 5.984-8.358 23.44-32.558L454.11 501.38l-42.82 136.613-290.287-162.867zm82.049 251.006l-.1-169.63 226.371 126.99 28.646-91.387.894 276.697-255.811-142.67zM473.818 476.5L225.705 359.87l304.674-100.98v196.376l-56.56 21.234zm373.948 248.116L489.772 871.571l-.956-301.186 45.281 109.166 313.668-121.216v166.281zM551.062 639.89L493.82 501.95l347.098-130.084 83.065 123.893-372.921 144.13z' fill='%23bfbfbf'/%3E%3C/svg%3E")}.header{padding:.5em}.header .finished{font-size:14px;text-align:center;color:#bfbfbf}.header .spinner{font-size:10px;margin:0 auto;text-indent:-9999em;width:15px;height:15px;border-radius:50%;background:#fff;background:linear-gradient(90deg,#ccc 10%,hsla(0,0%,100%,0) 42%);position:relative;-webkit-animation:load3 1.4s linear infinite;animation:load3 1.4s linear infinite;transform:translateZ(0)}.header .spinner:before{width:50%;height:50%;background:#ccc;border-radius:100% 0 0 0;position:absolute;top:0;left:0;content:""}.header .spinner:after{background:#fff;width:75%;height:75%;border-radius:50%;content:"";margin:auto;position:absolute;top:0;left:0;bottom:0;right:0}@-webkit-keyframes load3{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@keyframes load3{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}} -------------------------------------------------------------------------------- /docs/css/app.12df65ee.css: -------------------------------------------------------------------------------- 1 | body{font-size:18px}h1,h2,h3,h4,h5,h6{letter-spacing:0}@media (max-width:640px){#app{padding:3px;width:100%}}#nav{position:fixed;top:0;left:0;right:0;background-color:#9b4cca;color:#fff;height:40px;display:flex;align-items:center;padding-left:1em;white-space:nowrap;overflow-x:auto;z-index:2}@media (max-width:640px){#nav{padding:0;position:relative;background-color:unset;color:unset;height:unset;padding-left:unset;align-items:unset}}#nav a{display:inline-block;color:#fff}@media (max-width:640px){#nav a{margin-bottom:0;margin-right:1em;color:#9b4dca}}#nav .line{margin:0 1em;font-size:14px;color:pink}@media (max-width:640px){#nav .line{display:none}}#nav .router-link-exact-active,#nav .router-link-exact-active:hover{color:inherit;cursor:default;text-decoration:underline}@media (max-width:640px){#nav .router-link-exact-active,#nav .router-link-exact-active:hover{border-bottom:1px solid;border-color:#606c76;text-decoration:none}}.example{margin:0 auto;padding:0 2em;width:776px;padding-top:3em}@media (max-width:640px){.example{margin:unset;padding:unset;width:unset;padding-top:unset}}.example-content{margin-top:1em;background-color:#fff;position:relative;z-index:1}.scroll-touch{-webkit-overflow-scrolling:touch}.scroll-touch::-webkit-scrollbar{width:10px}.scroll-touch::-webkit-scrollbar-track{background:#f4f4f4}.scroll-touch::-webkit-scrollbar-thumb{border-radius:0;background:rgba(0,0,0,.12)}.scroll-touch::-webkit-scrollbar-thumb:hover{background:#b2b2b2}code{background-color:pink!important}.name[data-v-1c868cc0]{margin-top:1em}.desc[data-v-1c868cc0]{padding-bottom:3em}.icons[data-v-1c868cc0]{padding-bottom:1em}.icons .btn[data-v-1c868cc0]{margin-right:1em}.head[data-v-1c868cc0]{margin-bottom:3em}.head img[data-v-1c868cc0]{margin-right:.5em;width:auto;height:24px}.title[data-v-1c868cc0]{margin:1em 0}ul[data-v-1c868cc0]{padding-left:.5em}li[data-v-1c868cc0]{list-style-position:outside;margin-left:1em}.introduction[data-v-0c507b69]{font-size:16px;padding:.5em 1em;border-radius:3px;background-color:#f8f8ff}@media (max-width:640px){.introduction[data-v-0c507b69]{display:none}}pre[data-v-99fd125a]{border:none;padding:1em;font-size:14px;border-radius:3px;font-family:Consolas,Monaco,Andale Mono,Lucida Console,monospace}.github-corner[data-v-fc50af34]{z-index:3;text-transform:capitalize;padding-left:30px;width:90px;height:40px;line-height:40px;position:fixed;right:0;top:0;color:#fff;font-size:14px;font-weight:700;background-size:30px 30px;background-repeat:no-repeat;background-position:0;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg class='icon' viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cpath d='M841.6 268.8c32-57.6-3.2-134.4-3.2-134.4-80 0-137.6 54.4-137.6 54.4-32-19.2-134.4-19.2-134.4-19.2s-102.4 0-134.4 19.2c0 0-57.6-54.4-137.6-54.4 0 0-35.2 76.8-3.2 134.4 0 0-70.4 67.2-44.8 211.2 25.6 134.4 144 169.6 220.8 169.6 0 0-32 25.6-25.6 70.4 0 0-44.8 25.6-89.6 9.6-44.8-19.2-67.2-64-67.2-64S240 608 195.2 630.4c0 0-12.8 12.8 35.2 35.2 0 0 35.2 54.4 48 86.4 12.8 32 86.4 57.6 156.8 41.6V896s0 9.6-19.2 12.8c-19.2 3.2-19.2 12.8-9.6 12.8h323.2c9.6 0 9.6-9.6-9.6-12.8-19.2-3.2-19.2-12.8-19.2-12.8V723.2c0-44.8-32-70.4-32-70.4 76.8 0 195.2-35.2 220.8-169.6 22.4-147.2-48-214.4-48-214.4z' fill='%23fff'/%3E%3C/svg%3E")}.tab[data-v-13711752]{width:100%;display:flex;margin-bottom:1em;position:relative}@media (max-width:640px){.tab[data-v-13711752]{display:none}}.tab .tab-item[data-v-13711752]{font-size:14px;font-weight:400;width:85px;margin-right:.5em;height:35px;display:flex;align-items:center;justify-content:center;position:relative;top:1px;cursor:pointer;opacity:.3;background-size:20px 20px;background-repeat:no-repeat;background-position:0}.tab .tab-item.active[data-v-13711752]{opacity:1}.tab .tab-item.view[data-v-13711752]{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg class='icon' viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cdefs%3E%3Cstyle/%3E%3C/defs%3E%3Cpath d='M512 251.853c192.768 0 358.707 113.1 436.378 276.275H1024c-82.022-202.394-280.218-345.293-512-345.293S82.022 325.735 0 528.128h75.674C153.344 364.954 319.284 251.853 512 251.853zm0 552.55c-192.717 0-358.656-113.05-436.326-276.275H0c82.022 202.445 280.166 345.344 512 345.344s430.029-142.9 512-345.344h-75.674C870.707 691.354 704.768 804.403 512 804.403zM327.834 528.128a184.115 184.115 0 10368.281.051 184.115 184.115 0 00-368.281-.051zm299.315 0a115.2 115.2 0 11-230.298 0 115.2 115.2 0 01230.298 0zm0 0'/%3E%3C/svg%3E")}.tab .tab-item.code[data-v-13711752]{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg class='icon' viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cdefs%3E%3Cstyle/%3E%3C/defs%3E%3Cpath d='M549.973 128a42.453 42.453 0 00-42.154 37.76l-75.904 683.136a42.325 42.325 0 1084.266 9.387l75.904-683.179A42.325 42.325 0 00550.016 128zM243.541 286.165a42.539 42.539 0 00-30.208 12.502L30.165 481.835a42.624 42.624 0 000 60.33l183.168 183.168a42.667 42.667 0 0060.331-60.33L120.661 512l153.003-153.003a42.624 42.624 0 00-30.165-72.832zm537.003 0a42.667 42.667 0 00-30.165 72.832L903.339 512 750.336 665.003a42.667 42.667 0 0060.33 60.33l183.169-183.168a42.624 42.624 0 000-60.33L810.667 298.667a42.539 42.539 0 00-30.166-12.502z'/%3E%3C/svg%3E")}.tab .complete-code-url[data-v-13711752]{position:absolute;right:0;top:50%;transform:translateY(-50%);font-size:12px} -------------------------------------------------------------------------------- /example/src/common/sentences.js: -------------------------------------------------------------------------------- 1 | import { Random } from './mock' 2 | import { isMobile } from './ua' 3 | 4 | // retrun several random sentences 5 | export default function getSentences (min = 1, max = 6) { 6 | const sentences = sentenceArray[Random.pick([0, 1, 2])] 7 | const results = [] 8 | 9 | let counts = Random.integer(min, isMobile ? 3 : max) 10 | while (counts--) { 11 | results.push(Random.pick(sentences)) 12 | } 13 | return results.join('. ') + '.' 14 | } 15 | 16 | // Try Everything (From Zootopia) 17 | const sentence1 = [ 18 | "I messed up tonight I lost another fight", 19 | "I still mess up but I'll just start again", 20 | "I keep falling down I keep on hitting the ground", 21 | "I always get up now to see what's next", 22 | "Birds don't just fly they fall down and get up", 23 | "Nobody learns without getting it won", 24 | "I won't give up no I won't give in", 25 | "Till I reach the end and then I'll start again", 26 | "No I won't leave I wanna try everything", 27 | "I wanna try even though I could fail", 28 | "I won't give up no I won't give in", 29 | "Till I reach the end and then I'll start again", 30 | "No I won't leave I wanna try everything", 31 | "I wanna try even though I could fail", 32 | "Look at how far you've come you filled your heart with love", 33 | "Baby you've done enough that cut your breath", 34 | "Don't beat yourself up don't need to run so fast", 35 | "Sometimes we come last but we did our best", 36 | "I won't give up no I won't give in", 37 | "Till I reach the end and then I'll start again", 38 | "No I won't leave I wanna try everything", 39 | "I wanna try even though I could fail", 40 | "I won't give up no I won't give in", 41 | "Till I reach the end and then I'll start again", 42 | "No I won't leave I wanna try everything", 43 | "I wanna try even though I could fail", 44 | "I'll keep on making those new mistakes", 45 | "I'll keep on making them every day", 46 | "Those new mistakes" 47 | ] 48 | 49 | // Dream It Possible (From Delacey) 50 | const sentence2 = [ 51 | "I will run I will climb I will soar", 52 | "I'm undefeated", 53 | "Jumping out of my skin pull the chord", 54 | "Yeah I believe it", 55 | "The past is everything we were don't make us who we are", 56 | "So I'll dream until I make it real and all I see is stars", 57 | "It's not until you fall that you fly", 58 | "When your dreams come alive you're unstoppable", 59 | "Take a shot chase the sun find the beautiful", 60 | "We will glow in the dark turning dust to gold", 61 | "And we'll dream it possible", 62 | "And we'll dream it possible", 63 | "I will chase I will reach I will fly", 64 | "Until I'm breaking until I'm breaking", 65 | "Out of my cage like a bird in the night", 66 | "I know I'm changing I know I'm changing", 67 | "In into something big better than before", 68 | "And if it takes takes a thousand lives", 69 | "Then it's worth fighting for", 70 | "It's not until you fall that you fly", 71 | "When your dreams come alive you're unstoppable", 72 | "Take a shot chase the sun find the beautiful", 73 | "We will glow in the dark turning dust to gold", 74 | "And we'll dream it possible", 75 | "It possible", 76 | "From the bottom to the top", 77 | "We're sparking wild fire's", 78 | "Never quit and never stop", 79 | "The rest of our lives", 80 | "From the bottom to the top", 81 | "We're sparking wild fire's", 82 | "Never quit and never stop", 83 | "It's not until you fall that you fly", 84 | "When your dreams come alive you're unstoppable", 85 | "Take a shot chase the sun find the beautiful", 86 | "We will glow in the dark turning dust to gold", 87 | "And we'll dream it possible", 88 | "And we'll dream it possible" 89 | ] 90 | 91 | // The Climb (From Miley Cyrus) 92 | const sentence3 = [ 93 | "I can almost see it", 94 | "That dream I'm dreamin' but", 95 | "There's a voice inside my head saying", 96 | "You'll never reach it", 97 | "Every step I'm taking", 98 | "Every move I make feels", 99 | "Lost with no direction", 100 | "My faith is shakin", 101 | "But I I gotta keep tryin", 102 | "Gotta keep my head held high", 103 | "There's always gonna be another mountain", 104 | "I'm always gonna wanna make it move", 105 | "Always gonna be an uphill battle", 106 | "Sometimes I'm gonna have to lose", 107 | "Ain't about how fast I get there", 108 | "Ain't about what's waitin on the other side", 109 | "It's the climb", 110 | "The struggles I'm facing", 111 | "The chances I'm taking", 112 | "Sometimes might knock me down but", 113 | "No I'm not breaking", 114 | "I may not know it", 115 | "But these are the moments that", 116 | "I'm gonna remember most yeah", 117 | "Just gotta keep going", 118 | "And I I gotta be strong", 119 | "Just keep pushing on 'cause", 120 | "There's always gonna be another mountain", 121 | "I'm always gonna wanna make it move", 122 | "Always gonna be an uphill battle", 123 | "But Sometimes I'm gonna have to lose", 124 | "Ain't about how fast I get there", 125 | "Ain't about what's waitin on the other side", 126 | "It's the climb", 127 | "Yeah-yeah", 128 | "There's always gonna be another mountain", 129 | "I'm always gonna wanna make it move", 130 | "Always gonna be an uphill battle", 131 | "Sometimes you're gonna have to lose", 132 | "Ain't about how fast I get there", 133 | "Ain't about what's waitin on the other side", 134 | "It's the climb", 135 | "Yeah-yeah-yea", 136 | "Keep on moving", 137 | "Keep climbing", 138 | "Keep the faith", 139 | "Baby It's all about", 140 | "It's all about the climb", 141 | "Keep your faith", 142 | "Whoa O Whoa" 143 | ] 144 | 145 | const sentenceArray = [ 146 | sentence1, 147 | sentence2, 148 | sentence3 149 | ] 150 | -------------------------------------------------------------------------------- /docs/js/page-mode.7fc3339d.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["page-mode"],{"2fcf":function(e,t,n){"use strict";var a=n("7fe2"),o=n.n(a);o.a},"6e70":function(e,t,n){"use strict";n.r(t);var a=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"example"},[n("github-corner"),n("introduction",{attrs:{description:"In page-mode virtual list using global document to scroll through the list."}}),n("div",{staticClass:"example-content"},[n("div",[n("virtual-list",{ref:"vsl",staticClass:"list-page scroll-touch",attrs:{"data-key":"id","data-sources":e.items,"data-component":e.itemComponent,"estimate-size":135,"item-class":"list-item-page","page-mode":!0},on:{totop:e.totop,tobottom:e.tobottom}}),e._m(0)],1)])],1)},o=[function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"bottom"},[n("h2",[e._v("This is page footer")])])}],i=(n("b0c0"),function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"item-inner"},[n("div",{staticClass:"head"},[n("span",[e._v("# "+e._s(e.source.index))]),n("span",[e._v(e._s(e.source.name))])]),n("div",{staticClass:"desc"},[e._v(e._s(e.source.desc))])])}),s=[],l={name:"page-mode-item",props:{source:{type:Object,default:function(){return{}}}}},r=l,h=(n("2fcf"),n("2877")),u=Object(h["a"])(r,i,s,!1,null,"1df8c792",null),m=u.exports,d=n("adf9"),c=n("c927"),g=n("c57d"),w=1e3,I=[],p=w;while(p--){var f=w-p;I.push({index:f,name:d["Random"].name(),id:Object(g["a"])(f),desc:Object(c["a"])()})}var b={name:"page-mode",components:{},data:function(){return{items:I,itemComponent:m}},methods:{totop:function(){console.log("reach totop")},tobottom:function(){console.log("reach tobottom")}}},v=b,y=(n("e5d8"),Object(h["a"])(v,a,o,!1,null,null,null));t["default"]=y.exports},"7fe2":function(e,t,n){},c57d:function(e,t,n){"use strict";n("99af"),n("d3b7"),n("25f0");t["a"]=function(e){return"".concat(e,"$").concat(Math.random().toString(16).substr(9))}},c927:function(e,t,n){"use strict";n.d(t,"a",(function(){return i}));n("a15b");var a=n("adf9"),o=n("835c");function i(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:6,n=h[a["Random"].pick([0,1,2])],i=[],s=a["Random"].integer(e,o["a"]?3:t);while(s--)i.push(a["Random"].pick(n));return i.join(". ")+"."}var s=["I messed up tonight I lost another fight","I still mess up but I'll just start again","I keep falling down I keep on hitting the ground","I always get up now to see what's next","Birds don't just fly they fall down and get up","Nobody learns without getting it won","I won't give up no I won't give in","Till I reach the end and then I'll start again","No I won't leave I wanna try everything","I wanna try even though I could fail","I won't give up no I won't give in","Till I reach the end and then I'll start again","No I won't leave I wanna try everything","I wanna try even though I could fail","Look at how far you've come you filled your heart with love","Baby you've done enough that cut your breath","Don't beat yourself up don't need to run so fast","Sometimes we come last but we did our best","I won't give up no I won't give in","Till I reach the end and then I'll start again","No I won't leave I wanna try everything","I wanna try even though I could fail","I won't give up no I won't give in","Till I reach the end and then I'll start again","No I won't leave I wanna try everything","I wanna try even though I could fail","I'll keep on making those new mistakes","I'll keep on making them every day","Those new mistakes"],l=["I will run I will climb I will soar","I'm undefeated","Jumping out of my skin pull the chord","Yeah I believe it","The past is everything we were don't make us who we are","So I'll dream until I make it real and all I see is stars","It's not until you fall that you fly","When your dreams come alive you're unstoppable","Take a shot chase the sun find the beautiful","We will glow in the dark turning dust to gold","And we'll dream it possible","And we'll dream it possible","I will chase I will reach I will fly","Until I'm breaking until I'm breaking","Out of my cage like a bird in the night","I know I'm changing I know I'm changing","In into something big better than before","And if it takes takes a thousand lives","Then it's worth fighting for","It's not until you fall that you fly","When your dreams come alive you're unstoppable","Take a shot chase the sun find the beautiful","We will glow in the dark turning dust to gold","And we'll dream it possible","It possible","From the bottom to the top","We're sparking wild fire's","Never quit and never stop","The rest of our lives","From the bottom to the top","We're sparking wild fire's","Never quit and never stop","It's not until you fall that you fly","When your dreams come alive you're unstoppable","Take a shot chase the sun find the beautiful","We will glow in the dark turning dust to gold","And we'll dream it possible","And we'll dream it possible"],r=["I can almost see it","That dream I'm dreamin' but","There's a voice inside my head saying","You'll never reach it","Every step I'm taking","Every move I make feels","Lost with no direction","My faith is shakin","But I I gotta keep tryin","Gotta keep my head held high","There's always gonna be another mountain","I'm always gonna wanna make it move","Always gonna be an uphill battle","Sometimes I'm gonna have to lose","Ain't about how fast I get there","Ain't about what's waitin on the other side","It's the climb","The struggles I'm facing","The chances I'm taking","Sometimes might knock me down but","No I'm not breaking","I may not know it","But these are the moments that","I'm gonna remember most yeah","Just gotta keep going","And I I gotta be strong","Just keep pushing on 'cause","There's always gonna be another mountain","I'm always gonna wanna make it move","Always gonna be an uphill battle","But Sometimes I'm gonna have to lose","Ain't about how fast I get there","Ain't about what's waitin on the other side","It's the climb","Yeah-yeah","There's always gonna be another mountain","I'm always gonna wanna make it move","Always gonna be an uphill battle","Sometimes you're gonna have to lose","Ain't about how fast I get there","Ain't about what's waitin on the other side","It's the climb","Yeah-yeah-yea","Keep on moving","Keep climbing","Keep the faith","Baby It's all about","It's all about the climb","Keep your faith","Whoa O Whoa"],h=[s,l,r]},cc99:function(e,t,n){},e5d8:function(e,t,n){"use strict";var a=n("cc99"),o=n.n(a);o.a}}]); -------------------------------------------------------------------------------- /docs/js/dynamic-size.3003e8a2.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["dynamic-size"],{2146:function(e,t,n){"use strict";var a=n("fe3f"),i=n.n(a);i.a},"960e":function(e,t,n){"use strict";n.r(t);var a=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"example"},[n("github-corner"),n("introduction",{attrs:{description:"The size of each item is dynamic,\n you don't have to care about size, it will calculate automatic,\n but you have to make sure that there's an unique id for every array data."}}),n("div",{staticClass:"example-content"},[n("tab",{on:{"tab-change":e.onTabChange}}),n("div",{directives:[{name:"show",rawName:"v-show",value:e.isShowView,expression:"isShowView"}]},[n("virtual-list",{staticClass:"list-dynamic scroll-touch",attrs:{"data-key":"id","data-sources":e.items,"data-component":e.itemComponent,"estimate-size":80,"item-class":"list-item-dynamic"}})],1),n("codeblock",{directives:[{name:"show",rawName:"v-show",value:!e.isShowView,expression:"!isShowView"}]})],1)],1)},i=[],o=(n("b0c0"),function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"item-inner"},[n("div",{staticClass:"head"},[n("span",[e._v("# "+e._s(e.source.index))]),n("span",[e._v(e._s(e.source.name))])]),n("div",{staticClass:"desc"},[e._v(e._s(e.source.desc))])])}),s=[],l={name:"dynamic-size-item",props:{source:{type:Object,default:function(){return{}}}}},r=l,h=(n("ed29"),n("2877")),u=Object(h["a"])(r,o,s,!1,null,"5f14b5b8",null),m=u.exports,d=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",[n("code-high-light",{attrs:{type:"html",code:e.html}}),n("code-high-light",{attrs:{type:"js",code:e.js}})],1)},c=[],w='\n\n',g="\nimport Item from './Item'\nconst items = [\n {\n id: 'unique-id-xxx',\n ...\n },\n ....\n]\n\nexport default {\n ...\n data () {\n return {\n items: items,\n itemComponent: Item,\n }\n }\n ...\n}\n",I={name:"dynamic-size-code",data:function(){return{html:w,js:g}}},y=I,b=Object(h["a"])(y,d,c,!1,null,null,null),v=b.exports,p=n("adf9"),f=n("c927"),k=n("c57d"),T=n("b95e"),A=1e4,x=[],S=A;while(S--){var j=A-S;x.push({index:j,name:p["Random"].name(),id:Object(k["a"])(j),desc:Object(f["a"])()})}var C={name:"dynamic-size",components:{codeblock:v},data:function(){return{total:A.toLocaleString(),items:x,itemComponent:m,isShowView:T["a"]===T["b"].VIEW}},methods:{onTabChange:function(e){this.isShowView=e===T["b"].VIEW}}},W=C,_=(n("2146"),Object(h["a"])(W,a,i,!1,null,null,null));t["default"]=_.exports},c57d:function(e,t,n){"use strict";n("99af"),n("d3b7"),n("25f0");t["a"]=function(e){return"".concat(e,"$").concat(Math.random().toString(16).substr(9))}},c927:function(e,t,n){"use strict";n.d(t,"a",(function(){return o}));n("a15b");var a=n("adf9"),i=n("835c");function o(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:6,n=h[a["Random"].pick([0,1,2])],o=[],s=a["Random"].integer(e,i["a"]?3:t);while(s--)o.push(a["Random"].pick(n));return o.join(". ")+"."}var s=["I messed up tonight I lost another fight","I still mess up but I'll just start again","I keep falling down I keep on hitting the ground","I always get up now to see what's next","Birds don't just fly they fall down and get up","Nobody learns without getting it won","I won't give up no I won't give in","Till I reach the end and then I'll start again","No I won't leave I wanna try everything","I wanna try even though I could fail","I won't give up no I won't give in","Till I reach the end and then I'll start again","No I won't leave I wanna try everything","I wanna try even though I could fail","Look at how far you've come you filled your heart with love","Baby you've done enough that cut your breath","Don't beat yourself up don't need to run so fast","Sometimes we come last but we did our best","I won't give up no I won't give in","Till I reach the end and then I'll start again","No I won't leave I wanna try everything","I wanna try even though I could fail","I won't give up no I won't give in","Till I reach the end and then I'll start again","No I won't leave I wanna try everything","I wanna try even though I could fail","I'll keep on making those new mistakes","I'll keep on making them every day","Those new mistakes"],l=["I will run I will climb I will soar","I'm undefeated","Jumping out of my skin pull the chord","Yeah I believe it","The past is everything we were don't make us who we are","So I'll dream until I make it real and all I see is stars","It's not until you fall that you fly","When your dreams come alive you're unstoppable","Take a shot chase the sun find the beautiful","We will glow in the dark turning dust to gold","And we'll dream it possible","And we'll dream it possible","I will chase I will reach I will fly","Until I'm breaking until I'm breaking","Out of my cage like a bird in the night","I know I'm changing I know I'm changing","In into something big better than before","And if it takes takes a thousand lives","Then it's worth fighting for","It's not until you fall that you fly","When your dreams come alive you're unstoppable","Take a shot chase the sun find the beautiful","We will glow in the dark turning dust to gold","And we'll dream it possible","It possible","From the bottom to the top","We're sparking wild fire's","Never quit and never stop","The rest of our lives","From the bottom to the top","We're sparking wild fire's","Never quit and never stop","It's not until you fall that you fly","When your dreams come alive you're unstoppable","Take a shot chase the sun find the beautiful","We will glow in the dark turning dust to gold","And we'll dream it possible","And we'll dream it possible"],r=["I can almost see it","That dream I'm dreamin' but","There's a voice inside my head saying","You'll never reach it","Every step I'm taking","Every move I make feels","Lost with no direction","My faith is shakin","But I I gotta keep tryin","Gotta keep my head held high","There's always gonna be another mountain","I'm always gonna wanna make it move","Always gonna be an uphill battle","Sometimes I'm gonna have to lose","Ain't about how fast I get there","Ain't about what's waitin on the other side","It's the climb","The struggles I'm facing","The chances I'm taking","Sometimes might knock me down but","No I'm not breaking","I may not know it","But these are the moments that","I'm gonna remember most yeah","Just gotta keep going","And I I gotta be strong","Just keep pushing on 'cause","There's always gonna be another mountain","I'm always gonna wanna make it move","Always gonna be an uphill battle","But Sometimes I'm gonna have to lose","Ain't about how fast I get there","Ain't about what's waitin on the other side","It's the climb","Yeah-yeah","There's always gonna be another mountain","I'm always gonna wanna make it move","Always gonna be an uphill battle","Sometimes you're gonna have to lose","Ain't about how fast I get there","Ain't about what's waitin on the other side","It's the climb","Yeah-yeah-yea","Keep on moving","Keep climbing","Keep the faith","Baby It's all about","It's all about the climb","Keep your faith","Whoa O Whoa"],h=[s,l,r]},ed28:function(e,t,n){},ed29:function(e,t,n){"use strict";var a=n("ed28"),i=n.n(a);i.a},fe3f:function(e,t,n){}}]); -------------------------------------------------------------------------------- /docs/js/infinite-loading.ce8d4372.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["infinite-loading"],{"077d":function(t,e,n){"use strict";n.r(e);var a=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"example"},[n("github-corner"),n("introduction",{attrs:{description:"Use v-on:tobottom to listen scroll reach bottom, add a footer slot as loading, then append next parts data into data-sources array."}}),n("div",{staticClass:"example-content"},[n("tab",{on:{"tab-change":t.onTabChange}}),n("div",{staticClass:"result"},[t._v("Items count: "+t._s(t.items.length)+".")]),n("div",{directives:[{name:"show",rawName:"v-show",value:t.isShowView,expression:"isShowView"}]},[n("virtual-list",{staticClass:"list-infinite scroll-touch",attrs:{"data-key":"id","data-sources":t.items,"data-component":t.itemComponent,"estimate-size":70,"item-class":"list-item-infinite","footer-class":"loader-wrapper"},on:{totop:t.onScrollToTop,tobottom:t.onScrollToBottom}},[n("div",{staticClass:"loader",attrs:{slot:"footer"},slot:"footer"})])],1),n("codeblock",{directives:[{name:"show",rawName:"v-show",value:!t.isShowView,expression:"!isShowView"}]})],1)],1)},o=[],i=(n("99af"),n("b0c0"),function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"item-inner"},[n("div",{staticClass:"head"},[n("span",{staticClass:"index"},[t._v("# "+t._s(t.source.index))]),n("span",{staticClass:"name"},[t._v(t._s(t.source.name))])]),n("div",{staticClass:"desc"},[t._v(t._s(t.source.desc))])])}),s=[],l={name:"infinite-loading-item",props:{source:{type:Object,default:function(){return{}}}}},r=l,h=(n("d316"),n("2877")),u=Object(h["a"])(r,i,s,!1,null,"b5a02d2e",null),d=u.exports,c=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",[n("code-high-light",{attrs:{type:"html",code:t.html}}),n("code-high-light",{attrs:{type:"js",code:t.js}})],1)},m=[],g='\n\n
Loading ...
\n
\n',w="\nexport default {\n data () {\n return {\n itemComponent: Item,\n items: getPageData(pageSize, 0)\n }\n },\n\n methods: {\n onScrollToBottom () {\n this.items = this.items.concat(getPageData(pageSize, pageNum))\n }\n }\n}\n",I={name:"infinite-loading-code",data:function(){return{html:g,js:w}}},p=I,f=Object(h["a"])(p,c,m,!1,null,null,null),v=f.exports,b=n("adf9"),y=n("c927"),k=n("c57d"),T=n("b95e"),S=function(t,e){for(var n=[],a=0;a0&&void 0!==arguments[0]?arguments[0]:1,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:6,n=h[a["Random"].pick([0,1,2])],i=[],s=a["Random"].integer(t,o["a"]?3:e);while(s--)i.push(a["Random"].pick(n));return i.join(". ")+"."}var s=["I messed up tonight I lost another fight","I still mess up but I'll just start again","I keep falling down I keep on hitting the ground","I always get up now to see what's next","Birds don't just fly they fall down and get up","Nobody learns without getting it won","I won't give up no I won't give in","Till I reach the end and then I'll start again","No I won't leave I wanna try everything","I wanna try even though I could fail","I won't give up no I won't give in","Till I reach the end and then I'll start again","No I won't leave I wanna try everything","I wanna try even though I could fail","Look at how far you've come you filled your heart with love","Baby you've done enough that cut your breath","Don't beat yourself up don't need to run so fast","Sometimes we come last but we did our best","I won't give up no I won't give in","Till I reach the end and then I'll start again","No I won't leave I wanna try everything","I wanna try even though I could fail","I won't give up no I won't give in","Till I reach the end and then I'll start again","No I won't leave I wanna try everything","I wanna try even though I could fail","I'll keep on making those new mistakes","I'll keep on making them every day","Those new mistakes"],l=["I will run I will climb I will soar","I'm undefeated","Jumping out of my skin pull the chord","Yeah I believe it","The past is everything we were don't make us who we are","So I'll dream until I make it real and all I see is stars","It's not until you fall that you fly","When your dreams come alive you're unstoppable","Take a shot chase the sun find the beautiful","We will glow in the dark turning dust to gold","And we'll dream it possible","And we'll dream it possible","I will chase I will reach I will fly","Until I'm breaking until I'm breaking","Out of my cage like a bird in the night","I know I'm changing I know I'm changing","In into something big better than before","And if it takes takes a thousand lives","Then it's worth fighting for","It's not until you fall that you fly","When your dreams come alive you're unstoppable","Take a shot chase the sun find the beautiful","We will glow in the dark turning dust to gold","And we'll dream it possible","It possible","From the bottom to the top","We're sparking wild fire's","Never quit and never stop","The rest of our lives","From the bottom to the top","We're sparking wild fire's","Never quit and never stop","It's not until you fall that you fly","When your dreams come alive you're unstoppable","Take a shot chase the sun find the beautiful","We will glow in the dark turning dust to gold","And we'll dream it possible","And we'll dream it possible"],r=["I can almost see it","That dream I'm dreamin' but","There's a voice inside my head saying","You'll never reach it","Every step I'm taking","Every move I make feels","Lost with no direction","My faith is shakin","But I I gotta keep tryin","Gotta keep my head held high","There's always gonna be another mountain","I'm always gonna wanna make it move","Always gonna be an uphill battle","Sometimes I'm gonna have to lose","Ain't about how fast I get there","Ain't about what's waitin on the other side","It's the climb","The struggles I'm facing","The chances I'm taking","Sometimes might knock me down but","No I'm not breaking","I may not know it","But these are the moments that","I'm gonna remember most yeah","Just gotta keep going","And I I gotta be strong","Just keep pushing on 'cause","There's always gonna be another mountain","I'm always gonna wanna make it move","Always gonna be an uphill battle","But Sometimes I'm gonna have to lose","Ain't about how fast I get there","Ain't about what's waitin on the other side","It's the climb","Yeah-yeah","There's always gonna be another mountain","I'm always gonna wanna make it move","Always gonna be an uphill battle","Sometimes you're gonna have to lose","Ain't about how fast I get there","Ain't about what's waitin on the other side","It's the climb","Yeah-yeah-yea","Keep on moving","Keep climbing","Keep the faith","Baby It's all about","It's all about the climb","Keep your faith","Whoa O Whoa"],h=[s,l,r]},d316:function(t,e,n){"use strict";var a=n("9f22"),o=n.n(a);o.a}}]); -------------------------------------------------------------------------------- /src/virtual.js: -------------------------------------------------------------------------------- 1 | /** 2 | * virtual list core calculating center 3 | */ 4 | 5 | const DIRECTION_TYPE = { 6 | FRONT: 'FRONT', // scroll up or left 7 | BEHIND: 'BEHIND' // scroll down or right 8 | } 9 | const CALC_TYPE = { 10 | INIT: 'INIT', 11 | FIXED: 'FIXED', 12 | DYNAMIC: 'DYNAMIC' 13 | } 14 | const LEADING_BUFFER = 0 15 | 16 | export default class Virtual { 17 | constructor (param, callUpdate) { 18 | this.init(param, callUpdate) 19 | } 20 | 21 | init (param, callUpdate) { 22 | // param data 23 | this.param = param 24 | this.callUpdate = callUpdate 25 | 26 | // size data 27 | this.sizes = new Map() 28 | this.firstRangeTotalSize = 0 29 | this.firstRangeAverageSize = 0 30 | this.fixedSizeValue = 0 31 | this.calcType = CALC_TYPE.INIT 32 | 33 | // scroll data 34 | this.offset = 0 35 | this.direction = '' 36 | 37 | // range data 38 | this.range = Object.create(null) 39 | if (param) { 40 | this.checkRange(0, param.keeps - 1) 41 | } 42 | 43 | // benchmark test data 44 | // this.__bsearchCalls = 0 45 | // this.__getIndexOffsetCalls = 0 46 | } 47 | 48 | destroy () { 49 | this.init(null, null) 50 | } 51 | 52 | // return current render range 53 | getRange () { 54 | const range = Object.create(null) 55 | range.start = this.range.start 56 | range.end = this.range.end 57 | range.padFront = this.range.padFront 58 | range.padBehind = this.range.padBehind 59 | return range 60 | } 61 | 62 | isBehind () { 63 | return this.direction === DIRECTION_TYPE.BEHIND 64 | } 65 | 66 | isFront () { 67 | return this.direction === DIRECTION_TYPE.FRONT 68 | } 69 | 70 | // return start index offset 71 | getOffset (start) { 72 | return (start < 1 ? 0 : this.getIndexOffset(start)) + this.param.slotHeaderSize 73 | } 74 | 75 | updateParam (key, value) { 76 | if (this.param && (key in this.param)) { 77 | // if uniqueIds change, find out deleted id and remove from size map 78 | if (key === 'uniqueIds') { 79 | this.sizes.forEach((v, key) => { 80 | if (!value.includes(key)) { 81 | this.sizes.delete(key) 82 | } 83 | }) 84 | } 85 | this.param[key] = value 86 | } 87 | } 88 | 89 | // save each size map by id 90 | saveSize (id, size) { 91 | this.sizes.set(id, size) 92 | 93 | // we assume size type is fixed at the beginning and remember first size value 94 | // if there is no size value different from this at next comming saving 95 | // we think it's a fixed size list, otherwise is dynamic size list 96 | if (this.calcType === CALC_TYPE.INIT) { 97 | this.fixedSizeValue = size 98 | this.calcType = CALC_TYPE.FIXED 99 | } else if (this.calcType === CALC_TYPE.FIXED && this.fixedSizeValue !== size) { 100 | this.calcType = CALC_TYPE.DYNAMIC 101 | // it's no use at all 102 | delete this.fixedSizeValue 103 | } 104 | 105 | // calculate the average size only in the first range 106 | if (this.calcType !== CALC_TYPE.FIXED && typeof this.firstRangeTotalSize !== 'undefined') { 107 | if (this.sizes.size < Math.min(this.param.keeps, this.param.uniqueIds.length)) { 108 | this.firstRangeTotalSize = [...this.sizes.values()].reduce((acc, val) => acc + val, 0) 109 | this.firstRangeAverageSize = Math.round(this.firstRangeTotalSize / this.sizes.size) 110 | } else { 111 | // it's done using 112 | delete this.firstRangeTotalSize 113 | } 114 | } 115 | } 116 | 117 | // in some special situation (e.g. length change) we need to update in a row 118 | // try goiong to render next range by a leading buffer according to current direction 119 | handleDataSourcesChange () { 120 | let start = this.range.start 121 | 122 | if (this.isFront()) { 123 | start = start - LEADING_BUFFER 124 | } else if (this.isBehind()) { 125 | start = start + LEADING_BUFFER 126 | } 127 | 128 | start = Math.max(start, 0) 129 | 130 | this.updateRange(this.range.start, this.getEndByStart(start)) 131 | } 132 | 133 | // when slot size change, we also need force update 134 | handleSlotSizeChange () { 135 | this.handleDataSourcesChange() 136 | } 137 | 138 | // calculating range on scroll 139 | handleScroll (offset) { 140 | this.direction = offset < this.offset || offset === 0 ? DIRECTION_TYPE.FRONT : DIRECTION_TYPE.BEHIND 141 | this.offset = offset 142 | 143 | if (!this.param) { 144 | return 145 | } 146 | 147 | if (this.direction === DIRECTION_TYPE.FRONT) { 148 | this.handleFront() 149 | } else if (this.direction === DIRECTION_TYPE.BEHIND) { 150 | this.handleBehind() 151 | } 152 | } 153 | 154 | // ----------- public method end ----------- 155 | 156 | handleFront () { 157 | const overs = this.getScrollOvers() 158 | // should not change range if start doesn't exceed overs 159 | if (overs > this.range.start) { 160 | return 161 | } 162 | 163 | // move up start by a buffer length, and make sure its safety 164 | const start = Math.max(overs - this.param.buffer, 0) 165 | this.checkRange(start, this.getEndByStart(start)) 166 | } 167 | 168 | handleBehind () { 169 | const overs = this.getScrollOvers() 170 | // range should not change if scroll overs within buffer 171 | if (overs < this.range.start + this.param.buffer) { 172 | return 173 | } 174 | 175 | this.checkRange(overs, this.getEndByStart(overs)) 176 | } 177 | 178 | // return the pass overs according to current scroll offset 179 | getScrollOvers () { 180 | // if slot header exist, we need subtract its size 181 | const offset = this.offset - this.param.slotHeaderSize 182 | if (offset <= 0) { 183 | return 0 184 | } 185 | 186 | // if is fixed type, that can be easily 187 | if (this.isFixedType()) { 188 | return Math.floor(offset / this.fixedSizeValue) 189 | } 190 | 191 | let low = 0 192 | let middle = 0 193 | let middleOffset = 0 194 | let high = this.param.uniqueIds.length 195 | 196 | while (low <= high) { 197 | // this.__bsearchCalls++ 198 | middle = low + Math.floor((high - low) / 2) 199 | middleOffset = this.getIndexOffset(middle) 200 | 201 | if (middleOffset === offset) { 202 | return middle 203 | } else if (middleOffset < offset) { 204 | low = middle + 1 205 | } else if (middleOffset > offset) { 206 | high = middle - 1 207 | } 208 | } 209 | 210 | return low > 0 ? --low : 0 211 | } 212 | 213 | // return a scroll offset from given index, can efficiency be improved more here? 214 | // although the call frequency is very high, its only a superposition of numbers 215 | getIndexOffset (givenIndex) { 216 | if (!givenIndex) { 217 | return 0 218 | } 219 | 220 | let offset = 0 221 | let indexSize = 0 222 | for (let index = 0; index < givenIndex; index++) { 223 | // this.__getIndexOffsetCalls++ 224 | indexSize = this.sizes.get(this.param.uniqueIds[index]) 225 | offset = offset + (typeof indexSize === 'number' ? indexSize : this.getEstimateSize()) 226 | } 227 | 228 | return offset 229 | } 230 | 231 | // is fixed size type 232 | isFixedType () { 233 | return this.calcType === CALC_TYPE.FIXED 234 | } 235 | 236 | // return the real last index 237 | getLastIndex () { 238 | return this.param.uniqueIds.length - 1 239 | } 240 | 241 | // in some conditions range is broke, we need correct it 242 | // and then decide whether need update to next range 243 | checkRange (start, end) { 244 | const keeps = this.param.keeps 245 | const total = this.param.uniqueIds.length 246 | 247 | // datas less than keeps, render all 248 | if (total <= keeps) { 249 | start = 0 250 | end = this.getLastIndex() 251 | } else if (end - start < keeps - 1) { 252 | // if range length is less than keeps, corrent it base on end 253 | start = end - keeps + 1 254 | } 255 | 256 | if (this.range.start !== start) { 257 | this.updateRange(start, end) 258 | } 259 | } 260 | 261 | // setting to a new range and rerender 262 | updateRange (start, end) { 263 | this.range.start = start 264 | this.range.end = end 265 | this.range.padFront = this.getPadFront() 266 | this.range.padBehind = this.getPadBehind() 267 | this.callUpdate(this.getRange()) 268 | } 269 | 270 | // return end base on start 271 | getEndByStart (start) { 272 | const theoryEnd = start + this.param.keeps - 1 273 | const truelyEnd = Math.min(theoryEnd, this.getLastIndex()) 274 | return truelyEnd 275 | } 276 | 277 | // return total front offset 278 | getPadFront () { 279 | if (this.isFixedType()) { 280 | return this.fixedSizeValue * this.range.start 281 | } else { 282 | return this.getIndexOffset(this.range.start) 283 | } 284 | } 285 | 286 | // return total behind offset 287 | getPadBehind () { 288 | const end = this.range.end 289 | const lastIndex = this.getLastIndex() 290 | 291 | if (this.isFixedType()) { 292 | return (lastIndex - end) * this.fixedSizeValue 293 | } 294 | 295 | return (lastIndex - end) * this.getEstimateSize() 296 | } 297 | 298 | // get the item estimate size 299 | getEstimateSize () { 300 | return this.isFixedType() ? this.fixedSizeValue : (this.firstRangeAverageSize || this.param.estimateSize) 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /example/src/views/chat-room/Main.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 180 | 181 | 294 | -------------------------------------------------------------------------------- /docs/js/chat-room.c08970e6.js: -------------------------------------------------------------------------------- 1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chat-room"],{"498a":function(e,t,n){"use strict";var a=n("23e7"),s=n("58a8").trim,i=n("c8d2");a({target:"String",proto:!0,forced:i("trim")},{trim:function(){return s(this)}})},5254:function(e,t,n){},"6c2b":function(e,t,n){"use strict";var a=n("f2eb"),s=n.n(a);s.a},"6f9a":function(e,t,n){},"808e":function(e,t,n){"use strict";var a=n("5254"),s=n.n(a);s.a},b10c:function(e,t,n){},bd0f:function(e,t,n){"use strict";var a=n("6f9a"),s=n.n(a);s.a},c8d2:function(e,t,n){var a=n("d039"),s=n("5899"),i="​…᠎";e.exports=function(e){return a((function(){return!!s[e]()||i[e]()!=i||s[e].name!==e}))}},c927:function(e,t,n){"use strict";n.d(t,"a",(function(){return i}));n("a15b");var a=n("adf9"),s=n("835c");function i(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1,t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:6,n=c[a["Random"].pick([0,1,2])],i=[],o=a["Random"].integer(e,s["a"]?3:t);while(o--)i.push(a["Random"].pick(n));return i.join(". ")+"."}var o=["I messed up tonight I lost another fight","I still mess up but I'll just start again","I keep falling down I keep on hitting the ground","I always get up now to see what's next","Birds don't just fly they fall down and get up","Nobody learns without getting it won","I won't give up no I won't give in","Till I reach the end and then I'll start again","No I won't leave I wanna try everything","I wanna try even though I could fail","I won't give up no I won't give in","Till I reach the end and then I'll start again","No I won't leave I wanna try everything","I wanna try even though I could fail","Look at how far you've come you filled your heart with love","Baby you've done enough that cut your breath","Don't beat yourself up don't need to run so fast","Sometimes we come last but we did our best","I won't give up no I won't give in","Till I reach the end and then I'll start again","No I won't leave I wanna try everything","I wanna try even though I could fail","I won't give up no I won't give in","Till I reach the end and then I'll start again","No I won't leave I wanna try everything","I wanna try even though I could fail","I'll keep on making those new mistakes","I'll keep on making them every day","Those new mistakes"],r=["I will run I will climb I will soar","I'm undefeated","Jumping out of my skin pull the chord","Yeah I believe it","The past is everything we were don't make us who we are","So I'll dream until I make it real and all I see is stars","It's not until you fall that you fly","When your dreams come alive you're unstoppable","Take a shot chase the sun find the beautiful","We will glow in the dark turning dust to gold","And we'll dream it possible","And we'll dream it possible","I will chase I will reach I will fly","Until I'm breaking until I'm breaking","Out of my cage like a bird in the night","I know I'm changing I know I'm changing","In into something big better than before","And if it takes takes a thousand lives","Then it's worth fighting for","It's not until you fall that you fly","When your dreams come alive you're unstoppable","Take a shot chase the sun find the beautiful","We will glow in the dark turning dust to gold","And we'll dream it possible","It possible","From the bottom to the top","We're sparking wild fire's","Never quit and never stop","The rest of our lives","From the bottom to the top","We're sparking wild fire's","Never quit and never stop","It's not until you fall that you fly","When your dreams come alive you're unstoppable","Take a shot chase the sun find the beautiful","We will glow in the dark turning dust to gold","And we'll dream it possible","And we'll dream it possible"],l=["I can almost see it","That dream I'm dreamin' but","There's a voice inside my head saying","You'll never reach it","Every step I'm taking","Every move I make feels","Lost with no direction","My faith is shakin","But I I gotta keep tryin","Gotta keep my head held high","There's always gonna be another mountain","I'm always gonna wanna make it move","Always gonna be an uphill battle","Sometimes I'm gonna have to lose","Ain't about how fast I get there","Ain't about what's waitin on the other side","It's the climb","The struggles I'm facing","The chances I'm taking","Sometimes might knock me down but","No I'm not breaking","I may not know it","But these are the moments that","I'm gonna remember most yeah","Just gotta keep going","And I I gotta be strong","Just keep pushing on 'cause","There's always gonna be another mountain","I'm always gonna wanna make it move","Always gonna be an uphill battle","But Sometimes I'm gonna have to lose","Ain't about how fast I get there","Ain't about what's waitin on the other side","It's the climb","Yeah-yeah","There's always gonna be another mountain","I'm always gonna wanna make it move","Always gonna be an uphill battle","Sometimes you're gonna have to lose","Ain't about how fast I get there","Ain't about what's waitin on the other side","It's the climb","Yeah-yeah-yea","Keep on moving","Keep climbing","Keep the faith","Baby It's all about","It's all about the climb","Keep your faith","Whoa O Whoa"],c=[o,r,l]},cbe9:function(e,t,n){"use strict";n.r(t);var a=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"example"},[n("github-corner"),n("tool-bar"),n("div",{staticClass:"main"},[n("div",{staticClass:"list-container"},[n("virtual-list",{directives:[{name:"show",rawName:"v-show",value:!!e.messages.length,expression:"!!messages.length"}],ref:"vsl",staticClass:"stream scroll-touch",class:{overflow:e.overflow},attrs:{"data-key":"sid","data-sources":e.messages,"data-component":e.messageComponent,"estimate-size":100,"item-class":"stream-item","item-class-add":e.addItemClass},on:{resized:e.onItemRendered,totop:e.onTotop}},[n("div",{directives:[{name:"show",rawName:"v-show",value:e.overflow,expression:"overflow"}],staticClass:"header",attrs:{slot:"header"},slot:"header"},[n("div",{directives:[{name:"show",rawName:"v-show",value:!e.finished,expression:"!finished"}],staticClass:"spinner"}),n("div",{directives:[{name:"show",rawName:"v-show",value:e.finished,expression:"finished"}],staticClass:"finished"},[e._v("No more data")])])]),n("div",{directives:[{name:"show",rawName:"v-show",value:!e.messages.length,expression:"!messages.length"}],staticClass:"empty"},[e._m(0)])],1),n("massage-eidtor",{attrs:{send:e.sendRandomMessage,received:e.receivedRandomMessage},on:{send:e.onSendMessage}})],1)],1)},s=[function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"wrapper"},[n("div",{staticClass:"icon"}),n("div",{staticClass:"tips"},[e._v("No chats")])])}],i=(n("99af"),n("13d5"),function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"item",class:{creator:e.source.isCreator}},[n("div",{staticClass:"avatar"},[n("img",{attrs:{src:e.source.user.avatar}})]),n("div",{staticClass:"body"},[e.source.isCreator?e._e():n("div",{staticClass:"name"},[e._v(e._s(e.source.user.name))]),n("div",{staticClass:"content"},[n("div",{staticClass:"text"},[e._v(e._s(e.source.content))])])])])}),o=[],r={name:"chat-room-item",props:{source:{type:Object,default:function(){return{sid:"",user:{},content:"",images:[]}}}}},l=r,c=(n("bd0f"),n("2877")),u=Object(c["a"])(l,i,o,!1,null,"5b53987b",null),h=u.exports,d=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"editor"},[n("div",{ref:"rich",staticClass:"rich",attrs:{contenteditable:"true",placehoder:e.placehoder},on:{keydown:e.onKeydown,input:e.onInput,paste:e.onPaste}}),n("div",{staticClass:"send",class:{disabled:e.disabledSend},on:{mousedown:function(e){e.preventDefault()},click:e.eventSend}}),n("img",{staticClass:"author-avatar",attrs:{src:"https://avatars1.githubusercontent.com/u/6846113",alt:"preload-avatar"}}),n("div",{staticClass:"auto"},[n("button",{staticClass:"button button-clear item",on:{mousedown:function(e){e.preventDefault()},click:e.eventClickMockReceived}},[e._v("received one random")]),n("span",{staticClass:"line"},[e._v("|")]),n("button",{staticClass:"button button-clear item",on:{mousedown:function(e){e.preventDefault()},click:e.eventClickMockSend}},[e._v("send one random")])])])},m=[],v=(n("498a"),n("835c")),f=(n("d81d"),n("d3b7"),n("adf9")),g=n("c927"),p=(n("b0c0"),function(){return"https://avatars1.githubusercontent.com/u/".concat(f["Random"].integer(1e4,99999))}),w=function(){return{name:f["Random"].name(),avatar:p()}},b=0,I=500;function y(){return"sid-".concat(b++)}function k(){return{user:{},sid:"",content:"",images:[],isCreator:!1}}function C(e,t){return new Promise((function(n){b>=I?n([]):setTimeout((function(){var t=[];while(e--){var a=k();a.user=w(),a.content=Object(g["a"])(),a.sid=y(),t.push(a)}n(t)}),t?f["Random"].pick([300,500,800]):0)}))}function T(e){return e.map((function(e){return e.sid}))}var S={EMPTY:"EMPTY",PAGES:"PAGES",FEW:"FEW"};function _(e){try{sessionStorage.setItem("LOAD_TYPES",e)}catch(t){console.error(t)}}function E(){try{return sessionStorage.getItem("LOAD_TYPES")||S.PAGES}catch(e){return console.error(e),S.EMPTY}}var $=function(e){var t=k();return t.user={name:"tangbc",avatar:"https://avatars1.githubusercontent.com/u/6846113"},t.sid=y(),t.content=e,t.isCreator=!0,t},A={name:"editor",props:{send:{type:Function},received:{type:Function}},data:function(){return{disabledSend:!0,placehoder:v["a"]?"Input message here":"Input message here and press enter to send"}},mounted:function(){this.$refs.rich&&!v["a"]&&this.$refs.rich.focus()},methods:{onKeydown:function(e){13===e.keyCode&&(this.checkDisable(),this.eventSend(),e.preventDefault())},onInput:function(){this.checkDisable()},onPaste:function(e){e.preventDefault();var t=(e.originalEvent||e).clipboardData.getData("text/plain");document.execCommand("insertText",!1,t)},checkDisable:function(){this.$refs.rich&&(this.disabledSend=!(this.$refs.rich.textContent||"").trim(""))},eventSend:function(){if(this.$refs.rich&&!this.disabledSend){var e=(this.$refs.rich.textContent||"").trim(),t=$(e);this.$refs.rich.innerHTML="",this.checkDisable(),this.$emit("send",t)}},eventClickMockReceived:function(){this.received()},eventClickMockSend:function(){this.send($(""))}}},x=A,P=(n("e7e9"),Object(c["a"])(x,d,m,!1,null,"0655a508",null)),F=P.exports,M=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"toolbar"},[n("button",{staticClass:"button button-clear item",on:{click:e.eventClickPages}},[e._v("reload with pages")]),n("span",{staticClass:"line"},[e._v("|")]),n("button",{staticClass:"button button-clear item",on:{click:e.eventClickFew}},[e._v("reload with few")]),n("span",{staticClass:"line"},[e._v("|")]),n("button",{staticClass:"button button-clear item",on:{click:e.eventClickEmpty}},[e._v("reload with empty")])])},R=[],O={name:"tool-bar",methods:{eventClickEmpty:function(){_(S.EMPTY),this.reload()},eventClickPages:function(){_(S.PAGES),this.reload()},eventClickFew:function(){_(S.FEW),this.reload()},reload:function(){window.location.reload()}}},N=O,D=(n("6c2b"),Object(c["a"])(N,M,R,!1,null,"53de24ea",null)),W=D.exports,L={name:"chat-room",components:{"massage-eidtor":F,"tool-bar":W},data:function(){return{finished:!1,messages:[],messageComponent:h,overflow:!1}},created:function(){var e=this,t=E();this.param={pageSize:t===S.FEW?2:10,isFirstPageReady:!1,isFetching:!1},this.finished=t!==S.PAGES,t!==S.EMPTY&&C(this.param.pageSize).then((function(t){e.messages=e.messages.concat(t)}))},methods:{onTotop:function(){var e=this;E()!==S.PAGES||this.param.isFetching||(this.param.isFetching=!0,C(this.param.pageSize,!0).then((function(t){if(t.length){var n=T(t);e.messages=t.concat(e.messages),e.$nextTick((function(){var t=e.$refs.vsl,a=n.reduce((function(n,a){var s="string"===typeof n?t.getSize(n):n;return s+e.$refs.vsl.getSize(a)}));e.setVirtualListToOffset(a),e.param.isFetching=!1}))}else e.finished=!0})))},onItemRendered:function(){this.$refs.vsl&&(!this.param.isFirstPageReady&&this.$refs.vsl.getSizes()>=this.param.pageSize&&(this.param.isFirstPageReady=!0,this.setVirtualListToBottom()),this.checkOverFlow())},onSendMessage:function(e){var t=this;this.messages.push(e),this.$nextTick((function(){t.setVirtualListToBottom()}))},addItemClass:function(e){return this.messages[e]&&this.messages[e].isCreator?"creator":""},checkOverFlow:function(){var e=this.$refs.vsl;e&&(this.overflow=e.getScrollSize()>e.getClientSize())},receivedRandomMessage:function(){var e=this;C(1).then((function(t){e.messages=e.messages.concat(t),e.$nextTick((function(){e.setVirtualListToBottom()}))}))},sendRandomMessage:function(e){e.content=Object(g["a"])(),this.onSendMessage(e)},setVirtualListToIndex:function(e){this.$refs.vsl&&this.$refs.vsl.scrollToIndex(e)},setVirtualListToOffset:function(e){this.$refs.vsl&&this.$refs.vsl.scrollToOffset(e)},setVirtualListToBottom:function(){this.$refs.vsl&&this.$refs.vsl.scrollToBottom()}}},z=L,B=(n("808e"),Object(c["a"])(z,a,s,!1,null,null,null));t["default"]=B.exports},e7e9:function(e,t,n){"use strict";var a=n("b10c"),s=n.n(a);s.a},f2eb:function(e,t,n){}}]); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * virtual list default component 3 | */ 4 | 5 | import Vue from 'vue' 6 | import Virtual from './virtual' 7 | import { Item, Slot } from './item' 8 | import { VirtualProps } from './props' 9 | 10 | const EVENT_TYPE = { 11 | ITEM: 'item_resize', 12 | SLOT: 'slot_resize' 13 | } 14 | const SLOT_TYPE = { 15 | HEADER: 'thead', // string value also use for aria role attribute 16 | FOOTER: 'tfoot' 17 | } 18 | 19 | const VirtualList = Vue.component('virtual-list', { 20 | props: VirtualProps, 21 | 22 | data () { 23 | return { 24 | range: null 25 | } 26 | }, 27 | 28 | watch: { 29 | 'dataSources.length' () { 30 | this.virtual.updateParam('uniqueIds', this.getUniqueIdFromDataSources()) 31 | this.virtual.handleDataSourcesChange() 32 | }, 33 | 34 | keeps (newValue) { 35 | this.virtual.updateParam('keeps', newValue) 36 | this.virtual.handleSlotSizeChange() 37 | }, 38 | 39 | start (newValue) { 40 | this.scrollToIndex(newValue) 41 | }, 42 | 43 | offset (newValue) { 44 | this.scrollToOffset(newValue) 45 | } 46 | }, 47 | 48 | created () { 49 | this.isHorizontal = this.direction === 'horizontal' 50 | this.directionKey = this.isHorizontal ? 'scrollLeft' : 'scrollTop' 51 | 52 | this.installVirtual() 53 | 54 | // listen item size change 55 | this.$on(EVENT_TYPE.ITEM, this.onItemResized) 56 | 57 | // listen slot size change 58 | if (this.$slots.header || this.$slots.footer) { 59 | this.$on(EVENT_TYPE.SLOT, this.onSlotResized) 60 | } 61 | }, 62 | 63 | activated () { 64 | // set back offset when awake from keep-alive 65 | this.scrollToOffset(this.virtual.offset) 66 | 67 | if (this.pageMode) { 68 | document.addEventListener('scroll', this.onScroll, { 69 | passive: false 70 | }) 71 | } 72 | }, 73 | 74 | deactivated () { 75 | if (this.pageMode) { 76 | document.removeEventListener('scroll', this.onScroll) 77 | } 78 | }, 79 | 80 | mounted () { 81 | // set position 82 | if (this.start) { 83 | this.scrollToIndex(this.start) 84 | } else if (this.offset) { 85 | this.scrollToOffset(this.offset) 86 | } 87 | 88 | // in page mode we bind scroll event to document 89 | if (this.pageMode) { 90 | this.updatePageModeFront() 91 | 92 | document.addEventListener('scroll', this.onScroll, { 93 | passive: false 94 | }) 95 | } 96 | }, 97 | 98 | beforeDestroy () { 99 | this.virtual.destroy() 100 | if (this.pageMode) { 101 | document.removeEventListener('scroll', this.onScroll) 102 | } 103 | }, 104 | 105 | methods: { 106 | // get item size by id 107 | getSize (id) { 108 | return this.virtual.sizes.get(id) 109 | }, 110 | 111 | // get the total number of stored (rendered) items 112 | getSizes () { 113 | return this.virtual.sizes.size 114 | }, 115 | 116 | // return current scroll offset 117 | getOffset () { 118 | if (this.pageMode) { 119 | return document.documentElement[this.directionKey] || document.body[this.directionKey] 120 | } else { 121 | const { root } = this.$refs 122 | return root ? Math.ceil(root[this.directionKey]) : 0 123 | } 124 | }, 125 | 126 | // return client viewport size 127 | getClientSize () { 128 | const key = this.isHorizontal ? 'clientWidth' : 'clientHeight' 129 | if (this.pageMode) { 130 | return document.documentElement[key] || document.body[key] 131 | } else { 132 | const { root } = this.$refs 133 | return root ? Math.ceil(root[key]) : 0 134 | } 135 | }, 136 | 137 | // return all scroll size 138 | getScrollSize () { 139 | const key = this.isHorizontal ? 'scrollWidth' : 'scrollHeight' 140 | if (this.pageMode) { 141 | return document.documentElement[key] || document.body[key] 142 | } else { 143 | const { root } = this.$refs 144 | return root ? Math.ceil(root[key]) : 0 145 | } 146 | }, 147 | 148 | // set current scroll position to a expectant offset 149 | scrollToOffset (offset) { 150 | if (this.pageMode) { 151 | document.body[this.directionKey] = offset 152 | document.documentElement[this.directionKey] = offset 153 | } else { 154 | const { root } = this.$refs 155 | if (root) { 156 | root[this.directionKey] = offset 157 | } 158 | } 159 | }, 160 | 161 | // set current scroll position to a expectant index 162 | scrollToIndex (index) { 163 | // scroll to bottom 164 | if (index >= this.dataSources.length - 1) { 165 | this.scrollToBottom() 166 | } else { 167 | const offset = this.virtual.getOffset(index) 168 | this.scrollToOffset(offset) 169 | } 170 | }, 171 | 172 | // set current scroll position to bottom 173 | scrollToBottom () { 174 | const { shepherd } = this.$refs 175 | if (shepherd) { 176 | const offset = shepherd[this.isHorizontal ? 'offsetLeft' : 'offsetTop'] 177 | this.scrollToOffset(offset) 178 | 179 | // check if it's really scrolled to the bottom 180 | // maybe list doesn't render and calculate to last range 181 | // so we need retry in next event loop until it really at bottom 182 | setTimeout(() => { 183 | if (this.getOffset() + this.getClientSize() + 1 < this.getScrollSize()) { 184 | this.scrollToBottom() 185 | } 186 | }, 3) 187 | } 188 | }, 189 | 190 | // when using page mode we need update slot header size manually 191 | // taking root offset relative to the browser as slot header size 192 | updatePageModeFront () { 193 | const { root } = this.$refs 194 | if (root) { 195 | const rect = root.getBoundingClientRect() 196 | const { defaultView } = root.ownerDocument 197 | const offsetFront = this.isHorizontal ? (rect.left + defaultView.pageXOffset) : (rect.top + defaultView.pageYOffset) 198 | this.virtual.updateParam('slotHeaderSize', offsetFront) 199 | } 200 | }, 201 | 202 | // reset all state back to initial 203 | reset () { 204 | this.virtual.destroy() 205 | this.scrollToOffset(0) 206 | this.installVirtual() 207 | }, 208 | 209 | // ----------- public method end ----------- 210 | 211 | installVirtual () { 212 | this.virtual = new Virtual({ 213 | slotHeaderSize: 0, 214 | slotFooterSize: 0, 215 | keeps: this.keeps, 216 | estimateSize: this.estimateSize, 217 | buffer: Math.round(this.keeps / 3), // recommend for a third of keeps 218 | uniqueIds: this.getUniqueIdFromDataSources() 219 | }, this.onRangeChanged) 220 | 221 | // sync initial range 222 | this.range = this.virtual.getRange() 223 | }, 224 | 225 | getUniqueIdFromDataSources () { 226 | const { dataKey } = this 227 | return this.dataSources.map((dataSource) => typeof dataKey === 'function' ? dataKey(dataSource) : dataSource[dataKey]) 228 | }, 229 | 230 | // event called when each item mounted or size changed 231 | onItemResized (id, size) { 232 | this.virtual.saveSize(id, size) 233 | this.$emit('resized', id, size) 234 | }, 235 | 236 | // event called when slot mounted or size changed 237 | onSlotResized (type, size, hasInit) { 238 | if (type === SLOT_TYPE.HEADER) { 239 | this.virtual.updateParam('slotHeaderSize', size) 240 | } else if (type === SLOT_TYPE.FOOTER) { 241 | this.virtual.updateParam('slotFooterSize', size) 242 | } 243 | 244 | if (hasInit) { 245 | this.virtual.handleSlotSizeChange() 246 | } 247 | }, 248 | 249 | // here is the rerendering entry 250 | onRangeChanged (range) { 251 | this.range = range 252 | }, 253 | 254 | onScroll (evt) { 255 | const offset = this.getOffset() 256 | const clientSize = this.getClientSize() 257 | const scrollSize = this.getScrollSize() 258 | 259 | // iOS scroll-spring-back behavior will make direction mistake 260 | if (offset < 0 || (offset + clientSize > scrollSize + 1) || !scrollSize) { 261 | return 262 | } 263 | 264 | this.virtual.handleScroll(offset) 265 | this.emitEvent(offset, clientSize, scrollSize, evt) 266 | }, 267 | 268 | // emit event in special position 269 | emitEvent (offset, clientSize, scrollSize, evt) { 270 | this.$emit('scroll', evt, this.virtual.getRange()) 271 | 272 | if (this.virtual.isFront() && !!this.dataSources.length && (offset - this.topThreshold <= 0)) { 273 | this.$emit('totop') 274 | } else if (this.virtual.isBehind() && (offset + clientSize + this.bottomThreshold >= scrollSize)) { 275 | this.$emit('tobottom') 276 | } 277 | }, 278 | 279 | // get the real render slots based on range data 280 | // in-place patch strategy will try to reuse components as possible 281 | // so those components that are reused will not trigger lifecycle mounted 282 | getRenderSlots (h) { 283 | const slots = [] 284 | const { start, end } = this.range 285 | const { dataSources, dataKey, itemClass, itemTag, itemStyle, isHorizontal, extraProps, dataComponent, itemScopedSlots, tableMode } = this 286 | const slotComponent = this.$scopedSlots && this.$scopedSlots.item 287 | for (let index = start; index <= end; index++) { 288 | const dataSource = dataSources[index] 289 | if (dataSource) { 290 | const uniqueKey = typeof dataKey === 'function' ? dataKey(dataSource) : dataSource[dataKey] 291 | if (typeof uniqueKey === 'string' || typeof uniqueKey === 'number') { 292 | slots.push(h(Item, { 293 | props: { 294 | index, 295 | tag: tableMode ? 'tr' : itemTag, 296 | event: EVENT_TYPE.ITEM, 297 | horizontal: isHorizontal, 298 | uniqueKey: uniqueKey, 299 | source: dataSource, 300 | extraProps: extraProps, 301 | component: dataComponent, 302 | slotComponent: slotComponent, 303 | scopedSlots: itemScopedSlots, 304 | tableMode: tableMode 305 | }, 306 | style: itemStyle, 307 | class: `${itemClass}${this.itemClassAdd ? ' ' + this.itemClassAdd(index) : ''}` 308 | })) 309 | } else { 310 | console.warn(`Cannot get the data-key '${dataKey}' from data-sources.`) 311 | } 312 | } else { 313 | console.warn(`Cannot get the index '${index}' from data-sources.`) 314 | } 315 | } 316 | return slots 317 | } 318 | }, 319 | 320 | // render function, a closer-to-the-compiler alternative to templates 321 | // https://vuejs.org/v2/guide/render-function.html#The-Data-Object-In-Depth 322 | render (h) { 323 | const { before, header, footer } = this.$slots 324 | const { padFront, padBehind } = this.range 325 | const { isHorizontal, pageMode, rootTag, wrapTag, wrapClass, wrapStyle, headerTag, headerClass, headerStyle, footerTag, footerClass, footerStyle, tableMode } = this 326 | const paddingStyle = { padding: isHorizontal ? `0px ${padBehind}px 0px ${padFront}px` : `${padFront}px 0px ${padBehind}px` } 327 | const wrapperStyle = wrapStyle ? Object.assign({}, wrapStyle, paddingStyle) : paddingStyle 328 | 329 | return h(tableMode ? 'table' : rootTag, { 330 | ref: 'root', 331 | on: { 332 | '&scroll': !pageMode && this.onScroll 333 | } 334 | }, [ 335 | // before slot 336 | before ? h(Slot, { 337 | props: { 338 | tag: 'div', 339 | event: EVENT_TYPE.SLOT 340 | } 341 | }, before) : null, 342 | 343 | // header slot 344 | header ? h(Slot, { 345 | class: headerClass, 346 | style: headerStyle, 347 | props: { 348 | tag: tableMode ? 'thead' : headerTag, 349 | event: EVENT_TYPE.SLOT, 350 | uniqueKey: SLOT_TYPE.HEADER 351 | } 352 | }, header) : null, 353 | 354 | // main list 355 | h(tableMode ? 'tbody' : wrapTag, { 356 | class: wrapClass, 357 | attrs: { 358 | role: tableMode ? null : 'group' 359 | }, 360 | style: wrapperStyle 361 | }, this.getRenderSlots(h)), 362 | 363 | // footer slot 364 | footer ? h(Slot, { 365 | class: footerClass, 366 | style: footerStyle, 367 | props: { 368 | tag: tableMode ? 'tfoot' : footerTag, 369 | event: EVENT_TYPE.SLOT, 370 | uniqueKey: SLOT_TYPE.FOOTER 371 | } 372 | }, footer) : null, 373 | 374 | // an empty element use to scroll to bottom 375 | h('div', { 376 | ref: 'shepherd', 377 | style: { 378 | width: isHorizontal ? '0px' : '100%', 379 | height: isHorizontal ? '100%' : '0px' 380 | } 381 | }) 382 | ]) 383 | } 384 | }) 385 | 386 | export default VirtualList 387 | -------------------------------------------------------------------------------- /docs/milligram.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Milligram v1.3.0 3 | * https://milligram.github.io 4 | * 5 | * Copyright (c) 2017 CJ Patoilo 6 | * Licensed under the MIT license 7 | */ 8 | 9 | *, 10 | *:after, 11 | *:before { 12 | box-sizing: inherit; 13 | } 14 | 15 | html { 16 | box-sizing: border-box; 17 | font-size: 62.5%; 18 | } 19 | 20 | body { 21 | color: #606c76; 22 | font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; 23 | font-size: 1.6em; 24 | font-weight: 300; 25 | letter-spacing: .01em; 26 | line-height: 1.6; 27 | } 28 | 29 | blockquote { 30 | border-left: 0.3rem solid #d1d1d1; 31 | margin-left: 0; 32 | margin-right: 0; 33 | padding: 1rem 1.5rem; 34 | } 35 | 36 | blockquote *:last-child { 37 | margin-bottom: 0; 38 | } 39 | 40 | .button, 41 | button, 42 | input[type='button'], 43 | input[type='reset'], 44 | input[type='submit'] { 45 | background-color: #9b4dca; 46 | border: 0.1rem solid #9b4dca; 47 | border-radius: .4rem; 48 | color: #fff; 49 | cursor: pointer; 50 | display: inline-block; 51 | font-size: 1.1rem; 52 | font-weight: 700; 53 | height: 3.8rem; 54 | letter-spacing: .1rem; 55 | line-height: 3.8rem; 56 | padding: 0 3.0rem; 57 | text-align: center; 58 | text-decoration: none; 59 | text-transform: uppercase; 60 | white-space: nowrap; 61 | } 62 | 63 | .button:focus, .button:hover, 64 | button:focus, 65 | button:hover, 66 | input[type='button']:focus, 67 | input[type='button']:hover, 68 | input[type='reset']:focus, 69 | input[type='reset']:hover, 70 | input[type='submit']:focus, 71 | input[type='submit']:hover { 72 | background-color: #606c76; 73 | border-color: #606c76; 74 | color: #fff; 75 | outline: 0; 76 | } 77 | 78 | .button[disabled], 79 | button[disabled], 80 | input[type='button'][disabled], 81 | input[type='reset'][disabled], 82 | input[type='submit'][disabled] { 83 | cursor: default; 84 | opacity: .5; 85 | } 86 | 87 | .button[disabled]:focus, .button[disabled]:hover, 88 | button[disabled]:focus, 89 | button[disabled]:hover, 90 | input[type='button'][disabled]:focus, 91 | input[type='button'][disabled]:hover, 92 | input[type='reset'][disabled]:focus, 93 | input[type='reset'][disabled]:hover, 94 | input[type='submit'][disabled]:focus, 95 | input[type='submit'][disabled]:hover { 96 | background-color: #9b4dca; 97 | border-color: #9b4dca; 98 | } 99 | 100 | .button.button-outline, 101 | button.button-outline, 102 | input[type='button'].button-outline, 103 | input[type='reset'].button-outline, 104 | input[type='submit'].button-outline { 105 | background-color: transparent; 106 | color: #9b4dca; 107 | } 108 | 109 | .button.button-outline:focus, .button.button-outline:hover, 110 | button.button-outline:focus, 111 | button.button-outline:hover, 112 | input[type='button'].button-outline:focus, 113 | input[type='button'].button-outline:hover, 114 | input[type='reset'].button-outline:focus, 115 | input[type='reset'].button-outline:hover, 116 | input[type='submit'].button-outline:focus, 117 | input[type='submit'].button-outline:hover { 118 | background-color: transparent; 119 | border-color: #606c76; 120 | color: #606c76; 121 | } 122 | 123 | .button.button-outline[disabled]:focus, .button.button-outline[disabled]:hover, 124 | button.button-outline[disabled]:focus, 125 | button.button-outline[disabled]:hover, 126 | input[type='button'].button-outline[disabled]:focus, 127 | input[type='button'].button-outline[disabled]:hover, 128 | input[type='reset'].button-outline[disabled]:focus, 129 | input[type='reset'].button-outline[disabled]:hover, 130 | input[type='submit'].button-outline[disabled]:focus, 131 | input[type='submit'].button-outline[disabled]:hover { 132 | border-color: inherit; 133 | color: #9b4dca; 134 | } 135 | 136 | .button.button-clear, 137 | button.button-clear, 138 | input[type='button'].button-clear, 139 | input[type='reset'].button-clear, 140 | input[type='submit'].button-clear { 141 | background-color: transparent; 142 | border-color: transparent; 143 | color: #9b4dca; 144 | } 145 | 146 | .button.button-clear:focus, .button.button-clear:hover, 147 | button.button-clear:focus, 148 | button.button-clear:hover, 149 | input[type='button'].button-clear:focus, 150 | input[type='button'].button-clear:hover, 151 | input[type='reset'].button-clear:focus, 152 | input[type='reset'].button-clear:hover, 153 | input[type='submit'].button-clear:focus, 154 | input[type='submit'].button-clear:hover { 155 | background-color: transparent; 156 | border-color: transparent; 157 | color: #606c76; 158 | } 159 | 160 | .button.button-clear[disabled]:focus, .button.button-clear[disabled]:hover, 161 | button.button-clear[disabled]:focus, 162 | button.button-clear[disabled]:hover, 163 | input[type='button'].button-clear[disabled]:focus, 164 | input[type='button'].button-clear[disabled]:hover, 165 | input[type='reset'].button-clear[disabled]:focus, 166 | input[type='reset'].button-clear[disabled]:hover, 167 | input[type='submit'].button-clear[disabled]:focus, 168 | input[type='submit'].button-clear[disabled]:hover { 169 | color: #9b4dca; 170 | } 171 | 172 | code { 173 | background: #f4f5f6; 174 | border-radius: .4rem; 175 | font-size: 86%; 176 | margin: 0 .2rem; 177 | padding: .2rem .5rem; 178 | white-space: nowrap; 179 | } 180 | 181 | pre { 182 | background: #f4f5f6; 183 | border-left: 0.3rem solid #9b4dca; 184 | overflow-y: hidden; 185 | } 186 | 187 | pre > code { 188 | border-radius: 0; 189 | display: block; 190 | padding: 1rem 1.5rem; 191 | white-space: pre; 192 | } 193 | 194 | hr { 195 | border: 0; 196 | border-top: 0.1rem solid #f4f5f6; 197 | margin: 3.0rem 0; 198 | } 199 | 200 | input[type='email'], 201 | input[type='number'], 202 | input[type='password'], 203 | input[type='search'], 204 | input[type='tel'], 205 | input[type='text'], 206 | input[type='url'], 207 | textarea, 208 | select { 209 | -webkit-appearance: none; 210 | -moz-appearance: none; 211 | appearance: none; 212 | background-color: transparent; 213 | border: 0.1rem solid #d1d1d1; 214 | border-radius: .4rem; 215 | box-shadow: none; 216 | box-sizing: inherit; 217 | height: 3.8rem; 218 | padding: .6rem 1.0rem; 219 | width: 100%; 220 | } 221 | 222 | input[type='email']:focus, 223 | input[type='number']:focus, 224 | input[type='password']:focus, 225 | input[type='search']:focus, 226 | input[type='tel']:focus, 227 | input[type='text']:focus, 228 | input[type='url']:focus, 229 | textarea:focus, 230 | select:focus { 231 | border-color: #9b4dca; 232 | outline: 0; 233 | } 234 | 235 | select { 236 | background: url('data:image/svg+xml;utf8,') center right no-repeat; 237 | padding-right: 3.0rem; 238 | } 239 | 240 | select:focus { 241 | background-image: url('data:image/svg+xml;utf8,'); 242 | } 243 | 244 | textarea { 245 | min-height: 6.5rem; 246 | } 247 | 248 | label, 249 | legend { 250 | display: block; 251 | font-size: 1.6rem; 252 | font-weight: 700; 253 | margin-bottom: .5rem; 254 | } 255 | 256 | fieldset { 257 | border-width: 0; 258 | padding: 0; 259 | } 260 | 261 | input[type='checkbox'], 262 | input[type='radio'] { 263 | display: inline; 264 | } 265 | 266 | .label-inline { 267 | display: inline-block; 268 | font-weight: normal; 269 | margin-left: .5rem; 270 | } 271 | 272 | .container { 273 | margin: 0 auto; 274 | max-width: 112.0rem; 275 | padding: 0 2.0rem; 276 | position: relative; 277 | width: 100%; 278 | } 279 | 280 | .row { 281 | display: flex; 282 | flex-direction: column; 283 | padding: 0; 284 | width: 100%; 285 | } 286 | 287 | .row.row-no-padding { 288 | padding: 0; 289 | } 290 | 291 | .row.row-no-padding > .column { 292 | padding: 0; 293 | } 294 | 295 | .row.row-wrap { 296 | flex-wrap: wrap; 297 | } 298 | 299 | .row.row-top { 300 | align-items: flex-start; 301 | } 302 | 303 | .row.row-bottom { 304 | align-items: flex-end; 305 | } 306 | 307 | .row.row-center { 308 | align-items: center; 309 | } 310 | 311 | .row.row-stretch { 312 | align-items: stretch; 313 | } 314 | 315 | .row.row-baseline { 316 | align-items: baseline; 317 | } 318 | 319 | .row .column { 320 | display: block; 321 | flex: 1 1 auto; 322 | margin-left: 0; 323 | max-width: 100%; 324 | width: 100%; 325 | } 326 | 327 | .row .column.column-offset-10 { 328 | margin-left: 10%; 329 | } 330 | 331 | .row .column.column-offset-20 { 332 | margin-left: 20%; 333 | } 334 | 335 | .row .column.column-offset-25 { 336 | margin-left: 25%; 337 | } 338 | 339 | .row .column.column-offset-33, .row .column.column-offset-34 { 340 | margin-left: 33.3333%; 341 | } 342 | 343 | .row .column.column-offset-50 { 344 | margin-left: 50%; 345 | } 346 | 347 | .row .column.column-offset-66, .row .column.column-offset-67 { 348 | margin-left: 66.6666%; 349 | } 350 | 351 | .row .column.column-offset-75 { 352 | margin-left: 75%; 353 | } 354 | 355 | .row .column.column-offset-80 { 356 | margin-left: 80%; 357 | } 358 | 359 | .row .column.column-offset-90 { 360 | margin-left: 90%; 361 | } 362 | 363 | .row .column.column-10 { 364 | flex: 0 0 10%; 365 | max-width: 10%; 366 | } 367 | 368 | .row .column.column-20 { 369 | flex: 0 0 20%; 370 | max-width: 20%; 371 | } 372 | 373 | .row .column.column-25 { 374 | flex: 0 0 25%; 375 | max-width: 25%; 376 | } 377 | 378 | .row .column.column-33, .row .column.column-34 { 379 | flex: 0 0 33.3333%; 380 | max-width: 33.3333%; 381 | } 382 | 383 | .row .column.column-40 { 384 | flex: 0 0 40%; 385 | max-width: 40%; 386 | } 387 | 388 | .row .column.column-50 { 389 | flex: 0 0 50%; 390 | max-width: 50%; 391 | } 392 | 393 | .row .column.column-60 { 394 | flex: 0 0 60%; 395 | max-width: 60%; 396 | } 397 | 398 | .row .column.column-66, .row .column.column-67 { 399 | flex: 0 0 66.6666%; 400 | max-width: 66.6666%; 401 | } 402 | 403 | .row .column.column-75 { 404 | flex: 0 0 75%; 405 | max-width: 75%; 406 | } 407 | 408 | .row .column.column-80 { 409 | flex: 0 0 80%; 410 | max-width: 80%; 411 | } 412 | 413 | .row .column.column-90 { 414 | flex: 0 0 90%; 415 | max-width: 90%; 416 | } 417 | 418 | .row .column .column-top { 419 | align-self: flex-start; 420 | } 421 | 422 | .row .column .column-bottom { 423 | align-self: flex-end; 424 | } 425 | 426 | .row .column .column-center { 427 | -ms-grid-row-align: center; 428 | align-self: center; 429 | } 430 | 431 | @media (min-width: 40rem) { 432 | .row { 433 | flex-direction: row; 434 | margin-left: -1.0rem; 435 | width: calc(100% + 2.0rem); 436 | } 437 | .row .column { 438 | margin-bottom: inherit; 439 | padding: 0 1.0rem; 440 | } 441 | } 442 | 443 | a { 444 | color: #9b4dca; 445 | text-decoration: none; 446 | } 447 | 448 | a:focus, a:hover { 449 | color: #606c76; 450 | } 451 | 452 | dl, 453 | ol, 454 | ul { 455 | list-style: none; 456 | margin-top: 0; 457 | padding-left: 0; 458 | } 459 | 460 | dl dl, 461 | dl ol, 462 | dl ul, 463 | ol dl, 464 | ol ol, 465 | ol ul, 466 | ul dl, 467 | ul ol, 468 | ul ul { 469 | font-size: 90%; 470 | margin: 1.5rem 0 1.5rem 3.0rem; 471 | } 472 | 473 | ol { 474 | list-style: decimal inside; 475 | } 476 | 477 | ul { 478 | list-style: circle inside; 479 | } 480 | 481 | .button, 482 | button, 483 | dd, 484 | dt, 485 | li { 486 | margin-bottom: 1.0rem; 487 | } 488 | 489 | fieldset, 490 | input, 491 | select, 492 | textarea { 493 | margin-bottom: 1.5rem; 494 | } 495 | 496 | blockquote, 497 | dl, 498 | figure, 499 | form, 500 | ol, 501 | p, 502 | pre, 503 | table, 504 | ul { 505 | margin-bottom: 2.5rem; 506 | } 507 | 508 | table { 509 | border-spacing: 0; 510 | width: 100%; 511 | } 512 | 513 | td, 514 | th { 515 | border-bottom: 0.1rem solid #e1e1e1; 516 | padding: 1.2rem 1.5rem; 517 | text-align: left; 518 | } 519 | 520 | td:first-child, 521 | th:first-child { 522 | padding-left: 0; 523 | } 524 | 525 | td:last-child, 526 | th:last-child { 527 | padding-right: 0; 528 | } 529 | 530 | b, 531 | strong { 532 | font-weight: bold; 533 | } 534 | 535 | p { 536 | margin-top: 0; 537 | } 538 | 539 | h1, 540 | h2, 541 | h3, 542 | h4, 543 | h5, 544 | h6 { 545 | font-weight: 300; 546 | letter-spacing: -.1rem; 547 | margin-bottom: 2.0rem; 548 | margin-top: 0; 549 | } 550 | 551 | h1 { 552 | font-size: 4.6rem; 553 | line-height: 1.2; 554 | } 555 | 556 | h2 { 557 | font-size: 3.6rem; 558 | line-height: 1.25; 559 | } 560 | 561 | h3 { 562 | font-size: 2.8rem; 563 | line-height: 1.3; 564 | } 565 | 566 | h4 { 567 | font-size: 2.2rem; 568 | letter-spacing: -.08rem; 569 | line-height: 1.35; 570 | } 571 | 572 | h5 { 573 | font-size: 1.8rem; 574 | letter-spacing: -.05rem; 575 | line-height: 1.5; 576 | } 577 | 578 | h6 { 579 | font-size: 1.6rem; 580 | letter-spacing: 0; 581 | line-height: 1.4; 582 | } 583 | 584 | img { 585 | max-width: 100%; 586 | } 587 | 588 | .clearfix:after { 589 | clear: both; 590 | content: ' '; 591 | display: table; 592 | } 593 | 594 | .float-left { 595 | float: left; 596 | } 597 | 598 | .float-right { 599 | float: right; 600 | } 601 | --------------------------------------------------------------------------------