├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── actions │ └── gh-pages │ │ ├── Dockerfile │ │ ├── action.yml │ │ └── entrypoint.sh └── workflows │ └── docs.deploy.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── __tests__ ├── Arrow.spec.js ├── Option.spec.js ├── SelectMenu.spec.js ├── VueCascaderSelect.spec.js └── validators.spec.js ├── babel.config.js ├── build └── rollup.config.js ├── docs ├── .vuepress │ ├── components │ │ ├── TheFooter.vue │ │ ├── VCSBasic.vue │ │ └── VCSTheming.vue │ ├── config.js │ ├── enhanceApp.js │ └── public │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ └── logo.svg ├── README.md ├── es │ ├── README.md │ └── guide │ │ ├── README.md │ │ ├── basic_usage.md │ │ ├── installation.md │ │ └── theming.md └── guide │ ├── README.md │ ├── basic_usage.md │ ├── installation.md │ └── theming.md ├── jest.config.js ├── package.json ├── public └── logo.png ├── src ├── VueCascaderSelect.vue ├── components │ ├── Arrow.vue │ ├── Option.vue │ └── SelectMenu.vue ├── entry.js ├── serve-dev.js ├── serve-dev.vue └── utils │ ├── teams.js │ └── validators.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | current node 2 | last 2 versions and > 2% 3 | ie > 10 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 100 8 | 9 | [*.{js,jsx,ts,tsx,vue}] 10 | indent_style = space 11 | indent_size = 2 12 | end_of_line = lf 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | max_line_length = 100 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | 4 | env: { 5 | node: true, 6 | }, 7 | 8 | extends: [ 9 | 'plugin:vue/essential', 10 | '@vue/airbnb', 11 | ], 12 | 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 16 | }, 17 | 18 | parserOptions: { 19 | parser: 'babel-eslint', 20 | }, 21 | 22 | overrides: [ 23 | { 24 | files: [ 25 | '**/__tests__/*.{j,t}s?(x)', 26 | '**/tests/unit/**/*.spec.{j,t}s?(x)', 27 | ], 28 | env: { 29 | jest: true, 30 | }, 31 | }, 32 | ], 33 | }; 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Expected Behavior 2 | 3 | 4 | ## Actual Behavior 5 | 6 | 7 | ## Steps to Reproduce the Problem 8 | 9 | 1. 10 | 1. 11 | 1. 12 | 13 | ## Specifications 14 | 15 | - Version: 16 | - Platform: 17 | - Subsystem: -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | ## Proposed Changes 4 | 5 | - 6 | - 7 | - -------------------------------------------------------------------------------- /.github/actions/gh-pages/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:11 2 | 3 | ADD entrypoint.sh /entrypoint.sh 4 | ENTRYPOINT ["/entrypoint.sh"] 5 | -------------------------------------------------------------------------------- /.github/actions/gh-pages/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Deploy vuepress to specified branch' 2 | description: 'Buildi and deploy vuepress to GitHub Pages.' 3 | runs: 4 | using: 'docker' 5 | image: 'Dockerfile' 6 | branding: 7 | icon: 'git-commit' 8 | color: 'green' 9 | -------------------------------------------------------------------------------- /.github/actions/gh-pages/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | if [ -z "$ACCESS_TOKEN" ] && [ -z "$GITHUB_TOKEN" ] 6 | then 7 | echo "You must provide the action with either a Personal Access Token or the GitHub Token secret in order to deploy." 8 | exit 1 9 | fi 10 | 11 | if [ -z "$BRANCH" ] 12 | then 13 | echo "You must provide the action with a branch name it should deploy to, for example gh-pages or docs." 14 | exit 1 15 | fi 16 | 17 | if [ -z "$FOLDER" ] 18 | then 19 | echo "You must provide the action with the folder name in the repository where your compiled page lives." 20 | exit 1 21 | fi 22 | 23 | case "$FOLDER" in /*|./*) 24 | echo "The deployment folder cannot be prefixed with '/' or './'. Instead reference the folder name directly." 25 | exit 1 26 | esac 27 | 28 | # Installs Git and jq. 29 | apt-get update && \ 30 | apt-get install -y git && \ 31 | apt-get install -y jq && \ 32 | 33 | # Gets the commit email/name if it exists in the push event payload. 34 | COMMIT_EMAIL=`jq '.pusher.email' ${GITHUB_EVENT_PATH}` 35 | COMMIT_NAME=`jq '.pusher.name' ${GITHUB_EVENT_PATH}` 36 | 37 | # If the commit email/name is not found in the event payload then it falls back to the actor. 38 | if [ -z "$COMMIT_EMAIL" ] 39 | then 40 | COMMIT_EMAIL="${GITHUB_ACTOR:-github-pages-deploy-action}@users.noreply.github.com" 41 | fi 42 | 43 | if [ -z "$COMMIT_NAME" ] 44 | then 45 | COMMIT_NAME="${GITHUB_ACTOR:-GitHub Pages Deploy Action}" 46 | fi 47 | 48 | # Directs the action to the the Github workspace. 49 | cd $GITHUB_WORKSPACE && \ 50 | 51 | # Configures Git. 52 | git init && \ 53 | git config --global user.email "${COMMIT_EMAIL}" && \ 54 | git config --global user.name "${COMMIT_NAME}" && \ 55 | 56 | ## Initializes the repository path using the access token. 57 | REPOSITORY_PATH="https://${ACCESS_TOKEN:-"x-access-token:$GITHUB_TOKEN"}@github.com/${GITHUB_REPOSITORY}.git" && \ 58 | 59 | # Checks to see if the remote exists prior to deploying. 60 | # If the branch doesn't exist it gets created here as an orphan. 61 | if [ "$(git ls-remote --heads "$REPOSITORY_PATH" "$BRANCH" | wc -l)" -eq 0 ]; 62 | then 63 | echo "Creating remote branch ${BRANCH} as it doesn't exist..." 64 | git checkout "${BASE_BRANCH:-master}" && \ 65 | git checkout --orphan $BRANCH && \ 66 | git rm -rf . && \ 67 | touch README.md && \ 68 | git add README.md && \ 69 | git commit -m "Initial ${BRANCH} commit" && \ 70 | git push $REPOSITORY_PATH $BRANCH 71 | fi 72 | 73 | echo "Going to ${BASE_BRANCH}" 74 | git fetch --all && \ 75 | # Checks out the base branch to begin the deploy process. 76 | git checkout "${BASE_BRANCH:-master}" && \ 77 | 78 | # Builds the project if a build script is provided. 79 | echo "Running build scripts... $BUILD_SCRIPT" && \ 80 | eval "$BUILD_SCRIPT" && \ 81 | 82 | # Commits the data to Github. 83 | echo "Deploying to GitHub..." && \ 84 | 85 | git add -A . && \ 86 | git commit -m "Deploying to ${BRANCH} from ${BASE_BRANCH:-master} ${GITHUB_SHA}" --quiet --no-verify&& \ 87 | git push $REPOSITORY_PATH `git subtree split --prefix $FOLDER ${BASE_BRANCH:-master}`:$BRANCH --force && \ 88 | 89 | echo "Deployment succesful!" 90 | -------------------------------------------------------------------------------- /.github/workflows/docs.deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy Docs 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | build-and-deploy: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@master 14 | - name: Build and Deploy 15 | uses: ./.github/actions/gh-pages 16 | env: 17 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 18 | BASE_BRANCH: master 19 | BRANCH: gh-pages 20 | FOLDER: docs/.vuepress/dist 21 | BUILD_SCRIPT: npm install && npm run docs:build 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | !docs/.vuepress/dist 5 | coverage/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | 5 | cache: 6 | directories: 7 | - node_modules 8 | 9 | script: 10 | - npm run test:unit && npm run codecov -- --token=$CODECOV_TOKEN 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 NeoCoast 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |

