├── .eslintignore
├── src
├── common
│ ├── find.js
│ ├── Options.vue
│ ├── CascaderMixin.js
│ └── Mixin.js
├── index.js
├── components
│ ├── SelectBase.vue
│ ├── SelectMulti.vue
│ ├── Cascader.vue
│ └── CascaderMulti.vue
└── css
│ └── index.scss
├── .gitignore
├── test
└── index.spec.js
├── .npmignore
├── .prettierrc.js
├── .eslintrc.js
├── karma.conf.js
├── webpack.config.js
├── rollup.config.base.js
├── LICENSE
├── rollup.config.js
├── watch.js
├── index.d.ts
├── package.json
├── README.md
└── examples
└── test.html
/.eslintignore:
--------------------------------------------------------------------------------
1 | # /node_modules/* and /bower_components/* in the project root are ignored by default
2 |
3 | /coverage/
4 | /node_modules/
5 | /lib/
6 | /test/
7 |
8 | **/*.ts
9 |
10 | **/*.spec.js
11 | **/*.test.js
12 | **/*.e2e.js
13 | **/webpack.**.js
14 |
--------------------------------------------------------------------------------
/src/common/find.js:
--------------------------------------------------------------------------------
1 | export function find(arr, rule, defaultVal = {}) {
2 | let item = defaultVal
3 | arr.some(item1 => {
4 | if (rule(item1)) {
5 | item = item1
6 | return true
7 | }
8 | return false
9 | })
10 | return item
11 | }
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | npm-debug.log*
4 | yarn-debug.log*
5 | yarn-error.log*
6 | package-lock.json
7 | yarn.lock
8 |
9 | # Editor directories and files
10 | .idea
11 | .vscode
12 | *.suo
13 | *.ntvs*
14 | *.njsproj
15 | *.sln
16 |
17 | /coverage/
18 | /lib/
19 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Cascader from './components/Cascader.vue'
2 | import CascaderMulti from './components/CascaderMulti.vue'
3 | import SelectBase from './components/SelectBase.vue'
4 | import SelectMulti from './components/SelectMulti.vue'
5 |
6 | export { SelectBase, SelectMulti, Cascader, CascaderMulti }
7 |
--------------------------------------------------------------------------------
/test/index.spec.js:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils'
2 | import VueSelect from '../src/components/Index.vue'
3 |
4 | describe('Index.vue', () => {
5 | it('Rendered', () => {
6 | const wrapper = shallowMount(VueSelect)
7 | expect(wrapper.find('div').exists()).to.equal(true)
8 | })
9 | })
10 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | npm-debug.log*
3 | yarn-debug.log*
4 | yarn-error.log*
5 | package-lock.json
6 | yarn.lock
7 |
8 | # Editor directories and files
9 | .idea
10 | .vscode
11 | *.suo
12 | *.ntvs*
13 | *.njsproj
14 | *.sln
15 |
16 | /coverage/
17 | /examples/
18 | /node_modules/
19 | /src/
20 | /test/
21 |
22 | /karma.conf.js
23 | /.eslintrc.js
24 | /.eslintignore
25 | /.prettierrc.js
26 | /.babelrc.js
27 | /.tsconfig.json
28 | /webpack.*.js
29 | /rollup.*.js
30 | /watch.js
31 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 80,
3 | tabWidth: 2,
4 | semi: false,
5 | singleQuote: true,
6 | quoteProps: 'as-needed',
7 | jsxSingleQuote: false,
8 | trailingComma: 'all',
9 | bracketSpacing: true,
10 | jsxBracketSameLine: false,
11 | arrowParens: 'avoid',
12 | rangeStart: 0,
13 | rangeEnd: Infinity,
14 | requirePragma: false,
15 | insertPragma: false,
16 | proseWrap: 'preserve',
17 | htmlWhitespaceSensitivity: 'css',
18 | endOfLine: 'auto',
19 | }
20 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 'parserOptions': {
3 | 'parser': 'babel-eslint',
4 | },
5 | 'plugins': [
6 | 'vue',
7 | 'prettier'
8 | ],
9 | 'extends': [
10 | 'airbnb-base',
11 | 'plugin:vue/essential',
12 | 'plugin:prettier/recommended',
13 | ],
14 | 'rules': {
15 | 'import/prefer-default-export': 'off',
16 | 'no-param-reassign': 'off',
17 | 'prettier/prettier': 'error',
18 | },
19 | 'settings': {
20 | 'import/resolver': {
21 | 'node': {
22 | 'extensions': ['.js', 'jsx', '.vue'],
23 | },
24 | },
25 | },
26 | }
27 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | const webpackConfig = require('./webpack.config.js')
2 |
3 | module.exports = config => {
4 | config.set({
5 | // 浏览器环境
6 | browsers: ['Chrome'],
7 | // 测试框架
8 | frameworks: ['mocha', 'chai'],
9 | // 需要测试的文件,在 browsers 里面运行,使用 frameworks 测试js,通过 reporters 输出报告
10 | files: ['test/**/*.spec.js'],
11 | // 为入口文件制定预处理器,测试 js 之前用 webpack 和 sourcemap 处理一下
12 | preprocessors: {
13 | '**/*.spec.js': ['webpack', 'sourcemap'],
14 | },
15 | webpack: webpackConfig,
16 | // 输出报告
17 | reporters: ['spec', 'coverage'],
18 | // 覆盖报告的配置
19 | coverageReporter: {
20 | dir: './coverage',
21 | reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }],
22 | },
23 | })
24 | }
25 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* For test */
2 | const VueLoaderPlugin = require('vue-loader/lib/plugin')
3 |
4 | const config = {
5 | mode: 'production',
6 | entry: { index: './src/index.js' },
7 | module: {
8 | rules: [
9 | { test: /\.vue$/, exclude: /node_modules/, loader: 'vue-loader' },
10 | {
11 | test: /\.js$/,
12 | exclude: /node_modules/,
13 | loader: 'babel-loader',
14 | options: {
15 | presets: [
16 | [
17 | '@babel/preset-env',
18 | {
19 | modules: false,
20 | targets: {
21 | browsers: ['> 1%', 'last 2 versions', 'not ie <= 8'],
22 | },
23 | },
24 | ],
25 | ],
26 | plugins: ['@babel/plugin-transform-runtime', 'istanbul'],
27 | },
28 | },
29 | ],
30 | },
31 | plugins: [new VueLoaderPlugin()],
32 | }
33 |
34 | module.exports = config
35 |
--------------------------------------------------------------------------------
/rollup.config.base.js:
--------------------------------------------------------------------------------
1 | const commonjs = require('rollup-plugin-commonjs')
2 | const resolve = require('rollup-plugin-node-resolve')
3 | const vuePlugin = require('rollup-plugin-vue')
4 | const babel = require('rollup-plugin-babel')
5 | const { DEFAULT_EXTENSIONS } = require('@babel/core')
6 |
7 | const vue = vuePlugin.default || vuePlugin
8 |
9 | module.exports = {
10 | plugins: [
11 | resolve({
12 | extensions: [...DEFAULT_EXTENSIONS, '.vue'],
13 | }),
14 | commonjs(),
15 | vue({ css: true }),
16 | babel({
17 | babelrc: false,
18 | externalHelpers: false,
19 | runtimeHelpers: true,
20 | extensions: [...DEFAULT_EXTENSIONS, '.vue'],
21 | presets: [
22 | [
23 | '@babel/preset-env',
24 | {
25 | modules: false,
26 | targets: {
27 | browsers: ['> 1%', 'last 2 versions', 'not ie <= 8'],
28 | },
29 | },
30 | ],
31 | ],
32 | }),
33 | ],
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 2631541504@qq.com
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 |
--------------------------------------------------------------------------------
/src/common/Options.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
18 |
19 |
20 |
21 |
22 |
70 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | /* For build */
2 | import fs from 'fs'
3 | import path from 'path'
4 | import license from 'rollup-plugin-license'
5 | import { uglify } from 'rollup-plugin-uglify'
6 | import packageConf from './package.json'
7 |
8 | const baseConf = require('./rollup.config.base')
9 |
10 | const formats = ['es', 'umd']
11 |
12 | const isWatch = process.env.BUILD_ENV === 'watch'
13 |
14 | function getEntries() {
15 | const reg = /\.vue$/
16 | return fs
17 | .readdirSync(path.resolve(__dirname, './src/components'))
18 | .filter(
19 | filename =>
20 | reg.test(filename) &&
21 | !fs
22 | .statSync(path.resolve(__dirname, './src/components', filename))
23 | .isDirectory(),
24 | )
25 | .map(filename => ({
26 | name: filename.replace(reg, ''),
27 | filename: path.resolve(__dirname, './src/components', filename),
28 | formats: formats.filter(f => f !== 'es'),
29 | }))
30 | }
31 |
32 | const conf = entry => ({
33 | ...baseConf,
34 | input: entry.filename,
35 | output: entry.formats.map(format => ({
36 | file: `./lib/${format}/${entry.name}.js`,
37 | format,
38 | name: entry.name === 'index' ? 'VueSelect' : `${entry.name}VueSelect`,
39 | })),
40 | external: entry.external ? Object.keys(packageConf.dependencies || {}) : [],
41 | plugins: [
42 | ...baseConf.plugins,
43 | entry.needUglify !== false && uglify(),
44 | license({
45 | banner: `Bundle of <%= pkg.name %>
46 | Generated: <%= moment().format('YYYY-MM-DD') %>
47 | Version: <%= pkg.version %>
48 | License: <%= pkg.license %>
49 | Author: <%= pkg.author %>`,
50 | }),
51 | ],
52 | })
53 |
54 | export default (isWatch
55 | ? [{ name: 'index', filename: './src/index.js', formats: ['umd'] }]
56 | : [
57 | {
58 | name: 'index',
59 | filename: './src/index.js',
60 | formats: ['es'],
61 | needUglify: false,
62 | external: true,
63 | },
64 | { name: 'index', filename: './src/index.js', formats: ['umd'] },
65 | ...getEntries(),
66 | ]
67 | ).map(conf)
68 |
--------------------------------------------------------------------------------
/src/components/SelectBase.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
20 |
21 |
29 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
73 |
--------------------------------------------------------------------------------
/watch.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies, no-console */
2 | const chokidar = require('chokidar')
3 | const spawn = require('cross-spawn')
4 | const chalk = require('chalk')
5 | const { singletonObj } = require('@livelybone/singleton')
6 | const express = require('express')
7 | const path = require('path')
8 |
9 | const port = process.env.PORT || 3000
10 |
11 | const watcher = chokidar.watch('src')
12 |
13 | const debounceTimer = {
14 | js: null,
15 | css: null,
16 | serve: null,
17 | }
18 |
19 | function serve() {
20 | return singletonObj(
21 | 'serve',
22 | () =>
23 | new Promise(res => {
24 | const app = express()
25 | app.use('/examples', express.static(path.resolve('./examples')))
26 | app.use('/lib', express.static(path.resolve('./lib')))
27 | app.listen(port, e => {
28 | if (e) {
29 | console.log(chalk.red(e))
30 | process.exit(1)
31 | }
32 | res()
33 | })
34 | }),
35 | ).then(() => {
36 | debounceTimer.serve = setTimeout(() => {
37 | console.log(
38 | `\r\nThe example of your component is listening on ${port}...\r\n`,
39 | )
40 | console.log(
41 | chalk.cyan(
42 | ` Open http://127.0.0.1:${port}/examples/test.html in your browser`,
43 | ),
44 | '\r\n',
45 | )
46 | }, 200)
47 | })
48 | }
49 |
50 | function spawnConsole(resource, ls) {
51 | ls.stdout.on('data', data => {
52 | if (debounceTimer.serve) clearTimeout(debounceTimer.serve)
53 | console.log(`${data}`.replace(/[\r\n]/g, ''))
54 | })
55 | ls.stderr.on('data', data => {
56 | if (debounceTimer.serve) clearTimeout(debounceTimer.serve)
57 | console.log(`${data}`.replace(/[\r\n]/g, ''))
58 | })
59 | ls.on('close', code => {
60 | if (+code === 0) {
61 | console.log('\r')
62 | console.log(chalk.cyan(`>>> ${resource} building successful`), '\r\n')
63 | serve()
64 | }
65 | })
66 | }
67 |
68 | function build(cmd = 'build:js') {
69 | const resource = cmd.replace('build:', '')
70 | if (debounceTimer[resource]) clearTimeout(debounceTimer[resource])
71 | debounceTimer[resource] = setTimeout(() => {
72 | console.log(chalk.cyan(`>>> Building for ${resource}...`), '\r\n')
73 | spawnConsole(resource, spawn('npm', ['run', cmd]))
74 | }, 1000)
75 | }
76 |
77 | watcher.on('all', (event, filename) => {
78 | const isCss = /.s?css$/
79 | if (isCss.test(filename)) build('build:css')
80 | else build()
81 | })
82 |
--------------------------------------------------------------------------------
/src/common/CascaderMixin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @prop {Array} options
3 | * example: [{name: 'a', value:'1', children: [{name:'a1', value:'11', children:[...]}]}]
4 | * @prop {String} ['hover','click'] expandType, the action to trigger expand sub options
5 | * default: 'click'
6 | * */
7 | export default {
8 | beforeMount() {
9 | this.initTemp()
10 | },
11 | props: {
12 | value: {
13 | default() {
14 | return []
15 | },
16 | type: Array,
17 | },
18 | expandType: String,
19 | },
20 | data() {
21 | return {
22 | tempVal: [],
23 | optionsHeight: 0,
24 | optionsRight: 0,
25 | positionFixed: true,
26 | }
27 | },
28 | computed: {
29 | $lineStyle() {
30 | return { height: `${this.optionsHeight}px` }
31 | },
32 | showOptions() {
33 | return this.options.map(op => this.setSelect(op, 0))
34 | },
35 | selectedOptions() {
36 | const selected = this.getSelected(this.showOptions)
37 | return selected.map(s => s.children).filter(ops => ops)
38 | },
39 | },
40 | watch: {
41 | optionsHidden(val) {
42 | if (!val) {
43 | this.$nextTick(this.listenOptionsStyle)
44 | }
45 | },
46 | selectedOptions(val) {
47 | if (val) {
48 | this.listenOptionsStyle()
49 | }
50 | },
51 | },
52 | methods: {
53 | listenOptionsStyle() {
54 | if (this.$refs.optionsEl) {
55 | const { clientHeight } = this.$refs.optionsEl.$el
56 | if (clientHeight && this.optionsHeight !== clientHeight) {
57 | this.optionsHeight = clientHeight
58 | }
59 | }
60 | },
61 | getSelected(source, values = null) {
62 | const arr = values || this.tempVal
63 | const item0 = this.find(source, op => this.isSelected(op, arr[0]), '')
64 | return item0
65 | ? arr.slice(1).reduce(
66 | (pre, val) => {
67 | const options = pre[pre.length - 1].children
68 | if (options instanceof Array) {
69 | const item = this.find(
70 | options,
71 | op => this.isSelected(op, val),
72 | '',
73 | )
74 | if (item) pre.push(item)
75 | }
76 | return pre
77 | },
78 | [item0],
79 | )
80 | : []
81 | },
82 | isSelected(op, val) {
83 | return op.value === val
84 | },
85 | isEnd(op) {
86 | return !(op.children instanceof Array && op.children.length > 0)
87 | },
88 | },
89 | }
90 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | import { VuePopperProps } from '@livelybone/vue-popper'
2 | import Vue from 'vue'
3 | import { VueScrollbarProps } from 'vue-scrollbar-live'
4 |
5 | declare class Common extends Vue {
6 | id?: string | number
7 | /**
8 | * Default to true
9 | * */
10 | canEdit?: boolean
11 | /**
12 | * Default to false
13 | * */
14 | canSearch?: boolean
15 | placeholder?: string
16 | searchPlaceholder?: string
17 | /**
18 | * Props of popper.js
19 | *
20 | * Defaults to:
21 | * {
22 | * arrowPosition: 'start',
23 | * arrowOffsetScaling: 1,
24 | * popperOptions: {
25 | * placement: 'bottom-start',
26 | * modifiers: {
27 | * preventOverflow: {
28 | * boundariesElement:
29 | * typeof document !== 'undefined' ? document.body : '',
30 | * },
31 | * },
32 | * },
33 | * }
34 | * */
35 | popperProps?: VuePopperProps
36 | /**
37 | * Props of vue-scrollbar-live
38 | *
39 | * Default to:
40 | * {
41 | * isMobile: false,
42 | * maxHeight: '50vh',
43 | * }
44 | * */
45 | scrollbarProps?: VueScrollbarProps
46 | }
47 |
48 | export interface SelectOptions {
49 | name: string
50 | value: string | number
51 | }
52 |
53 | declare class SelectBase extends Common {
54 | value: string | number
55 | options: SelectOptions
56 | inputWrapStyle?: CSSStyleDeclaration | string
57 | }
58 |
59 | declare class SelectMulti extends Common {
60 | value: Array
61 | options: SelectOptions
62 | }
63 |
64 | export interface CascaderOptions extends SelectOptions {
65 | children?: CascaderOptions
66 | }
67 |
68 | export type ExpandType = 'click' | 'hover'
69 |
70 | declare class Cascader extends Common {
71 | value: Array
72 | options: CascaderOptions
73 | /**
74 | * Set how to expand children options
75 | *
76 | * Default to 'click'
77 | * */
78 | expandType?: ExpandType
79 | /**
80 | * If set to true, options of all level can be selected
81 | *
82 | * Default to false
83 | * */
84 | changeOnSelect?: boolean
85 | inputWrapStyle?: CSSStyleDeclaration | string
86 | }
87 |
88 | declare class CasecaderMulti extends Common {
89 | value: Array>
90 | options: CascaderOptions
91 | /**
92 | * Set how to expand children options
93 | *
94 | * Default to 'click'
95 | * */
96 | expandType?: ExpandType
97 | }
98 |
99 | export { SelectBase, SelectMulti, Cascader, CasecaderMulti }
100 |
--------------------------------------------------------------------------------
/src/components/SelectMulti.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
17 | {{
18 | $placeholder
19 | }}
20 |
21 |
22 |
23 |
24 |
32 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
104 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@livelybone/vue-select",
3 | "version": "2.7.0",
4 | "description": "A vue select component, includes cascader",
5 | "main": "./lib/umd/index.js",
6 | "module": "./lib/es/index.js",
7 | "unpkg": "./lib/umd/index.js",
8 | "css_path": "./lib/css/index.css",
9 | "scss_path": "./lib/css/index.scss",
10 | "types": "./index.d.ts",
11 | "scripts": {
12 | "build:css": "cross-env NODE_ENV=production node-sass ./src/css/index.scss ./lib/css/index.css --output-style compressed && ncp ./src/css/index.scss ./lib/css/index.scss",
13 | "build:js": "cross-env NODE_ENV=production rollup -c",
14 | "dev": "rimraf ./lib && cross-env BUILD_ENV=watch node watch.js",
15 | "build": "rimraf ./lib && npm run build:css && npm run build:js",
16 | "eslint": "eslint ./ --ext .vue,.js --fix",
17 | "test": "cross-env BABEL_ENV=test karma start --single-run",
18 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
19 | "commit": "git-cz",
20 | "release": "npm publish --registry=https://registry.npmjs.org",
21 | "release:alpha": "npm publish --tag alpha --registry=https://registry.npmjs.org"
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "https://github.com/livelybone/vue-select.git"
26 | },
27 | "keywords": [
28 | "vue",
29 | "select",
30 | "cascader"
31 | ],
32 | "author": "2631541504@qq.com",
33 | "license": "MIT",
34 | "bugs": {
35 | "url": "https://github.com/livelybone/vue-select/issues"
36 | },
37 | "homepage": "https://github.com/livelybone/vue-select#readme",
38 | "devDependencies": {
39 | "@babel/core": "^7.5.0",
40 | "@babel/plugin-transform-runtime": "^7.5.0",
41 | "@babel/preset-env": "^7.5.0",
42 | "@babel/runtime": "^7.5.1",
43 | "@livelybone/singleton": "^1.1.1",
44 | "@vue/test-utils": "^1.0.0-beta.19",
45 | "babel-eslint": "^10.0.2",
46 | "babel-loader": "^8.0.6",
47 | "babel-plugin-istanbul": "^5.1.4",
48 | "chai": "^4.2.0",
49 | "chalk": "^2.4.2",
50 | "chokidar": "^3.0.2",
51 | "commitizen": "^3.0.7",
52 | "conventional-changelog-cli": "^2.0.12",
53 | "cross-env": "^5.2.0",
54 | "cross-spawn": "^6.0.5",
55 | "cz-conventional-changelog": "^2.1.0",
56 | "eslint": "^5.3.0",
57 | "eslint-config-airbnb-base": "^13.0.0",
58 | "eslint-config-prettier": "^6.0.0",
59 | "eslint-plugin-import": "^2.12.0",
60 | "eslint-plugin-prettier": "^3.1.0",
61 | "eslint-plugin-vue": "^5.1.0",
62 | "express": "^4.17.1",
63 | "husky": "^3.0.0",
64 | "karma": "^4.1.0",
65 | "karma-chai": "^0.1.0",
66 | "karma-chrome-launcher": "^2.2.0",
67 | "karma-coverage": "^1.1.2",
68 | "karma-mocha": "^1.3.0",
69 | "karma-sourcemap-loader": "^0.3.7",
70 | "karma-spec-reporter": "0.0.32",
71 | "karma-webpack": "^4.0.2",
72 | "lint-staged": "^9.2.0",
73 | "mocha": "^6.1.4",
74 | "ncp": "^2.0.0",
75 | "prettier": "^1.18.2",
76 | "rollup": "^1.16.7",
77 | "rollup-plugin-babel": "^4.3.3",
78 | "rollup-plugin-commonjs": "^10.0.1",
79 | "rollup-plugin-license": "^0.9.0",
80 | "rollup-plugin-node-resolve": "^5.2.0",
81 | "rollup-plugin-uglify": "^6.0.2",
82 | "rollup-plugin-vue": "^5.0.1",
83 | "vue": "^2.5.16",
84 | "vue-loader": "^15.2.4",
85 | "vue-template-compiler": "^2.5.16",
86 | "webpack": "^4.12.0",
87 | "webpack-command": "^0.2.1"
88 | },
89 | "dependencies": {
90 | "@livelybone/copy": "^2.5.4",
91 | "@livelybone/vue-popper": "^2.3.1",
92 | "vue-scrollbar-live": "^5.2.2"
93 | },
94 | "config": {
95 | "commitizen": {
96 | "path": "./node_modules/cz-conventional-changelog"
97 | }
98 | },
99 | "husky": {
100 | "hooks": {
101 | "pre-commit": "lint-staged"
102 | }
103 | },
104 | "lint-staged": {
105 | "**/*.{js,vue}": [
106 | "eslint --fix",
107 | "git update-index --again"
108 | ],
109 | "**/*.scss": [
110 | "prettier --write",
111 | "git update-index --again"
112 | ]
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/components/Cascader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
20 |
21 |
29 |
39 |
40 |
41 |
42 |
43 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
129 |
--------------------------------------------------------------------------------
/src/common/Mixin.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-useless-path-segments */
2 | import { objectDeepMerge } from '@livelybone/copy'
3 | import VuePopper from '@livelybone/vue-popper'
4 | import Options from '../common/Options.vue'
5 | import { find } from './find'
6 |
7 | export default {
8 | components: { Options, popper: VuePopper },
9 | props: {
10 | id: String,
11 | options: {
12 | default() {
13 | return []
14 | },
15 | type: Array,
16 | },
17 | canEdit: {
18 | default: true,
19 | type: Boolean,
20 | },
21 | canSearch: Boolean,
22 | placeholder: String,
23 | searchPlaceholder: String,
24 | popperProps: Object,
25 | scrollbarProps: Object,
26 | },
27 | data() {
28 | return {
29 | mergedOptions: [],
30 | optionsHidden: true,
31 | shouldHide: true,
32 | inputVal: '',
33 | defaultPopperProps: Object.freeze({
34 | arrowPosition: 'start',
35 | arrowOffsetScaling: 1,
36 | popperOptions: {
37 | placement: 'bottom-start',
38 | modifiers: {
39 | preventOverflow: {
40 | boundariesElement:
41 | typeof document !== 'undefined' ? document.body : '',
42 | },
43 | },
44 | },
45 | }),
46 | }
47 | },
48 | computed: {
49 | $placeholder() {
50 | return this.placeholder || '请选择'
51 | },
52 | $searchPlaceholder() {
53 | return this.searchPlaceholder || '搜索'
54 | },
55 | valid() {
56 | let valid = true
57 | valid =
58 | !this.options ||
59 | !this.options.every(item => item.name && item.value !== undefined)
60 | if (!valid) {
61 | throw new Error(
62 | 'vue-select: Prop options is invalid! Right example: [{name: "option", value: 1}]',
63 | )
64 | }
65 | return valid
66 | },
67 | $popperProps() {
68 | return objectDeepMerge({}, this.defaultPopperProps, this.popperProps)
69 | },
70 | $_select_isMobile() {
71 | const { isMobile } = this.scrollbarProps || {}
72 | return isMobile
73 | },
74 | maxHeight() {
75 | const { maxHeight } = this.scrollbarProps || {}
76 | return maxHeight || '50vh'
77 | },
78 | marginToWrap() {
79 | const { marginToWrap } = this.scrollbarProps || {}
80 | return marginToWrap || 2
81 | },
82 | },
83 | watch: {
84 | inputVal(val) {
85 | this.$emit('search', val)
86 | },
87 | options(val) {
88 | this.mergedOptions = this.mergeOptions(this.mergedOptions, val)
89 | },
90 | },
91 | methods: {
92 | toggle(ev) {
93 | if (this.canEdit) {
94 | const isContains = ev && this.$refs.wrap.contains(ev.target)
95 | const containedInOptions = this.$refs.optionsEl.$el.contains(ev.target)
96 |
97 | // If `ev.target` is the child of DOM `div.options`, do nothing
98 | if (!containedInOptions) {
99 | if (this.optionsHidden && isContains) {
100 | this.optionsHidden = false
101 | if (this.canSearch) this.$nextTick(() => this.$refs.input.focus())
102 | if ('initTemp' in this) this.initTemp()
103 | } else {
104 | if (this.shouldHide) {
105 | this.optionsHidden = true
106 | }
107 | this.shouldHide = true
108 | }
109 | }
110 | }
111 | },
112 | endDrag() {
113 | setTimeout(() => {
114 | this.shouldHide = true
115 | }, 100)
116 | },
117 | bind(bool) {
118 | if (typeof window !== 'undefined') {
119 | window[`${bool ? 'add' : 'remove'}EventListener`](
120 | 'click',
121 | this.toggle,
122 | true,
123 | )
124 | }
125 | },
126 | find,
127 | mergeOptions(a1, a2) {
128 | const obj = [...a1, ...a2].reduce((pre, item) => {
129 | const item1 = pre[item.value]
130 | if (item1 && item1.name !== item.name) {
131 | throw new Error(
132 | `vue-select: the options at same level have conflict items(name: ${item.name} & name: ${item1.name}) that have the same value`,
133 | )
134 | } else if (
135 | item1 &&
136 | (item1.children instanceof Array || item.children instanceof Array)
137 | ) {
138 | pre[item.value] = {
139 | ...item,
140 | children: this.mergeOptions(item1.children, item.children),
141 | }
142 | } else pre[item.value] = item
143 | return pre
144 | }, {})
145 | return Object.keys(obj).map(k => obj[k])
146 | },
147 | },
148 | beforeMount() {
149 | this.mergedOptions = [...this.options]
150 | this.bind(true)
151 | },
152 | beforeDestroy() {
153 | this.bind(false)
154 | },
155 | }
156 |
--------------------------------------------------------------------------------
/src/components/CascaderMulti.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
20 | {{
21 | $placeholder
22 | }}
23 |
24 |
25 |
26 |
27 |
35 |
45 |
46 |
47 |
48 |
49 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
164 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @livelybone/vue-select
2 | [](https://www.npmjs.com/package/@livelybone/vue-select)
3 | [](https://www.npmjs.com/package/@livelybone/vue-select)
4 | 
5 | 
6 | 
7 | 
8 |
9 | > `pkg.module supported`, which means that you can apply tree-shaking in you project
10 |
11 | A vue select component, includes cascader
12 |
13 | ## repository
14 | https://github.com/livelybone/vue-select
15 |
16 | ## Demo
17 | https://livelybone.github.io/vue/vue-select/
18 |
19 | ## Run Example
20 | Your can see the usage by run the example of the module, here is the step:
21 |
22 | 1. Clone the library `git clone https://github.com/livelybone/vue-select.git`
23 | 2. Go to the directory `cd vue-select`
24 | 3. Install npm dependencies `npm i`(use taobao registry: `npm i --registry=http://registry.npm.taobao.org`)
25 | 4. Open service `npm run dev`
26 | 5. See the example(usually is `http://127.0.0.1/examples/test.html`) in your browser
27 |
28 | ## Installation
29 | ```bash
30 | npm i -S @livelybone/vue-select
31 | ```
32 |
33 | ## Register
34 | ```js
35 | // import all
36 | import {SelectBase, SelectMulti, Cascader, CascaderMulti} from '@livelybone/vue-select';
37 | // or
38 | import * as VueSelect from '@livelybone/vue-select';
39 |
40 | // Global register
41 | Vue.component('select-base', SelectBase);
42 | Vue.component('select-multi', SelectMulti);
43 | Vue.component('cascader', Cascader);
44 | Vue.component('cascader-multi', CascaderMulti);
45 |
46 | // Local register
47 | new Vue({
48 | components:{SelectBase, SelectMulti, Cascader, CascaderMulti}
49 | })
50 | ```
51 |
52 | Use in html, see what your can use in [CDN: unpkg](https://unpkg.com/@livelybone/vue-select/lib/umd/)
53 | ```html
54 | <-- use what you want -->
55 |
56 | ```
57 |
58 | ## Props
59 |
60 | ### Common
61 | | Name | Type | DefaultValue | Description |
62 | | ------------------------- | ----------------------------------------- | --------------------------------------------- | ------------ |
63 | | `id` | `[String, Number]` | none | |
64 | | `options` | `Array` | `[]` | Select options |
65 | | `canEdit` | `Boolean` | `true` | If it's set to false, the component can only be used for show |
66 | | `canSearch` | `Boolean` | `false` | Set to true to enable search |
67 | | `placeholder` | `String` | none | Placeholder |
68 | | `searchPlaceholder` | `String` | `true` | Placeholder of search input |
69 | | `popperProps` | `Object` | `defaultPopperProps` | Props of module [@livelybone/vue-popper](https://github.com/livelybone/vue-popper) |
70 | | `scrollbarProps` | `Object` | `{isMobile:false, maxHeight: '50vh'}` | Props of module [@livelybone/vue-scrollbar-live](https://github.com/livelybone/vue-scrollbar-live) |
71 |
72 | ```js
73 | const defaultPopperProps = {
74 | arrowPosition: 'start',
75 | arrowOffsetScaling: 1,
76 | popperOptions: {
77 | placement: 'bottom-start',
78 | // If component is Cascader or CascaderMulti -> `positionFixed: true`
79 | // More options in https://popper.js.org
80 | },
81 | }
82 | ```
83 |
84 | ### SelectBase
85 | | Name | Type | DefaultValue | Description |
86 | | ----------------- | --------------------- | --------------------- | ------------ |
87 | | `value` | `[String, Number]` | none | |
88 | | `inputWrapStyle` | `[String, Object]` | none | |
89 |
90 | ### SelectMulti
91 | | Name | Type | DefaultValue | Description |
92 | | ----------------- | --------------------- | --------------------- | ------------ |
93 | | `value` | `Array` | none | |
94 |
95 | ### Cascader
96 | | Name | Type | DefaultValue | Description |
97 | | ----------------- | --------------------- | --------------------- | ------------ |
98 | | `value` | `Array` | none | |
99 | | `expandType` | `String` | `click` | Options: `['click', 'hover']`. Set how to expand children options |
100 | | `changeOnSelect` | `Boolean` | `false` | If set to true, options of all level can be selected |
101 | | `inputWrapStyle` | `[String, Object]` | none | input wrap style |
102 |
103 | ### CascaderMulti
104 | | Name | Type | DefaultValue | Description |
105 | | ----------------- | --------------------- | --------------------- | ------------ |
106 | | `value` | `Array` | none | |
107 | | `expandType` | `String` | `click` | Options: `['click', 'hover']`. Set how to expand children options |
108 |
109 | ## Events
110 | | Name | EmittedData | Description |
111 | | ----------------- | --------------------- | ------------------------------------------------- |
112 | | `input` | `[Array, String]` | |
113 | | `search` | `String` | |
114 |
115 | ## style
116 | For building style, you can use the css or scss file in lib directory.
117 | ```js
118 | // scss
119 | import 'node_modules/@livelybone/vue-select/lib/css/index.scss'
120 |
121 | // css
122 | import 'node_modules/@livelybone/vue-select/lib/css/index.css'
123 | ```
124 | Or
125 | ```scss
126 | // scss
127 | @import 'node_modules/@livelybone/vue-select/lib/css/index.scss';
128 |
129 | // css
130 | @import 'node_modules/@livelybone/vue-select/lib/css/index.css';
131 | ```
132 |
133 | Or, you can build your custom style by copying and editing `index.scss`
134 |
135 | ## QA
136 |
137 | 1. Error `Error: spawn node-sass ENOENT`
138 |
139 | > You may need install node-sass globally, `npm i -g node-sass`
140 |
--------------------------------------------------------------------------------
/src/css/index.scss:
--------------------------------------------------------------------------------
1 | $main: #30b386;
2 | $border: #c2ccdc;
3 | $font: #666;
4 | $placeholder: #aaa;
5 | $select: #fff;
6 | $input-bg: #fff;
7 | $input-bg-disabled: #f9f9f9;
8 | $base-size: 10px;
9 | $option-hover-bg: lighten($main, 50%);
10 | $shadow: rgba(0, 0, 0, 0.1);
11 | $multi-value-color: #eee;
12 |
13 | @function size($factor) {
14 | @return $factor * $base-size;
15 | }
16 |
17 | $option-height: size(3);
18 | $multi-value-height: size(2.4);
19 | $scrollbar-wrap-min-width: size(16);
20 | $option-padding: size(0.4);
21 |
22 | @mixin placeholder($color: $placeholder) {
23 | &::-webkit-input-placeholder {
24 | /* WebKit browsers */
25 | color: $color;
26 | }
27 |
28 | &:-moz-placeholder {
29 | /* Mozilla Firefox 4 to 18 */
30 | color: $color;
31 | }
32 |
33 | &::-moz-placeholder {
34 | /* Mozilla Firefox 19+ */
35 | color: $color;
36 | }
37 |
38 | &:-ms-input-placeholder {
39 | /* Internet Explorer 10+ */
40 | color: $color;
41 | }
42 | }
43 |
44 | @mixin middleLine($color) {
45 | content: '';
46 | position: absolute;
47 | left: 0;
48 | right: 0;
49 | top: 50%;
50 | height: 1px;
51 | background: $color;
52 | }
53 |
54 | @mixin dot($color) {
55 | content: '';
56 | position: absolute;
57 | left: 50%;
58 | top: 50%;
59 | width: size(0.6);
60 | height: size(0.6);
61 | margin: size(-0.3) 0 0 size(-0.3);
62 | border-radius: size(0.3);
63 | background: $color;
64 | }
65 |
66 | /* scrollbar css */
67 | .scrollbar-wrap .scrollbar {
68 | width: size(0.4) !important;
69 | border-radius: size(0.6) !important;
70 | background: #eee !important;
71 | }
72 |
73 | /* popper css */
74 | $bg: $input-bg;
75 | $pseudoSize: size(0.6);
76 |
77 | @mixin pseudo($bg: $border, $direction: top, $borderWidth: $pseudoSize) {
78 | position: absolute;
79 | #{$direction}: -$pseudoSize;
80 | width: 0;
81 | height: 0;
82 | border: $borderWidth solid transparent;
83 |
84 | @if ($direction==top) {
85 | border-top: 0;
86 | border-bottom-color: $bg;
87 | } @else if ($direction==bottom) {
88 | border-bottom: 0;
89 | border-top-color: $bg;
90 | } @else if ($direction==left) {
91 | border-left: 0;
92 | border-right-color: $bg;
93 | } @else if ($direction==right) {
94 | border-right: 0;
95 | border-left-color: $bg;
96 | }
97 | }
98 |
99 | .vue-popper {
100 | border-color: rgba($border, 0.5) !important;
101 |
102 | &[x-placement^='bottom'] .arrow {
103 | border-bottom-color: rgba($border, 0.5) !important;
104 | }
105 | &[x-placement^='top'] .arrow {
106 | border-top-color: rgba($border, 0.5) !important;
107 | }
108 | }
109 |
110 | .split {
111 | color: $placeholder !important;
112 | }
113 |
114 | .select-base,
115 | .select-multi,
116 | .cascader,
117 | .cascader-multi {
118 | position: relative;
119 | padding: 0 size(1);
120 | font-size: size(1.4);
121 | color: $font;
122 | border: 1px solid $border;
123 | border-radius: size(0.3);
124 | background: transparent;
125 | cursor: pointer;
126 |
127 | * {
128 | box-sizing: border-box;
129 | outline: none !important;
130 | }
131 |
132 | .input {
133 | cursor: pointer;
134 | }
135 |
136 | &.disabled {
137 | background: $input-bg-disabled;
138 | cursor: default;
139 | }
140 |
141 | &.select-multi,
142 | &.cascader,
143 | &.cascader-multi {
144 | .options .option {
145 | &.selected {
146 | color: $main !important;
147 | background: transparent !important;
148 | font-weight: 600;
149 |
150 | .icon-selected {
151 | float: right;
152 | position: relative;
153 | width: $option-height/2;
154 | height: 100%;
155 |
156 | &:before {
157 | @include dot($main);
158 | }
159 | }
160 |
161 | .icon-expand:before {
162 | border-left-color: $main;
163 | }
164 | }
165 |
166 | &:hover {
167 | background: $option-hover-bg !important;
168 |
169 | .icon-expand:after {
170 | border-left-color: $option-hover-bg;
171 | }
172 | }
173 |
174 | .icon-expand {
175 | display: block;
176 | float: right;
177 | position: relative;
178 | top: calc((#{$option-height} - #{size(1)}) / 2);
179 | width: size(1);
180 | height: size(1);
181 |
182 | &:before {
183 | content: '';
184 | @include pseudo($font, right, size(0.5));
185 | right: 0;
186 | }
187 |
188 | &:after {
189 | content: '';
190 | @include pseudo($input-bg, right, size(0.5));
191 | right: 1px;
192 | }
193 | }
194 | }
195 | }
196 |
197 | &.cascader,
198 | &.cascader-multi {
199 | .options {
200 | display: -webkit-box;
201 | display: -webkit-flex;
202 | display: -ms-flexbox;
203 | display: flex;
204 | right: auto;
205 | width: auto;
206 |
207 | .scrollbar-wrap {
208 | width: auto;
209 | min-width: $scrollbar-wrap-min-width;
210 | }
211 |
212 | .option {
213 | &.selected {
214 | font-weight: 500;
215 | }
216 | }
217 |
218 | .line {
219 | float: left;
220 | width: 1px;
221 | margin: -$option-padding 0;
222 | background: rgba($border, 0.5);
223 | }
224 | }
225 | }
226 |
227 | .value,
228 | .input {
229 | width: 100%;
230 | height: $option-height;
231 | line-height: $option-height;
232 | margin: 0;
233 | padding: 0;
234 | border: none;
235 | }
236 |
237 | .placeholder {
238 | color: $placeholder;
239 | }
240 |
241 | .input {
242 | display: block;
243 | outline: none !important;
244 |
245 | @include placeholder($placeholder);
246 | }
247 |
248 | .values {
249 | min-height: $option-height;
250 | height: auto;
251 | overflow: hidden;
252 | }
253 |
254 | .val {
255 | display: inline-block;
256 | width: auto;
257 | height: $multi-value-height;
258 | line-height: $multi-value-height;
259 | padding: 0 size(0.6);
260 | margin: ($option-height - $multi-value-height)/2;
261 | margin-left: 0;
262 | border-radius: size(0.2);
263 | background: $multi-value-color;
264 | white-space: nowrap;
265 | vertical-align: top;
266 |
267 | &.input,
268 | &.placeholder {
269 | background: transparent;
270 | }
271 |
272 | .v {
273 | float: left;
274 | font-size: size(1.2);
275 | }
276 |
277 | .icon-del {
278 | float: right;
279 | margin: 0 0 0 size(1);
280 | position: relative;
281 | width: $multi-value-height / 2;
282 | height: 100%;
283 |
284 | &:hover {
285 | &:before,
286 | &:after {
287 | background: darken($font, 20%);
288 | }
289 | }
290 |
291 | &:before,
292 | &:after {
293 | @include middleLine($font);
294 | transform: rotate(45deg);
295 | }
296 |
297 | &:after {
298 | transform: rotate(-45deg);
299 | }
300 | }
301 | }
302 |
303 | .icon-arrow {
304 | display: block;
305 | position: absolute;
306 | right: size(0.4);
307 | top: 50%;
308 | width: size(1);
309 | height: size(0.5);
310 | margin: size(-0.25) 0 0;
311 | transition: transform 0.3s ease;
312 |
313 | &:before {
314 | content: '';
315 | @include pseudo($font, bottom, size(0.5));
316 | bottom: 0;
317 | }
318 |
319 | &:after {
320 | content: '';
321 | @include pseudo($input-bg, bottom, size(0.5));
322 | bottom: 1px;
323 | }
324 |
325 | &.reverse {
326 | transform: rotate(180deg);
327 | }
328 | }
329 |
330 | .options {
331 | width: 100%;
332 | padding: $option-padding 0;
333 | z-index: 9999;
334 | box-shadow: 0 1px size(1) $shadow;
335 |
336 | .option {
337 | height: $option-height;
338 | line-height: $option-height;
339 | padding: 0 size(1);
340 |
341 | &:hover {
342 | background: $option-hover-bg !important;
343 | }
344 |
345 | &.selected {
346 | color: $select !important;
347 | background: $main !important;
348 | }
349 | }
350 | }
351 | }
352 |
--------------------------------------------------------------------------------
/examples/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | vue-select
7 |
8 |
9 |
10 |
17 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
308 |
309 |
310 |
--------------------------------------------------------------------------------