├── pnpm-workspace.yaml ├── .gitignore ├── src ├── index.ts ├── Range.ts ├── shims.d.ts ├── Render.vue ├── type.ts ├── resolver.ts ├── utils.ts ├── RangeThumb.vue └── Range.vue ├── eslint.config.js ├── playground ├── tsconfig.json ├── src │ ├── main.ts │ ├── Count.vue │ ├── ranges │ │ ├── 01Number.vue │ │ ├── 02NumberList.vue │ │ ├── 05Vertical.vue │ │ ├── 03NumberList.vue │ │ └── 04RangeDataList.vue │ └── App.vue ├── components.d.ts ├── package.json ├── index.html ├── vite.config.ts ├── public │ └── favicon.svg └── unocss.config.ts ├── vitest.config.ts ├── netlify.toml ├── tsconfig.json ├── .github └── workflows │ ├── release.yml │ └── ci.yml ├── vite.config.ts ├── LICENSE ├── .vscode └── settings.json ├── test ├── utils.test.ts └── range.test.ts ├── package.json ├── unocss.config.ts └── README.md /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - playground 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | .idea 4 | *.log 5 | *.tgz 6 | coverage 7 | dist 8 | lib-cov 9 | logs 10 | node_modules 11 | temp 12 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import 'uno.css' 2 | 3 | export { default as Range } from './Range.vue' 4 | export * from './resolver' 5 | export * from './type' 6 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu( 4 | { 5 | unocss: true, 6 | formatters: true, 7 | }, 8 | ) 9 | -------------------------------------------------------------------------------- /src/Range.ts: -------------------------------------------------------------------------------- 1 | import type { InjectionKey, Ref } from 'vue' 2 | 3 | export const RangeTrackRefKey: InjectionKey> = Symbol('RangeTrackRefKey') 4 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "vue-range-multi": ["./src"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /playground/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import 'uno.css' 4 | import '@unocss/reset/tailwind.css' 5 | 6 | createApp(App).mount('#app') 7 | -------------------------------------------------------------------------------- /src/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | 4 | const component: DefineComponent 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, mergeConfig } from 'vitest/config' 2 | import viteConfig from './vite.config' 3 | 4 | export default mergeConfig(viteConfig, defineConfig({ 5 | test: { 6 | include: ['test/**/*.test.ts'], 7 | environment: 'jsdom', 8 | }, 9 | })) 10 | -------------------------------------------------------------------------------- /src/Render.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "playground/dist" 3 | command = "pnpm run build:play" 4 | 5 | [build.environment] 6 | NODE_VERSION = "18" 7 | 8 | [[redirects]] 9 | from = "/*" 10 | to = "/index.html" 11 | status = 200 12 | 13 | [[headers]] 14 | for = "/manifest.webmanifest" 15 | 16 | [headers.values] 17 | Content-Type = "application/manifest+json" 18 | -------------------------------------------------------------------------------- /playground/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | // biome-ignore lint: disable 6 | export {} 7 | 8 | /* prettier-ignore */ 9 | declare module 'vue' { 10 | export interface GlobalComponents { 11 | Range: typeof import('vue-range-multi')['Range'] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@unocss/reset": "^0.65.3", 12 | "@vueuse/core": "^12.2.0", 13 | "vue": "^3.5.13", 14 | "vue-range-multi": "workspace:*" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /playground/src/Count.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue Range Multi Demo 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["esnext", "dom"], 5 | "baseUrl": ".", 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "types": [ 10 | "vitest", 11 | "vitest/config", 12 | "vite/client" 13 | ], 14 | "strict": true, 15 | "strictNullChecks": true, 16 | "esModuleInterop": true, 17 | "skipDefaultLibCheck": true, 18 | "skipLibCheck": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /playground/src/ranges/01Number.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 25 | 26 | 29 | -------------------------------------------------------------------------------- /playground/src/ranges/02NumberList.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 28 | 29 | 32 | -------------------------------------------------------------------------------- /src/type.ts: -------------------------------------------------------------------------------- 1 | import type { CSSProperties, VNode } from 'vue' 2 | 3 | export type RangeValueType = number | RangeData 4 | export interface RangeData> { 5 | value: number 6 | data?: T 7 | disabled?: boolean 8 | unremovable?: boolean 9 | limits?: [number, number] 10 | renderTop?: RangeRenderFn 11 | renderBottom?: RangeRenderFn 12 | } 13 | export type RangeRenderFn> = (data: U) => VNode 14 | export type RangeValue> = U | U[] 15 | export type RangeProgress = ([number, number] | { 16 | range: [number, number] 17 | style?: CSSProperties 18 | class?: string 19 | })[] 20 | export type RangeMarks = Record 25 | -------------------------------------------------------------------------------- /src/resolver.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentResolver } from 'unplugin-vue-components' 2 | 3 | export interface VueRangeMultiResolverOptions { 4 | /** 5 | * The name of the component. It should always CapitalCase 6 | * 7 | * @default 'MRange' 8 | */ 9 | name?: string 10 | } 11 | 12 | export function VueRangeMultiResolver(option: VueRangeMultiResolverOptions = {}): ComponentResolver { 13 | option = { 14 | name: 'MRange', 15 | ...option, 16 | } 17 | 18 | return { 19 | type: 'component', 20 | resolve: (name: string) => { 21 | if (name === option.name) { 22 | return { 23 | name: 'Range', 24 | as: name, 25 | from: 'vue-range-multi', 26 | sideEffects: 'vue-range-multi/style.css', 27 | } 28 | } 29 | }, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import Vue from '@vitejs/plugin-vue' 3 | import Unocss from 'unocss/vite' 4 | import Component from 'unplugin-vue-components/vite' 5 | import { defineConfig } from 'vite' 6 | import Inspect from 'vite-plugin-inspect' 7 | import { VueRangeMultiResolver } from '../src/resolver' 8 | 9 | export default defineConfig(({ mode }) => ({ 10 | plugins: [ 11 | Vue(), 12 | Component({ 13 | resolvers: [VueRangeMultiResolver({ name: 'Range' })], 14 | }), 15 | Unocss(), 16 | Inspect(), 17 | { 18 | name: 'blank', 19 | load(id) { 20 | if (mode === 'play' && id.endsWith('vue-range-multi/style.css')) 21 | return '' 22 | }, 23 | }, 24 | ], 25 | resolve: { 26 | alias: mode === 'play' 27 | ? [ 28 | { find: /^vue-range-multi$/, replacement: path.resolve(__dirname, '../src/index.ts') }, 29 | ] 30 | : [], 31 | }, 32 | })) 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | permissions: 11 | contents: write 12 | id-token: write 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Install pnpm 20 | uses: pnpm/action-setup@v3 21 | 22 | - name: Set node 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: lts/* 26 | registry-url: 'https://registry.npmjs.org' 27 | 28 | - run: npx changelogithub 29 | env: 30 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 31 | 32 | - name: Setup 33 | run: npm i -g @antfu/ni 34 | 35 | - name: Install 36 | run: nci 37 | 38 | - run: pnpm publish --access public --no-git-checks 39 | env: 40 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 41 | NPM_CONFIG_PROVENANCE: true 42 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import Vue from '@vitejs/plugin-vue' 3 | import Unocss from 'unocss/vite' 4 | import { defineConfig } from 'vite' 5 | import dts from 'vite-plugin-dts' 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | Vue(), 10 | 11 | Unocss(), 12 | 13 | dts({ 14 | rollupTypes: true, 15 | }), 16 | ], 17 | build: { 18 | sourcemap: true, 19 | lib: { 20 | // Could also be a dictionary or array of multiple entry points 21 | entry: resolve(__dirname, 'src/index.ts'), 22 | name: 'vue-range-multi', 23 | // the proper extensions will be added 24 | fileName: 'index', 25 | cssFileName: 'style', 26 | }, 27 | rollupOptions: { 28 | // make sure to externalize deps that shouldn't be bundled 29 | // into your library 30 | external: ['vue'], 31 | output: { 32 | // Provide global variables to use in the UMD build 33 | // for externalized deps 34 | globals: { 35 | vue: 'Vue', 36 | }, 37 | }, 38 | }, 39 | }, 40 | }) 41 | -------------------------------------------------------------------------------- /playground/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 wiidede 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 | -------------------------------------------------------------------------------- /playground/unocss.config.ts: -------------------------------------------------------------------------------- 1 | import { defu } from 'defu' 2 | import { 3 | defineConfig, 4 | presetAttributify, 5 | presetIcons, 6 | presetTypography, 7 | presetWind3, 8 | transformerDirectives, 9 | transformerVariantGroup, 10 | } from 'unocss' 11 | import rootConfig from '../unocss.config' 12 | 13 | export default defineConfig(defu({ 14 | shortcuts: [ 15 | ['tag', 'inline-block bg-zinc-100 dark:bg-zinc-900 text-zinc-600 dark:text-zinc-400 text-0.8em px-2 leading-normal rd h-fit w-fit'], 16 | ['type-title', 'text-2xl inline font-normal font-mono text-zinc-800 dark:text-zinc-200'], 17 | ['label', 'text-zinc-900 dark:text-zinc-200 after:content-[":"] mr1 ws-nowrap'], 18 | ['value', 'text-sm inline-block m0 p0 ws-pre-wrap text-zinc-700 dark:text-zinc-400 b b-solid b-zinc-300 bg-zinc-50 dark:bg-zinc-900 dark:b-zinc-700 rd'], 19 | ], 20 | presets: [ 21 | presetWind3(), 22 | presetAttributify(), 23 | presetIcons({ 24 | scale: 1.2, 25 | warn: true, 26 | }), 27 | presetTypography(), 28 | ], 29 | transformers: [ 30 | transformerDirectives(), 31 | transformerVariantGroup(), 32 | ], 33 | rules: [ 34 | ], 35 | }, rootConfig)) 36 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function swap(arr: T, i: keyof T, j: keyof T): void { 2 | [arr[i], arr[j]] = [arr[j], arr[i]] 3 | } 4 | 5 | export function percentage2value(percentage: number, min: number, max: number, step: number) { 6 | if (min === undefined || max === undefined || step === undefined) 7 | return Number.NaN 8 | if (max <= min) 9 | return Number.NaN 10 | if (percentage < 0) 11 | return min 12 | if (percentage > 100) 13 | return max 14 | const value = min + (max - min) * percentage / 100 15 | return Math.round(value / step) * step 16 | } 17 | 18 | export function value2percentage(value: number, min: number, max: number, step: number) { 19 | if (min === undefined || max === undefined || step === undefined) 20 | return Number.NaN 21 | if (max <= min) 22 | return Number.NaN 23 | if (value < min) 24 | return 0 25 | if (value > max) 26 | return 100 27 | const percentage = (value - min) / (max - min) * 100 28 | const percentageStep = step / (max - min) 29 | return Math.round(percentage / percentageStep) * percentageStep 30 | } 31 | 32 | export const PromiseTimeout = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) 33 | -------------------------------------------------------------------------------- /playground/src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 32 | 33 | 38 | -------------------------------------------------------------------------------- /playground/src/ranges/05Vertical.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 49 | 50 | 55 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Disable the default formatter, use eslint instead 3 | "prettier.enable": false, 4 | "editor.formatOnSave": false, 5 | 6 | // Auto fix 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": "explicit", 9 | "source.organizeImports": "never" 10 | }, 11 | 12 | // Silent the stylistic rules in you IDE, but still auto fix them 13 | "eslint.rules.customizations": [ 14 | { "rule": "style/*", "severity": "off", "fixable": true }, 15 | { "rule": "format/*", "severity": "off", "fixable": true }, 16 | { "rule": "*-indent", "severity": "off", "fixable": true }, 17 | { "rule": "*-spacing", "severity": "off", "fixable": true }, 18 | { "rule": "*-spaces", "severity": "off", "fixable": true }, 19 | { "rule": "*-order", "severity": "off", "fixable": true }, 20 | { "rule": "*-dangle", "severity": "off", "fixable": true }, 21 | { "rule": "*-newline", "severity": "off", "fixable": true }, 22 | { "rule": "*quotes", "severity": "off", "fixable": true }, 23 | { "rule": "*semi", "severity": "off", "fixable": true } 24 | ], 25 | 26 | // Enable eslint for all supported languages 27 | "eslint.validate": [ 28 | "javascript", 29 | "javascriptreact", 30 | "typescript", 31 | "typescriptreact", 32 | "vue", 33 | "html", 34 | "markdown", 35 | "json", 36 | "jsonc", 37 | "yaml", 38 | "toml", 39 | "xml", 40 | "gql", 41 | "graphql", 42 | "astro", 43 | "svelte", 44 | "css", 45 | "less", 46 | "scss", 47 | "pcss", 48 | "postcss" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /playground/src/ranges/03NumberList.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 44 | 45 | 60 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v3 20 | 21 | - name: Set node 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: lts/* 25 | 26 | - name: Setup 27 | run: npm i -g @antfu/ni 28 | 29 | - name: Install 30 | run: nci 31 | 32 | - name: Lint 33 | run: nr lint 34 | 35 | typecheck: 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v4 39 | 40 | - name: Install pnpm 41 | uses: pnpm/action-setup@v3 42 | 43 | - name: Set node 44 | uses: actions/setup-node@v4 45 | with: 46 | node-version: lts/* 47 | 48 | - name: Setup 49 | run: npm i -g @antfu/ni 50 | 51 | - name: Install 52 | run: nci 53 | 54 | - name: Build 55 | run: nr build 56 | 57 | - name: Typecheck 58 | run: nr typecheck 59 | 60 | test: 61 | runs-on: ubuntu-latest 62 | 63 | steps: 64 | - uses: actions/checkout@v4 65 | 66 | - name: Install pnpm 67 | uses: pnpm/action-setup@v3 68 | 69 | - name: Set node 70 | uses: actions/setup-node@v4 71 | with: 72 | node-version: lts/* 73 | 74 | - name: Setup 75 | run: npm i -g @antfu/ni 76 | 77 | - name: Install 78 | run: nci 79 | 80 | - name: Build 81 | run: nr build 82 | 83 | - name: Test 84 | run: nr test 85 | -------------------------------------------------------------------------------- /test/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { percentage2value, swap, value2percentage } from '../src/utils' 3 | 4 | describe('swap', () => { 5 | it('should swap elements in an array', () => { 6 | const arr = [1, 2, 3] 7 | swap(arr, 0, 2) 8 | expect(arr).toEqual([3, 2, 1]) 9 | }) 10 | it('should swap elements in an object', () => { 11 | const obj = { a: 1, b: 2, c: 3 } 12 | swap(obj, 'a', 'c') 13 | expect(obj).toEqual({ a: 3, b: 2, c: 1 }) 14 | }) 15 | }) 16 | 17 | describe('value and percentage conversion', () => { 18 | it('should convert percentage to value', () => { 19 | expect(percentage2value(-1, 0, 100, 1)).toEqual(0) 20 | expect(percentage2value(0, 0, 100, 1)).toEqual(0) 21 | expect(percentage2value(50, 0, 100, 1)).toEqual(50) 22 | expect(percentage2value(50.0001, 0, 100, 1)).toEqual(50) 23 | expect(percentage2value(100, 0, 100, 1)).toEqual(100) 24 | expect(percentage2value(101, 0, 100, 1)).toEqual(100) 25 | expect(percentage2value(1, 1, 0, 1)).toEqual(Number.NaN) 26 | expect(percentage2value(1, 1, 1, 1)).toEqual(Number.NaN) 27 | expect(percentage2value(Number.NaN, 0, 100, 1)).toEqual(Number.NaN) 28 | expect(percentage2value(Number.NaN, Number.NaN, Number.NaN, Number.NaN)).toEqual(Number.NaN) 29 | }) 30 | 31 | it('should convert value to percentage', () => { 32 | expect(value2percentage(-1, 0, 100, 1)).toEqual(0) 33 | expect(value2percentage(0, 0, 100, 1)).toEqual(0) 34 | expect(value2percentage(50, 0, 100, 1)).toEqual(50) 35 | expect(value2percentage(50.0001, 0, 100, 1)).toEqual(50) 36 | expect(value2percentage(100, 0, 100, 1)).toEqual(100) 37 | expect(value2percentage(101, 0, 100, 1)).toEqual(100) 38 | expect(value2percentage(1, 1, 0, 1)).toEqual(Number.NaN) 39 | expect(value2percentage(1, 1, 1, 1)).toEqual(Number.NaN) 40 | expect(value2percentage(Number.NaN, 0, 100, 1)).toEqual(Number.NaN) 41 | expect(value2percentage(Number.NaN, Number.NaN, Number.NaN, Number.NaN)).toEqual(Number.NaN) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /playground/src/ranges/04RangeDataList.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 61 | 62 | 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-range-multi", 3 | "type": "module", 4 | "version": "0.5.0", 5 | "packageManager": "pnpm@10.7.0", 6 | "description": "A Vue range / slider component that supports one or more thumb", 7 | "author": "wiidede ", 8 | "license": "MIT", 9 | "funding": "https://wiidede.space/sponsor", 10 | "homepage": "https://github.com/wiidede/vue-range-multi#readme", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/wiidede/vue-range-multi.git" 14 | }, 15 | "bugs": "https://github.com/wiidede/vue-range-multi/issues", 16 | "keywords": [ 17 | "vue", 18 | "vue3", 19 | "range", 20 | "slider", 21 | "vue-range-multi", 22 | "multi thumb" 23 | ], 24 | "sideEffects": false, 25 | "exports": { 26 | ".": { 27 | "types": "./dist/index.d.ts", 28 | "import": "./dist/index.js", 29 | "require": "./dist/index.umd.cjs" 30 | }, 31 | "./style.css": "./dist/style.css", 32 | "./dist/style.css": "./dist/style.css" 33 | }, 34 | "main": "./dist/index.umd.cjs", 35 | "module": "./dist/index.js", 36 | "types": "./dist/index.d.ts", 37 | "typesVersions": { 38 | "*": { 39 | "*": [ 40 | "./dist/*", 41 | "./dist/index.d.ts" 42 | ] 43 | } 44 | }, 45 | "files": [ 46 | "dist" 47 | ], 48 | "scripts": { 49 | "build": "vite build", 50 | "dev": "vite playground --mode play", 51 | "dev:pkg": "vite playground", 52 | "preview": "vite preview playground", 53 | "build:play": "vite build && vite build playground", 54 | "lint": "eslint .", 55 | "prepublishOnly": "nr build", 56 | "release": "pnpm lint && pnpm typecheck && pnpm build && bumpp", 57 | "start": "esno src/index.ts", 58 | "test": "vitest", 59 | "typecheck": "vue-tsc --noEmit", 60 | "prepare": "simple-git-hooks" 61 | }, 62 | "peerDependencies": { 63 | "vue": "^3.0.0" 64 | }, 65 | "devDependencies": { 66 | "@antfu/eslint-config": "^4.11.0", 67 | "@antfu/ni": "^24.3.0", 68 | "@antfu/utils": "^9.1.0", 69 | "@iconify-json/carbon": "^1.2.8", 70 | "@types/jsdom": "^21.1.7", 71 | "@types/node": "^22.13.14", 72 | "@unocss/eslint-config": "^66.1.0-beta.7", 73 | "@vitejs/plugin-vue": "^5.2.3", 74 | "@vue/test-utils": "^2.4.6", 75 | "bumpp": "^10.1.0", 76 | "defu": "^6.1.4", 77 | "eslint": "^9.23.0", 78 | "eslint-plugin-format": "^1.0.1", 79 | "esno": "^4.8.0", 80 | "jsdom": "^26.0.0", 81 | "lint-staged": "^15.5.0", 82 | "pnpm": "^10.7.0", 83 | "rimraf": "^6.0.1", 84 | "simple-git-hooks": "^2.12.1", 85 | "typescript": "^5.8.2", 86 | "unocss": "^66.1.0-beta.7", 87 | "unplugin-vue-components": "^28.4.1", 88 | "vite": "^6.2.3", 89 | "vite-plugin-dts": "^4.5.3", 90 | "vite-plugin-inspect": "^11.0.0", 91 | "vitest": "^3.0.9", 92 | "vue": "^3.5.13", 93 | "vue-tsc": "^2.2.8" 94 | }, 95 | "pnpm": { 96 | "onlyBuiltDependencies": [ 97 | "esbuild", 98 | "simple-git-hooks" 99 | ] 100 | }, 101 | "simple-git-hooks": { 102 | "pre-commit": "pnpm lint-staged" 103 | }, 104 | "lint-staged": { 105 | "*": "eslint --fix" 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /unocss.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | presetAttributify, 4 | presetIcons, 5 | presetTypography, 6 | presetWind3, 7 | transformerDirectives, 8 | transformerVariantGroup, 9 | } from 'unocss' 10 | 11 | export default defineConfig({ 12 | shortcuts: [ 13 | // common 14 | ['m-range-border', 'border-2 border-primary border-solid'], 15 | // classes 16 | ['m-range', 'min-h-1 min-w-1 box-content'], 17 | // theme 18 | ['m-range-theme', '[--c-primary:#409EFF] [--c-fill:#E4E7ED] [--c-fill-stop:#F5F5F5] [--c-fill-thumb:#fff] [--c-drop:var(--c-primary)]'], 19 | ['m-range-theme-dark', '[--c-primary:#3070ED] [--c-fill:#444] [--c-fill-stop:#555] [--c-fill-thumb:#333] [--c-drop:var(--c-primary)]'], 20 | // track 21 | ['m-range-small', 'h2 [--m-range-h:0.5rem]'], 22 | ['m-range-medium', 'h4 [--m-range-h:1rem]'], 23 | ['m-range-large', 'h8 [--m-range-h:2rem]'], 24 | ['m-range-track', 'relative h-full bg-fill select-none rd-full'], 25 | ['m-range-v-small', 'w2 [--m-range-w:0.5rem]'], 26 | ['m-range-v-medium', 'w4 [--m-range-w:1rem]'], 27 | ['m-range-v-large', 'w8 [--m-range-w:2rem]'], 28 | ['m-range-v-track', 'relative w-full h-full bg-fill select-none rd-full'], 29 | // highlight 30 | ['m-range-highlight-container', 'absolute h-full w-full overflow-hidden rd-inherit'], 31 | ['m-range-highlight', 'h-full bg-primary absolute'], 32 | ['m-range-v-highlight', 'w-full bg-primary absolute'], 33 | // progress 34 | ['m-range-progress-container', 'absolute h-full w-full overflow-hidden rd-inherit'], 35 | ['m-range-progress', 'h-full bg-primary absolute'], 36 | ['m-range-v-progress', 'w-full bg-primary absolute'], 37 | // points 38 | ['m-range-points-area', 'absolute h-full w-full rd-inherit overflow-hidden'], 39 | ['m-range-points-container', 'absolute h-full left--3px right--3px flex justify-between items-center'], 40 | ['m-range-points', 'w-6px h-6px rd-3px bg-fill-stop'], 41 | ['m-range-v-points-container', 'absolute w-full top--3px bottom--3px flex flex-col justify-between items-center'], 42 | // marks 43 | ['m-range-marks', 'absolute h-full w-full left-0 top-0 translate-y-[calc(var(--m-range-h)_+_var(--m-range-thumb-h))]'], 44 | ['m-range-mark-item', 'absolute top-0 translate-x--50%'], 45 | ['m-range-v-marks', 'absolute h-full w-full left-0 top-0 translate-x-[calc(var(--m-range-w)_+_var(--m-range-thumb-w))]'], 46 | ['m-range-v-mark-item', 'absolute left-0 translate-y--50%'], 47 | // thumb 48 | ['m-range-thumb', 'm-range-border touch-none absolute bg-fill-thumb translate-x--50% transform-origin-center transition transition-property-[opacity,transform]'], 49 | ['m-range-thumb-circle', 'w-[calc(var(--m-range-h)_+_var(--m-range-thumb-h)_*_2)] rd-full'], 50 | ['m-range-thumb-square', 'w-[calc(var(--m-range-h)_+_var(--m-range-thumb-h)_*_2)]'], 51 | ['m-range-thumb-rect', 'w3 rd-full'], 52 | ['m-range-thumb-small', 'top--0 bottom--0 [--m-range-thumb-h:0rem]'], 53 | ['m-range-thumb-medium', 'top--1 bottom--1 [--m-range-thumb-h:0.25rem]'], 54 | ['m-range-thumb-large', 'top--2 bottom--2 [--m-range-thumb-h:0.5rem]'], 55 | ['m-range-thumb-active', 'z-1 drop-shadow-[0.1rem_0.15rem_0.25rem_var(--c-drop)]'], 56 | ['m-range-transition-container', 'absolute h-full w0 left-50%'], 57 | ['m-range-thumb-top-container', 'absolute left-50% top-0 translate-x--50% translate-y-[calc(-100%_-_4px)]'], 58 | ['m-range-thumb-bottom-container', 'absolute left-50% bottom-0 translate-x--50% translate-y-[calc(100%_+_4px)]'], 59 | ['m-range-v-thumb', 'm-range-border touch-none absolute bg-fill-thumb translate-y--50% transform-origin-center transition transition-property-[opacity,transform]'], 60 | ['m-range-v-thumb-circle', 'h-[calc(var(--m-range-w)_+_var(--m-range-thumb-w)_*_2)] rd-full'], 61 | ['m-range-v-thumb-square', 'h-[calc(var(--m-range-w)_+_var(--m-range-thumb-w)_*_2)]'], 62 | ['m-range-v-thumb-rect', 'h3 rd-full'], 63 | ['m-range-v-thumb-small', 'left--0 right--0 [--m-range-thumb-w:0rem]'], 64 | ['m-range-v-thumb-medium', 'left--1 right--1 [--m-range-thumb-w:0.25rem]'], 65 | ['m-range-v-thumb-large', 'left--2 right--2 [--m-range-thumb-w:0.5rem]'], 66 | ['m-range-v-transition-container', 'absolute w-full h0 top-50%'], 67 | ['m-range-v-thumb-top-container', 'absolute top-50% left-0 translate-y--50% translate-x-[calc(-100%_-_4px)]'], 68 | ['m-range-v-thumb-bottom-container', 'absolute top-50% right-0 translate-y--50% translate-x-[calc(100%_+_4px)]'], 69 | ], 70 | theme: { 71 | colors: { 72 | fill: { 73 | DEFAULT: 'var(--c-fill)', 74 | stop: 'var(--c-fill-stop)', 75 | thumb: 'var(--c-fill-thumb)', 76 | }, 77 | drop: 'var(--c-drop)', 78 | primary: 'var(--c-primary)', 79 | }, 80 | }, 81 | safelist: [ 82 | ...['small', 'medium', 'large'].flatMap(size => [`m-range-${size}`, `m-range-v-${size}`]), 83 | ...['circle', 'square', 'rect'].flatMap(size => [`m-range-thumb-${size}`, `m-range-v-thumb-${size}`]), 84 | ...['small', 'medium', 'large'].flatMap(size => [`m-range-thumb-${size}`, `m-range-v-thumb-${size}`]), 85 | ], 86 | presets: [ 87 | presetWind3(), 88 | presetAttributify(), 89 | presetIcons({ 90 | scale: 1.2, 91 | warn: true, 92 | }), 93 | presetTypography(), 94 | ], 95 | transformers: [ 96 | transformerDirectives(), 97 | transformerVariantGroup(), 98 | ], 99 | rules: [ 100 | ], 101 | }) 102 | -------------------------------------------------------------------------------- /src/RangeThumb.vue: -------------------------------------------------------------------------------- 1 | 129 | 130 | 164 | 165 | 186 | -------------------------------------------------------------------------------- /test/range.test.ts: -------------------------------------------------------------------------------- 1 | import type { RangeData } from '../src/type' 2 | import { mount } from '@vue/test-utils' 3 | import { describe, expect, it, vi } from 'vitest' 4 | import { h, nextTick, ref } from 'vue' 5 | import { Range } from '../src/index' 6 | import { PromiseTimeout } from '../src/utils' 7 | 8 | describe('range', () => { 9 | it('should created a range', () => { 10 | const wrapper = mount(Range, { 11 | props: { modelValue: 0 }, 12 | }) 13 | expect(wrapper.props().modelValue).toEqual(0) 14 | expect(wrapper.find('.m-range-thumb').exists()).toBe(true) 15 | }) 16 | 17 | it('should not have thumb while modelValue is undefined', () => { 18 | const wrapper = mount(Range) 19 | expect(wrapper.find('.m-range-thumb').exists()).toBe(false) 20 | }) 21 | 22 | it('should be vertical', () => { 23 | const wrapper = mount(Range, { 24 | props: { modelValue: 0, vertical: true, max: 10, marks: { 0: '1' } }, 25 | }) 26 | expect(wrapper.find('.m-range-v-small').exists()).toBe(true) 27 | expect(wrapper.find('.m-range-v-track').exists()).toBe(true) 28 | expect(wrapper.find('.m-range-v-highlight').exists()).toBe(true) 29 | expect(wrapper.find('.m-range-v-points-container').exists()).toBe(true) 30 | expect(wrapper.find('.m-range-v-marks').exists()).toBe(true) 31 | expect(wrapper.find('.m-range-v-mark-item').exists()).toBe(true) 32 | expect(wrapper.find('.m-range-v-thumb').exists()).toBe(true) 33 | expect(wrapper.find('.m-range-v-thumb-circle').exists()).toBe(true) 34 | expect(wrapper.find('.m-range-v-thumb-medium').exists()).toBe(true) 35 | expect(wrapper.find('.m-range-v-transition-container').exists()).toBe(true) 36 | expect(wrapper.find('.m-range-v-thumb-top-container').exists()).toBe(true) 37 | expect(wrapper.find('.m-range-v-thumb-bottom-container').exists()).toBe(true) 38 | }) 39 | 40 | it('should show stops', () => { 41 | const wrapper = mount(Range, { 42 | props: { modelValue: 0, min: 0, max: 100, step: 10 }, 43 | }) 44 | expect(wrapper.find('.m-range-points').exists()).toBe(true) 45 | }) 46 | 47 | it('should not show stops', () => { 48 | const wrapper = mount(Range, { 49 | props: { modelValue: 0, min: 0, max: 10, step: 1, showStops: 10 }, 50 | }) 51 | expect(wrapper.find('.m-range-points').exists()).toBe(false) 52 | }) 53 | 54 | it('should work with size, thumbSize, thumbType', () => { 55 | const wrapper = mount(Range, { 56 | props: { modelValue: 0, size: 'large', thumbSize: 'small', thumbType: 'rect' }, 57 | }) 58 | expect(wrapper.find('.m-range-large').exists()).toBe(true) 59 | expect(wrapper.find('.m-range-thumb-small').exists()).toBe(true) 60 | expect(wrapper.find('.m-range-thumb-rect').exists()).toBe(true) 61 | }) 62 | 63 | it('should work with render function', () => { 64 | const wrapper = mount(Range, { 65 | props: { 66 | modelValue: [0, 1], 67 | renderTop: value => h('p', { class: 'top-render' }, String(value)), 68 | renderBottom: value => h('p', { class: 'bottom-render' }, String(value)), 69 | }, 70 | }) 71 | expect(wrapper.find('p.top-render').exists()).toBe(true) 72 | expect(wrapper.find('p.bottom-render').exists()).toBe(true) 73 | }) 74 | 75 | it('should work with render on active', async () => { 76 | const wrapper = mount(Range, { 77 | props: { 78 | modelValue: [0, 1], 79 | renderTop: value => h('p', { class: 'top-render' }, String(value)), 80 | renderTopOnActive: true, 81 | renderBottom: value => h('p', { class: 'bottom-render' }, String(value)), 82 | renderBottomOnActive: true, 83 | }, 84 | }) 85 | expect(wrapper.find('p.top-render').exists()).toBe(false) 86 | expect(wrapper.find('p.bottom-render').exists()).toBe(false) 87 | const thumb = wrapper.findComponent({ name: 'RangeThumb' }) 88 | await thumb.trigger('pointerdown', { clientX: 0 }) 89 | expect(wrapper.find('p.top-render').exists()).toBe(true) 90 | expect(wrapper.find('p.bottom-render').exists()).toBe(true) 91 | }) 92 | 93 | it('should work with marks', () => { 94 | const wrapper = mount(Range, { 95 | props: { modelValue: 0, marks: { 0: '1' } }, 96 | }) 97 | expect(wrapper.find('.m-range-mark-item').exists()).toBe(true) 98 | }) 99 | 100 | it('should work with drag', async () => { 101 | const model = ref(0) 102 | const wrapper = mount(Range, { 103 | props: { modelValue: model, style: 'width:200px' }, 104 | attachTo: document.getElementById('app') || document.body, 105 | }) 106 | const track = wrapper.find('.m-range-track') 107 | // @ts-expect-error no mock of getBoundingClientRect 108 | vi.spyOn(track.element, 'getBoundingClientRect', 'get').mockImplementation(() => () => ({ left: 0, top: 0, width: 200, height: 200 })) 109 | const thumb = wrapper.findComponent({ name: 'RangeThumb' }) 110 | thumb.trigger('pointerdown', { clientX: 0 }) 111 | 112 | const mousemove = new MouseEvent('pointermove', { 113 | screenX: 100, 114 | screenY: 0, 115 | clientX: 100, 116 | clientY: 0, 117 | }) 118 | window.dispatchEvent(mousemove) 119 | await nextTick() 120 | expect(model.value).toBe(50) 121 | 122 | const mouseup = new MouseEvent('pointerup', { 123 | screenX: 100, 124 | screenY: 0, 125 | clientX: 100, 126 | clientY: 0, 127 | }) 128 | window.dispatchEvent(mouseup) 129 | await nextTick() 130 | expect(model.value).toBe(50) 131 | }) 132 | 133 | it('should work with outside model value change', async () => { 134 | const model = ref[]>(Array.from({ length: 10 }, (_, i) => ({ 135 | value: i * 10, 136 | data: i, 137 | }))) 138 | mount(Range, { 139 | props: { 140 | modelValue: model, 141 | }, 142 | }) 143 | model.value.splice(5, 1) 144 | await PromiseTimeout(10) 145 | expect(model.value).toMatchInlineSnapshot(` 146 | [ 147 | { 148 | "data": 0, 149 | "value": 0, 150 | }, 151 | { 152 | "data": 1, 153 | "value": 10, 154 | }, 155 | { 156 | "data": 2, 157 | "value": 20, 158 | }, 159 | { 160 | "data": 3, 161 | "value": 30, 162 | }, 163 | { 164 | "data": 4, 165 | "value": 40, 166 | }, 167 | { 168 | "data": 6, 169 | "value": 60, 170 | }, 171 | { 172 | "data": 7, 173 | "value": 70, 174 | }, 175 | { 176 | "data": 8, 177 | "value": 80, 178 | }, 179 | { 180 | "data": 9, 181 | "value": 90, 182 | }, 183 | ] 184 | `) 185 | }) 186 | }) 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue Range Multi 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![bundle][bundle-src]][bundle-href] 6 | [![JSDocs][jsdocs-src]][jsdocs-href] 7 | [![License][license-src]][license-href] 8 | 9 | 10 | Vue Range Multi Logo 11 | 12 | 13 | A Vue range(slider) component that supports one or more thumb 14 | 15 | - ✨ Support for one or more thumbs. 16 | - 🔄 Auto-detect the type of model and display the corresponding thumb(s). 17 | - 🔀 Automatically sort the model values without sorting the DOM. 18 | - ➕ Ability to add or remove thumbs dynamically. 19 | - 🚫 Avoid duplicate thumbs by rejecting them. 20 | - 🍡 Smooth movement or jump movement over the stops. 21 | - 🎨 Customizable style and theme. 22 | - 🌓 Supports dark mode. 23 | - 📍 Render content above or below the thumb(render function / slot). 24 | - 🏷 Support display marks under the track. 25 | 26 | ## Demo 27 | 28 | [Demo](https://range.wiidede.space/) 29 | 30 | ## Quick Start 31 | 32 | 1. Install 33 | 34 | ```bash 35 | pnpm add vue-range-multi 36 | ``` 37 | 38 | 2. Use in Vue 39 | 40 | in SFC 41 | 42 | ```vue 43 | 50 | 51 | 54 | ``` 55 | 56 | install globally 57 | 58 | ```ts 59 | // main.ts 60 | import { Range } from 'vue-range-multi' 61 | import 'vue-range-multi/style.css' 62 | 63 | app.component('MRange', Range) 64 | ``` 65 | 66 | ```ts 67 | declare module 'vue' { 68 | export interface GlobalComponents { 69 | MRange: typeof import('vue-range-multi')['Range'] 70 | } 71 | } 72 | ``` 73 | 74 | unplugin-vue-components 75 | 76 | ```ts 77 | import { VueRangeMultiResolver } from 'vue-range-multi' 78 | 79 | // and then add `VueRangeMultiResolver()` into resolvers 80 | ``` 81 | 82 | ```ts 83 | // type of options 84 | interface VueRangeMultiResolverOptions { 85 | /** 86 | * The name of the component. It should always CapitalCase 87 | * 88 | * @default 'MRange' 89 | */ 90 | name?: string 91 | } 92 | ``` 93 | 94 | ## Props 95 | 96 | > [!NOTE] 97 | > After v0.4, `marks`'s key means value rather than percentage 98 | 99 | generic="T = any, U = number | RangeData\" 100 | 101 | | Name | Type | Description | Default | 102 | | -------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | 103 | | v-model:modelValue\* | U \ U[] | Model value. It will automatically detect the type of model and show the corresponding thumb(s) | [] | 104 | | min | number | The minimum value allowed | 0 | 105 | | max | number | The maximum value allowed | 100 | 106 | | step | number | Step | 1 | 107 | | vertical | boolean | Determines if the range is vertical. Note that it will generate new classes like 'm-range-v-xxx' | false | 108 | | addable | boolean | Determines if new data can be added/deleted. You can specify the data to be added by `addData` prop | false | 109 | | addData | (value: number) => RangeData | Data to be added. This will only effect while modelValue is RangeData[]. It will return { value } by default | undefined | 110 | | limit | number | the limit can be add | undefined | 111 | | thumbLimits | [number, number] | Global limit for all thumbs' movement range. Individual thumb can override this with its own limits property | undefined | 112 | | smooth | boolean | Determines if the thumb(s) should only be displayed on the stop points or not | false | 113 | | deduplicate | boolean | Determines if the thumb(s) can be duplicated | true | 114 | | rangeHighlight | boolean | Determines if the range between the minimum and maximum values should be highlighted. | false | 115 | | progress | RangeProgress | Custom track highlight segment | undefined | 116 | | showStops | boolean \| number | Determines if dots should be displayed on the track. When set to a number, dots will only be displayed if the number of stops is less than the specified value | 12 | 117 | | size | 'small' \| 'medium' \| 'large' | Track size | 'small' | 118 | | thumbType | 'circle' \| 'square' \| 'rect' | Thumb type(default 'rect' while size is 'large', otherwise 'small') | 'circle' \| 'rect' | 119 | | thumbSize | 'small' \| 'medium' \| 'large' | Thumb size | 'medium' | 120 | | renderTop | (data: U) => VNode | A render function for displaying content above the thumb | undefined | 121 | | renderTopOnActive | boolean | Specifies whether to render only while the thumb is active | false | 122 | | renderBottom | (data: U) => VNode | A render function for displaying content below the thumb | undefined | 123 | | renderBottomOnActive | boolean | Specifies whether to render only while the thumb is active | false | 124 | | marks | RangeMarks | Show marks under the track | undefined | 125 | 126 | ## Events 127 | 128 | | Name | Type | Description | 129 | | ------ | -------------------------------------------------------------------- | ---------------------------------------------------------- | 130 | | change | (value: RangeValue, thumbValue: U, thumbIndex: number) => void | It will emit when thumb `pointerup` (after move the thumb) | 131 | 132 | ## slots 133 | 134 | | Name | Type | Description | 135 | | ------ | ----------- | ------------------------------------------------------------------- | 136 | | top | { data: U } | render above the thumb, only effect while renderTop is undefined | 137 | | bottom | { data: U } | render below the thumb, only effect while renderBottom is undefined | 138 | 139 | ## types 140 | 141 | ```ts 142 | export type RangeValueType = number | RangeData 143 | export interface RangeData> { 144 | value: number 145 | data?: T 146 | disabled?: boolean 147 | unremovable?: boolean 148 | limits?: [number, number] // Min and max limits for this specific thumb 149 | renderTop?: RangeRenderFn 150 | renderBottom?: RangeRenderFn 151 | } 152 | export type RangeRenderFn> = (data: U) => VNode 153 | export type RangeValue> = U | U[] 154 | export type RangeProgress = ([number, number] | { 155 | range: [number, number] 156 | style?: CSSProperties 157 | class?: string 158 | })[] 159 | export type RangeMarks = Record 164 | ``` 165 | 166 | ## theme 167 | 168 | If you want to customize the theme, just use CSS variables to override the default theme. 169 | 170 | ```css 171 | .m-range-theme { 172 | --c-primary: #409eff; /* primary color */ 173 | --c-fill: #e4e7ed; /* track's fill color */ 174 | --c-fill-stop: #f5f5f5; /* stop's fill color */ 175 | --c-fill-thumb: #fff; /* thumb's fill color */ 176 | } 177 | ``` 178 | 179 | ## License 180 | 181 | [MIT](./LICENSE) License © 2023-PRESENT [wiidede](https://github.com/wiidede) 182 | 183 | 184 | 185 | [npm-version-src]: https://img.shields.io/npm/v/vue-range-multi?style=flat&colorA=080f12&colorB=1fa669 186 | [npm-version-href]: https://npmjs.com/package/vue-range-multi 187 | [npm-downloads-src]: https://img.shields.io/npm/dm/vue-range-multi?style=flat&colorA=080f12&colorB=1fa669 188 | [npm-downloads-href]: https://npmjs.com/package/vue-range-multi 189 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/vue-range-multi?style=flat&colorA=080f12&colorB=1fa669&label=minzip 190 | [bundle-href]: https://bundlephobia.com/result?p=vue-range-multi 191 | [license-src]: https://img.shields.io/github/license/wiidede/vue-range-multi.svg?style=flat&colorA=080f12&colorB=1fa669 192 | [license-href]: https://github.com/wiidede/vue-range-multi/blob/main/LICENSE 193 | [jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669 194 | [jsdocs-href]: https://www.jsdocs.io/package/vue-range-multi 195 | -------------------------------------------------------------------------------- /src/Range.vue: -------------------------------------------------------------------------------- 1 | 256 | 257 | 352 | 353 | 355 | --------------------------------------------------------------------------------