├── .prettierignore ├── docs ├── demo │ ├── basic.md │ ├── table.md │ ├── horizontal.md │ ├── group.md │ ├── scrollto.md │ ├── infinity.md │ └── scroller.md ├── .vitepress │ ├── theme │ │ └── index.js │ └── config.mjs ├── guide │ ├── emits.md │ ├── install.md │ ├── methods.md │ └── props.md ├── index.md ├── components │ ├── scroller.vue │ ├── basic.vue │ ├── horizontal.vue │ ├── table.vue │ ├── group.vue │ ├── infinity.vue │ └── scrollto.vue └── public │ └── sentence.js ├── .gitmodules ├── .babelrc ├── .editorconfig ├── .gitignore ├── .prettierrc ├── tsconfig.json ├── .github └── workflows │ ├── publish.yml │ └── deploy.yml ├── LICENSE ├── rollup.config.js ├── src ├── item.tsx ├── props.ts └── index.tsx ├── package.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist/* 2 | /node_modules/** 3 | 4 | **/*.sh 5 | cache -------------------------------------------------------------------------------- /docs/demo/basic.md: -------------------------------------------------------------------------------- 1 | # Basic usage 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/demo/table.md: -------------------------------------------------------------------------------- 1 | # Table Mode 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/demo/horizontal.md: -------------------------------------------------------------------------------- 1 | # Horizontal list 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/core"] 2 | path = src/core 3 | url = https://github.com/mfuu/virtual-dnd-list-core.git 4 | -------------------------------------------------------------------------------- /docs/demo/group.md: -------------------------------------------------------------------------------- 1 | # Group usage 2 | 3 | Drag and drop between groups 4 | 5 | 6 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-typescript"], 4 | ["@babel/preset-env", { "modules": false }] 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /docs/demo/scrollto.md: -------------------------------------------------------------------------------- 1 | # Scroll to 2 | 3 | Scroll to the specified location 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/demo/infinity.md: -------------------------------------------------------------------------------- 1 | # Infinity scroll 2 | 3 | scroll to the bottom of the list to load more 4 | 5 | -------------------------------------------------------------------------------- /docs/demo/scroller.md: -------------------------------------------------------------------------------- 1 | # Customize Scroller 2 | 3 | This Demo uses `scroller: document` 4 | 5 | 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | dev 4 | issues 5 | coverage 6 | /dist 7 | /types 8 | 9 | # dependencies 10 | node_modules 11 | 12 | # Logs 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # lock files 18 | package-lock.json 19 | yarn.lock 20 | 21 | # vitepress 22 | cache 23 | docs/*/dist 24 | 25 | # System files 26 | .DS_Store 27 | Thumbs.db 28 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "endOfLine": "auto", 7 | "singleQuote": true, 8 | "jsxSingleQuote": true, 9 | "trailingComma": "es5", 10 | "bracketSpacing": true, 11 | "jsxBracketSameLine": true, 12 | "arrowParens": "always", 13 | "proseWrap": "preserve", 14 | "htmlWhitespaceSensitivity": "css" 15 | } 16 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme'; 2 | import { ElementPlusContainer } from '@vitepress-demo-preview/component'; 3 | import '@vitepress-demo-preview/component/dist/style.css'; 4 | 5 | export default { 6 | ...DefaultTheme, 7 | async enhanceApp({ app }) { 8 | if (!import.meta.env.SSR) { 9 | const VirtualList = (await import('../../../src/index')).default; 10 | app.component('virtual-list', VirtualList); 11 | } 12 | app.component('demo-preview', ElementPlusContainer); 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /docs/guide/emits.md: -------------------------------------------------------------------------------- 1 | # Emits 2 | 3 | ## `top` 4 | 5 | scrolled to the top of list 6 | 7 | ## `bottom` 8 | 9 | scrolled to the bottom of list 10 | 11 | ## `drag` 12 | 13 | drag is started 14 | 15 | ```ts 16 | const { 17 | item, 18 | key, 19 | index, 20 | event, 21 | } = dragEvent 22 | ``` 23 | 24 | ## `drop` 25 | 26 | drag is completed 27 | 28 | ```ts 29 | const { 30 | key, 31 | item, 32 | list, 33 | event, 34 | changed, 35 | oldList, 36 | oldIndex, 37 | newIndex, 38 | } = dropEvent 39 | ``` 40 | 41 | ## `rangeChange` 42 | 43 | drag is completed 44 | 45 | ```ts 46 | const { 47 | start, 48 | end, 49 | front, 50 | behind, 51 | } = range; 52 | ``` 53 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "jsx": "react", 7 | "strictNullChecks": true, 8 | "noImplicitAny": false, 9 | "experimentalDecorators": true, 10 | "allowSyntheticDefaultImports": true, 11 | "noUnusedParameters": true, 12 | "noUnusedLocals": true, 13 | "noEmitHelpers": true, 14 | "importHelpers": true, 15 | "lib": [ 16 | "dom", 17 | "es2015", 18 | "es2016", 19 | "es2017" 20 | ] 21 | }, 22 | "include": [ 23 | "./src/**/*" 24 | ], 25 | "exclude": [ 26 | "node_modules", 27 | "dist" 28 | ], 29 | "types": [ 30 | "typePatches" 31 | ] 32 | } -------------------------------------------------------------------------------- /docs/guide/install.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Installation 4 | 5 | ::: code-group 6 | 7 | ```sh [npm] 8 | $ npm i vue-virtual-sortable@next 9 | ``` 10 | 11 | ```sh [yarn] 12 | $ yarn add vue-virtual-sortable@next 13 | ``` 14 | 15 | ::: 16 | 17 | ## Simple Usage 18 | 19 | ```vue 20 | 21 | 25 | 26 | 27 | item slot content 28 | 29 | 30 | 31 | 32 | 33 | 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/guide/methods.md: -------------------------------------------------------------------------------- 1 | # Methods 2 | 3 | ## `getSize(key: string)` 4 | 5 | Get the size of the current item by unique key value 6 | 7 | ## `getOffset()` 8 | 9 | Get the current scroll height 10 | 11 | ## `getClientSize()` 12 | 13 | Get wrapper element client viewport size (width or height) 14 | 15 | ## `getScrollSize()` 16 | 17 | Get all scroll size (scrollHeight or scrollWidth) 18 | 19 | ## `scrollToTop()` 20 | 21 | Scroll to top of list 22 | 23 | ## `scrollToBottom()` 24 | 25 | Scroll to bottom of list 26 | 27 | ## `scrollToKey(key: string, align: 'top' | 'bottom' | 'auto')` 28 | 29 | Scroll to the specified `data-key` position 30 | 31 | ## `scrollToIndex(index: number, align: 'top' | 'bottom' | 'auto')` 32 | 33 | Scroll to the specified `index` position 34 | 35 | ## `scrollToOffset(offset: number)` 36 | 37 | Scroll to the specified offset 38 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: npm publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | permissions: 7 | id-token: write # Required for OIDC 8 | contents: read 9 | 10 | jobs: 11 | publish-npm: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup Node 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 20 21 | registry-url: https://registry.npmjs.org/ 22 | 23 | # Ensure npm 11.5.1 or later is installed 24 | - name: Install npm latest 25 | run: npm install -g npm@latest 26 | 27 | - name: Install dependencies 28 | run: npm install 29 | 30 | - name: Install submodules 31 | run: git submodule update --init --recursive 32 | 33 | - name: Build 34 | run: npm run build 35 | 36 | - name: Publish to npm 37 | run: npm publish --tag=next 38 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | push: 7 | branches: 8 | - main 9 | 10 | permissions: 11 | contents: write 12 | 13 | concurrency: 14 | group: pages 15 | cancel-in-progress: false 16 | 17 | jobs: 18 | build-and-deploy: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Setup Node 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: 20 30 | 31 | - name: Install dependencies 32 | run: npm install 33 | 34 | - name: Build with VitePress 35 | run: | 36 | git submodule update --init --recursive 37 | npm run docs:build 38 | ls 39 | 40 | - name: Deploy Github Pages 41 | uses: JamesIves/github-pages-deploy-action@v4 42 | with: 43 | folder: docs/.vitepress/dist 44 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | title: vue-virtual-sortable 5 | titleTemplate: A virtual scrolling list component that can be sorted by dragging 6 | 7 | hero: 8 | name: vue-virtual-sortable 9 | tagline: A virtual scrolling list component that can be sorted by dragging 10 | actions: 11 | - theme: brand 12 | text: Quickstart 13 | link: /guide/install 14 | - theme: alt 15 | text: GitHub 16 | link: https://github.com/mfuu/vue-virtual-sortable 17 | --- 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 mfuu 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. 22 | -------------------------------------------------------------------------------- /docs/components/scroller.vue: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | #{{ record.index }} 14 | ☰ 15 | 16 | {{ record.desc }} 17 | 18 | 19 | 20 | 21 | 22 | 29 | 30 | 56 | -------------------------------------------------------------------------------- /docs/components/basic.vue: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | #{{ record.index }} 13 | ☰ 14 | 15 | {{ record.desc }} 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | 59 | -------------------------------------------------------------------------------- /docs/components/horizontal.vue: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | #{{ record.index }} 15 | ☰ 16 | 17 | {{ record.desc }} 18 | 19 | 20 | 21 | 22 | 23 | 29 | 30 | 63 | -------------------------------------------------------------------------------- /docs/components/table.vue: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | index 14 | name 15 | content 16 | 17 | 18 | 19 | 20 | 21 | 22 | #{{ record.index }} 23 | ☰ 24 | 25 | {{ record.name }} 26 | {{ record.desc }} 27 | 28 | 29 | 30 | 31 | 32 | 38 | 39 | 70 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import terser from '@rollup/plugin-terser'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import commonJs from '@rollup/plugin-commonjs'; 5 | import typescript from '@rollup/plugin-typescript'; 6 | import dts from 'rollup-plugin-dts'; 7 | 8 | const packageJson = require('./package.json'); 9 | const version = packageJson.version; 10 | const homepage = packageJson.homepage; 11 | const extensions = ['.js', '.jsx', '.ts', '.tsx']; 12 | const banner = ` 13 | /*! 14 | * vue-virtual-sortable v${version} 15 | * open source under the MIT license 16 | * ${homepage} 17 | */ 18 | `; 19 | 20 | export default [ 21 | { 22 | external: ['vue'], 23 | input: 'src/index.tsx', 24 | output: [ 25 | { 26 | format: 'umd', 27 | file: 'dist/virtual-list.js', 28 | name: 'VirtualList', 29 | sourcemap: false, 30 | globals: { 31 | vue: 'Vue', 32 | }, 33 | banner: banner.replace(/\n/, ''), 34 | }, 35 | { 36 | format: 'umd', 37 | file: 'dist/virtual-list.min.js', 38 | name: 'VirtualList', 39 | sourcemap: false, 40 | globals: { 41 | vue: 'Vue', 42 | }, 43 | banner: banner.replace(/\n/, ''), 44 | plugins: [terser()], 45 | }, 46 | ], 47 | plugins: [resolve(), commonJs(), typescript(), babel({ extensions, babelHelpers: 'bundled' })], 48 | }, 49 | { 50 | input: 'src/index.tsx', 51 | output: { 52 | file: 'types/index.d.ts', 53 | format: 'es', 54 | }, 55 | plugins: [dts()], 56 | }, 57 | ]; 58 | -------------------------------------------------------------------------------- /src/item.tsx: -------------------------------------------------------------------------------- 1 | import { h, defineComponent } from 'vue'; 2 | import { ItemProps } from './props'; 3 | 4 | type CallFun = (vnodeEl: HTMLElement) => void; 5 | type Funs = Record<'mounted' | 'updated' | 'unmounted', CallFun>; 6 | const createSlot = ({ mounted, updated, unmounted }: Funs) => { 7 | return defineComponent({ 8 | props: ['vnode'], 9 | mounted() { 10 | mounted(this.$el); 11 | }, 12 | onUpdated() { 13 | updated(this.$el); 14 | }, 15 | onUnmounted() { 16 | unmounted(this.$el); 17 | }, 18 | render(props) { 19 | return props.vnode; 20 | }, 21 | }); 22 | }; 23 | 24 | const Item = defineComponent({ 25 | props: ItemProps, 26 | emits: ['resize'], 27 | setup(props, { emit, slots }) { 28 | let observer: ResizeObserver | null = null; 29 | 30 | const onSizeChange = (el: HTMLElement) => { 31 | const sizeKey = props.horizontal ? 'offsetWidth' : 'offsetHeight'; 32 | const size = el ? el[sizeKey] : 0; 33 | emit('resize', size, props.dataKey); 34 | }; 35 | 36 | const mounted = (el: HTMLElement) => { 37 | if (typeof ResizeObserver !== 'undefined') { 38 | observer = new ResizeObserver(() => { 39 | onSizeChange(el); 40 | }); 41 | el && observer.observe(el); 42 | } 43 | }; 44 | 45 | const updated = (el: HTMLElement) => { 46 | onSizeChange(el); 47 | }; 48 | 49 | const unmounted = () => { 50 | if (observer) { 51 | observer.disconnect(); 52 | observer = null; 53 | } 54 | }; 55 | 56 | const customSlot = createSlot({ mounted, updated, unmounted }); 57 | 58 | return () => { 59 | const { dataKey } = props; 60 | const [defaultSlot] = slots.default?.() || []; 61 | return h( 62 | customSlot, 63 | { 64 | key: dataKey, 65 | role: 'item', 66 | vnode: defaultSlot, 67 | 'data-key': dataKey, 68 | }, 69 | { default: () => slots.default?.() } 70 | ); 71 | }; 72 | }, 73 | }); 74 | 75 | export default Item; 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-virtual-sortable", 3 | "version": "3.1.0", 4 | "description": "A virtual scrolling list component that can be sorted by dragging, support vue3", 5 | "main": "dist/virtual-list.min.js", 6 | "types": "types/index.d.ts", 7 | "files": [ 8 | "dist", 9 | "types" 10 | ], 11 | "scripts": { 12 | "build": "cross-env mode=production rollup -c rollup.config.js", 13 | "publish": "npm publish --tag=next", 14 | "core:update": "git submodule update --remote", 15 | "docs:dev": "vitepress dev docs", 16 | "docs:build": "vitepress build docs", 17 | "docs:preview": "vitepress preview docs" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/mfuu/vue3-virtual-sortable.git" 22 | }, 23 | "keywords": [ 24 | "vue", 25 | "vue3", 26 | "sort", 27 | "drag", 28 | "drop", 29 | "draggable", 30 | "sortable", 31 | "virtual", 32 | "virtual-list", 33 | "big-data", 34 | "big-list", 35 | "infinite" 36 | ], 37 | "author": "mfuu", 38 | "license": "MIT", 39 | "private": false, 40 | "bugs": { 41 | "url": "https://github.com/mfuu/vue3-virtual-sortable/issues" 42 | }, 43 | "homepage": "https://github.com/mfuu/vue3-virtual-sortable#readme", 44 | "dependencies": { 45 | "sortable-dnd": "^0.7.1" 46 | }, 47 | "devDependencies": { 48 | "@babel/core": "^7.15.0", 49 | "@babel/preset-env": "^7.15.0", 50 | "@babel/preset-typescript": "^7.15.0", 51 | "@rollup/plugin-babel": "^5.3.1", 52 | "@rollup/plugin-commonjs": "^22.0.1", 53 | "@rollup/plugin-node-resolve": "^13.3.0", 54 | "@rollup/plugin-terser": "^0.4.4", 55 | "@rollup/plugin-typescript": "^8.3.3", 56 | "@vitepress-demo-preview/component": "^2.3.2", 57 | "@vitepress-demo-preview/plugin": "^1.2.3", 58 | "cross-env": "^7.0.3", 59 | "mockjs": "^1.1.0", 60 | "prettier": "^2.3.2", 61 | "rollup": "^2.56.2", 62 | "rollup-plugin-dts": "^4.2.3", 63 | "tslib": "^2.8.1", 64 | "typescript": "^4.3.5", 65 | "vitepress": "~1.4.1", 66 | "vue": "~3.2.2" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /docs/components/group.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | # {{ record.index }}-{{ record.name }} 15 | ☰ 16 | 17 | 18 | 19 | 20 | 21 | 29 | 30 | 31 | 32 | # {{ record.index }}-{{ record.name }} 33 | ☰ 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 49 | 50 | 90 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress'; 2 | import { componentPreview, containerPreview } from '@vitepress-demo-preview/plugin'; 3 | 4 | export default defineConfig({ 5 | base: '/vue3-virtual-sortable/', 6 | lang: 'en-US', 7 | title: 'vue3-virtual-sortable', 8 | description: 'A virtual scrolling list component that can be sorted by dragging', 9 | 10 | themeConfig: { 11 | search: { 12 | provider: 'local', 13 | }, 14 | 15 | socialLinks: [{ icon: 'github', link: 'https://github.com/mfuu/vue3-virtual-sortable' }], 16 | 17 | footer: { 18 | message: 'Released under the MIT License.', 19 | copyright: `Copyright © 2019-${new Date().getFullYear()} mfuu`, 20 | }, 21 | 22 | nav: [ 23 | { 24 | text: 'Guide', 25 | link: '/guide/install', 26 | activeMatch: '/guide/', 27 | }, 28 | { 29 | text: 'Demo', 30 | link: '/demo/basic', 31 | activeMatch: '/demo/', 32 | }, 33 | ], 34 | 35 | sidebar: { 36 | '/guide/': { 37 | base: '/guide/', 38 | items: [ 39 | { text: 'Start', link: 'install' }, 40 | { text: 'Props', link: 'props' }, 41 | { text: 'Emits', link: 'emits' }, 42 | { text: 'Methods', link: 'methods' }, 43 | ], 44 | }, 45 | '/demo/': { 46 | base: '/demo/', 47 | items: [ 48 | { text: 'Basic', link: 'basic' }, 49 | { text: 'Group', link: 'group' }, 50 | { text: 'Infinity', link: 'infinity' }, 51 | { text: 'Horizontal', link: 'horizontal' }, 52 | { text: 'ScrollTo', link: 'scrollto' }, 53 | { text: 'Scroller', link: 'scroller' }, 54 | { text: 'TableMode', link: 'table' }, 55 | ], 56 | }, 57 | }, 58 | }, 59 | markdown: { 60 | theme: { 61 | light: 'vitesse-light', 62 | dark: 'vitesse-dark', 63 | }, 64 | codeTransformers: [ 65 | { 66 | postprocess(code) { 67 | return code.replace(/\[!!code/g, '[!code'); 68 | }, 69 | }, 70 | ], 71 | config: (md) => { 72 | md.use(containerPreview); 73 | md.use(componentPreview); 74 | }, 75 | }, 76 | }); 77 | -------------------------------------------------------------------------------- /docs/components/infinity.vue: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | #{{ record.index }} 14 | ☰ 15 | 16 | {{ record.desc }} 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | 41 | 42 | 102 | -------------------------------------------------------------------------------- /src/props.ts: -------------------------------------------------------------------------------- 1 | import { Direction } from 'sortable-dnd'; 2 | import { PropType } from 'vue'; 3 | 4 | export type LockAxis = 'x' | 'y'; 5 | export type KeyValueType = string | number; 6 | 7 | export const VirtualProps = { 8 | modelValue: { 9 | type: Array as PropType, 10 | default: () => [], 11 | required: true, 12 | }, 13 | dataKey: { 14 | type: String, 15 | default: '', 16 | required: true, 17 | }, 18 | tableMode: { 19 | type: Boolean, 20 | default: false, 21 | }, 22 | draggable: { 23 | type: String, 24 | default: '[role="item"]', 25 | }, 26 | sortable: { 27 | type: Boolean, 28 | default: true, 29 | }, 30 | handle: { 31 | type: [Function, String], 32 | default: undefined, 33 | }, 34 | group: { 35 | type: [Object, String], 36 | default: undefined, 37 | }, 38 | scroller: { 39 | type: [Document, HTMLElement], 40 | default: undefined, 41 | }, 42 | lockAxis: { 43 | type: String as PropType, 44 | default: '', 45 | }, 46 | direction: { 47 | type: String as PropType, 48 | default: 'vertical', 49 | }, 50 | keeps: { 51 | type: Number, 52 | default: 30, 53 | }, 54 | size: { 55 | type: Number, 56 | default: undefined, 57 | }, 58 | debounceTime: { 59 | type: Number, 60 | default: 0, 61 | }, 62 | throttleTime: { 63 | type: Number, 64 | default: 0, 65 | }, 66 | animation: { 67 | type: Number, 68 | default: 150, 69 | }, 70 | autoScroll: { 71 | type: Boolean, 72 | default: true, 73 | }, 74 | scrollSpeed: { 75 | type: Object, 76 | default: () => ({ x: 10, y: 10 }), 77 | }, 78 | scrollThreshold: { 79 | type: Number, 80 | default: 55, 81 | }, 82 | keepOffset: { 83 | type: Boolean, 84 | default: false, 85 | }, 86 | disabled: { 87 | type: Boolean, 88 | default: false, 89 | }, 90 | appendToBody: { 91 | type: Boolean, 92 | default: false, 93 | }, 94 | delay: { 95 | type: Number, 96 | default: 0, 97 | }, 98 | delayOnTouchOnly: { 99 | type: Boolean, 100 | default: false, 101 | }, 102 | dropOnAnimationEnd: { 103 | type: Boolean, 104 | default: true, 105 | }, 106 | rootTag: { 107 | type: String, 108 | default: 'div', 109 | }, 110 | wrapTag: { 111 | type: String, 112 | default: 'div', 113 | }, 114 | wrapClass: { 115 | type: String, 116 | default: '', 117 | }, 118 | wrapStyle: { 119 | type: Object, 120 | default: () => ({}), 121 | }, 122 | ghostClass: { 123 | type: String, 124 | default: '', 125 | }, 126 | ghostStyle: { 127 | type: Object, 128 | default: () => ({}), 129 | }, 130 | chosenClass: { 131 | type: String, 132 | default: '', 133 | }, 134 | placeholderClass: { 135 | type: String, 136 | default: '', 137 | }, 138 | }; 139 | 140 | export const ItemProps = { 141 | dataKey: { 142 | type: [String, Number], 143 | default: undefined, 144 | }, 145 | horizontal: { 146 | type: Boolean, 147 | default: false, 148 | }, 149 | }; 150 | -------------------------------------------------------------------------------- /docs/components/scrollto.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | scroll to index: 5 | 6 | align: 7 | 8 | top 9 | bottom 10 | auto 11 | 12 | 13 | 14 | scroll to offset: 15 | 16 | 17 | 18 | 26 | 27 | 28 | 29 | #{{ record.index }} 30 | ☰ 31 | 32 | {{ record.desc }} 33 | 34 | 35 | 36 | 37 | 38 | 61 | 62 | 145 | -------------------------------------------------------------------------------- /docs/public/sentence.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs'; 2 | 3 | export function getPageData(count, currentLength) { 4 | const DataItems = []; 5 | for (let i = 0; i < count; i++) { 6 | const index = currentLength + i; 7 | DataItems.push({ 8 | index, 9 | name: Mock.Random.name(), 10 | id: getUniqueId(index), 11 | desc: getSentences(), 12 | }); 13 | } 14 | return DataItems; 15 | } 16 | 17 | function getSentences(min = 1, max = 6) { 18 | const sentences = sentenceArray[Mock.Random.pick([0, 1, 2])]; 19 | const results = []; 20 | 21 | let counts = Mock.Random.integer(min, max); 22 | while (counts--) { 23 | results.push(Mock.Random.pick(sentences)); 24 | } 25 | return results.join('. ') + '.'; 26 | } 27 | 28 | function getUniqueId(prefix) { 29 | return `${prefix}$${Math.random().toString(16).substr(9)}`; 30 | } 31 | 32 | // Try Everything (From Zootopia) 33 | const sentence1 = [ 34 | 'I messed up tonight I lost another fight', 35 | "I still mess up but I'll just start again", 36 | 'I keep falling down I keep on hitting the ground', 37 | "I always get up now to see what's next", 38 | "Birds don't just fly they fall down and get up", 39 | 'Nobody learns without getting it won', 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 won't give up no I won't give in", 45 | "Till I reach the end and then I'll start again", 46 | "No I won't leave I wanna try everything", 47 | 'I wanna try even though I could fail', 48 | "Look at how far you've come you filled your heart with love", 49 | "Baby you've done enough that cut your breath", 50 | "Don't beat yourself up don't need to run so fast", 51 | 'Sometimes we come last but we did our best', 52 | "I won't give up no I won't give in", 53 | "Till I reach the end and then I'll start again", 54 | "No I won't leave I wanna try everything", 55 | 'I wanna try even though I could fail', 56 | "I won't give up no I won't give in", 57 | "Till I reach the end and then I'll start again", 58 | "No I won't leave I wanna try everything", 59 | 'I wanna try even though I could fail', 60 | "I'll keep on making those new mistakes", 61 | "I'll keep on making them every day", 62 | 'Those new mistakes', 63 | ]; 64 | 65 | // Dream It Possible (From Delacey) 66 | const sentence2 = [ 67 | 'I will run I will climb I will soar', 68 | "I'm undefeated", 69 | 'Jumping out of my skin pull the chord', 70 | 'Yeah I believe it', 71 | "The past is everything we were don't make us who we are", 72 | "So I'll dream until I make it real and all I see is stars", 73 | "It's not until you fall that you fly", 74 | "When your dreams come alive you're unstoppable", 75 | 'Take a shot chase the sun find the beautiful', 76 | 'We will glow in the dark turning dust to gold', 77 | "And we'll dream it possible", 78 | "And we'll dream it possible", 79 | 'I will chase I will reach I will fly', 80 | "Until I'm breaking until I'm breaking", 81 | 'Out of my cage like a bird in the night', 82 | "I know I'm changing I know I'm changing", 83 | 'In into something big better than before', 84 | 'And if it takes takes a thousand lives', 85 | "Then it's worth fighting for", 86 | "It's not until you fall that you fly", 87 | "When your dreams come alive you're unstoppable", 88 | 'Take a shot chase the sun find the beautiful', 89 | 'We will glow in the dark turning dust to gold', 90 | "And we'll dream it possible", 91 | 'It possible', 92 | 'From the bottom to the top', 93 | "We're sparking wild fire's", 94 | 'Never quit and never stop', 95 | 'The rest of our lives', 96 | 'From the bottom to the top', 97 | "We're sparking wild fire's", 98 | 'Never quit and never stop', 99 | "It's not until you fall that you fly", 100 | "When your dreams come alive you're unstoppable", 101 | 'Take a shot chase the sun find the beautiful', 102 | 'We will glow in the dark turning dust to gold', 103 | "And we'll dream it possible", 104 | "And we'll dream it possible", 105 | ]; 106 | 107 | // The Climb (From Miley Cyrus) 108 | const sentence3 = [ 109 | 'I can almost see it', 110 | "That dream I'm dreamin' but", 111 | "There's a voice inside my head saying", 112 | "You'll never reach it", 113 | "Every step I'm taking", 114 | 'Every move I make feels', 115 | 'Lost with no direction', 116 | 'My faith is shakin', 117 | 'But I I gotta keep tryin', 118 | 'Gotta keep my head held high', 119 | "There's always gonna be another mountain", 120 | "I'm always gonna wanna make it move", 121 | 'Always gonna be an uphill battle', 122 | "Sometimes I'm gonna have to lose", 123 | "Ain't about how fast I get there", 124 | "Ain't about what's waitin on the other side", 125 | "It's the climb", 126 | "The struggles I'm facing", 127 | "The chances I'm taking", 128 | 'Sometimes might knock me down but', 129 | "No I'm not breaking", 130 | 'I may not know it', 131 | 'But these are the moments that', 132 | "I'm gonna remember most yeah", 133 | 'Just gotta keep going', 134 | 'And I I gotta be strong', 135 | "Just keep pushing on 'cause", 136 | "There's always gonna be another mountain", 137 | "I'm always gonna wanna make it move", 138 | 'Always gonna be an uphill battle', 139 | "But Sometimes I'm gonna have to lose", 140 | "Ain't about how fast I get there", 141 | "Ain't about what's waitin on the other side", 142 | "It's the climb", 143 | 'Yeah-yeah', 144 | "There's always gonna be another mountain", 145 | "I'm always gonna wanna make it move", 146 | 'Always gonna be an uphill battle', 147 | "Sometimes you're gonna have to lose", 148 | "Ain't about how fast I get there", 149 | "Ain't about what's waitin on the other side", 150 | "It's the climb", 151 | 'Yeah-yeah-yea', 152 | 'Keep on moving', 153 | 'Keep climbing', 154 | 'Keep the faith', 155 | "Baby It's all about", 156 | "It's all about the climb", 157 | 'Keep your faith', 158 | 'Whoa O Whoa', 159 | ]; 160 | 161 | const sentenceArray = [sentence1, sentence2, sentence3]; 162 | -------------------------------------------------------------------------------- /docs/guide/props.md: -------------------------------------------------------------------------------- 1 | # Props 2 | 3 | ## `v-model` 4 | 5 | | **Type** | **Default** | **Required** | 6 | | -------- | ----------- | ------------ | 7 | | `Array` | `[]` | `true` | 8 | 9 | The data that needs to be rendered 10 | 11 | ## `data-key` 12 | 13 | | **Type** | **Default** | **Required** | 14 | | -------- | ----------- | ------------ | 15 | | `String` | `-` | `true` | 16 | 17 | The unique identifier of each piece of data, in the form of `'a.b.c'` 18 | 19 | ## `keeps` 20 | 21 | | **Type** | **Default** | 22 | | -------- | ----------- | 23 | | `Number` | `30` | 24 | 25 | The number of lines rendered by the virtual scroll 26 | 27 | ## `size` 28 | 29 | | **Type** | **Default** | 30 | | -------- | ----------- | 31 | | `Number` | `0` | 32 | 33 | The estimated height of each piece of data, you can choose to pass it or not, it will be automatically calculated 34 | 35 | ## `handle` 36 | 37 | | **Type** | **Default** | 38 | | -------- | ----------- | 39 | | `String` | `-` | 40 | 41 | Drag handle selector within list items 42 | 43 | ## `group` 44 | 45 | | **Type** | **Default** | 46 | | --------------- | ----------- | 47 | | `Object/String` | `-` | 48 | 49 | ```js 50 | string: 'name' 51 | object: { 52 | name: 'group', 53 | put: true | false, 54 | pull: true | false | 'clone', 55 | revertDrag: true | false 56 | } 57 | ``` 58 | 59 | ## `tableMode` 60 | 61 | | **Type** | **Default** | 62 | | --------- | ----------- | 63 | | `Boolean` | `false` | 64 | 65 | Display with table 66 | 67 | ## `keepOffset` 68 | 69 | | **Type** | **Default** | 70 | | --------- | ----------- | 71 | | `Boolean` | `false` | 72 | 73 | When scrolling up to load data, keep the same offset as the previous scroll 74 | 75 | ## `direction` 76 | 77 | | **Type** | **Default** | 78 | | ------------------------ | ----------- | 79 | | `vertical \| horizontal` | `vertical` | 80 | 81 | Virtual list scroll direction 82 | 83 | ## `scroller` 84 | 85 | | **Type** | **Default** | 86 | | ------------------------- | ----------------- | 87 | | `Document \| HTMLElement` | Virtual list wrap | 88 | 89 | Virtual list scrolling element 90 | 91 | ## `lockAxis` 92 | 93 | | **Type** | **Default** | 94 | | -------- | ----------- | 95 | | `x \| y` | `-` | 96 | 97 | Axis on which dragging will be locked 98 | 99 | ## `debounceTime` 100 | 101 | | **Type** | **Default** | 102 | | -------- | ----------- | 103 | | `Number` | `0` | 104 | 105 | debounce time on scroll 106 | 107 | ## `throttleTime` 108 | 109 | | **Type** | **Default** | 110 | | -------- | ----------- | 111 | | `Number` | `0` | 112 | 113 | throttle time on scroll 114 | 115 | ## `sortable` 116 | 117 | | **Type** | **Default** | 118 | | --------- | ----------- | 119 | | `Boolean` | `true` | 120 | 121 | Whether the current list can be sorted by dragging 122 | 123 | ## `disabled` 124 | 125 | | **Type** | **Default** | 126 | | --------- | ----------- | 127 | | `Boolean` | `false` | 128 | 129 | Disables the sortable if set to true 130 | 131 | ## `draggable` 132 | 133 | | **Type** | **Default** | 134 | | -------- | --------------- | 135 | | `String` | `[role="item"]` | 136 | 137 | Specifies which items inside the element should be draggable 138 | 139 | ## `animation` 140 | 141 | | **Type** | **Default** | 142 | | -------- | ----------- | 143 | | `Number` | `150` | 144 | 145 | Animation speed moving items when sorting 146 | 147 | ## `autoScroll` 148 | 149 | | **Type** | **Default** | 150 | | --------- | ----------- | 151 | | `Boolean` | `true` | 152 | 153 | Automatic scrolling when moving to the edge of the container 154 | 155 | ## `scrollSpeed` 156 | 157 | | **Type** | **Default** | 158 | | -------- | ------------------ | 159 | | `Object` | `{ x: 10, y: 10 }` | 160 | 161 | Vertical&Horizontal scrolling speed (px) 162 | 163 | ## `scrollThreshold` 164 | 165 | | **Type** | **Default** | 166 | | -------- | ----------- | 167 | | `Number` | `55` | 168 | 169 | Threshold to trigger autoscroll 170 | 171 | ## `delay` 172 | 173 | | **Type** | **Default** | 174 | | -------- | ----------- | 175 | | `Number` | `0` | 176 | 177 | Time in milliseconds to define when the sorting should start 178 | 179 | ## `delayOnTouchOnly` 180 | 181 | | **Type** | **Default** | 182 | | --------- | ----------- | 183 | | `Boolean` | `false` | 184 | 185 | Only delay on press if user is using touch 186 | 187 | ## `appendToBody` 188 | 189 | | **Type** | **Default** | 190 | | --------- | ----------- | 191 | | `Boolean` | `false` | 192 | 193 | Appends the ghost element into the document's body 194 | 195 | ## `dropOnAnimationEnd` 196 | 197 | | **Type** | **Default** | 198 | | --------- | ----------- | 199 | | `Boolean` | `true` | 200 | 201 | Whether to trigger the drop event when the animation ends 202 | 203 | ## `rootTag` 204 | 205 | | **Type** | **Default** | 206 | | -------- | ----------- | 207 | | `String` | `div` | 208 | 209 | Label type for root element 210 | 211 | ## `wrapTag` 212 | 213 | | **Type** | **Default** | 214 | | -------- | ----------- | 215 | | `String` | `div` | 216 | 217 | Label type for wrap element 218 | 219 | ## `wrapClass` 220 | 221 | | **Type** | **Default** | 222 | | -------- | ----------- | 223 | | `String` | `-` | 224 | 225 | List wrapper element class 226 | 227 | ## `wrapStyle` 228 | 229 | | **Type** | **Default** | 230 | | -------- | ----------- | 231 | | `Object` | `{}` | 232 | 233 | List wrapper element style 234 | 235 | ## `ghostClass` 236 | 237 | | **Type** | **Default** | 238 | | -------- | ----------- | 239 | | `String` | `-` | 240 | 241 | The class of the mask element when dragging 242 | 243 | ## `ghostStyle` 244 | 245 | | **Type** | **Default** | 246 | | -------- | ----------- | 247 | | `Object` | `{}` | 248 | 249 | The style of the mask element when dragging 250 | 251 | ## `chosenClass` 252 | 253 | | **Type** | **Default** | 254 | | -------- | ----------- | 255 | | `String` | `-` | 256 | 257 | Class name for the chosen item 258 | 259 | ## `placeholderClass` 260 | 261 | | **Type** | **Default** | 262 | | -------- | ----------- | 263 | | `String` | `-` | 264 | 265 | Class name for the drop placeholder 266 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-virtual-sortable 2 | 3 | [](https://www.npmjs.com/package/vue-virtual-sortable) [](https://www.npmjs.com/package/vue-virtual-sortable) [](https://vuejs.org/) [](LICENSE) 4 | 5 | A virtual scrolling list component that can be sorted by dragging, support vue3 6 | 7 | If you use vue with 2.x, see [here](https://github.com/mfuu/vue-virtual-sortable) 8 | 9 | ### [Live demo](https://mfuu.github.io/vue3-virtual-sortable/) 10 | 11 | ## Simple usage 12 | 13 | ```bash 14 | npm i vue-virtual-sortable@next 15 | ``` 16 | 17 | Root component: 18 | 19 | ```vue 20 | 21 | 22 | 27 | 28 | 29 | 30 | handle 31 | {{ record.text }} 32 | 33 | 34 | 35 | header 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 61 | ``` 62 | 63 | ## Emits 64 | 65 | | **Emit** | **Description** | 66 | | ------------- | ------------------------------------ | 67 | | `top` | Scrolled to top of scroll element | 68 | | `bottom` | Scrolled to bottom of scroll element | 69 | | `drag` | Element dragging started | 70 | | `drop` | Element dragging is completed | 71 | | `rangeChange` | List rendering range changed | 72 | 73 | ## Props 74 | 75 | ### Required props 76 | 77 | | **Prop** | **Type** | **Description** | 78 | | ---------- | -------- | --------------------------------------------------------------------- | 79 | | `v-model` | `Array` | The data that needs to be rendered | 80 | | `data-key` | `String` | The unique identifier of each piece of data, in the form of `'a.b.c'` | 81 | 82 | ### Optional props 83 | 84 | **Commonly used** 85 | 86 | | **Prop** | **Type** | **Default** | **Description** | 87 | | -------------- | ------------------------- | ----------- | ------------------------------------------------------------------------------- | 88 | | `keeps` | `Number` | `30` | The number of lines rendered by the virtual scroll | 89 | | `size` | `Number` | `-` | The estimated height of each piece of data, it will be automatically calculated | 90 | | `handle` | `Function/String` | `-` | Drag handle selector within list items | 91 | | `group` | `Object/String` | `-` | Set value to allow drag between different lists | 92 | | `direction` | `vertical \| horizontal` | `vertical` | Scroll direction | 93 | | `scroller` | `Document \| HTMLElement` | `-` | Virtual list scrolling element | 94 | | `lockAxis` | `x \| y` | `-` | Axis on which dragging will be locked | 95 | | `tableMode` | `Boolean` | `false` | Display with table and tbody | 96 | | `keepOffset` | `Boolean` | `false` | When scrolling up to load data, keep the same offset as the previous scroll | 97 | | `debounceTime` | `Number` | `0` | Scroll debounce time | 98 | | `throttleTime` | `Number` | `0` | Scroll throttle time | 99 | 100 | **Uncommonly used** 101 | 102 | | **Prop** | **Type** | **Default** | **Description** | 103 | | -------------------- | --------- | ------------------ | ------------------------------------------------------------- | 104 | | `disabled` | `Boolean` | `false` | Disables the sortable if set to true | 105 | | `sortable` | `Boolean` | `true` | Whether the current list can be sorted by dragging | 106 | | `draggable` | `String` | `[role="item"]` | Specifies which items inside the element should be draggable. | 107 | | `animation` | `Number` | `150` | Animation speed moving items when sorting | 108 | | `autoScroll` | `Boolean` | `true` | Automatic scrolling when moving to the edge of the container | 109 | | `scrollSpeed` | `Object` | `{ x: 10, y: 10 }` | Vertical&Horizontal scrolling speed (px) | 110 | | `scrollThreshold` | `Number` | `55` | Threshold to trigger autoscroll | 111 | | `delay` | `Number` | `0` | Time in milliseconds to define when the sorting should start | 112 | | `delayOnTouchOnly` | `Boolean` | `false` | Only delay on press if user is using touch | 113 | | `appendToBody` | `Boolean` | `false` | Appends the ghost element into the document's body | 114 | | `dropOnAnimationEnd` | `Boolean` | `true` | Whether to trigger the drop event when the animation ends | 115 | | `rootTag` | `String` | `div` | Label type for root element | 116 | | `wrapTag` | `String` | `div` | Label type for list wrap element | 117 | | `wrapClass` | `String` | `''` | Class name for list wrap element | 118 | | `wrapStyle` | `Object` | `{}` | Style object for list wrap element | 119 | | `ghostClass` | `String` | `''` | Class name for the mask element when dragging | 120 | | `ghostStyle` | `Object` | `{}` | Style object for the mask element when dragging | 121 | | `chosenClass` | `String` | `''` | Class name for the chosen item | 122 | | `placeholderClass` | `String` | `''` | Class name for the drop placeholder | 123 | 124 | ## Methods 125 | 126 | | **Method** | **Description** | 127 | | ----------------------------- | ---------------------------------------------------------- | 128 | | `getSize(key)` | Get the size of the current item by unique key value | 129 | | `getOffset()` | Get the current scroll height | 130 | | `getClientSize()` | Get wrapper element client viewport size (width or height) | 131 | | `getScrollSize()` | Get all scroll size (scrollHeight or scrollWidth) | 132 | | `scrollToTop()` | Scroll to top of list | 133 | | `scrollToBottom()` | Scroll to bottom of list | 134 | | `scrollToKey(key, align)` | Scroll to the specified data-key position | 135 | | `scrollToIndex(index, align)` | Scroll to the specified index position | 136 | | `scrollToOffset(offset)` | Scroll to the specified offset | 137 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | h, 3 | ref, 4 | Ref, 5 | isRef, 6 | watch, 7 | computed, 8 | onMounted, 9 | onActivated, 10 | onUnmounted, 11 | onDeactivated, 12 | onBeforeMount, 13 | defineComponent, 14 | } from 'vue'; 15 | import { 16 | getDataKey, 17 | isEqual, 18 | throttle, 19 | SortableAttrs, 20 | VirtualAttrs, 21 | VirtualSortable, 22 | type DragEvent, 23 | type DropEvent, 24 | type Options, 25 | type Range, 26 | type ScrollEvent, 27 | } from './core'; 28 | import { KeyValueType, VirtualProps } from './props'; 29 | import Item from './item'; 30 | 31 | let draggingItem: any; 32 | 33 | const getList = (source: Ref | any[]) => { 34 | return isRef(source) ? source.value : source; 35 | }; 36 | 37 | const VirtualList = defineComponent({ 38 | props: VirtualProps, 39 | emits: ['update:modelValue', 'top', 'bottom', 'drag', 'drop', 'rangeChange'], 40 | setup(props, { emit, slots, expose }) { 41 | const list = ref([]); 42 | const range = ref({ start: 0, end: props.keeps - 1, front: 0, behind: 0 }); 43 | const dragging = ref(''); 44 | const horizontal = computed(() => props.direction !== 'vertical'); 45 | 46 | const rootElRef = ref(); 47 | const wrapElRef = ref(); 48 | 49 | function getSize(key: KeyValueType) { 50 | return VS.call('getSize', key); 51 | } 52 | 53 | function getOffset() { 54 | return VS.call('getOffset'); 55 | } 56 | 57 | function getClientSize() { 58 | return VS.call('getClientSize'); 59 | } 60 | 61 | function getScrollSize() { 62 | return VS.call('getScrollSize'); 63 | } 64 | 65 | function scrollToKey(key: KeyValueType, align?: 'top' | 'bottom' | 'auto') { 66 | const index = uniqueKeys.indexOf(key); 67 | if (index > -1) { 68 | VS.call('scrollToIndex', index, align); 69 | } 70 | } 71 | 72 | function scrollToOffset(offset: number) { 73 | VS.call('scrollToOffset', offset); 74 | } 75 | 76 | function scrollToIndex(index: number, align?: 'top' | 'bottom' | 'auto') { 77 | VS.call('scrollToIndex', index, align); 78 | } 79 | 80 | function scrollToTop() { 81 | scrollToOffset(0); 82 | } 83 | 84 | function scrollToBottom() { 85 | VS.call('scrollToBottom'); 86 | } 87 | 88 | expose({ 89 | getSize, 90 | getOffset, 91 | getClientSize, 92 | getScrollSize, 93 | scrollToTop, 94 | scrollToBottom, 95 | scrollToKey, 96 | scrollToIndex, 97 | scrollToOffset, 98 | }); 99 | 100 | // ========================================== model change ========================================== 101 | watch( 102 | () => [props.modelValue], 103 | () => { 104 | onModelUpdate(); 105 | }, 106 | { 107 | deep: true, 108 | } 109 | ); 110 | 111 | onBeforeMount(() => { 112 | onModelUpdate(); 113 | }); 114 | 115 | // set back offset when awake from keep-alive 116 | onActivated(() => { 117 | scrollToOffset(VS.virtual.offset); 118 | 119 | VS.call('addScrollEventListener'); 120 | }); 121 | 122 | onDeactivated(() => { 123 | VS.call('removeScrollEventListener'); 124 | }); 125 | 126 | onMounted(() => { 127 | initVirtualSortable(); 128 | }); 129 | 130 | onUnmounted(() => { 131 | VS.destroy(); 132 | }); 133 | 134 | let uniqueKeys: KeyValueType[] = []; 135 | let lastListLength: number = 0; 136 | let listLengthWhenTopLoading: number = 0; 137 | const onModelUpdate = () => { 138 | const data = getList(props.modelValue); 139 | if (!data) return; 140 | 141 | list.value = data; 142 | updateUniqueKeys(); 143 | detectRangeChange(lastListLength, data.length); 144 | 145 | // if auto scroll to the last offset 146 | if (listLengthWhenTopLoading && props.keepOffset) { 147 | const index = data.length - listLengthWhenTopLoading; 148 | if (index > 0) { 149 | scrollToIndex(index); 150 | } 151 | listLengthWhenTopLoading = 0; 152 | } 153 | 154 | lastListLength = data.length; 155 | }; 156 | 157 | const updateUniqueKeys = () => { 158 | uniqueKeys = list.value.map((item) => getDataKey(item, props.dataKey)); 159 | VS?.option('uniqueKeys', uniqueKeys); 160 | }; 161 | 162 | const detectRangeChange = (oldListLength: number, newListLength: number) => { 163 | if (!oldListLength && !newListLength) { 164 | return; 165 | } 166 | 167 | if (oldListLength === newListLength) { 168 | return; 169 | } 170 | 171 | let newRange = { ...range.value }; 172 | if ( 173 | oldListLength > props.keeps && 174 | newListLength > oldListLength && 175 | newRange.end === oldListLength - 1 && 176 | VS?.call('isReachedBottom') 177 | ) { 178 | newRange.start++; 179 | } 180 | 181 | VS?.call('updateRange', newRange); 182 | }; 183 | 184 | // ========================================== virtual sortable ========================================== 185 | let VS: VirtualSortable; 186 | 187 | const vsAttributes = computed(() => { 188 | return [...VirtualAttrs, ...SortableAttrs].reduce((res, key) => { 189 | res[key] = props[key]; 190 | return res; 191 | }, {}); 192 | }); 193 | 194 | watch( 195 | () => [vsAttributes], 196 | (newVal, oldVal) => { 197 | if (!VS) return; 198 | 199 | for (let key in newVal) { 200 | if (newVal[key] !== oldVal[key]) { 201 | VS.option(key as keyof Options, newVal[key]); 202 | } 203 | } 204 | } 205 | ); 206 | 207 | const handleToTop = throttle(() => { 208 | listLengthWhenTopLoading = list.value.length; 209 | emit('top'); 210 | }, 50); 211 | 212 | const handleToBottom = throttle(() => { 213 | emit('bottom'); 214 | }, 50); 215 | 216 | const onScroll = (event: ScrollEvent) => { 217 | listLengthWhenTopLoading = 0; 218 | if (!!list.value.length && event.top) { 219 | handleToTop(); 220 | } else if (event.bottom) { 221 | handleToBottom(); 222 | } 223 | }; 224 | 225 | const onUpdate = (newRange: Range, changed: boolean) => { 226 | range.value = newRange; 227 | 228 | changed && emit('rangeChange', newRange); 229 | }; 230 | 231 | const onItemResized = (size: number, key: KeyValueType) => { 232 | // ignore changes for dragging element 233 | if (isEqual(key, dragging.value) || !VS) { 234 | return; 235 | } 236 | 237 | const sizes = VS.virtual.sizes.size; 238 | VS.call('updateItemSize', key, size); 239 | 240 | if (sizes === props.keeps - 1 && list.value.length > props.keeps) { 241 | VS.call('updateRange', range.value); 242 | } 243 | }; 244 | 245 | const onDrag = (event: DragEvent) => { 246 | const { key, index } = event; 247 | const item = list.value[index]; 248 | 249 | draggingItem = item; 250 | dragging.value = key; 251 | 252 | if (!props.sortable) { 253 | VS.call('enableScroll', false); 254 | VS.option('autoScroll', false); 255 | } 256 | 257 | emit('drag', { ...event, item }); 258 | }; 259 | 260 | const onDrop = (event: DropEvent) => { 261 | const item = draggingItem; 262 | const { oldIndex, newIndex } = event; 263 | 264 | const oldList = [...list.value]; 265 | const newList = [...list.value]; 266 | 267 | if (oldIndex === -1) { 268 | newList.splice(newIndex, 0, item); 269 | } else if (newIndex === -1) { 270 | newList.splice(oldIndex, 1); 271 | } else { 272 | newList.splice(oldIndex, 1); 273 | newList.splice(newIndex, 0, item); 274 | } 275 | 276 | VS.call('enableScroll', true); 277 | VS.option('autoScroll', props.autoScroll); 278 | 279 | dragging.value = ''; 280 | 281 | if (event.changed) { 282 | emit('update:modelValue', newList); 283 | } 284 | emit('drop', { ...event, item, list: newList, oldList }); 285 | }; 286 | 287 | const initVirtualSortable = () => { 288 | VS = new VirtualSortable(rootElRef.value!, { 289 | ...vsAttributes.value, 290 | buffer: Math.round(props.keeps / 3), 291 | wrapper: wrapElRef.value!, 292 | scroller: props.scroller || rootElRef.value!, 293 | uniqueKeys: uniqueKeys, 294 | ghostContainer: wrapElRef.value, 295 | onDrag, 296 | onDrop, 297 | onScroll, 298 | onUpdate, 299 | }); 300 | }; 301 | 302 | // ========================================== layout ========================================== 303 | const renderSpacer = (offset: number) => { 304 | if (props.tableMode) { 305 | const offsetKey = horizontal.value ? 'width' : 'height'; 306 | const tdStyle = { padding: 0, border: 0, [offsetKey]: `${offset}px` }; 307 | 308 | return h('tr', {}, [h('td', { style: tdStyle })]); 309 | } 310 | 311 | return null; 312 | }; 313 | 314 | const renderItems = () => { 315 | const renders: any[] = []; 316 | const { start, end, front, behind } = range.value; 317 | 318 | renders.push(renderSpacer(front)); 319 | 320 | for (let index = start; index <= end; index++) { 321 | const record = list.value[index]; 322 | if (record) { 323 | const dataKey = getDataKey(record, props.dataKey); 324 | const isDragging = isEqual(dataKey, dragging.value); 325 | 326 | renders.push( 327 | slots.item 328 | ? h( 329 | Item, 330 | { 331 | key: dataKey, 332 | style: isDragging && { display: 'none' }, 333 | dataKey: dataKey, 334 | horizontal: horizontal.value, 335 | onResize: onItemResized, 336 | }, 337 | { 338 | default: () => slots.item?.({ record, index, dataKey }), 339 | } 340 | ) 341 | : null 342 | ); 343 | } 344 | } 345 | 346 | renders.push(renderSpacer(behind)); 347 | 348 | return renders; 349 | }; 350 | 351 | return () => { 352 | const { front, behind } = range.value; 353 | const { tableMode, rootTag, wrapTag, scroller, wrapClass, wrapStyle } = props; 354 | 355 | const overflow = horizontal.value ? 'auto hidden' : 'hidden auto'; 356 | const padding = horizontal.value ? `0 ${behind}px 0 ${front}px` : `${front}px 0 ${behind}px`; 357 | 358 | const rootElTag = tableMode ? 'table' : rootTag; 359 | const wrapElTag = tableMode ? 'tbody' : wrapTag; 360 | 361 | return h( 362 | rootElTag, 363 | { 364 | ref: rootElRef, 365 | style: !scroller && !tableMode && { overflow }, 366 | }, 367 | { 368 | default: () => [ 369 | slots.header?.(), 370 | h( 371 | wrapElTag, 372 | { 373 | ref: wrapElRef, 374 | class: wrapClass, 375 | style: { ...wrapStyle, padding: !tableMode && padding }, 376 | }, 377 | { 378 | default: () => renderItems(), 379 | } 380 | ), 381 | slots.footer?.(), 382 | ], 383 | } 384 | ); 385 | }; 386 | }, 387 | }); 388 | 389 | export default VirtualList; 390 | --------------------------------------------------------------------------------
{{ record.desc }}
{{ record.text }}