25 | 26 | # Vue Cascader Select 27 | 28 | ## Installation 29 | 30 | ```bash 31 | npm install --save vue-cascader-select@latest 32 | or 33 | yarn add vue-cascader-select@latest 34 | ``` 35 | 36 | ## Usage 37 | 38 | ```js 39 | import Vue from 'vue'; 40 | import VueCascaderSelect from 'vue-cascader-select'; 41 | 42 | Vue.use(VueCascaderSelect); 43 | ``` 44 | 45 | ``` 46 | 54 | ``` 55 | 56 | For more information see the [complete docs](https://NeoCoast.github.io/vue-cascader-select/) 57 | 58 |

59 | Icons made by Flat Icons from www.flaticon.com 60 |

61 | -------------------------------------------------------------------------------- /__tests__/Arrow.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount, mount } from '@vue/test-utils'; 2 | import Arrow from '@/components/Arrow.vue'; 3 | 4 | const mountWithProps = props => ( 5 | mount(Arrow, { propsData: props }) 6 | ); 7 | 8 | describe('Arrow.vue', () => { 9 | it('renders', () => { 10 | const wrapper = shallowMount(Arrow); 11 | expect(wrapper.exists()).toBe(true); 12 | }); 13 | 14 | describe('props:', () => { 15 | it('direction: up', () => { 16 | const wrapper = mountWithProps({ direction: 'up' }); 17 | expect(wrapper.classes('up')).toBe(true); 18 | }); 19 | 20 | it('direction: down', () => { 21 | const wrapper = mountWithProps({ direction: 'down' }); 22 | expect(wrapper.classes('down')).toBe(true); 23 | }); 24 | 25 | it('direction: right', () => { 26 | const wrapper = mountWithProps({ direction: 'right' }); 27 | expect(wrapper.classes('right')).toBe(true); 28 | }); 29 | 30 | it('borderColor', () => { 31 | const borderColor = '#ccc'; 32 | const wrapper = mountWithProps({ borderColor }); 33 | expect(wrapper.attributes('style')).toBe(`border-color: ${borderColor};`); 34 | }); 35 | }); 36 | 37 | it('should change direction', (done) => { 38 | const wrapper = mountWithProps({ direction: 'up' }); 39 | expect(wrapper.classes('up')).toBe(true); 40 | 41 | wrapper.setProps({ direction: 'down' }); 42 | 43 | wrapper.vm.$nextTick(() => { 44 | expect(wrapper.classes('down')).toBe(true); 45 | done(); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /__tests__/Option.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount, mount } from '@vue/test-utils'; 2 | import Arrow from '@/components/Arrow.vue'; 3 | import Option from '@/components/Option.vue'; 4 | 5 | const mountWithProps = props => ( 6 | mount(Option, { propsData: props }) 7 | ); 8 | 9 | describe('Option.vue', () => { 10 | it('renders', () => { 11 | const wrapper = shallowMount(Option); 12 | expect(wrapper.exists()).toBe(true); 13 | }); 14 | 15 | it('should render the label', () => { 16 | const label = 'NeoCoast'; 17 | const wrapper = mountWithProps({ label }); 18 | expect(wrapper.text()).toBe(label); 19 | }); 20 | 21 | it('shouldn\'t be active', () => { 22 | let wrapper = mountWithProps({}); 23 | expect(wrapper.classes('vcs__option--active')).toBe(false); 24 | 25 | wrapper = mountWithProps({ active: false }); 26 | expect(wrapper.classes('vcs__option--active')).toBe(false); 27 | }); 28 | 29 | it('should be active', () => { 30 | const wrapper = mountWithProps({ active: true }); 31 | expect(wrapper.classes('vcs__option--active')).toBe(true); 32 | }); 33 | 34 | it('shouldn\'t be disabled', () => { 35 | let wrapper = mountWithProps({}); 36 | expect(wrapper.classes('vcs__option--disabled')).toBe(false); 37 | 38 | wrapper = mountWithProps({ disabled: false }); 39 | expect(wrapper.classes('vcs__option--disabled')).toBe(false); 40 | }); 41 | 42 | it('should be disabled', () => { 43 | const wrapper = mountWithProps({ disabled: true }); 44 | expect(wrapper.classes('vcs__option--disabled')).toBe(true); 45 | }); 46 | 47 | it('should render an Arrow', () => { 48 | const wrapper = mountWithProps({ 49 | options: [{ label: 'NeoCoast', value: 'neocoast' }], 50 | }); 51 | 52 | expect(wrapper.contains(Arrow)).toBe(true); 53 | }); 54 | 55 | it('shouldn\'t render an Arrow', () => { 56 | const wrapper = mountWithProps({}); 57 | 58 | expect(wrapper.contains(Arrow)).toBe(false); 59 | }); 60 | 61 | it('should emit on mouseenter', () => { 62 | const wrapper = mountWithProps({}); 63 | 64 | wrapper.trigger('mouseenter'); 65 | wrapper.vm.$nextTick(() => { 66 | expect(wrapper.emitted().openMenu).toBeTruthy(); 67 | }); 68 | }); 69 | 70 | it('should call onSelect on click if the option is selectable', () => { 71 | const onSelect = jest.fn(); 72 | const wrapper = mountWithProps({ onSelect }); 73 | 74 | wrapper.trigger('click'); 75 | wrapper.vm.$nextTick(() => { 76 | expect(onSelect).toBeCalled(); 77 | expect(onSelect).toBeCalledTimes(1); 78 | }); 79 | }); 80 | 81 | it('shouldn\'t call onSelect on click if the option isn\'t selectable', () => { 82 | const onSelect = jest.fn(); 83 | const wrapper = mountWithProps({ onSelect, selectable: false }); 84 | 85 | wrapper.trigger('click'); 86 | wrapper.vm.$nextTick(() => { 87 | expect(onSelect).not.toBeCalled(); 88 | expect(onSelect).toBeCalledTimes(0); 89 | }); 90 | }); 91 | 92 | it('shouldn\'t call onSelect on click if the option is disabled', () => { 93 | const onSelect = jest.fn(); 94 | const wrapper = mountWithProps({ onSelect, disabled: true }); 95 | 96 | wrapper.trigger('click'); 97 | wrapper.vm.$nextTick(() => { 98 | expect(onSelect).not.toBeCalled(); 99 | expect(onSelect).toBeCalledTimes(0); 100 | }); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /__tests__/SelectMenu.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import Option from '@/components/Option.vue'; 3 | import SelectMenu from '@/components/SelectMenu.vue'; 4 | 5 | const options = [ 6 | { 7 | label: 'Sibling 1', 8 | value: 'sibling1', 9 | options: [ 10 | { 11 | label: 'Sibling 1 - Child 1', 12 | value: 'sibling1-child1', 13 | }, 14 | ], 15 | }, 16 | { 17 | label: 'Sibling 2', 18 | value: 'sibling2', 19 | }, 20 | { 21 | label: 'Sibling 3', 22 | value: 'sibling3', 23 | options: [ 24 | { 25 | label: 'Sibling 3 - Child 1', 26 | value: 'sibling3-child1', 27 | options: [ 28 | { 29 | label: 'Sibling 3 - Grand Child 1', 30 | value: 'sibling3-grand-child1', 31 | }, 32 | ], 33 | }, 34 | ], 35 | }, 36 | ]; 37 | 38 | const mountWithProps = props => ( 39 | mount(SelectMenu, { 40 | propsData: { 41 | onSelect: jest.fn(), 42 | ...props, 43 | }, 44 | attachToDocument: true, 45 | }) 46 | ); 47 | 48 | describe('SelectMenu.vue', () => { 49 | it('renders', () => { 50 | const wrapper = mountWithProps({}); 51 | expect(wrapper.exists()).toBe(true); 52 | }); 53 | 54 | it('should be a "main" select menu', () => { 55 | const wrapper = mountWithProps({}); 56 | expect(wrapper.classes('vcs__select-menu__not-main')).toBe(false); 57 | }); 58 | 59 | it('shouldn\'t be a "main" select menu', () => { 60 | const wrapper = mountWithProps({ notMain: true }); 61 | expect(wrapper.classes('vcs__select-menu__not-main')).toBe(true); 62 | }); 63 | 64 | it('should open next menu on mouseenter if there are more options', (done) => { 65 | const wrapper = mountWithProps({ options }); 66 | 67 | const option = wrapper.find(Option); 68 | expect(option.is(Option)).toBe(true); 69 | 70 | option.trigger('mouseenter'); 71 | wrapper.vm.$nextTick(() => { 72 | expect(wrapper.findAll(SelectMenu).length).toBe(2); 73 | done(); 74 | }); 75 | }); 76 | 77 | it('shouldn\'t open next menu on mouseenter if there\'re no more options', (done) => { 78 | const wrapper = mountWithProps({ options }); 79 | 80 | wrapper.vm.$nextTick(() => { 81 | const optionsWrapper = wrapper.findAll(Option); 82 | expect(optionsWrapper.length).toBe(3); 83 | 84 | optionsWrapper.at(1).trigger('mouseenter'); 85 | wrapper.vm.$nextTick(() => { 86 | expect(wrapper.findAll(SelectMenu).length).toBe(1); 87 | done(); 88 | }); 89 | }); 90 | }); 91 | 92 | it('should close childMenu', (done) => { 93 | const wrapper = mountWithProps({ options }); 94 | 95 | wrapper.vm.$nextTick(() => { 96 | let optionsWrapper = wrapper.findAll(Option); 97 | expect(optionsWrapper.length).toBe(3); 98 | 99 | optionsWrapper.at(2).trigger('mouseenter'); 100 | wrapper.vm.$nextTick(() => { 101 | expect(wrapper.findAll(SelectMenu).length).toBe(2); 102 | 103 | optionsWrapper = wrapper.findAll(Option); 104 | optionsWrapper.at(-1).trigger('mouseenter'); 105 | 106 | wrapper.vm.$nextTick(() => { 107 | // Open grand child menu 108 | expect(wrapper.findAll(SelectMenu).length).toBe(3); 109 | 110 | optionsWrapper = wrapper.findAll(Option); 111 | 112 | // Change opened child menu 113 | optionsWrapper.at(0).trigger('mouseenter'); 114 | wrapper.vm.$nextTick(() => { 115 | // Grand child menu shouldn't be rendered 116 | expect(wrapper.findAll(SelectMenu).length).toBe(2); 117 | done(); 118 | }); 119 | }); 120 | }); 121 | }); 122 | }); 123 | 124 | 125 | describe('should be able to navigate with keyboard', () => { 126 | let wrapper; 127 | beforeEach(() => { 128 | wrapper = mountWithProps({ options, withKeyboard: true }); 129 | }); 130 | 131 | it('should open and focus first option', () => { 132 | const optionsWrapper = wrapper.findAll(Option); 133 | expect(optionsWrapper.at(0).element).toBe(document.activeElement); 134 | }); 135 | 136 | it('should go to next option', (done) => { 137 | const optionsWrapper = wrapper.findAll(Option); 138 | 139 | optionsWrapper.at(0).trigger('keydown.down'); 140 | wrapper.vm.$nextTick(() => { 141 | expect(optionsWrapper.at(1).element).toBe(document.activeElement); 142 | done(); 143 | }); 144 | }); 145 | 146 | it('should go to previous option', (done) => { 147 | const optionsWrapper = wrapper.findAll(Option); 148 | 149 | optionsWrapper.at(0).trigger('keydown.down'); 150 | wrapper.vm.$nextTick(() => { 151 | optionsWrapper.at(1).trigger('keydown.up'); 152 | wrapper.vm.$nextTick(() => { 153 | expect(optionsWrapper.at(0).element).toBe(document.activeElement); 154 | done(); 155 | }); 156 | }); 157 | }); 158 | 159 | it('should open next menu', (done) => { 160 | const option = wrapper.find(Option); 161 | 162 | option.trigger('keydown.right'); 163 | wrapper.vm.$nextTick(() => { 164 | expect(wrapper.findAll(SelectMenu).length).toBe(2); 165 | done(); 166 | }); 167 | }); 168 | 169 | it('should close next menu', (done) => { 170 | const option = wrapper.find(Option); 171 | 172 | option.trigger('keydown.right'); 173 | wrapper.vm.$nextTick(() => { 174 | const nextMenu = wrapper.findAll(SelectMenu).at(1); 175 | 176 | nextMenu.find(Option).trigger('keydown.left'); 177 | wrapper.vm.$nextTick(() => { 178 | expect(wrapper.findAll(SelectMenu).length).toBe(1); 179 | wrapper.find(SelectMenu).find(Option).trigger('keydown.left'); 180 | done(); 181 | }); 182 | }); 183 | }); 184 | 185 | it('should go back to first option', (done) => { 186 | const optionsWrapper = wrapper.findAll(Option); 187 | 188 | optionsWrapper.at(0).trigger('keydown.down'); 189 | wrapper.vm.$nextTick(() => { 190 | expect(optionsWrapper.at(1).element).toBe(document.activeElement); 191 | optionsWrapper.at(1).trigger('keydown.down'); 192 | 193 | wrapper.vm.$nextTick(() => { 194 | expect(optionsWrapper.at(2).element).toBe(document.activeElement); 195 | optionsWrapper.at(2).trigger('keydown.down'); 196 | 197 | wrapper.vm.$nextTick(() => { 198 | expect(optionsWrapper.at(0).element).toBe(document.activeElement); 199 | done(); 200 | }); 201 | }); 202 | }); 203 | }); 204 | 205 | it('should go back the last option', (done) => { 206 | const optionsWrapper = wrapper.findAll(Option); 207 | 208 | optionsWrapper.at(0).trigger('keydown.up'); 209 | wrapper.vm.$nextTick(() => { 210 | expect(optionsWrapper.at(2).element).toBe(document.activeElement); 211 | done(); 212 | }); 213 | }); 214 | 215 | it('should close childMenu', (done) => { 216 | wrapper.vm.$nextTick(() => { 217 | let optionsWrapper = wrapper.findAll(Option); 218 | expect(optionsWrapper.length).toBe(3); 219 | 220 | optionsWrapper.at(2).trigger('keydown.right'); 221 | wrapper.vm.$nextTick(() => { 222 | expect(wrapper.findAll(SelectMenu).length).toBe(2); 223 | 224 | optionsWrapper = wrapper.findAll(Option); 225 | optionsWrapper.at(-1).trigger('keydown.right'); 226 | 227 | wrapper.vm.$nextTick(() => { 228 | // Open grand child menu 229 | expect(wrapper.findAll(SelectMenu).length).toBe(3); 230 | 231 | optionsWrapper = wrapper.findAll(Option); 232 | 233 | // Change opened child menu 234 | optionsWrapper.at(0).trigger('keydown.right'); 235 | wrapper.vm.$nextTick(() => { 236 | // Grand child menu shouldn't be rendered 237 | expect(wrapper.findAll(SelectMenu).length).toBe(2); 238 | done(); 239 | }); 240 | }); 241 | }); 242 | }); 243 | }); 244 | }); 245 | }); 246 | -------------------------------------------------------------------------------- /__tests__/VueCascaderSelect.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import Arrow from '@/components/Arrow.vue'; 3 | import Option from '@/components/Option.vue'; 4 | import SelectMenu from '@/components/SelectMenu.vue'; 5 | import VueCascaderSelect from '@/VueCascaderSelect.vue'; 6 | 7 | const options = [ 8 | { 9 | label: 'Sibling 1', 10 | value: 'sibling1', 11 | options: [ 12 | { 13 | label: 'Sibling 1 - Child 1', 14 | value: 'sibling1-child1', 15 | }, 16 | ], 17 | }, 18 | { 19 | label: 'Sibling 2', 20 | value: 'sibling2', 21 | }, 22 | { 23 | label: 'Sibling 3', 24 | value: 'sibling3', 25 | options: [ 26 | { 27 | label: 'Sibling 3 - Child 1', 28 | value: 'sibling3-child1', 29 | options: [ 30 | { 31 | label: 'Sibling 3 - Grand Child 1', 32 | value: 'sibling3-grand-child1', 33 | }, 34 | ], 35 | }, 36 | ], 37 | }, 38 | ]; 39 | 40 | const defaultProps = { 41 | options, 42 | value: '', 43 | }; 44 | 45 | const mountWithProps = props => ( 46 | mount(VueCascaderSelect, { 47 | propsData: { 48 | ...defaultProps, 49 | ...props, 50 | }, 51 | }) 52 | ); 53 | 54 | describe('VueCascaderSelect.vue', () => { 55 | let wrapper; 56 | beforeEach(() => { 57 | wrapper = mountWithProps({}); 58 | }); 59 | 60 | it('renders', () => { 61 | expect(wrapper.exists()).toBe(true); 62 | }); 63 | 64 | it('should have one Arrow pointing down', (done) => { 65 | const arrows = wrapper.findAll(Arrow); 66 | 67 | expect(arrows.length).toBe(1); 68 | 69 | const arrow = arrows.at(0); 70 | expect(arrow.classes('down')).toBe(true); 71 | done(); 72 | }); 73 | 74 | it('should have an input', () => { 75 | expect(wrapper.contains('input')).toBe(true); 76 | }); 77 | 78 | it('the input should contain the default placeholder', () => { 79 | const input = wrapper.find('input'); 80 | expect(input.attributes('placeholder')) 81 | .toBe(VueCascaderSelect.props.placeholder.default); 82 | }); 83 | 84 | it('the input should contain the placeholder passed as a prop', (done) => { 85 | const placeholder = 'Custom placeholder'; 86 | wrapper.setProps({ placeholder }); 87 | 88 | wrapper.vm.$nextTick(() => { 89 | const input = wrapper.find('input'); 90 | expect(input.attributes('placeholder')).toBe(placeholder); 91 | done(); 92 | }); 93 | }); 94 | 95 | it('triggering the menu should make to Arrow point up', (done) => { 96 | wrapper.find('.vcs__picker').trigger('click'); 97 | 98 | wrapper.vm.$nextTick(() => { 99 | const arrow = wrapper.find(Arrow); 100 | expect(arrow.classes('up')).toBe(true); 101 | done(); 102 | }); 103 | }); 104 | 105 | it('triggering the menu should open the SelectMenu', (done) => { 106 | expect(wrapper.contains(SelectMenu)).toBe(false); 107 | 108 | wrapper.find('.vcs__picker').trigger('click'); 109 | 110 | wrapper.vm.$nextTick(() => { 111 | expect(wrapper.contains(SelectMenu)).toBe(true); 112 | done(); 113 | }); 114 | }); 115 | 116 | it('pressing escape should close the SelectMenu', (done) => { 117 | wrapper.find('.vcs__picker').trigger('click'); 118 | 119 | wrapper.vm.$nextTick(() => { 120 | expect(wrapper.contains(SelectMenu)).toBe(true); 121 | 122 | wrapper.trigger('keydown.esc'); 123 | wrapper.vm.$nextTick(() => { 124 | expect(wrapper.contains(SelectMenu)).toBe(false); 125 | done(); 126 | }); 127 | }); 128 | }); 129 | 130 | it('changing the value should close the SelectMenu', (done) => { 131 | wrapper.find('.vcs__picker').trigger('click'); 132 | 133 | wrapper.vm.$nextTick(() => { 134 | wrapper.setProps({ value: 'test' }); 135 | 136 | wrapper.vm.$nextTick(() => { 137 | expect(wrapper.contains(SelectMenu)).toBe(false); 138 | done(); 139 | }); 140 | }); 141 | }); 142 | 143 | it('shouldn\'t have a cross', () => { 144 | expect(wrapper.classes('vcs__cross')).toBe(false); 145 | }); 146 | 147 | it('shoud have a cross if there\'s a value', (done) => { 148 | wrapper.setProps({ value: 'test' }); 149 | 150 | wrapper.vm.$nextTick(() => { 151 | const cross = wrapper.find('.vcs__cross'); 152 | expect(cross.contains('button')).toBe(true); 153 | expect(cross.find('button').text()).toBe('×'); 154 | done(); 155 | }); 156 | }); 157 | 158 | it('the cross should emit a "clear" event', (done) => { 159 | wrapper.setProps({ value: 'test' }); 160 | 161 | wrapper.vm.$nextTick(() => { 162 | const cross = wrapper.find('.vcs__cross'); 163 | cross.find('button').trigger('click'); 164 | 165 | wrapper.vm.$nextTick(() => { 166 | expect(wrapper.emitted('clear')).toBeTruthy(); 167 | expect(wrapper.emitted('clear').length).toBe(1); 168 | done(); 169 | }); 170 | }); 171 | }); 172 | 173 | it('selecting should emit a "select" event', (done) => { 174 | wrapper.find('.vcs__picker').trigger('click'); 175 | 176 | wrapper.vm.$nextTick(() => { 177 | const opts = wrapper.findAll(Option); 178 | opts.at(0).trigger('click'); 179 | 180 | wrapper.vm.$nextTick(() => { 181 | expect(wrapper.emitted('select')).toBeTruthy(); 182 | expect(wrapper.emitted('select').length).toBe(1); 183 | done(); 184 | }); 185 | }); 186 | }); 187 | }); 188 | -------------------------------------------------------------------------------- /__tests__/validators.spec.js: -------------------------------------------------------------------------------- 1 | import * as validators from '@/utils/validators'; 2 | 3 | describe('validateOptions', () => { 4 | const { validateOptions } = validators; 5 | 6 | it('should return true for valid options', () => { 7 | const validOptions = [ 8 | { label: 'Test1', value: 'test1' }, 9 | { 10 | label: 'Test2', 11 | value: 'test2', 12 | options: [ 13 | { label: 'Test3', value: 'test3' }, 14 | ], 15 | }, 16 | ]; 17 | 18 | expect(validateOptions(validOptions)).toBe(true); 19 | }); 20 | 21 | it('should return false for invalid options', (done) => { 22 | let invalidOptions = [{ label: 'Test1' }]; 23 | expect(validateOptions(invalidOptions)).toBe(false); 24 | 25 | invalidOptions = [{ value: 'test1' }]; 26 | expect(validateOptions(invalidOptions)).toBe(false); 27 | 28 | invalidOptions = [{ label: 1 }]; 29 | expect(validateOptions(invalidOptions)).toBe(false); 30 | 31 | invalidOptions = [{ label: 'Test1', options: 'options' }]; 32 | expect(validateOptions(invalidOptions)).toBe(false); 33 | 34 | invalidOptions = [ 35 | { label: 'Test1', value: 'test1' }, 36 | { 37 | label: 'Test2', 38 | options: [ 39 | { label: 'Test3' }, 40 | ], 41 | }, 42 | ]; 43 | expect(validateOptions(invalidOptions)).toBe(false); 44 | done(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /build/rollup.config.js: -------------------------------------------------------------------------------- 1 | // rollup.config.js 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import vue from 'rollup-plugin-vue'; 5 | import alias from '@rollup/plugin-alias'; 6 | import commonjs from '@rollup/plugin-commonjs'; 7 | import replace from '@rollup/plugin-replace'; 8 | import babel from 'rollup-plugin-babel'; 9 | import { terser } from 'rollup-plugin-terser'; 10 | import minimist from 'minimist'; 11 | 12 | // Get browserslist config and remove ie from es build targets 13 | const esbrowserslist = fs.readFileSync('./.browserslistrc') 14 | .toString() 15 | .split('\n') 16 | .filter((entry) => entry && entry.substring(0, 2) !== 'ie'); 17 | 18 | const argv = minimist(process.argv.slice(2)); 19 | 20 | const projectRoot = path.resolve(__dirname, '..'); 21 | 22 | const baseConfig = { 23 | input: 'src/entry.js', 24 | plugins: { 25 | preVue: [ 26 | replace({ 27 | 'process.env.NODE_ENV': JSON.stringify('production'), 28 | }), 29 | alias({ 30 | resolve: ['.js', '.jsx', '.ts', '.tsx', '.vue'], 31 | entries: { 32 | '@': path.resolve(projectRoot, 'src'), 33 | }, 34 | }), 35 | ], 36 | vue: { 37 | css: true, 38 | template: { 39 | isProduction: true, 40 | }, 41 | }, 42 | babel: { 43 | exclude: 'node_modules/**', 44 | extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue'], 45 | }, 46 | }, 47 | }; 48 | 49 | // ESM/UMD/IIFE shared settings: externals 50 | // Refer to https://rollupjs.org/guide/en/#warning-treating-module-as-external-dependency 51 | const external = [ 52 | // list external dependencies, exactly the way it is written in the import statement. 53 | // eg. 'jquery' 54 | 'vue', 55 | ]; 56 | 57 | // UMD/IIFE shared settings: output.globals 58 | // Refer to https://rollupjs.org/guide/en#output-globals for details 59 | const globals = { 60 | // Provide global variable names to replace your external imports 61 | // eg. jquery: '$' 62 | vue: 'Vue', 63 | }; 64 | 65 | // Customize configs for individual targets 66 | const buildFormats = []; 67 | if (!argv.format || argv.format === 'es') { 68 | const esConfig = { 69 | ...baseConfig, 70 | external, 71 | output: { 72 | file: 'dist/vue-cascader-select.esm.js', 73 | format: 'esm', 74 | exports: 'named', 75 | }, 76 | plugins: [ 77 | ...baseConfig.plugins.preVue, 78 | vue(baseConfig.plugins.vue), 79 | babel({ 80 | ...baseConfig.plugins.babel, 81 | presets: [ 82 | [ 83 | '@babel/preset-env', 84 | { 85 | targets: esbrowserslist, 86 | }, 87 | ], 88 | ], 89 | }), 90 | commonjs(), 91 | ], 92 | }; 93 | buildFormats.push(esConfig); 94 | } 95 | 96 | if (!argv.format || argv.format === 'cjs') { 97 | const umdConfig = { 98 | ...baseConfig, 99 | external, 100 | output: { 101 | compact: true, 102 | file: 'dist/vue-cascader-select.ssr.js', 103 | format: 'cjs', 104 | name: 'VueCascaderSelect', 105 | exports: 'named', 106 | globals, 107 | }, 108 | plugins: [ 109 | ...baseConfig.plugins.preVue, 110 | vue({ 111 | ...baseConfig.plugins.vue, 112 | template: { 113 | ...baseConfig.plugins.vue.template, 114 | optimizeSSR: true, 115 | }, 116 | }), 117 | babel(baseConfig.plugins.babel), 118 | commonjs(), 119 | ], 120 | }; 121 | buildFormats.push(umdConfig); 122 | } 123 | 124 | if (!argv.format || argv.format === 'iife') { 125 | const unpkgConfig = { 126 | ...baseConfig, 127 | external, 128 | output: { 129 | compact: true, 130 | file: 'dist/vue-cascader-select.min.js', 131 | format: 'iife', 132 | name: 'VueCascaderSelect', 133 | exports: 'named', 134 | globals, 135 | }, 136 | plugins: [ 137 | ...baseConfig.plugins.preVue, 138 | vue(baseConfig.plugins.vue), 139 | babel(baseConfig.plugins.babel), 140 | commonjs(), 141 | terser({ 142 | output: { 143 | ecma: 5, 144 | }, 145 | }), 146 | ], 147 | }; 148 | buildFormats.push(unpkgConfig); 149 | } 150 | 151 | // Export config 152 | export default buildFormats; 153 | -------------------------------------------------------------------------------- /docs/.vuepress/components/TheFooter.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /docs/.vuepress/components/VCSBasic.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 48 | 49 | 54 | -------------------------------------------------------------------------------- /docs/.vuepress/components/VCSTheming.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 48 | 49 | 74 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | base: '/vue-cascader-select/', 3 | head: [ 4 | ['link', { rel: "apple-touch-icon", sizes: "180x180", href: "apple-touch-icon.png" }], 5 | ['link', { rel: "icon", type: "image/png", sizes: "32x32", href: "favicon-32x32.png" }], 6 | ['link', { rel: "icon", type: "image/png", sizes: "16x16", href: "favicon-16x16.png" }], 7 | ['link', { rel: "shortcut icon", href: "favicon.ico" }], 8 | ['meta', { name: "msapplication-TileColor", content: "#3a0839" }], 9 | ['meta', { name: "theme-color", content: "#ffffff" }], 10 | ], 11 | 12 | locales: { 13 | '/': { 14 | lang: 'en-US', 15 | title: 'Vue Cascader Select', 16 | description: 'Documentation site for the Vue Cascader Select component' 17 | }, 18 | '/es/': { 19 | lang: 'es-UY', 20 | title: 'Vue Cascader Select', 21 | description: 'Documentación oficial de Vue Cascader Select' 22 | }, 23 | }, 24 | 25 | themeConfig: { 26 | logo: '/logo.svg', 27 | editLinks: true, 28 | search: false, 29 | locales: { 30 | '/': { 31 | label: 'English', 32 | selectText: 'Languages', 33 | lastUpdated: 'Last Updated', 34 | serviceWorker: { 35 | updatePopup: { 36 | message: 'New content is available.', 37 | buttonText: 'Refresh', 38 | }, 39 | }, 40 | nav: [ 41 | { text: 'Guide', link: '/guide/' }, 42 | { text: 'Github', link: 'https://github.com/NeoCoast/vue-cascader-select' }, 43 | ], 44 | sidebar: [ 45 | { 46 | title: 'Guide', 47 | path: '/guide/', 48 | collapsable: false, 49 | children: [ 50 | '/guide/installation', 51 | '/guide/basic_usage', 52 | '/guide/theming', 53 | ], 54 | }, 55 | ], 56 | }, 57 | '/es/': { 58 | label: 'Español', 59 | selectText: 'Idiomas', 60 | lastUpdated: 'Última actualización', 61 | serviceWorker: { 62 | updatePopup: { 63 | message: 'Nuevo contenido disponible.', 64 | buttonText: 'Actualizar' 65 | }, 66 | }, 67 | nav: [ 68 | { text: 'Guía', link: '/es/guide/' }, 69 | { text: 'Github', link: 'https://github.com/NeoCoast/vue-cascader-select' }, 70 | ], 71 | sidebar: [ 72 | { 73 | title: 'Guía', 74 | path: '/es/guide/', 75 | collapsable: false, 76 | children: [ 77 | '/es/guide/installation', 78 | '/es/guide/basic_usage', 79 | '/es/guide/theming', 80 | ], 81 | }, 82 | ], 83 | }, 84 | }, 85 | }, 86 | }; 87 | -------------------------------------------------------------------------------- /docs/.vuepress/enhanceApp.js: -------------------------------------------------------------------------------- 1 | import VueCascaderSelect from '../../src/entry.js'; 2 | 3 | export default ({ Vue }) => { 4 | Vue.use(VueCascaderSelect); 5 | } 6 | -------------------------------------------------------------------------------- /docs/.vuepress/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeoCoast/vue-cascader-select/80167d9c3826b18c571d380dce96dfb916b2ee5c/docs/.vuepress/public/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeoCoast/vue-cascader-select/80167d9c3826b18c571d380dce96dfb916b2ee5c/docs/.vuepress/public/favicon-16x16.png -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeoCoast/vue-cascader-select/80167d9c3826b18c571d380dce96dfb916b2ee5c/docs/.vuepress/public/favicon-32x32.png -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeoCoast/vue-cascader-select/80167d9c3826b18c571d380dce96dfb916b2ee5c/docs/.vuepress/public/favicon.ico -------------------------------------------------------------------------------- /docs/.vuepress/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 297 | 299 | 311 | 317 | 319 | 321 | 323 | 325 | 333 | 335 | 338 | 339 | 341 | 344 | 346 | 352 | 354 | 356 | 358 | 360 | 362 | 364 | 366 | 387 | 389 | 398 | 401 | 403 | 405 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: 4 | actionText: Get Started → 5 | actionLink: /guide/installation 6 | --- 7 | 8 | -------------------------------------------------------------------------------- /docs/es/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: 4 | actionText: Comenzar → 5 | actionLink: /es/guide/installation 6 | --- 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/es/guide/README.md: -------------------------------------------------------------------------------- 1 | # Guía 2 | -------------------------------------------------------------------------------- /docs/es/guide/basic_usage.md: -------------------------------------------------------------------------------- 1 | # Uso básico 2 | 3 | Veamos el uso más básico del componente: 4 | 5 | ```vue 6 | 14 | ``` 15 | 16 | ¡Esto renderizará el `cascader-select` con las opciones correspondientes, y dejará que el usuario seleccione cualquiera de ellas! 17 | 18 | 21 | 22 | ## Props 23 | 24 | | Nombre | Tipo | Requerido | Valor por defecto | 25 | |-------------|----------|:---------:|--------------------| 26 | | options | Array | ✅ | | 27 | | placeholder | String | | 'Please select...' | 28 | | value | String | ✅ | | 29 | 30 | ### options 31 | 32 | Opciones que mostrará el `cascader-select`. Esta prop es un arreglo de objetos, en los cuales las claves `label` y `value` son requeridas, mientras que `options`, `selectable` y `disabled` son opcionales. 33 | 34 | Veamos un ejemplo: 35 | 36 | ```js 37 | [ 38 | { 39 | label: 'Frontend', 40 | value: 'Frontend', 41 | disabled: true, 42 | options: [ 43 | { label: 'Vue', value: 'Vue' }, 44 | { label: 'React', value: 'React' }, 45 | { label: 'Svelte', value: 'Svelte' }, 46 | ], 47 | }, 48 | { 49 | label: 'Backend', 50 | value: 'Backend', 51 | disabled: true, 52 | options: [ 53 | { label: 'Ruby on Rails', value: 'Ruby on Rails' }, 54 | { label: 'NodeJS', value: 'NodeJS' }, 55 | { label: 'Elixir', value: 'Elixir' }, 56 | ], 57 | }, 58 | ]; 59 | ``` 60 | 61 | ## Events 62 | 63 | | Nombre | Parámetros | ¿Cuándo? | 64 | |-------------|----------------|:---------------------------------------:| 65 | | clear | | Al clickear en el ícono de la cruz | 66 | | select | value | Al seleccionar una opción | 67 | -------------------------------------------------------------------------------- /docs/es/guide/installation.md: -------------------------------------------------------------------------------- 1 | # Instalación 2 | 3 | ## npm/yarn 4 | 5 | ``` bash 6 | npm install --save vue-cascader-select@latest 7 | or 8 | yarn add vue-cascader-select@latest 9 | ``` 10 | 11 | Usted puede registrar el componente de forma global: 12 | 13 | ``` js 14 | import Vue from 'vue'; 15 | import VueCascaderSelect from 'vue-cascader-select'; 16 | 17 | Vue.use(VueCascaderSelect); 18 | ... 19 | ``` 20 | 21 | O utilizarlo solamente en cualquier componente: 22 | 23 | ```js 24 | import Vue from 'vue'; 25 | import VueCascaderSelect from 'vue-cascader-select'; 26 | 27 | export default { 28 | name: 'MyComponent', 29 | components: { 30 | VueCascaderSelect, 31 | }, 32 | ... 33 | }; 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/es/guide/theming.md: -------------------------------------------------------------------------------- 1 | # Personalización 2 | 3 | La forma más fácil de personalizar el componente es sobrescribir los estilos por defecto. La siguiente tabla muestra las clases disponibles y su propósito: 4 | 5 | | Clase | Propósito | 6 | |------------------------|------------------------------------------------------| 7 | | .vcs__picker input | Estilos para el input | 8 | | .vcs__arrow-container | Estilos para el contenedor de la flecha del dropdown | 9 | | .vcs__select-menu | Estilos para todos los menús del cascader select | 10 | | .vcs__option | Estilos para las opciones | 11 | | .vcs__option--active | Estilos para la opción activa | 12 | | .vcs__option--disabled | Estilos para las opciones que están deshabilitadas | 13 | | .vcs__arrow | Estilos para todas las flechas del cascader select | 14 | | .vcs__cross button | Estilos para la cruz | 15 | 16 | ## Ejemplo 17 | 18 | ```css 19 | .vcs__picker input, 20 | .vcs__select-menu { 21 | background: #282b34; 22 | color: white; 23 | border-color: #282b34; 24 | } 25 | 26 | .vcs__picker input::placeholder { 27 | color: #bbb; 28 | } 29 | 30 | .vcs__option--active { 31 | background: #41444e; 32 | } 33 | 34 | .vcs__option:hover { 35 | background: #474b56; 36 | } 37 | ``` 38 | 39 | 42 | -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | # Guide 2 | -------------------------------------------------------------------------------- /docs/guide/basic_usage.md: -------------------------------------------------------------------------------- 1 | # Basic usage 2 | 3 | Let's see the most basic usage of the component: 4 | 5 | ```vue 6 | 14 | ``` 15 | 16 | This will render the `cascader-select` with the given options, and allow the user to select any of them! 17 | 18 | 19 | 20 | ## Props 21 | 22 | | Name | Type | Required | Default value | 23 | |-------------|----------|:--------:|--------------------| 24 | | options | Array | ✅ | | 25 | | placeholder | String | | 'Please select...' | 26 | | value | String | ✅ | | 27 | 28 | ### options 29 | 30 | Options passed down to the `cascader-select`. This prop is an array of objects. In each object the `label` and `value` keys are required while `options`, `selectable` and `disabled` keys are optional. 31 | 32 | Let's see an example: 33 | 34 | ```js 35 | [ 36 | { 37 | label: 'Frontend', 38 | value: 'Frontend', 39 | disabled: true, 40 | options: [ 41 | { label: 'Vue', value: 'Vue' }, 42 | { label: 'React', value: 'React' }, 43 | { label: 'Svelte', value: 'Svelte' }, 44 | ], 45 | }, 46 | { 47 | label: 'Backend', 48 | value: 'Backend', 49 | disabled: true, 50 | options: [ 51 | { label: 'Ruby on Rails', value: 'Ruby on Rails' }, 52 | { label: 'NodeJS', value: 'NodeJS' }, 53 | { label: 'Elixir', value: 'Elixir' }, 54 | ], 55 | }, 56 | ]; 57 | ``` 58 | 59 | ## Events 60 | 61 | | Name | Parameters | When? | 62 | |-------------|----------------|:---------------------------------------:| 63 | | clear | | Emitted when clicking on the cross icon | 64 | | select | value | Emitted when an option is selected | 65 | -------------------------------------------------------------------------------- /docs/guide/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## npm/yarn 4 | 5 | ``` bash 6 | npm install --save vue-cascader-select@latest 7 | or 8 | yarn add vue-cascader-select@latest 9 | ``` 10 | 11 | You can either register the component globally: 12 | 13 | ``` js 14 | import Vue from 'vue'; 15 | import VueCascaderSelect from 'vue-cascader-select'; 16 | 17 | Vue.use(VueCascaderSelect); 18 | ... 19 | ``` 20 | 21 | Or use it inside any given component: 22 | 23 | ```js 24 | import Vue from 'vue'; 25 | import VueCascaderSelect from 'vue-cascader-select'; 26 | 27 | export default { 28 | name: 'MyComponent', 29 | components: { 30 | VueCascaderSelect, 31 | }, 32 | ... 33 | }; 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/guide/theming.md: -------------------------------------------------------------------------------- 1 | # Theming 2 | 3 | The easiest way to customize the component is to override the default styles. The following table shows all the available classes and their purpose: 4 | 5 | | Class | Purpose | 6 | |------------------------|------------------------------------------------------| 7 | | .vcs__picker input | Styles for the input element | 8 | | .vcs__arrow-container | Styles for the container of the dropdown arrow | 9 | | .vcs__select-menu | Styles for all the menus across the cascader select | 10 | | .vcs__option | Styles for the options | 11 | | .vcs__option--active | Styles for the active option | 12 | | .vcs__option--disabled | Styles for disabled options | 13 | | .vcs__arrow | Styles for all arrows across the cascader select | 14 | | .vcs__cross button | Styles for the cross | 15 | 16 | ## Example 17 | 18 | ```css 19 | .vcs__picker input, 20 | .vcs__select-menu { 21 | background: #282b34; 22 | color: white; 23 | border-color: #282b34; 24 | } 25 | 26 | .vcs__picker input::placeholder { 27 | color: #bbb; 28 | } 29 | 30 | .vcs__option--active { 31 | background: #41444e; 32 | } 33 | 34 | .vcs__option:hover { 35 | background: #474b56; 36 | } 37 | ``` 38 | 39 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest', 3 | collectCoverage: true, 4 | coverageReporters: ['lcov', 'text-summary'], 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-cascader-select", 3 | "version": "1.2.0", 4 | "description": "Cascader Select for Vue", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/NeoCoast/vue-cascader-select.git" 8 | }, 9 | "author": "Nicolás Tinte", 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/NeoCoast/vue-cascader-select/issues" 13 | }, 14 | "homepage": "https://neocoast.github.io/vue-cascader-select/", 15 | "keywords": [ 16 | "vue", 17 | "cascader", 18 | "select", 19 | "dropdown", 20 | "cascader-select" 21 | ], 22 | "scripts": { 23 | "serve": "vue-cli-service serve src/serve-dev.js", 24 | "build": "cross-env NODE_ENV=production rollup --config build/rollup.config.js", 25 | "lint": "vue-cli-service lint", 26 | "build:es": "cross-env NODE_ENV=production rollup --config build/rollup.config.js --format es", 27 | "build:ssr": "cross-env NODE_ENV=production rollup --config build/rollup.config.js --format cjs", 28 | "build:unpkg": "cross-env NODE_ENV=production rollup --config build/rollup.config.js --format iife", 29 | "docs:build": "vuepress build docs", 30 | "docs:dev": "DEBUG=true vuepress dev docs", 31 | "test:unit": "vue-cli-service test:unit", 32 | "codecov": "codecov" 33 | }, 34 | "main": "dist/vue-cascader-select.ssr.js", 35 | "module": "dist/vue-cascader-select.esm.js", 36 | "browser": "dist/vue-cascader-select.esm.js", 37 | "unpkg": "dist/vue-cascader-select.min.js", 38 | "files": [ 39 | "dist/*", 40 | "src/**/*.vue", 41 | "!src/serve-dev.*" 42 | ], 43 | "dependencies": { 44 | "v-click-outside": "^3.0.1" 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "^7.7.7", 48 | "@babel/preset-env": "^7.7.7", 49 | "@rollup/plugin-alias": "^2.2.0", 50 | "@rollup/plugin-commonjs": "^11.0.1", 51 | "@rollup/plugin-replace": "^2.2.1", 52 | "@vue/cli-plugin-babel": "^4.1.2", 53 | "@vue/cli-plugin-eslint": "^4.1.2", 54 | "@vue/cli-plugin-unit-jest": "^4.2.2", 55 | "@vue/cli-service": "^4.1.0", 56 | "@vue/eslint-config-airbnb": "^4.0.0", 57 | "@vue/test-utils": "1.0.0-beta.31", 58 | "babel-eslint": "^10.0.3", 59 | "codecov": "^3.6.5", 60 | "cross-env": "^6.0.3", 61 | "eslint": "^5.16.0", 62 | "eslint-plugin-vue": "^5.0.0", 63 | "lint-staged": "^9.5.0", 64 | "minimist": "^1.2.0", 65 | "rollup": "^1.27.13", 66 | "rollup-plugin-babel": "^4.3.3", 67 | "rollup-plugin-terser": "^5.1.3", 68 | "rollup-plugin-vue": "^5.1.5", 69 | "vue": "^2.6.10", 70 | "vue-template-compiler": "^2.6.10", 71 | "vuepress": "^1.0.0-rc.1" 72 | }, 73 | "peerDependencies": { 74 | "vue": "^2.6.10" 75 | }, 76 | "engines": { 77 | "node": ">=8" 78 | }, 79 | "gitHooks": { 80 | "pre-commit": "npm run test:unit && lint-staged" 81 | }, 82 | "lint-staged": { 83 | "*.{js,vue}": [ 84 | "vue-cli-service lint", 85 | "git add" 86 | ] 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeoCoast/vue-cascader-select/80167d9c3826b18c571d380dce96dfb916b2ee5c/public/logo.png -------------------------------------------------------------------------------- /src/VueCascaderSelect.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 101 | 102 | 190 | -------------------------------------------------------------------------------- /src/components/Arrow.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | 22 | 52 | -------------------------------------------------------------------------------- /src/components/Option.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 82 | 83 | 123 | -------------------------------------------------------------------------------- /src/components/SelectMenu.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 145 | 146 | 173 | -------------------------------------------------------------------------------- /src/entry.js: -------------------------------------------------------------------------------- 1 | import component from './VueCascaderSelect.vue'; 2 | 3 | // install function executed by Vue.use() 4 | const install = function installVueCascaderSelect(Vue) { 5 | if (install.installed) return; 6 | install.installed = true; 7 | Vue.component('VueCascaderSelect', component); 8 | }; 9 | 10 | // Create module definition for Vue.use() 11 | const plugin = { 12 | install, 13 | }; 14 | 15 | // To auto-install when vue is found 16 | // eslint-disable-next-line no-redeclare 17 | /* global window, global */ 18 | let GlobalVue = null; 19 | if (typeof window !== 'undefined') { 20 | GlobalVue = window.Vue; 21 | } else if (typeof global !== 'undefined') { 22 | GlobalVue = global.Vue; 23 | } 24 | if (GlobalVue) { 25 | GlobalVue.use(plugin); 26 | } 27 | 28 | // Inject install function into component - allows component 29 | // to be registered via Vue.use() as well as Vue.component() 30 | component.install = install; 31 | 32 | // Export component by default 33 | export default component; 34 | 35 | // It's possible to expose named exports when writing components that can 36 | // also be used as directives, etc. - eg. import { RollupDemoDirective } from 'rollup-demo'; 37 | // export const RollupDemoDirective = component; 38 | -------------------------------------------------------------------------------- /src/serve-dev.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Dev from '@/serve-dev.vue'; 3 | 4 | Vue.config.productionTip = false; 5 | 6 | new Vue({ 7 | render: h => h(Dev), 8 | }).$mount('#app'); 9 | -------------------------------------------------------------------------------- /src/serve-dev.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 36 | 37 | 52 | -------------------------------------------------------------------------------- /src/utils/teams.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | label: 'Eastern Conference', 4 | value: 'Eastern Conference', 5 | disabled: true, 6 | options: [ 7 | { 8 | label: 'Southeast', 9 | value: 'Southeast', 10 | options: [ 11 | { 12 | label: 'Atlanta Hawks', 13 | value: 'Atlanta Hawks', 14 | options: [ 15 | { label: 'Trae Young', value: 'Trae Young' }, 16 | { label: 'Clint Capela', value: 'Clint Capela' }, 17 | ], 18 | }, 19 | { label: 'Charlotte Hornets', value: 'Charlotte Hornets' }, 20 | { label: 'Miami Heat', value: 'Miami Heat' }, 21 | { label: 'Orlando Magic', value: 'Orlando Magic' }, 22 | { label: 'Washington Wizards', value: 'Washington Wizards' }, 23 | ], 24 | }, 25 | { 26 | label: 'Atlantic', 27 | value: 'Atlantic', 28 | options: [ 29 | { label: 'Boston Celtics', value: 'Boston Celtics' }, 30 | { label: 'Brooklyn Nets', value: 'Brooklyn Nets' }, 31 | { label: 'New York Knicks', value: 'New York Knicks' }, 32 | { label: 'Philadelphia 76ers', value: 'Philadelphia 76ers' }, 33 | { label: 'Toronto Raptors', value: 'Toronto Raptors' }, 34 | ], 35 | }, 36 | { 37 | label: 'Central', 38 | value: 'Central', 39 | options: [ 40 | { label: 'Chicago Bulls', value: 'Chicago Bulls' }, 41 | { label: 'Cleveland Cavaliers', value: 'Cleveland Cavaliers' }, 42 | { label: 'Detroit Pistons', value: 'Detroit Pistons' }, 43 | { label: 'Indiana Pacers', value: 'Indiana Pacers' }, 44 | { label: 'Milwaukee Bucks', value: 'Milwaukee Bucks' }, 45 | ], 46 | }, 47 | ], 48 | }, 49 | { 50 | label: 'Western Conference', 51 | value: 'Western Conference', 52 | disabled: true, 53 | options: [ 54 | { 55 | label: 'Southwest', 56 | value: 'Southwest', 57 | options: [ 58 | { label: 'Dallas Mavericks', value: 'Dallas Mavericks' }, 59 | { label: 'Houston Rockets', value: 'Houston Rockets' }, 60 | { label: 'Memphis Grizzlies', value: 'Memphis Grizzlies' }, 61 | { label: 'New Orleans Pelicans', value: 'New Orleans Pelicans' }, 62 | { label: 'San Antonio Spurs', value: 'San Antonio Spurs' }, 63 | ], 64 | }, 65 | { 66 | label: 'Northwest', 67 | value: 'Northwest', 68 | options: [ 69 | { label: 'Denver Nuggets', value: 'Denver Nuggets' }, 70 | { label: 'Minnesota Timberwolves', value: 'Minnesota Timberwolves' }, 71 | { label: 'Oklahoma City Thunder', value: 'Oklahoma City Thunder' }, 72 | { label: 'Portland Trail Blazers', value: 'Portland Trail Blazers' }, 73 | { label: 'Utah Jazz', value: 'Utah Jazz' }, 74 | ], 75 | }, 76 | { 77 | label: 'Pacific', 78 | value: 'Pacific', 79 | options: [ 80 | { label: 'Golden State Warriors', value: 'Golden State Warriors' }, 81 | { label: 'Los Angeles Clippers', value: 'Los Angeles Clippers' }, 82 | { label: 'Los Angeles Lakers', value: 'Los Angeles Lakers' }, 83 | { label: 'Phoenix Suns', value: 'Phoenix Suns' }, 84 | { label: 'Sacramento Kings', value: 'Sacramento Kings' }, 85 | ], 86 | }, 87 | ], 88 | }, 89 | ]; 90 | -------------------------------------------------------------------------------- /src/utils/validators.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | /* 3 | Every option should have a string|number label 4 | and either a value or an options Array 5 | */ 6 | export const validateOptions = validatingOptions => ( 7 | validatingOptions.every(({ label, options, value }) => ( 8 | label && typeof label === 'string' && ( 9 | value || (options && Array.isArray(options) && validateOptions(options)) 10 | ) 11 | )) 12 | ); 13 | --------------------------------------------------------------------------------