├── .husky
├── .gitignore
└── commit-msg
├── example
├── CNAME
├── entry.ts
├── CurrentValue.vue
├── index.html
├── vite.config.ts
├── ExampleFullBinding.vue
├── DefaultController.vue
├── ExampleTransition.vue
├── ExampleReactiveStyle.vue
├── ExampleDisabledItems.vue
├── ExampleDynamicOptions.vue
├── ExampleDialog.vue
├── ExampleSlot.vue
├── ExampleSensitivity.vue
├── ExampleEvent.vue
├── example.css
├── ExampleMultiple.vue
└── App.vue
├── .gitignore
├── .npmignore
├── .prettierrc
├── .vscode
├── settings.json
└── extensions.json
├── commitlint.config.js
├── types
└── vue.d.ts
├── renovate.json
├── vitest.config.ts
├── eslint.config.mjs
├── .github
└── workflows
│ └── ci.yml
├── tsconfig.json
├── src
├── index.ts
├── components
│ ├── VueScrollPicker.spec.ts
│ └── VueScrollPicker.vue
└── style.css
├── scripts
└── test-build.mjs
├── vite.config.ts
├── package.json
└── README.md
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/example/CNAME:
--------------------------------------------------------------------------------
1 | vue-scroll-picker.dist.be
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | /dist
4 | /example-dist
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *
2 | !/dist/**/*
3 | !/src/**/*
4 | !README.md
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true
4 | }
5 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "Optionable"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | }
4 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install commitlint --edit
5 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "vue.volar",
4 | "dbaeumer.vscode-eslint",
5 | "streetsidesoftware.code-spell-checker"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/types/vue.d.ts:
--------------------------------------------------------------------------------
1 | // declare module '*.vue' {
2 | // import { DefineComponent } from 'vue'
3 | // const component: DefineComponent<{}, {}, any>
4 | // export default component
5 | // }
6 |
--------------------------------------------------------------------------------
/example/entry.ts:
--------------------------------------------------------------------------------
1 | import './example.css'
2 | import 'vue-scroll-picker/dist/style.css'
3 |
4 | import { createApp } from 'vue'
5 |
6 | import App from './App.vue'
7 |
8 | const app = createApp(App)
9 |
10 | app.mount('#app')
11 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base",
4 | "docker:disable",
5 | ":dependencyDashboard"
6 | ],
7 | "automerge": true,
8 | "major": {
9 | "automerge": false,
10 | "dependencyDashboardApproval": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/example/CurrentValue.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 | currentValue =
9 | {{
10 | typeof value === 'undefined' ? 'undefined' : JSON.stringify(value)
11 | }}
12 |
13 |
14 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { mergeConfig, ViteUserConfig } from 'vitest/config'
2 | import viteConfig from './vite.config'
3 |
4 | export default mergeConfig(viteConfig, {
5 | test: {
6 | include: ['src/**/*.spec.ts'],
7 | setupFiles: ['vitest-browser-vue'],
8 | browser: {
9 | enabled: true,
10 | provider: 'playwright',
11 | instances: [{ browser: 'chromium' }],
12 | },
13 | },
14 | } satisfies ViteUserConfig)
15 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
2 | import eslintPluginVue from 'eslint-plugin-vue'
3 | import {
4 | defineConfigWithVueTs,
5 | vueTsConfigs,
6 | } from '@vue/eslint-config-typescript'
7 |
8 | export default defineConfigWithVueTs(
9 | {
10 | ignores: ['example-dist/*', 'dist/*'],
11 | },
12 | eslintPluginVue.configs['flat/recommended'],
13 | vueTsConfigs.recommended,
14 | eslintPluginPrettierRecommended,
15 | {
16 | rules: {
17 | 'no-console': 'error',
18 | },
19 | },
20 | )
21 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | node-version: [18.x, 20.x, 22.x, 23.x]
15 |
16 | steps:
17 | - uses: actions/checkout@v3
18 | - name: Use Node.js ${{ matrix.node-version }}
19 | uses: actions/setup-node@v3
20 | with:
21 | node-version: ${{ matrix.node-version }}
22 | - run: npm ci
23 | - run: npx playwright install
24 | - run: npm run test
25 | env:
26 | CI: true
27 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Vue Scroll Picker Sample - Default
9 |
10 |
11 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "outDir": "dist",
5 | "baseUrl": "./",
6 | "paths": {
7 | "vue-scroll-picker": ["./src"]
8 | },
9 | "module": "commonjs",
10 | "moduleResolution": "node",
11 | "lib": [
12 | "DOM",
13 | "ESNEXT"
14 | ],
15 | "emitDeclarationOnly": true,
16 | "declaration": true,
17 | "pretty": true,
18 | "sourceMap": true,
19 | "strict": true,
20 | "esModuleInterop": true,
21 | "noImplicitReturns": true,
22 | "skipLibCheck": true,
23 | "types": ["vite/client", "@vue/runtime-dom"]
24 | },
25 | "include": [
26 | "example/**/*.ts",
27 | "example/**/*.vue",
28 | "src/**/*.ts",
29 | "src/**/*.vue",
30 | "types/**/*.ts"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/example/vite.config.ts:
--------------------------------------------------------------------------------
1 | import vue from '@vitejs/plugin-vue'
2 | import { resolve } from 'path'
3 | import { defineConfig } from 'vite'
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | plugins: [vue()],
8 | resolve: {
9 | alias: {
10 | 'vue-scroll-picker': resolve(__dirname, '..'),
11 | },
12 | },
13 | build: {
14 | outDir: resolve(__dirname, '../example-dist'),
15 | emptyOutDir: true,
16 | rollupOptions: {
17 | output: {
18 | manualChunks(id) {
19 | if (!id.includes('node_modules')) {
20 | return
21 | }
22 | return id
23 | .toString()
24 | .split('node_modules/')[1]
25 | .split('/')[0]
26 | .toString()
27 | },
28 | },
29 | },
30 | },
31 | })
32 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { App, Component, Plugin } from 'vue'
2 |
3 | import VueScrollPicker, {
4 | type ScrollPickerValue,
5 | type ScrollPickerOption,
6 | type ScrollPickerOptionable,
7 | } from './components/VueScrollPicker.vue'
8 |
9 | import './style.css'
10 |
11 | export function install(app: App) {
12 | app.component('VueScrollPicker', VueScrollPicker as unknown as Component)
13 | }
14 |
15 | if (typeof window !== 'undefined' && 'Vue' in window) {
16 | install(window.Vue as App)
17 | }
18 |
19 | const plugin: Plugin = {
20 | install,
21 | }
22 |
23 | export default plugin
24 |
25 | export { VueScrollPicker }
26 |
27 | export type VueScrollPickerValue = ScrollPickerValue
28 | export type VueScrollPickerOption = ScrollPickerOption
29 | export type VueScrollPickerOptionable = ScrollPickerOptionable
30 |
--------------------------------------------------------------------------------
/example/ExampleFullBinding.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/components/VueScrollPicker.spec.ts:
--------------------------------------------------------------------------------
1 | import '@vitest/browser/matchers.d.ts'
2 |
3 | import { describe, expect, test } from 'vitest'
4 | import { render } from 'vitest-browser-vue'
5 | import VueScrollPicker from './VueScrollPicker.vue'
6 |
7 | import '../style.css'
8 |
9 | describe('Picker', () => {
10 | test('should be able to select option', async () => {
11 | const screen = render(VueScrollPicker, {
12 | props: {
13 | options: [1, 2, 3],
14 | modelValue: 1,
15 | },
16 | })
17 |
18 | await expect
19 | .element(screen.getByRole('option', { selected: true }))
20 | .toHaveTextContent('1')
21 |
22 | screen.rerender({
23 | modelValue: 2,
24 | })
25 |
26 | await expect
27 | .element(screen.getByRole('option', { selected: true }))
28 | .toHaveTextContent('2')
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/example/DefaultController.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
33 |
34 |
--------------------------------------------------------------------------------
/scripts/test-build.mjs:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import { readFile, stat } from 'fs/promises'
4 |
5 | const __dirname = new URL('.', import.meta.url).pathname
6 |
7 | const pkg = JSON.parse(await readFile(`${__dirname}/../package.json`))
8 |
9 | const files = [pkg.main, pkg.module, pkg.types, pkg.typings, pkg.exports]
10 |
11 | const wrongFiles = []
12 | async function traverse(obj) {
13 | if (Array.isArray(obj)) {
14 | return Promise.all(obj.map((item) => traverse(item)))
15 | }
16 |
17 | if (typeof obj === 'object' && obj !== null) {
18 | return Promise.all(Object.values(obj).map((item) => traverse(item)))
19 | }
20 |
21 | if (typeof obj === 'string') {
22 | console.log('check', obj)
23 | try {
24 | await stat(`${__dirname}/../${obj}`)
25 | } catch {
26 | console.warn(`file(${file}) not found`)
27 | wrongFiles.push(file)
28 | }
29 | }
30 | }
31 |
32 | traverse(files).then(() => {
33 | if (wrongFiles.length > 0) {
34 | console.error('Build failed')
35 | process.exit(1)
36 | }
37 |
38 | console.log('Build succeeded')
39 | })
40 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import vue from '@vitejs/plugin-vue'
2 | import { defineConfig } from 'vite'
3 | import dts from 'vite-plugin-dts'
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | esbuild: {
8 | drop: process.env.BUILD_TARGET === 'npm' ? ['console', 'debugger'] : [],
9 | },
10 | plugins: [
11 | vue({
12 | exclude: ['src/**/*.spec.ts'],
13 | }),
14 | dts({
15 | outDir: 'dist',
16 | include: ['src/**/*.ts', 'src/**/*.vue'],
17 | exclude: ['src/**/*.spec.ts'],
18 | }),
19 | ],
20 | build: {
21 | outDir: 'dist',
22 | lib: {
23 | entry: 'src/index.ts',
24 | name: 'VueScrollPicker',
25 | formats: ['es', 'cjs'],
26 | fileName: (format, entryName) =>
27 | format === 'es'
28 | ? `${entryName}.mjs`
29 | : format === 'cjs'
30 | ? `${entryName}.cjs`
31 | : `${entryName}.js`,
32 | },
33 | rollupOptions: {
34 | external: ['vue'],
35 | output: {
36 | globals: {
37 | vue: 'Vue',
38 | },
39 | },
40 | },
41 | sourcemap: true,
42 | },
43 | })
44 |
--------------------------------------------------------------------------------
/example/ExampleTransition.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
36 |
37 |
47 |
--------------------------------------------------------------------------------
/example/ExampleReactiveStyle.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | Font Size: {{ fontSize }}px
26 |
34 |
35 |
36 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/example/ExampleDisabledItems.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/example/ExampleDynamicOptions.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
43 |
44 |
--------------------------------------------------------------------------------
/example/ExampleDialog.vue:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
29 |
30 |
31 | Open Dialog
32 |
33 |
34 |
35 |
36 |
37 | Close Dialog
38 |
39 |
40 |
41 |
42 |
75 |
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | .vue-scroll-picker {
2 | position: relative;
3 | width: 100%;
4 | height: 10em;
5 | overflow: hidden;
6 | user-select: none;
7 | }
8 |
9 | .vue-scroll-picker-rotator {
10 | position: absolute;
11 | left: 0;
12 | right: 0;
13 | top: calc(50% - .6em);
14 | }
15 |
16 | .vue-scroll-picker-rotator-transition {
17 | transition: top ease 150ms;
18 | }
19 |
20 | .vue-scroll-picker-item {
21 | text-align: center;
22 | line-height: 1.2em;
23 | color: #333;
24 | }
25 |
26 | .vue-scroll-picker-item[aria-selected='true'] {
27 | color: #007bff;
28 | }
29 |
30 | .vue-scroll-picker-item[data-value=''],
31 | .vue-scroll-picker-item[aria-disabled='true'] {
32 | color: #ccc;
33 | }
34 |
35 | .vue-scroll-picker-item[data-value=''][aria-selected='true'],
36 | .vue-scroll-picker-item[aria-disabled='true'][aria-selected='true'] {
37 | color: #aaa;
38 | }
39 |
40 | .vue-scroll-picker-layer {
41 | position: absolute;
42 | left: 0;
43 | right: 0;
44 | top: 0;
45 | bottom: 0;
46 | }
47 |
48 | .vue-scroll-picker-layer-top,
49 | .vue-scroll-picker-layer-selection,
50 | .vue-scroll-picker-layer-bottom {
51 | position: absolute;
52 | left: 0;
53 | right: 0;
54 | }
55 |
56 | .vue-scroll-picker-layer-top {
57 | box-sizing: border-box;
58 | border-bottom: 1px solid #c8c7cc;
59 | background: linear-gradient(180deg,#fff 10%,rgba(255, 255, 255, .7));
60 | top: 0;
61 | height: calc(50% - 1em);
62 | cursor: pointer;
63 | }
64 |
65 | .vue-scroll-picker-layer-selection {
66 | top: calc(50% - 1em);
67 | bottom: calc(50% - 1em);
68 | }
69 |
70 | .vue-scroll-picker-layer-bottom {
71 | border-top: 1px solid #c8c7cc;
72 | background: linear-gradient(0deg,#fff 10%,rgba(255, 255, 255, .7));
73 | bottom: 0;
74 | height: calc(50% - 1em);
75 | cursor: pointer;
76 | }
77 |
--------------------------------------------------------------------------------
/example/ExampleSlot.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
{{ option.name }}
40 |
41 |
42 |
43 |
44 |
45 |
59 |
--------------------------------------------------------------------------------
/example/ExampleSensitivity.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
Drag Sensitivity (default = 1.7)
27 |
28 |
35 | {{ dragSensitivity }}
36 |
37 |
38 |
39 |
Touch Sensitivity (default = 1.7)
40 |
41 |
48 | {{ touchSensitivity }}
49 |
50 |
51 |
52 |
Scroll Sensitivity (default = 1)
53 |
54 |
61 | {{ scrollSensitivity }}
62 |
63 |
64 |
65 |
66 |
73 |
74 |
75 |
86 |
--------------------------------------------------------------------------------
/example/ExampleEvent.vue:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
log('start', ...args)"
39 | @cancel="(...args) => log('cancel', ...args)"
40 | @end="(...args) => log('end', ...args)"
41 | @click="(...args) => log('click', ...args)"
42 | @move="(...args) => log('move', ...args)"
43 | @wheel="(...args) => log('wheel', ...args)"
44 | @update:model-value="(...args) => log('update:model-value', ...args)"
45 | />
46 |
47 |
48 |
49 |
50 | {{ logMessage }}
51 |
52 |
53 |
54 |
55 |
56 |
90 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-scroll-picker",
3 | "version": "2.0.1",
4 | "description": "iOS Style Scroll Picker Component for Vue 3. Support All Gestures of Mouse(also MouseWheel) and Touch.",
5 | "author": "Changwan Jun",
6 | "license": "MIT",
7 | "repository": {
8 | "type": "git",
9 | "url": "git://github.com/wan2land/vue-scroll-picker.git"
10 | },
11 | "scripts": {
12 | "prepack": "npm run build",
13 | "test": "npm run test:eslint && npm run test:vitest && npm run build",
14 | "test:eslint": "eslint",
15 | "test:vitest": "vitest run",
16 | "test:build": "npm run build:production && node scripts/test-build.mjs",
17 | "dev": "concurrently \"npm run dev:example\" \"npm run build:watch\"",
18 | "dev:example": "vite serve example --host 0.0.0.0",
19 | "build": "npm run build:production && node scripts/test-build.mjs",
20 | "build:watch": "vite build --watch",
21 | "build:production": "vite build",
22 | "build:example": "vite build example && cp example/CNAME example-dist/",
23 | "deploy:example": "npm run build:example && gh-pages -d example-dist"
24 | },
25 | "type": "module",
26 | "types": "dist/index.d.ts",
27 | "typings": "dist/index.d.ts",
28 | "main": "dist/index.mjs",
29 | "module": "dist/index.mjs",
30 | "exports": {
31 | ".": {
32 | "types": "./dist/index.d.ts",
33 | "default": "./dist/index.mjs",
34 | "import": "./dist/index.mjs",
35 | "require": "./dist/index.cjs"
36 | },
37 | "./style.css": "./dist/style.css",
38 | "./package.json": "./package.json"
39 | },
40 | "devDependencies": {
41 | "@commitlint/cli": "17.8.1",
42 | "@commitlint/config-conventional": "13.2.0",
43 | "@types/node": "18.19.130",
44 | "@vitejs/plugin-vue": "^4.6.2",
45 | "@vitest/browser": "^3.0.4",
46 | "@vue/compiler-sfc": "3.5.25",
47 | "@vue/eslint-config-typescript": "^14.3.0",
48 | "@vue/test-utils": "^2.4.6",
49 | "concurrently": "^9.1.2",
50 | "date-fns": "^4.1.0",
51 | "eslint": "^9.19.0",
52 | "eslint-config-prettier": "^10.0.1",
53 | "eslint-plugin-prettier": "^5.2.3",
54 | "eslint-plugin-vue": "^9.32.0",
55 | "gh-pages": "5.0.0",
56 | "happy-dom": "^16.7.3",
57 | "husky": "7.0.4",
58 | "playwright": "^1.50.0",
59 | "prettier": "^3.4.2",
60 | "simple-icons": "7.21.0",
61 | "typescript": "^5.7.3",
62 | "vite": "^4.5.2",
63 | "vite-plugin-dts": "^4.5.0",
64 | "vitest": "^3.0.4",
65 | "vitest-browser-vue": "^0.2.0",
66 | "vue": "3.5.25"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/example/example.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | html {
8 | font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
9 | Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji,
10 | Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
11 | font-weight: 400;
12 | }
13 |
14 | a {
15 | cursor: pointer;
16 | color: inherit;
17 | }
18 |
19 | h1,
20 | h2,
21 | h3,
22 | h4,
23 | h5,
24 | h6 {
25 | font-weight: 400;
26 | line-height: 1.5;
27 | }
28 |
29 | h1 {
30 | font-size: 1.625rem;
31 | }
32 |
33 | h2 {
34 | font-size: 1.5rem;
35 | }
36 |
37 | h3 {
38 | font-size: 1.375rem;
39 | }
40 |
41 | h4 {
42 | font-size: 1.25rem;
43 | }
44 |
45 | h5 {
46 | font-size: 1.125rem;
47 | }
48 |
49 | h1 a,
50 | h2 a,
51 | h3 a,
52 | h4 a,
53 | h5 a,
54 | h6 a {
55 | color: inherit;
56 | text-decoration: none;
57 | }
58 |
59 | h1 a:hover::after,
60 | h2 a:hover::after,
61 | h3 a:hover::after,
62 | h4 a:hover::after,
63 | h5 a:hover::after,
64 | h6 a:hover::after {
65 | /* if hover, appear a link icon */
66 | content: " 🔗";
67 | }
68 |
69 | .hero {
70 | position: relative;
71 | padding: 6rem 1rem;
72 | background: linear-gradient(150deg, #281483 15%, #8f6ed5 70%, #d782d9 94%);
73 | display: flex;
74 | flex-flow: column;
75 | align-items: center;
76 | justify-content: center;
77 | color: #fff;
78 | }
79 |
80 | .hero::after {
81 | content: "";
82 | display: block;
83 | position: absolute;
84 | left: 0;
85 | right: 0;
86 | bottom: 0;
87 | height: 60px;
88 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' preserveAspectRatio='none'%3E%3Cpolygon fill='white' points='16 0 16 16 0 16'/%3E%3C/svg%3E");
89 | background-repeat: no-repeat;
90 | background-size: 100% 100%;
91 | }
92 |
93 | .hero .github {
94 | position: absolute;
95 | right: 0;
96 | top: 0;
97 | padding: 8px;
98 | color: #ffffff;
99 | text-decoration: none;
100 | }
101 |
102 | .hero .github svg {
103 | width: 32px;
104 | height: 32px;
105 | fill: currentColor;
106 | }
107 |
108 | .section {
109 | padding: 2rem 1rem;
110 | }
111 |
112 | .container {
113 | width: 100%;
114 | margin-left: auto;
115 | margin-right: auto;
116 | }
117 |
118 | @media (min-width: 576px) {
119 | .container {
120 | max-width: 540px;
121 | }
122 | }
123 |
124 | @media (min-width: 768px) {
125 | .container {
126 | max-width: 720px;
127 | }
128 | }
129 |
130 | @media (min-width: 992px) {
131 | .container {
132 | max-width: 960px;
133 | }
134 | }
135 |
136 | @media (min-width: 1200px) {
137 | .container {
138 | max-width: 1140px;
139 | }
140 | }
141 |
142 | .button {
143 | display: inline-block;
144 | padding: 0.25rem 0.5rem;
145 | border: 1px solid #007bff;
146 | color: #007bff;
147 | background-color: transparent;
148 | font-size: 1rem;
149 | outline: none;
150 | cursor: pointer;
151 | }
152 |
153 | .button.active {
154 | background-color: #007bff;
155 | color: #fff;
156 | }
157 |
158 | .button.disabled {
159 | display: inline-block;
160 | padding: 0.25rem 0.5rem;
161 | border: 1px solid #aaaaaa;
162 | color: #aaaaaa;
163 | }
164 |
165 | .button.disabled.active {
166 | background-color: #aaaaaa;
167 | color: #fff;
168 | }
169 |
170 | code {
171 | display: inline-block;
172 | background-color: #f0f0f0;
173 | padding: 0.25rem 0.375rem;
174 | border-radius: 0.25rem;
175 | font-size: 0.875rem;
176 | border: 1px solid #e0e0e0;
177 | }
178 |
179 | .button-group {
180 | display: flex;
181 | flex-wrap: wrap;
182 | gap: 0.5rem;
183 | }
184 |
185 | .controller {
186 | display: flex;
187 | flex-direction: column;
188 | gap: 0.5rem;
189 | user-select: none;
190 | }
191 |
--------------------------------------------------------------------------------
/example/ExampleMultiple.vue:
--------------------------------------------------------------------------------
1 |
76 |
77 |
78 |
79 | Date =
80 | {{ format(currentValue, 'yyyy-MM-dd HH:mm:ss') }}
81 |
82 |
83 |
88 |
93 |
98 |
103 |
108 |
113 |
118 |
119 |
120 |
121 |
126 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vue Scroll Picker
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Vue Scroll Picker is an iOS-style scroll picker component for Vue 3. It supports all gestures, including mouse and touch interactions, ensuring a smooth and intuitive user experience.
13 |
14 | If you are using Vue 2, please refer to the [v0.x branch](https://github.com/wan2land/vue-scroll-picker/tree/0.x-vue2).
15 |
16 | [Live Demo](http://vue-scroll-picker.dist.be) ([source](./example))
17 |
18 | ## Features
19 |
20 | - **TypeScript Support**: Uses generics for strict type checking and improved developer experience.
21 | - **Native-like Behavior**: Mimics `` element behavior for consistency.
22 | - **Lightweight & Performant**: Minimal dependencies with optimized rendering.
23 |
24 | ## Installation
25 |
26 | ```bash
27 | npm install vue-scroll-picker
28 | ```
29 |
30 | ## Usage
31 |
32 | Vue Scroll Picker can be used both globally and locally in your Vue application. Below are examples of how to set it up.
33 |
34 | ### Global Registration
35 |
36 | To register Vue Scroll Picker globally in your Vue application, import it in your main file and apply it as a plugin:
37 |
38 | [Vue3 Global Registration Guide](https://v3.vuejs.org/guide/component-registration.html#global-registration)
39 |
40 | ```js
41 | import { createApp } from "vue";
42 |
43 | import VueScrollPicker from "vue-scroll-picker";
44 | import "vue-scroll-picker/style.css";
45 |
46 | const app = createApp(); /* */
47 |
48 | app.use(VueScrollPicker); // export default is plugin
49 | ```
50 |
51 | ### Local Registration
52 |
53 | To use Vue Scroll Picker in a specific component, import it and register it locally:
54 |
55 | [Vue3 Local Registration Guide](https://v3.vuejs.org/guide/component-registration.html#local-registration)
56 |
57 | ```vue
58 |
69 |
70 |
71 |
72 | ```
73 |
74 | ### Nuxt
75 |
76 | ```ts
77 | import VueScrollPicker from "vue-scroll-picker" // export default is plugin
78 | import 'vue-scroll-picker/style.css'
79 |
80 | export default defineNuxtPlugin({
81 | async setup({ vueApp }) {
82 | vueApp.use(VueScrollPicker)
83 | }
84 | })
85 | ```
86 |
87 | ## Options
88 |
89 | ### Props
90 |
91 | Vue Scroll Picker accepts several props to customize its behavior:
92 |
93 | | Prop | Type | Default | Description |
94 | |------|------|---------|-------------|
95 | | `modelValue` | `string \| number \| boolean \| null` | `undefined` | The selected value of the picker. |
96 | | `options` | `Array<{ name: string; value: any; disabled?: boolean }>` | `[]` | The list of options displayed in the picker. |
97 | | `emptyText` | `string` | `'No options available'` | Text displayed when there are no options available. |
98 | | `dragSensitivity` | `number` | `1.7` | Sensitivity of dragging interaction. |
99 | | `touchSensitivity` | `number` | `1.7` | Sensitivity of touch interaction. |
100 | | `wheelSensitivity` | `number` | `1` | Sensitivity of mouse wheel scrolling. |
101 |
102 | ### Events
103 |
104 | Vue Scroll Picker emits several events to notify changes:
105 |
106 | | Event | Payload | Description |
107 | |-------|---------|-------------|
108 | | `update:modelValue` | `string \| number \| boolean \| null` | Fired when the selected value changes. |
109 | | `start` | `void` | Fired when interaction starts. |
110 | | `move` | `string \| number \| boolean \| null` | Fired when the selection moves. |
111 | | `end` | `string \| number \| boolean \| null` | Fired when interaction ends. |
112 | | `cancel` | `void` | Fired when interaction is canceled. |
113 | | `wheel` | `string \| number \| boolean \| null` | Fired when using the mouse wheel. |
114 | | `click` | `string \| number \| boolean \| null` | Fired when the picker is clicked. |
115 |
116 | ### Slots
117 |
118 | Vue Scroll Picker provides slots for custom rendering:
119 |
120 | | Slot | Props | Description |
121 | |------|-------|-------------|
122 | | `default` | `{ option: { name: string; value: any; disabled?: boolean } }` | Custom rendering for each option. |
123 | | `empty` | `{ text: string }` | Custom rendering when no options are available. |
124 |
--------------------------------------------------------------------------------
/example/App.vue:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 |
34 |
Vue Scroll Picker
35 |
36 | iOS Style Scroll Picker Component for Vue 3. Support All Gestures of
37 | Mouse(also MouseWheel) and Touch.
38 |
39 |
45 |
46 |
47 |
48 |
49 |
50 | Vue Scroll Picker provides full data binding.
51 | [Source]
55 |
56 |
57 |
58 |
61 |
62 | You can add a disabled property to the select options. A disabled
63 | value cannot be selected.
64 | [Source]
68 |
69 |
70 |
71 |
72 |
73 | [Source]
77 |
78 |
79 |
80 |
81 |
82 | [Source]
86 |
87 |
88 |
89 |
90 |
91 | [Source]
95 |
96 |
97 |
98 |
99 |
100 | [Source]
104 |
105 |
106 |
107 |
110 |
111 | [Source]
115 |
116 |
117 |
118 |
121 |
122 | [Source]
126 |
127 |
128 |
129 |
130 |
131 | [Source]
135 |
136 |
137 |
138 |
139 |
140 | [Source]
144 |
145 |
146 |
147 |
148 |
149 |
150 |
--------------------------------------------------------------------------------
/src/components/VueScrollPicker.vue:
--------------------------------------------------------------------------------
1 |
498 |
499 |
549 |
550 |
--------------------------------------------------------------------------------