├── .npmrc
├── docs
├── CNAME
├── partials
│ ├── sidebar.pug
│ ├── api.pug
│ ├── introduction.pug
│ ├── dev.pug
│ ├── getting-started.pug
│ └── guides.pug
├── components
│ ├── dev
│ │ ├── PerformanceTest.vue
│ │ ├── ModalTest.vue
│ │ └── LongLabelTest.vue
│ ├── DisableBranchNodes.vue
│ ├── FlattenSearchResults.vue
│ ├── TreeselectValue.vue
│ ├── CustomizeKeyNames.vue
│ ├── CustomizeValueLabel.vue
│ ├── CustomizeOptionLabel.vue
│ ├── AsyncSearching.vue
│ ├── VuexSupport.vue
│ ├── Anchor.vue
│ ├── DisableItemSelection.vue
│ ├── FlatModeAndSortValues.vue
│ ├── utils.js
│ ├── DelayedRootOptions.vue
│ ├── Demo.vue
│ ├── BasicFeatures.vue
│ ├── DocSlots.vue
│ ├── DocEvents.vue
│ ├── PreventValueCombining.vue
│ ├── DelayedLoading.vue
│ ├── NestedSearch.vue
│ ├── MoreFeatures.vue
│ └── DocNode.vue
├── browserconfig.xml
├── main.js
├── mixins.pug
├── styles
│ ├── prism.less
│ └── docs.less
└── index.pug
├── .eslintignore
├── src
├── utils
│ ├── last.js
│ ├── noop.js
│ ├── once.js
│ ├── createMap.js
│ ├── isNaN.js
│ ├── isPromise.js
│ ├── constant.js
│ ├── debounce.js
│ ├── identity.js
│ ├── includes.js
│ ├── .eslintrc.js
│ ├── removeFromArray.js
│ ├── find.js
│ ├── onLeftClick.js
│ ├── quickDiff.js
│ ├── warning.js
│ ├── deepExtend.js
│ ├── scrollIntoView.js
│ ├── index.js
│ ├── setupResizeAndScrollEventListeners.js
│ └── watchSize.js
├── assets
│ ├── checkbox-checked.png
│ ├── checkbox-checked@2x.png
│ ├── checkbox-checked@3x.png
│ ├── checkbox-indeterminate.png
│ ├── checkbox-checked-disabled.png
│ ├── checkbox-indeterminate@2x.png
│ ├── checkbox-indeterminate@3x.png
│ ├── checkbox-checked-disabled@2x.png
│ ├── checkbox-checked-disabled@3x.png
│ ├── checkbox-indeterminate-disabled.png
│ ├── checkbox-indeterminate-disabled@2x.png
│ └── checkbox-indeterminate-disabled@3x.png
├── index.js
├── components
│ ├── icons
│ │ ├── Arrow.vue
│ │ └── Delete.vue
│ ├── Placeholder.vue
│ ├── Tip.vue
│ ├── SingleValue.vue
│ ├── HiddenFields.vue
│ ├── MultiValueItem.vue
│ ├── Treeselect.vue
│ ├── MultiValue.vue
│ ├── Control.vue
│ ├── MenuPortal.vue
│ ├── Input.vue
│ ├── Option.vue
│ └── Menu.vue
└── constants.js
├── .browserslistrc
├── screenshot.png
├── static
├── vue-logo.png
├── images
│ └── icons
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon-96x96.png
│ │ ├── ms-icon-70x70.png
│ │ ├── apple-icon-57x57.png
│ │ ├── apple-icon-60x60.png
│ │ ├── apple-icon-72x72.png
│ │ ├── apple-icon-76x76.png
│ │ ├── ms-icon-144x144.png
│ │ ├── ms-icon-150x150.png
│ │ ├── ms-icon-310x310.png
│ │ ├── apple-icon-114x114.png
│ │ ├── apple-icon-120x120.png
│ │ ├── apple-icon-144x144.png
│ │ ├── apple-icon-152x152.png
│ │ ├── apple-icon-180x180.png
│ │ └── android-icon-192x192.png
└── prism.min.js
├── .postcssrc.js
├── test
└── unit
│ ├── .eslintrc.js
│ ├── index.js
│ ├── specs
│ ├── Control.spec.js
│ ├── Slots.spec.js
│ ├── Events.spec.js
│ ├── SearchInput.spec.js
│ ├── HiddenFields.spec.js
│ ├── Menu.spec.js
│ ├── shared.js
│ ├── utils.spec.js
│ └── Methods.spec.js
│ └── karma.config.js
├── .gitignore
├── .size-limit.js
├── .editorconfig
├── .babelrc.js
├── .circleci
└── config.yml
├── stylelint.config.js
├── .eslintrc.js
├── LICENSE
├── package.json
└── README.md
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/docs/CNAME:
--------------------------------------------------------------------------------
1 | vue-treeselect.js.org
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 | static
3 | test/unit/coverage
4 |
--------------------------------------------------------------------------------
/src/utils/last.js:
--------------------------------------------------------------------------------
1 | export { default as last } from 'lodash/last'
2 |
--------------------------------------------------------------------------------
/src/utils/noop.js:
--------------------------------------------------------------------------------
1 | export { default as noop } from 'lodash/noop'
2 |
--------------------------------------------------------------------------------
/src/utils/once.js:
--------------------------------------------------------------------------------
1 | export { default as once } from 'lodash/once'
2 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | # Browsers that we support
2 |
3 | > 0.5%
4 | IE >= 9
5 |
--------------------------------------------------------------------------------
/src/utils/createMap.js:
--------------------------------------------------------------------------------
1 | export const createMap = () => Object.create(null)
2 |
--------------------------------------------------------------------------------
/src/utils/isNaN.js:
--------------------------------------------------------------------------------
1 | export function isNaN(x) {
2 | return x !== x
3 | }
4 |
--------------------------------------------------------------------------------
/src/utils/isPromise.js:
--------------------------------------------------------------------------------
1 | export { default as isPromise } from 'is-promise'
2 |
--------------------------------------------------------------------------------
/src/utils/constant.js:
--------------------------------------------------------------------------------
1 | export { default as constant } from 'lodash/constant'
2 |
--------------------------------------------------------------------------------
/src/utils/debounce.js:
--------------------------------------------------------------------------------
1 | export { default as debounce } from 'lodash/debounce'
2 |
--------------------------------------------------------------------------------
/src/utils/identity.js:
--------------------------------------------------------------------------------
1 | export { default as identity } from 'lodash/identity'
2 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/screenshot.png
--------------------------------------------------------------------------------
/static/vue-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/static/vue-logo.png
--------------------------------------------------------------------------------
/src/utils/includes.js:
--------------------------------------------------------------------------------
1 | export function includes(arrOrStr, elem) {
2 | return arrOrStr.indexOf(elem) !== -1
3 | }
4 |
--------------------------------------------------------------------------------
/src/assets/checkbox-checked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/src/assets/checkbox-checked.png
--------------------------------------------------------------------------------
/src/assets/checkbox-checked@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/src/assets/checkbox-checked@2x.png
--------------------------------------------------------------------------------
/src/assets/checkbox-checked@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/src/assets/checkbox-checked@3x.png
--------------------------------------------------------------------------------
/src/assets/checkbox-indeterminate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/src/assets/checkbox-indeterminate.png
--------------------------------------------------------------------------------
/static/images/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/static/images/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/static/images/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/static/images/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/static/images/icons/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/static/images/icons/favicon-96x96.png
--------------------------------------------------------------------------------
/static/images/icons/ms-icon-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/static/images/icons/ms-icon-70x70.png
--------------------------------------------------------------------------------
/src/assets/checkbox-checked-disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/src/assets/checkbox-checked-disabled.png
--------------------------------------------------------------------------------
/src/assets/checkbox-indeterminate@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/src/assets/checkbox-indeterminate@2x.png
--------------------------------------------------------------------------------
/src/assets/checkbox-indeterminate@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/src/assets/checkbox-indeterminate@3x.png
--------------------------------------------------------------------------------
/static/images/icons/apple-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/static/images/icons/apple-icon-57x57.png
--------------------------------------------------------------------------------
/static/images/icons/apple-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/static/images/icons/apple-icon-60x60.png
--------------------------------------------------------------------------------
/static/images/icons/apple-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/static/images/icons/apple-icon-72x72.png
--------------------------------------------------------------------------------
/static/images/icons/apple-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/static/images/icons/apple-icon-76x76.png
--------------------------------------------------------------------------------
/static/images/icons/ms-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/static/images/icons/ms-icon-144x144.png
--------------------------------------------------------------------------------
/static/images/icons/ms-icon-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/static/images/icons/ms-icon-150x150.png
--------------------------------------------------------------------------------
/static/images/icons/ms-icon-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/static/images/icons/ms-icon-310x310.png
--------------------------------------------------------------------------------
/static/images/icons/apple-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/static/images/icons/apple-icon-114x114.png
--------------------------------------------------------------------------------
/static/images/icons/apple-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/static/images/icons/apple-icon-120x120.png
--------------------------------------------------------------------------------
/static/images/icons/apple-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/static/images/icons/apple-icon-144x144.png
--------------------------------------------------------------------------------
/static/images/icons/apple-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/static/images/icons/apple-icon-152x152.png
--------------------------------------------------------------------------------
/static/images/icons/apple-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/static/images/icons/apple-icon-180x180.png
--------------------------------------------------------------------------------
/src/assets/checkbox-checked-disabled@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/src/assets/checkbox-checked-disabled@2x.png
--------------------------------------------------------------------------------
/src/assets/checkbox-checked-disabled@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/src/assets/checkbox-checked-disabled@3x.png
--------------------------------------------------------------------------------
/static/images/icons/android-icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/static/images/icons/android-icon-192x192.png
--------------------------------------------------------------------------------
/src/assets/checkbox-indeterminate-disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/src/assets/checkbox-indeterminate-disabled.png
--------------------------------------------------------------------------------
/src/assets/checkbox-indeterminate-disabled@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/src/assets/checkbox-indeterminate-disabled@2x.png
--------------------------------------------------------------------------------
/src/assets/checkbox-indeterminate-disabled@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/riophae/vue-treeselect/HEAD/src/assets/checkbox-indeterminate-disabled@3x.png
--------------------------------------------------------------------------------
/src/utils/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | rules: {
3 | 'import/no-default-export': 2,
4 | 'import/prefer-default-export': 0,
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/.postcssrc.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | plugins: {
5 | autoprefixer: {},
6 | },
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/removeFromArray.js:
--------------------------------------------------------------------------------
1 | export function removeFromArray(arr, elem) {
2 | const idx = arr.indexOf(elem)
3 | if (idx !== -1) arr.splice(idx, 1)
4 | }
5 |
--------------------------------------------------------------------------------
/test/unit/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | jasmine: true,
4 | },
5 | rules: {
6 | 'node/no-unpublished-import': 0,
7 | },
8 | }
9 |
--------------------------------------------------------------------------------
/docs/partials/sidebar.pug:
--------------------------------------------------------------------------------
1 | - const customClass = '{ sticky: isNavSticky }'
2 | div.sidebar-nav(:class=customClass)
3 | //- replaced later in mixins.pug:renderToc()
4 | ='%%_TOC_%%'
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | *debug.log*
4 | *error.log*
5 |
6 | .eslintcache
7 |
8 | node_modules/
9 | yarn.lock
10 |
11 | dist
12 | gh-pages
13 |
14 | test/unit/coverage
15 |
--------------------------------------------------------------------------------
/src/utils/find.js:
--------------------------------------------------------------------------------
1 | export function find(arr, predicate, ctx) {
2 | for (let i = 0, len = arr.length; i < len; i++) {
3 | if (predicate.call(ctx, arr[i], i, arr)) return arr[i]
4 | }
5 | return undefined
6 | }
7 |
--------------------------------------------------------------------------------
/docs/partials/api.pug:
--------------------------------------------------------------------------------
1 | +section('API')
2 |
3 | +subsection('Node')
4 | doc-node
5 |
6 | +subsection('Props')
7 | doc-props
8 |
9 | +subsection('Events')
10 | doc-events
11 |
12 | +subsection('Slots')
13 | doc-slots
14 |
--------------------------------------------------------------------------------
/.size-limit.js:
--------------------------------------------------------------------------------
1 | const shared = { webpack: false, running: false }
2 |
3 | module.exports = [
4 | { path: "dist/vue-treeselect.umd.min.js", limit: "16.5 KB", ...shared },
5 | { path: "dist/vue-treeselect.min.css", limit: "5 KB", ...shared },
6 | ]
7 |
--------------------------------------------------------------------------------
/src/utils/onLeftClick.js:
--------------------------------------------------------------------------------
1 | export function onLeftClick(mouseDownHandler) {
2 | return function onMouseDown(evt, ...args) {
3 | if (evt.type === 'mousedown' && evt.button === 0) {
4 | mouseDownHandler.call(this, evt, ...args)
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/utils/quickDiff.js:
--------------------------------------------------------------------------------
1 | export function quickDiff(arrA, arrB) {
2 | if (arrA.length !== arrB.length) return true
3 |
4 | for (let i = 0; i < arrA.length; i++) {
5 | if (arrA[i] !== arrB[i]) return true
6 | }
7 |
8 | return false
9 | }
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 2
9 | end_of_line = lf
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | trim_trailing_whitespace = false
15 |
--------------------------------------------------------------------------------
/test/unit/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | Vue.config.productionTip = false
4 | Vue.config.devtools = false
5 |
6 | function importAll(r) {
7 | r.keys().forEach(r)
8 | }
9 |
10 | importAll(require.context('./specs', true, /\.spec$/))
11 | importAll(require.context('../../src', true))
12 |
--------------------------------------------------------------------------------
/docs/components/dev/PerformanceTest.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Treeselect from './components/Treeselect'
2 | import treeselectMixin from './mixins/treeselectMixin'
3 | import './style.less'
4 |
5 | export default Treeselect
6 | export { Treeselect, treeselectMixin }
7 | export {
8 | // Delayed loading.
9 | LOAD_ROOT_OPTIONS,
10 | LOAD_CHILDREN_OPTIONS,
11 | ASYNC_SEARCH,
12 | } from './constants'
13 |
14 | export const VERSION = PKG_VERSION
15 |
--------------------------------------------------------------------------------
/.babelrc.js:
--------------------------------------------------------------------------------
1 | module.exports = api => {
2 | api.cache.never()
3 |
4 | const presets = [
5 | [ '@babel/preset-env', { modules: false } ],
6 | ]
7 | const plugins = [
8 | 'transform-vue-jsx',
9 | '@babel/plugin-transform-runtime',
10 | ]
11 |
12 | if (process.env.NODE_ENV === 'testing') {
13 | plugins.push('istanbul')
14 | }
15 |
16 | return { presets, plugins, comments: false }
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils/warning.js:
--------------------------------------------------------------------------------
1 | import { noop } from './noop'
2 |
3 | export const warning = process.env.NODE_ENV === 'production'
4 | ? /* istanbul ignore next */ noop
5 | : function warning(checker, complainer) {
6 | if (!checker()) {
7 | const message = [ '[Vue-Treeselect Warning]' ].concat(complainer())
8 | // eslint-disable-next-line no-console
9 | console.error(...message)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/docs/components/DisableBranchNodes.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
19 |
--------------------------------------------------------------------------------
/docs/components/FlattenSearchResults.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
19 |
--------------------------------------------------------------------------------
/docs/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | #ffffff
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/docs/components/TreeselectValue.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ stringifiedValue }}
3 |
4 |
5 |
21 |
--------------------------------------------------------------------------------
/src/components/icons/Arrow.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/src/components/Placeholder.vue:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/docs/components/CustomizeKeyNames.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
31 |
--------------------------------------------------------------------------------
/docs/components/CustomizeValueLabel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ node.raw.customLabel }}
5 |
6 |
7 | Multi-select
8 |
9 |
10 |
11 |
12 |
25 |
--------------------------------------------------------------------------------
/src/utils/deepExtend.js:
--------------------------------------------------------------------------------
1 | function isPlainObject(value) {
2 | if (value == null || typeof value !== 'object') return false
3 | return Object.getPrototypeOf(value) === Object.prototype
4 | }
5 |
6 | function copy(obj, key, value) {
7 | if (isPlainObject(value)) {
8 | obj[key] || (obj[key] = {})
9 | deepExtend(obj[key], value)
10 | } else {
11 | obj[key] = value
12 | }
13 | }
14 |
15 | export function deepExtend(target, source) {
16 | if (isPlainObject(source)) {
17 | const keys = Object.keys(source)
18 |
19 | for (let i = 0, len = keys.length; i < len; i++) {
20 | copy(target, keys[i], source[keys[i]])
21 | }
22 | }
23 |
24 | return target
25 | }
26 |
--------------------------------------------------------------------------------
/test/unit/specs/Control.spec.js:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 | import { leftClick } from './shared'
3 | import Treeselect from '@src/components/Treeselect'
4 |
5 | describe('Control', () => {
6 | it('should toggle the menu when the arrow is clicked', () => {
7 | const wrapper = mount(Treeselect, {
8 | sync: false,
9 | attachToDocument: true,
10 | propsData: {
11 | options: [],
12 | },
13 | })
14 | const arrow = wrapper.find('.vue-treeselect__control-arrow-container')
15 |
16 | leftClick(arrow)
17 | expect(wrapper.vm.menu.isOpen).toBe(true)
18 | leftClick(arrow)
19 | expect(wrapper.vm.menu.isOpen).toBe(false)
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/src/utils/scrollIntoView.js:
--------------------------------------------------------------------------------
1 | // from react-select
2 | export function scrollIntoView($scrollingEl, $focusedEl) {
3 | const scrollingReact = $scrollingEl.getBoundingClientRect()
4 | const focusedRect = $focusedEl.getBoundingClientRect()
5 | const overScroll = $focusedEl.offsetHeight / 3
6 |
7 | if (focusedRect.bottom + overScroll > scrollingReact.bottom) {
8 | $scrollingEl.scrollTop = Math.min(
9 | $focusedEl.offsetTop + $focusedEl.clientHeight - $scrollingEl.offsetHeight + overScroll,
10 | $scrollingEl.scrollHeight,
11 | )
12 | } else if (focusedRect.top - overScroll < scrollingReact.top) {
13 | $scrollingEl.scrollTop = Math.max($focusedEl.offsetTop - overScroll, 0)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/icons/Delete.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/docs/components/CustomizeOptionLabel.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 | {{ node.isBranch ? 'Branch' : 'Leaf' }}: {{ node.label }}
11 | ({{ count }})
12 |
13 |
14 |
15 |
16 |
26 |
--------------------------------------------------------------------------------
/docs/partials/introduction.pug:
--------------------------------------------------------------------------------
1 | +section('Introduction')
2 |
3 | section
4 | :markdown-it(html=true)
5 | [vue-treeselect](https://github.com/riophae/vue-treeselect) is a multi-select component with nested options support for [Vue.js](https://www.vuejs.org).
6 |
7 | - Single & multiple select with nested options support
8 | - Fuzzy matching
9 | - Async searching
10 | - Delayed loading (load data of deep level options only when needed)
11 | - Keyboard support (navigate using Arrow Up & Arrow Down keys, select option using Enter key, etc.)
12 | - Rich options & highly customizable
13 | - Supports [a wide range of browsers](https://github.com/riophae/vue-treeselect#browser-compatibility)
14 |
15 | *Requires Vue 2.2+*
16 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Javascript Node CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details
4 | #
5 | version: 2
6 |
7 | jobs:
8 | build:
9 | docker:
10 | - image: circleci/node:10-browsers
11 |
12 | working_directory: ~/repo
13 |
14 | steps:
15 | - checkout
16 |
17 | - run: sudo npm install -g npm@latest
18 |
19 | - restore_cache:
20 | key: dependency-cache-{{ checksum "package.json" }}
21 |
22 | - run: npm install
23 |
24 | - save_cache:
25 | paths:
26 | - node_modules
27 | key: dependency-cache-{{ checksum "package.json" }}
28 |
29 | - run: npm test
30 |
31 | - run: ./node_modules/.bin/codecov
32 |
33 | branches:
34 | ignore:
35 | - gh-pages
36 |
--------------------------------------------------------------------------------
/docs/components/AsyncSearching.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
32 |
--------------------------------------------------------------------------------
/docs/partials/dev.pug:
--------------------------------------------------------------------------------
1 | if NODE_ENV !== 'production'
2 | +section('Dev')
3 |
4 | +subsection('Performance Test')
5 | :markdown-it
6 | Checklist:
7 | - Opening & closing the menu should be fast
8 | - Hovering the mouse over the list should be smooth
9 | - Selecting & deselecting options should feel snappy
10 | +component('PerformanceTest')
11 |
12 |
13 | +subsection('Modal Test')
14 | :markdown-it
15 | Checklist:
16 | - The modal uses `overflow: hidden` and the menu should not be clipped
17 | - When the size of the control changes, the position of the menu should be recalculated
18 | - While scrolling the page, the menu should be kept correctly positioned
19 | +component('ModalTest')
20 |
21 | +subsection('Long Label Test')
22 | +component('LongLabelTest')
23 |
--------------------------------------------------------------------------------
/stylelint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: 'stylelint-config-xo-space',
3 | rules: {
4 | 'string-quotes': [ 'double', { avoidEscape: false } ],
5 | 'declaration-empty-line-before': null,
6 | 'at-rule-empty-line-before': null,
7 | 'selector-list-comma-newline-after': null,
8 | 'rule-empty-line-before': null,
9 | 'value-keyword-case': null, // [ 'lower', { ignoreProperties: [ 'font', 'font-family' ] } ],
10 | 'declaration-block-no-duplicate-properties': [ true, { ignore: [ 'consecutive-duplicates' ] } ],
11 | 'declaration-property-value-blacklist': null,
12 | 'property-blacklist': null,
13 | 'no-unknown-animations': null,
14 | 'font-weight-notation': null,
15 | 'no-descending-specificity': null,
16 | 'selector-max-compound-selectors': null,
17 | 'block-no-empty': [ true, { ignore: [] } ],
18 | },
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Tip.vue:
--------------------------------------------------------------------------------
1 |
33 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: [ 'riophae/vue' ],
4 | plugins: [ 'react' ],
5 | globals: {
6 | PKG_VERSION: true,
7 | },
8 | settings: {
9 | 'import/resolver': {
10 | node: null,
11 | webpack: {
12 | config: 'build/webpack-configs/base.js',
13 | },
14 | },
15 | },
16 | rules: {
17 | 'import/no-named-as-default': 0,
18 | 'unicorn/consistent-function-scoping': 0,
19 | 'vue/attributes-order': 0,
20 | 'vue/no-v-html': 0,
21 | 'no-confusing-arrow': 0,
22 | 'no-console': 0,
23 | 'no-warning-comments': 0,
24 | 'no-undefined': 0,
25 | 'prefer-destructuring': 0,
26 | },
27 | overrides: [ {
28 | files: [ 'src/**' ],
29 | rules: {
30 | 'unicorn/no-for-loop': 0,
31 | 'unicorn/prefer-includes': 0,
32 | 'unicorn/prefer-node-append': 0,
33 | },
34 | } ],
35 | }
36 |
--------------------------------------------------------------------------------
/docs/components/VuexSupport.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
47 |
--------------------------------------------------------------------------------
/docs/components/Anchor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/docs/components/dev/ModalTest.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Click Me
4 |
5 |
6 |
7 |
8 |
9 |
10 |
36 |
--------------------------------------------------------------------------------
/docs/components/DisableItemSelection.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
43 |
--------------------------------------------------------------------------------
/docs/components/FlatModeAndSortValues.vue:
--------------------------------------------------------------------------------
1 |
2 |
20 |
21 |
22 |
35 |
--------------------------------------------------------------------------------
/src/components/SingleValue.vue:
--------------------------------------------------------------------------------
1 |
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017-present Riophae Lee
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/docs/components/dev/LongLabelTest.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Multi-select mode
5 |
6 |
7 |
8 |
44 |
--------------------------------------------------------------------------------
/src/components/HiddenFields.vue:
--------------------------------------------------------------------------------
1 |
38 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | // ========================
2 | // Debugging Helpers
3 | // ========================
4 |
5 | export { warning } from './warning'
6 |
7 | // ========================
8 | // DOM Utilities
9 | // ========================
10 |
11 | export { onLeftClick } from './onLeftClick'
12 | export { scrollIntoView } from './scrollIntoView'
13 | export { debounce } from './debounce'
14 | export { watchSize } from './watchSize'
15 | export { setupResizeAndScrollEventListeners } from './setupResizeAndScrollEventListeners'
16 |
17 | // ========================
18 | // Language Helpers
19 | // ========================
20 |
21 | export { isNaN } from './isNaN'
22 | export { isPromise } from './isPromise'
23 | export { once } from './once'
24 | export { noop } from './noop'
25 | export { identity } from './identity'
26 | export { constant } from './constant'
27 | export { createMap } from './createMap'
28 | export { deepExtend } from './deepExtend'
29 | export { last } from './last'
30 | export { includes } from './includes'
31 | export { find } from './find'
32 | export { removeFromArray } from './removeFromArray'
33 |
34 | // ========================
35 | // Other Utilities
36 | // ========================
37 |
38 | export { quickDiff } from './quickDiff'
39 |
--------------------------------------------------------------------------------
/docs/components/utils.js:
--------------------------------------------------------------------------------
1 | import { encodeHTML } from 'entities'
2 |
3 | export const code = str => `${encodeHTML(str)}`
4 |
5 | export const strong = str => `${str} `
6 |
7 | export const link = (target, text = 'here') => `${text} `
8 |
9 | export const makeNameList = names => {
10 | names = names.map(code)
11 | const tail = names.pop()
12 | if (names.length < 2) return tail
13 | return names.join(', ') + ' & ' + tail
14 | }
15 |
16 | export const makePropList = propNames => {
17 | return '{' + propNames.map(code).join(', ') + '}'
18 | }
19 |
20 | export const makeArgNameList = argNames => {
21 | return '(' + argNames.map(code).join(', ') + ')'
22 | }
23 |
24 | function createArray(len, valueMaker) {
25 | const arr = []
26 | let i = 0
27 | while (i < len) arr.push(valueMaker(i++))
28 | return arr
29 | }
30 |
31 | export function generateOptions(maxLevel, itemsPerLevel = maxLevel) {
32 | const generate = parentId => createArray(itemsPerLevel, i => {
33 | const id = parentId + String.fromCharCode(97 + i)
34 | const option = { id, label: id.toUpperCase() }
35 | if (id.length < maxLevel) option.children = generate(id)
36 | return option
37 | })
38 |
39 | return generate('')
40 | }
41 |
--------------------------------------------------------------------------------
/test/unit/karma.config.js:
--------------------------------------------------------------------------------
1 | process.env.CHROME_BIN = require('puppeteer').executablePath()
2 |
3 | module.exports = config => {
4 | config.set({
5 | files: [ './index.js' ],
6 | browsers: [ 'ChromeHeadlessWithoutSandbox' ],
7 | customLaunchers: {
8 | ChromeHeadlessWithoutSandbox: {
9 | // `ChromeHeadless` without any flags used to be working
10 | // well, but it is not now for some unknown reason.
11 | // Adding `--no-sandbox` flag solves the issue, which
12 | // I know is insecure. But since we are only using
13 | // Chrome to run the tests, it should be just fine.
14 | base: 'ChromeHeadless',
15 | flags: [ '--no-sandbox' ],
16 | },
17 | },
18 | preprocessors: {
19 | './index.js': [ 'webpack', 'sourcemap' ],
20 | },
21 | webpack: require('../../build/webpack-configs/test'),
22 | webpackMiddleware: {
23 | noInfo: true,
24 | },
25 | frameworks: [ 'jasmine', 'jasmine-matchers' ],
26 | client: {
27 | jasmine: { random: false },
28 | },
29 | reporters: [ 'spec', 'coverage' ],
30 | coverageReporter: {
31 | dir: './coverage',
32 | reporters: [
33 | { type: 'lcov', subdir: '.' },
34 | { type: 'text-summary' },
35 | ],
36 | },
37 | })
38 | }
39 |
--------------------------------------------------------------------------------
/docs/components/DelayedRootOptions.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
44 |
--------------------------------------------------------------------------------
/docs/components/Demo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 | Code
12 |
13 | HTML
14 | |
15 | JavaScript
16 |
17 |
18 |
22 |
23 |
24 |
25 |
26 |
41 |
--------------------------------------------------------------------------------
/src/utils/setupResizeAndScrollEventListeners.js:
--------------------------------------------------------------------------------
1 | function findScrollParents($el) {
2 | const $scrollParents = []
3 | let $parent = $el.parentNode
4 |
5 | while ($parent && $parent.nodeName !== 'BODY' && $parent.nodeType === document.ELEMENT_NODE) {
6 | if (isScrollElment($parent)) $scrollParents.push($parent)
7 | $parent = $parent.parentNode
8 | }
9 | $scrollParents.push(window)
10 |
11 | return $scrollParents
12 | }
13 |
14 | function isScrollElment($el) {
15 | // Firefox wants us to check `-x` and `-y` variations as well
16 | const { overflow, overflowX, overflowY } = getComputedStyle($el)
17 | return /(auto|scroll|overlay)/.test(overflow + overflowY + overflowX)
18 | }
19 |
20 | export function setupResizeAndScrollEventListeners($el, listener) {
21 | const $scrollParents = findScrollParents($el)
22 |
23 | window.addEventListener('resize', listener, { passive: true })
24 | $scrollParents.forEach(scrollParent => {
25 | scrollParent.addEventListener('scroll', listener, { passive: true })
26 | })
27 |
28 | return function removeEventListeners() {
29 | window.removeEventListener('resize', listener, { passive: true })
30 | $scrollParents.forEach($scrollParent => {
31 | $scrollParent.removeEventListener('scroll', listener, { passive: true })
32 | })
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/docs/components/BasicFeatures.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
57 |
--------------------------------------------------------------------------------
/docs/components/DocSlots.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Name
6 | Props
7 | Description
8 |
9 |
10 |
11 |
12 | {{ slot.name }}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
45 |
--------------------------------------------------------------------------------
/src/components/MultiValueItem.vue:
--------------------------------------------------------------------------------
1 |
46 |
--------------------------------------------------------------------------------
/src/components/Treeselect.vue:
--------------------------------------------------------------------------------
1 |
42 |
--------------------------------------------------------------------------------
/test/unit/specs/Slots.spec.js:
--------------------------------------------------------------------------------
1 | // import { mount } from '@vue/test-utils'
2 | // import Treeselect from '@src/components/Treeselect'
3 | // import { findLabelByNodeId } from './shared'
4 |
5 | // Currently @vue/test-utils doesn't properly handle scoped slots.
6 |
7 | // describe('Slots', () => {
8 | // it('option-label', async () => {
9 | // const getLabelText = nodeId => findLabelByNodeId(wrapper, nodeId).element.textContent.trim()
10 | // const wrapper = mount(Treeselect, {
11 | // propsData: {
12 | // options: [ {
13 | // id: 'a',
14 | // label: 'a',
15 | // children: [ {
16 | // id: 'aa',
17 | // label: 'aa',
18 | // } ],
19 | // }, {
20 | // id: 'b',
21 | // label: 'b',
22 | // } ],
23 | // defaultExpandLevel: Infinity,
24 | // },
25 | // scopedSlots: {
26 | // 'option-label': `
27 | //
28 | // {{ node.isBranch ? 'Branch' : 'Leaf' }}: {{ node.label }}
29 | // ({{ count }})
30 | //
31 | // `,
32 | // },
33 | // })
34 | // const { vm } = wrapper
35 | //
36 | // vm.openMenu()
37 | // await vm.$nextTick()
38 | //
39 | // expect(getLabelText('a')).toBe('Branch: a')
40 | // expect(getLabelText('aa')).toBe('Leaf: aa')
41 | // expect(getLabelText('b')).toBe('Branch: b')
42 | // })
43 | // })
44 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | // Magic value that indicates a root level node.
2 | export const NO_PARENT_NODE = null
3 |
4 | // Types of checked state.
5 | export const UNCHECKED = 0
6 | export const INDETERMINATE = 1
7 | export const CHECKED = 2
8 |
9 | // Types of count number.
10 | export const ALL_CHILDREN = 'ALL_CHILDREN'
11 | export const ALL_DESCENDANTS = 'ALL_DESCENDANTS'
12 | export const LEAF_CHILDREN = 'LEAF_CHILDREN'
13 | export const LEAF_DESCENDANTS = 'LEAF_DESCENDANTS'
14 |
15 | // Action types of delayed loading.
16 | export const LOAD_ROOT_OPTIONS = 'LOAD_ROOT_OPTIONS'
17 | export const LOAD_CHILDREN_OPTIONS = 'LOAD_CHILDREN_OPTIONS'
18 | export const ASYNC_SEARCH = 'ASYNC_SEARCH'
19 |
20 | // Acceptable values of `valueConsistsOf` prop.
21 | export const ALL = 'ALL'
22 | export const BRANCH_PRIORITY = 'BRANCH_PRIORITY'
23 | export const LEAF_PRIORITY = 'LEAF_PRIORITY'
24 | export const ALL_WITH_INDETERMINATE = 'ALL_WITH_INDETERMINATE'
25 |
26 | // Acceptable values of `sortValueBy` prop.
27 | export const ORDER_SELECTED = 'ORDER_SELECTED'
28 | export const LEVEL = 'LEVEL'
29 | export const INDEX = 'INDEX'
30 |
31 | // Key codes look-up table.
32 | export const KEY_CODES = {
33 | BACKSPACE: 8,
34 | ENTER: 13,
35 | ESCAPE: 27,
36 | END: 35,
37 | HOME: 36,
38 | ARROW_LEFT: 37,
39 | ARROW_UP: 38,
40 | ARROW_RIGHT: 39,
41 | ARROW_DOWN: 40,
42 | DELETE: 46,
43 | }
44 |
45 | // Other constants.
46 | export const INPUT_DEBOUNCE_DELAY = process.env.NODE_ENV === 'testing'
47 | ? /* to speed up unit testing */ 10
48 | : /* istanbul ignore next */ 200
49 | export const MIN_INPUT_WIDTH = 5
50 | export const MENU_BUFFER = 40
51 |
--------------------------------------------------------------------------------
/docs/components/DocEvents.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Name
6 | Attributes
7 | Description
8 |
9 |
10 |
11 |
12 | {{ event.name }}
13 |
14 | {{ event.description }}
15 |
16 |
17 |
18 |
19 |
20 |
53 |
--------------------------------------------------------------------------------
/docs/components/PreventValueCombining.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
56 |
--------------------------------------------------------------------------------
/src/components/MultiValue.vue:
--------------------------------------------------------------------------------
1 |
57 |
--------------------------------------------------------------------------------
/test/unit/specs/Events.spec.js:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 | import { leftClick, findCheckboxByNodeId, findLabelContainerByNodeId } from './shared'
3 | import Treeselect from '@src/components/Treeselect'
4 |
5 | describe('Events', () => {
6 | describe('select & deselect', () => {
7 | let wrapper
8 |
9 | const aa = {
10 | id: 'aa',
11 | label: 'aa',
12 | }
13 | const ab = {
14 | id: 'ab',
15 | label: 'ab',
16 | isDisabled: true,
17 | }
18 | const a = {
19 | id: 'a',
20 | label: 'a',
21 | isDefaultExpanded: true,
22 | children: [ aa, ab ],
23 | }
24 |
25 | beforeEach(() => {
26 | wrapper = mount(Treeselect, {
27 | propsData: {
28 | options: [ a ],
29 | instanceId: 'test',
30 | multiple: true,
31 | value: [ 'ab' ],
32 | },
33 | })
34 | wrapper.vm.openMenu()
35 | })
36 |
37 | it('click on option label or checkbox', () => {
38 | leftClick(findLabelContainerByNodeId(wrapper, 'aa'))
39 | expect(wrapper.emitted().select).toEqual([
40 | [ aa, 'test' ],
41 | ])
42 |
43 | leftClick(findCheckboxByNodeId(wrapper, 'aa'))
44 | expect(wrapper.emitted().deselect).toEqual([
45 | [ aa, 'test' ],
46 | ])
47 | })
48 |
49 | it('click on disabled option', () => {
50 | leftClick(findLabelContainerByNodeId(wrapper, 'ab'))
51 | expect(wrapper.emitted().deselect).toBeUndefined()
52 | })
53 |
54 | it('click on value remove icon', () => {
55 | wrapper.setProps({ value: [ 'a' ] })
56 |
57 | // click on "×" of a
58 | leftClick(wrapper.find('.vue-treeselect__value-remove'))
59 | expect(wrapper.emitted().deselect).toEqual([
60 | [ a, 'test' ],
61 | ])
62 | })
63 | })
64 |
65 | // TODO
66 | })
67 |
--------------------------------------------------------------------------------
/src/utils/watchSize.js:
--------------------------------------------------------------------------------
1 | import watchSizeForBrowsersOtherThanIE9 from 'watch-size'
2 | import { removeFromArray } from './removeFromArray'
3 |
4 | let intervalId
5 | const registered = []
6 | const INTERVAL_DURATION = 100
7 |
8 | function run() {
9 | intervalId = setInterval(() => {
10 | registered.forEach(test)
11 | }, INTERVAL_DURATION)
12 | }
13 |
14 | function stop() {
15 | clearInterval(intervalId)
16 | intervalId = null
17 | }
18 |
19 | function test(item) {
20 | const { $el, listener, lastWidth, lastHeight } = item
21 | const width = $el.offsetWidth
22 | const height = $el.offsetHeight
23 |
24 | if (lastWidth !== width || lastHeight !== height) {
25 | item.lastWidth = width
26 | item.lastHeight = height
27 |
28 | listener({ width, height })
29 | }
30 | }
31 |
32 | function watchSizeForIE9($el, listener) {
33 | const item = {
34 | $el,
35 | listener,
36 | lastWidth: null,
37 | lastHeight: null,
38 | }
39 | const unwatch = () => {
40 | removeFromArray(registered, item)
41 | if (!registered.length) stop()
42 | }
43 |
44 | registered.push(item)
45 | // The original watch-size will call the listener on initialization.
46 | // Keep the same behavior here.
47 | test(item)
48 | run()
49 |
50 | return unwatch
51 | }
52 |
53 | export function watchSize($el, listener) {
54 | // See: https://stackoverflow.com/a/31293352
55 | const isIE9 = document.documentMode === 9
56 | // watch-size will call the listener on initialization.
57 | // Disable this behavior with a lock to achieve a clearer code logic.
58 | let locked = true
59 | const wrappedListener = (...args) => locked || listener(...args)
60 | const implementation = isIE9
61 | ? watchSizeForIE9
62 | : watchSizeForBrowsersOtherThanIE9
63 | const removeSizeWatcher = implementation($el, wrappedListener)
64 | locked = false // unlock after initialization
65 |
66 | return removeSizeWatcher
67 | }
68 |
--------------------------------------------------------------------------------
/test/unit/specs/SearchInput.spec.js:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 | import { findInput, typeSearchText } from './shared'
3 | import Treeselect from '@src/components/Treeselect'
4 |
5 | describe('Search Input', () => {
6 | it('should disable auto complete', () => {
7 | const wrapper = mount(Treeselect, {
8 | propsData: {
9 | options: [],
10 | },
11 | })
12 | const input = findInput(wrapper)
13 | expect(input.element.getAttribute('autocomplete')).toBe('off')
14 | })
15 |
16 | it('should be unable to focus when disabled=true', () => {
17 | const wrapper = mount(Treeselect, {
18 | propsData: {
19 | options: [],
20 | autoFocus: false,
21 | searchable: true,
22 | disabled: true,
23 | },
24 | })
25 |
26 | expect(wrapper.vm.trigger.isFocused).toBe(false)
27 | wrapper.vm.focusInput()
28 | expect(wrapper.vm.trigger.isFocused).toBe(false)
29 | })
30 |
31 | it('when multiple=true, input should fit the width of user-input text', async () => {
32 | const wrapper = mount(Treeselect, {
33 | propsData: {
34 | options: [],
35 | multiple: true,
36 | searchable: true,
37 | },
38 | attachToDocument: true,
39 | sync: false,
40 | })
41 | const input = findInput(wrapper)
42 | const fullText = 'hello world'
43 | let i = 0
44 | let prevWidth = input.element.offsetWidth
45 |
46 | expect(prevWidth).toBeGreaterThan(0)
47 |
48 | while (i < fullText.length) {
49 | await typeSearchText(wrapper, fullText.slice(0, i += 3))
50 | const width = input.element.offsetWidth
51 | expect(width).toBeGreaterThan(prevWidth)
52 | prevWidth = width
53 | }
54 |
55 | while (i > 0) {
56 | await typeSearchText(wrapper, fullText.slice(0, i -= 3))
57 | const width = input.element.offsetWidth
58 | expect(width).toBeLessThan(prevWidth)
59 | prevWidth = width
60 | }
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/docs/main.js:
--------------------------------------------------------------------------------
1 | import 'script-loader!../static/prism.min.js'
2 | import 'regenerator-runtime/runtime'
3 | import 'yaku/lib/global'
4 | import Vue from 'vue'
5 | import Treeselect from '../src'
6 |
7 | import './styles/docs.less'
8 | import './styles/prism.less'
9 |
10 | Vue.config.productionTip = false
11 | Vue.component('treeselect', Treeselect)
12 |
13 | let sections
14 |
15 | function calculateNavPositions() {
16 | sections = [].map.call(document.querySelectorAll('[data-section]'), section => ({
17 | id: section.id,
18 | offset: section.getBoundingClientRect().top + window.pageYOffset - 50,
19 | }))
20 | }
21 |
22 | function loadComponents() {
23 | const loadContext = context => {
24 | context.keys().forEach(key => {
25 | const componentName = key.replace(/^\.\/|\.vue$/g, '')
26 | const component = context(key).default
27 |
28 | Vue.component(componentName, component)
29 | })
30 | }
31 |
32 | loadContext(require.context('./components', false, /\.vue$/))
33 |
34 | if (process.env.NODE_ENV !== 'production') {
35 | loadContext(require.context('./components/dev', false, /\.vue$/))
36 | }
37 | }
38 | loadComponents()
39 |
40 | new Vue({
41 | el: '#app',
42 |
43 | data: () => ({
44 | currentPosition: '',
45 | isNavSticky: false,
46 | }),
47 |
48 | mounted() {
49 | this.adjustNav()
50 | window.addEventListener('scroll', this.adjustNav)
51 | setTimeout(calculateNavPositions, 1000)
52 | },
53 |
54 | methods: {
55 | adjustNav() {
56 | const sidebar = document.getElementById('sidebar')
57 | const scrollTop = document.documentElement.scrollTop || document.body.scrollTop
58 | const offset = sidebar.getBoundingClientRect().top + window.pageYOffset
59 | this.isNavSticky = scrollTop > offset
60 | if (!sections) calculateNavPositions()
61 | for (let i = sections.length - 1; i >= 0; i--) {
62 | if (scrollTop > sections[i].offset || i === 0) {
63 | this.currentPosition = sections[i].id
64 | break
65 | }
66 | }
67 | },
68 | },
69 | })
70 |
--------------------------------------------------------------------------------
/docs/mixins.pug:
--------------------------------------------------------------------------------
1 | - function kebabCase(path) {
2 | - return path
3 | - .replace(/ & /g, ' And ')
4 | - .replace(/[A-Z]+/, x => x[0] + x.slice(1).toLowerCase())
5 | - .replace(/^[A-Z]/, x => x.toLowerCase())
6 | - .replace(/[A-Z ]/g, x => '-' + x.toLowerCase())
7 | - .replace(/[^A-Za-z]/g, '-')
8 | - .replace(/(^-+|-+$)/g, '')
9 | - .replace(/-{2,}/g, '-')
10 | - }
11 | - function startCase(name) {
12 | - return name.replace(/\s/g, '')
13 | - }
14 | - function loadSFCSource(name, type) {
15 | - const stripIndent = require('strip-indent')
16 | - const source = require(`!raw-loader!./components/${name}.vue`).default
17 | - const startTag = `<${type}>`
18 | - const endTag = `${type}>`
19 | - const start = source.indexOf(startTag) + startTag.length
20 | - const end = source.indexOf(endTag)
21 | - return stripIndent(source.slice(start, end).replace(/^\n+/, ''))
22 | - }
23 | - const toc = []
24 |
25 | mixin section(name)
26 | - toc.push({ name, subs: [] })
27 | h1(id=kebabCase(name))
28 | =name
29 | a.anchor(href='#' + kebabCase(name))
30 | anchor
31 |
32 | mixin subsection(name)
33 | - toc[toc.length - 1].subs.push(name)
34 | section
35 | h2(id=kebabCase(name) data-section)
36 | =name
37 | a.anchor(href='#' + kebabCase(name))
38 | anchor
39 | block
40 |
41 | mixin component(name)
42 | #{kebabCase(name)}
43 |
44 | mixin demo(name)
45 | demo
46 | #{kebabCase(name)}(slot="demo")
47 | template(slot="html")
48 | code(v-pre)=loadSFCSource(name, 'template')
49 | template(slot="javascript")
50 | code(v-pre)=loadSFCSource(name, 'script')
51 |
52 | mixin renderToc()
53 | - const tocSwapBuffer = pug_html
54 | - pug_html = ''
55 | for section in toc
56 | section.sidebar-nav-section
57 | h4.sidebar-nav-section-title=section.name
58 | ul.sidebar-nav-list
59 | for sub in section.subs
60 | - const customClass = "{ current: currentPosition === '" + kebabCase(sub) + "' }"
61 | li.sidebar-nav-list-item(:class=customClass)
62 | a(href='#' + kebabCase(sub))=sub
63 | - pug_html = tocSwapBuffer.replace('%%_TOC_%%', pug_html)
64 |
--------------------------------------------------------------------------------
/docs/styles/prism.less:
--------------------------------------------------------------------------------
1 | @import "~material-colors/dist/colors.less";
2 |
3 | /**
4 | * prism.js default theme for JavaScript, CSS and HTML
5 | * Based on dabblet (http://dabblet.com)
6 | * Modified by Riophae Lee
7 | * @author Lea Verou
8 | */
9 |
10 | code[class*="language-"],
11 | pre[class*="language-"] {
12 | box-sizing: border-box;
13 | background: none;
14 | text-align: left;
15 | white-space: pre;
16 | word-spacing: normal;
17 | word-break: normal;
18 | word-wrap: normal;
19 | line-height: 1.5;
20 | font-size: 12px;
21 | tab-size: 2;
22 | hyphens: none;
23 | }
24 |
25 | /* Code blocks */
26 | pre[class*="language-"] {
27 | padding: 1.2em 1.4em;
28 | margin: 1.2em 0;
29 | overflow: auto;
30 | background: #f8f8f8;
31 | border-radius: 5px;
32 | }
33 |
34 | /* Inline code */
35 | :not(pre) > code[class*="language-"] {
36 | padding: 0.1em;
37 | border-radius: 0.3em;
38 | white-space: normal;
39 | }
40 |
41 | .token.comment,
42 | .token.prolog,
43 | .token.doctype,
44 | .token.cdata {
45 | color: @md-blue-grey-300;
46 | }
47 |
48 | .token.punctuation {
49 | color: #999;
50 | }
51 |
52 | .namespace {
53 | opacity: 0.7;
54 | }
55 |
56 | .token.property,
57 | .token.tag,
58 | .token.boolean,
59 | .token.number,
60 | .token.constant,
61 | .token.symbol,
62 | .token.deleted {
63 | color: @md-light-blue-800;
64 | }
65 |
66 | .token.selector,
67 | .token.attr-name,
68 | .token.string,
69 | .token.char,
70 | .token.builtin,
71 | .token.inserted {
72 | color: @md-green-700;
73 | }
74 |
75 | .token.operator,
76 | .token.entity,
77 | .token.url,
78 | .language-css .token.string,
79 | .style .token.string {
80 | color: #a67f59;
81 | }
82 |
83 | .token.atrule,
84 | .token.attr-value,
85 | .token.keyword {
86 | color: @md-orange-900;
87 | }
88 |
89 | .token.function {
90 | color: @md-red-900;
91 | }
92 |
93 | .token.regex,
94 | .token.important,
95 | .token.variable {
96 | color: #e90;
97 | }
98 |
99 | .token.important,
100 | .token.bold {
101 | font-weight: bold;
102 | }
103 |
104 | .token.italic {
105 | font-style: italic;
106 | }
107 |
108 | .token.entity {
109 | cursor: help;
110 | }
111 |
--------------------------------------------------------------------------------
/docs/components/DelayedLoading.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
77 |
--------------------------------------------------------------------------------
/docs/components/NestedSearch.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
82 |
--------------------------------------------------------------------------------
/docs/components/MoreFeatures.vue:
--------------------------------------------------------------------------------
1 |
2 |
43 |
44 |
45 |
76 |
--------------------------------------------------------------------------------
/test/unit/specs/HiddenFields.spec.js:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 | import Treeselect from '@src/components/Treeselect'
3 |
4 | describe('Hidden Fields', () => {
5 | let wrapper
6 |
7 | beforeEach(() => {
8 | wrapper = mount(Treeselect, {
9 | propsData: {
10 | options: [],
11 | },
12 | })
13 | })
14 |
15 | const getHiddenFields = () => wrapper.findAll('input[type="hidden"]')
16 |
17 | it('must have value & name', () => {
18 | wrapper.setProps({ value: 'value' })
19 | expect(getHiddenFields().length).toBe(0)
20 |
21 | wrapper.setProps({ value: null, name: 'test' })
22 | expect(getHiddenFields().length).toBe(0)
23 |
24 | wrapper.setProps({ value: 'value', name: 'test' })
25 | expect(getHiddenFields().length).not.toBe(0)
26 | })
27 |
28 | it('single-select mode', () => {
29 | wrapper.setProps({ name: 'single', value: 'value' })
30 | const hiddenFields = getHiddenFields()
31 | expect(hiddenFields.length).toBe(1)
32 | expect(hiddenFields.at(0).html()).toBe(' ')
33 | })
34 |
35 | it('multi-select mode', () => {
36 | wrapper.setProps({ name: 'multiple', multiple: true, value: [ 1, 2, 3 ] })
37 | const hiddenFields = getHiddenFields()
38 | expect(hiddenFields.length).toBe(3)
39 | expect(hiddenFields.wrappers.map(hf => hf.html())).toEqual([
40 | ' ',
41 | ' ',
42 | ' ',
43 | ])
44 | })
45 |
46 | it('join values', () => {
47 | wrapper.setProps({ name: 'join-values', multiple: true, value: [ 'a', 'b', 'c' ], joinValues: true })
48 | const hiddenFields = getHiddenFields()
49 | expect(hiddenFields.length).toBe(1)
50 | expect(hiddenFields.at(0).html()).toBe(' ')
51 | })
52 |
53 | it('delimiter', async () => {
54 | wrapper.setProps({ name: 'delimiter', multiple: true, value: [ 1, 2, 3 ], joinValues: true, delimiter: ';' })
55 | await wrapper.vm.$nextTick()
56 | const hiddenFields = getHiddenFields()
57 | expect(hiddenFields.length).toBe(1)
58 | expect(hiddenFields.at(0).html()).toBe(' ')
59 | })
60 |
61 | it('disabled', async () => {
62 | wrapper.setProps({ name: 'disabled', value: 'value', disabled: true })
63 | await wrapper.vm.$nextTick()
64 | const hiddenFields = getHiddenFields()
65 | expect(hiddenFields.length).toBe(0)
66 | })
67 | })
68 |
--------------------------------------------------------------------------------
/docs/components/DocNode.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Available properties of a node object.
4 |
5 |
6 |
7 | Key
8 | Type
9 | Description
10 |
11 |
12 |
13 |
14 | id (required)
15 | Number | String
16 | Used to identify the option within the tree. Its value must be unique across all options.
17 |
18 |
19 | label (required)
20 | String
21 | Used to display the option.
22 |
23 |
24 | children
25 | node[] | null
26 | Declares a branch node. You can:1) Set to an array of children options consisting of a. leaf nodes, b. branch nodes, or c. a mixture of these two. or 2) Set to empty array for no children options. or 3) Set to null to declare an unloaded branch node for delayed loading . You can reassign an array (regardless of whether it's empty or not) later in loadOptions() to register these children options, and mark this branch node as loaded . If you want to declare a leaf node, set children: undefined or simply omit this property.
27 |
28 |
29 | isDisabled
30 | Boolean
31 | Used for disabling item selection. See here for detailed information.
32 |
33 |
34 | isNew
35 | Boolean
36 | Used for giving new nodes a different color.
37 |
38 |
39 | isDefaultExpanded
40 | Boolean
41 | Whether this folder option should be expanded by default.
42 |
43 |
44 |
45 |
The value of label, children or isDisabled can be reassigned anytime.
46 |
Add more properties than the listed ones IS okay. You can even use these extra properties in your custom template by accessing node.raw.xxx.
47 |
48 |
49 |
--------------------------------------------------------------------------------
/docs/partials/getting-started.pug:
--------------------------------------------------------------------------------
1 | +section('Getting Started')
2 |
3 | section
4 | :markdown-it
5 | It's recommended to install vue-treeselect via npm, and build your app using a bundler like [webpack](https://webpack.js.org/).
6 |
7 | ```bash
8 | npm install --save @riophae/vue-treeselect
9 | ```
10 |
11 | :markdown-it
12 | This example shows how to integrate vue-treeselect with your [Vue SFCs](https://vuejs.org/v2/guide/single-file-components.html).
13 |
14 | ```vue
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
57 | ```
58 |
59 | If you just don't want to use webpack or any other bundlers, you can simply include the standalone UMD build in your page. In this way, make sure Vue as a dependency is included before vue-treeselect.
60 |
61 | ```html
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
105 |
106 | ```
107 |
--------------------------------------------------------------------------------
/docs/index.pug:
--------------------------------------------------------------------------------
1 | include ./mixins
2 |
3 | doctype html
4 | html(lang="en")
5 | head
6 | meta(charset="utf-8")
7 | meta(name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0")
8 | title Vue-Treeselect
9 | meta(name="description" content="A multi-select component with nested options support for Vue.js")
10 | meta(property="og:title" content="Vue-Treeselect")
11 | meta(property="og:site_name" content="Vue-Treeselect")
12 | meta(property="og:url" content="https://vue-treeselect.js.org/")
13 | meta(property="og:description" content="A multi-select component with nested options support for Vue.js")
14 | meta(property="og:image" content="https://vue-treeselect.js.org/static/vue-logo.png")
15 | meta(property="twitter:title" content="Vue-Treeselect")
16 | meta(property="twitter:description" content="A multi-select component with nested options support for Vue.js")
17 | meta(property="twitter:image" content="https://vue-treeselect.js.org/static/vue-logo.png")
18 | link(rel="apple-touch-icon" sizes="57x57" href="/static/images/icons/apple-icon-57x57.png")
19 | link(rel="apple-touch-icon" sizes="60x60" href="/static/images/icons/apple-icon-60x60.png")
20 | link(rel="apple-touch-icon" sizes="72x72" href="/static/images/icons/apple-icon-72x72.png")
21 | link(rel="apple-touch-icon" sizes="76x76" href="/static/images/icons/apple-icon-76x76.png")
22 | link(rel="apple-touch-icon" sizes="114x114" href="/static/images/icons/apple-icon-114x114.png")
23 | link(rel="apple-touch-icon" sizes="120x120" href="/static/images/icons/apple-icon-120x120.png")
24 | link(rel="apple-touch-icon" sizes="144x144" href="/static/images/icons/apple-icon-144x144.png")
25 | link(rel="apple-touch-icon" sizes="152x152" href="/static/images/icons/apple-icon-152x152.png")
26 | link(rel="apple-touch-icon" sizes="180x180" href="/static/images/icons/apple-icon-180x180.png")
27 | link(rel="icon" type="image/png" sizes="192x192" href="/static/images/icons/android-icon-192x192.png")
28 | link(rel="icon" type="image/png" sizes="32x32" href="/static/images/icons/favicon-32x32.png")
29 | link(rel="icon" type="image/png" sizes="96x96" href="/static/images/icons/favicon-96x96.png")
30 | link(rel="icon" type="image/png" sizes="16x16" href="/static/images/icons/favicon-16x16.png")
31 | meta(name="msapplication-TileImage" content="/static/images/icons/ms-icon-144x144.png")
32 | link(rel="icon" href="/static/vue-logo.png" type="image/png")
33 | meta(name="msapplication-TileColor" content="#4fc08d")
34 | meta(name="theme-color" content="#4fc08d")
35 | meta(name="msapplication-config" content="browserconfig.xml")
36 | if NODE_ENV === 'production'
37 | // Global site tag (gtag.js) - Google Analytics
38 | script(async src="https://www.googletagmanager.com/gtag/js?id=UA-108169693-1")
39 | script.
40 | window.dataLayer = window.dataLayer || [];
41 | function gtag(){dataLayer.push(arguments);}
42 | gtag('js', new Date());
43 | gtag('config', 'UA-108169693-1');
44 | link(rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600|Dosis:300,500")
45 | body
46 | #app
47 | header.site-header
48 | div#header.container
49 | div.site-header-logo
50 | a.site-header-logo-link(href="https://vue-treeselect.js.org/")
51 | | Vue
52 | span.site-header-logo-component-name Treeselect
53 | nav.site-header-nav
54 | a.site-header-nav-item(href="https://github.com/riophae/vue-treeselect/releases")
55 | =`v${require('../package.json').version}`
56 | span.site-header-nav-item
57 | a.github-button(href="https://github.com/riophae/vue-treeselect" data-show-count="true" aria-label="Star riophae/vue-treeselect on GitHub")
58 | | Star
59 | div.container
60 | section#main
61 | div#sidebar
62 | include ./partials/sidebar
63 | div#content
64 | include ./partials/dev
65 | include ./partials/introduction
66 | include ./partials/getting-started
67 | include ./partials/guides
68 | include ./partials/api
69 | footer.site-footer
70 | div.container
71 | | Copyright © 2017-#{new Date().getFullYear()} Riophae Lee . MIT Licensed.
72 | script(src="https://buttons.github.io/buttons.js" async defer)
73 |
74 | +renderToc()
75 |
--------------------------------------------------------------------------------
/src/components/Control.vue:
--------------------------------------------------------------------------------
1 |
154 |
--------------------------------------------------------------------------------
/test/unit/specs/Menu.spec.js:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 | import {
3 | generateOptions,
4 | leftClick, typeSearchText,
5 | findMenu, findOptionByNodeId,
6 | } from './shared'
7 | import Treeselect from '@src/components/Treeselect'
8 | import Input from '@src/components/Input'
9 |
10 | describe('Menu', () => {
11 | it('should blur the input & close the menu after clicking anywhere outside the component', async () => {
12 | const wrapper = mount(Treeselect, {
13 | sync: false,
14 | attachToDocument: true,
15 | propsData: {
16 | options: [],
17 | },
18 | })
19 | const { vm } = wrapper
20 |
21 | vm.openMenu()
22 | await vm.$nextTick()
23 |
24 | const event = document.createEvent('event')
25 | event.initEvent('mousedown', true, true)
26 | document.body.dispatchEvent(event)
27 | expect(vm.trigger.isFocused).toBe(false)
28 | expect(vm.menu.isOpen).toBe(false)
29 | })
30 |
31 | it('should open the menu after clicking the control when focused', () => {
32 | const wrapper = mount(Treeselect, {
33 | attachToDocument: true,
34 | propsData: {
35 | options: [],
36 | },
37 | })
38 | wrapper.vm.trigger.isFocused = true
39 | const valueContainer = wrapper.find('.vue-treeselect__value-container')
40 |
41 | leftClick(valueContainer)
42 | expect(wrapper.vm.menu.isOpen).toBe(true)
43 | })
44 |
45 | it('click on option arrow should toggle expanded', async () => {
46 | const wrapper = mount(Treeselect, {
47 | sync: false,
48 | attachToDocument: true,
49 | propsData: {
50 | options: [ {
51 | id: 'a',
52 | label: 'a',
53 | children: [],
54 | } ],
55 | },
56 | })
57 | const { vm } = wrapper
58 | const { a } = vm.forest.nodeMap
59 |
60 | vm.openMenu()
61 | await vm.$nextTick()
62 |
63 | expect(a.isExpanded).toBe(false)
64 | const optionArrow = findOptionByNodeId(wrapper, 'a').find('.vue-treeselect__option-arrow-container')
65 | leftClick(optionArrow)
66 | expect(a.isExpanded).toBe(true)
67 | leftClick(optionArrow)
68 | expect(a.isExpanded).toBe(false)
69 | })
70 |
71 | it('should highlight the option when the cursor hovering over it', async () => {
72 | const wrapper = mount(Treeselect, {
73 | propsData: {
74 | options: [ {
75 | id: 'a',
76 | label: 'a',
77 | }, {
78 | id: 'b',
79 | label: 'b',
80 | } ],
81 | },
82 | })
83 | const { vm } = wrapper
84 |
85 | vm.openMenu()
86 | await vm.$nextTick()
87 |
88 | expect(vm.menu.current).toBe('a')
89 | expect(vm.forest.nodeMap.a.isHighlighted).toBe(true)
90 |
91 | findOptionByNodeId(wrapper, 'b').trigger('mouseenter')
92 | expect(vm.menu.current).toBe('b')
93 | expect(vm.forest.nodeMap.b.isHighlighted).toBe(true)
94 |
95 | findOptionByNodeId(wrapper, 'a').trigger('mouseenter')
96 | expect(vm.menu.current).toBe('a')
97 | expect(vm.forest.nodeMap.a.isHighlighted).toBe(true)
98 | })
99 |
100 | it('retain scroll position on menu reopen', async () => {
101 | const maxHeight = 100
102 | const wrapper = mount(Treeselect, {
103 | propsData: {
104 | options: generateOptions(3),
105 | defaultExpandLevel: Infinity,
106 | maxHeight,
107 | },
108 | attachToDocument: true,
109 | })
110 | const { vm } = wrapper
111 |
112 | let i = 3
113 | let pos = 0
114 | const step = 20
115 | while (i--) {
116 | vm.openMenu()
117 | await vm.$nextTick()
118 | const menu = findMenu(wrapper)
119 | expect(menu.element.scrollHeight).toBeGreaterThan(maxHeight)
120 | expect(menu.element.scrollTop).toBe(pos)
121 | pos += step
122 | menu.element.scrollBy(0, step)
123 | vm.closeMenu()
124 | await vm.$nextTick()
125 | }
126 | })
127 |
128 | it('should reset the search box after closing menu', async () => {
129 | const wrapper = mount(Treeselect, {
130 | sync: false,
131 | propsData: {
132 | options: [],
133 | },
134 | })
135 | const { vm } = wrapper
136 | const input = wrapper.find(Input)
137 |
138 | const assertInputValue = expected => {
139 | expect(vm.trigger.searchQuery).toBe(expected)
140 | expect(input.vm.value).toBe(expected)
141 | expect(input.find('input[type="text"]').element.value).toBe(expected)
142 | }
143 |
144 | vm.openMenu()
145 | await vm.$nextTick()
146 |
147 | await typeSearchText(wrapper, 'a')
148 | assertInputValue('a')
149 |
150 | vm.closeMenu()
151 | await vm.$nextTick()
152 | assertInputValue('')
153 | })
154 |
155 | it('set appendToBody to true when alwaysOpen=true should not throw error', () => {
156 | const wrapper = mount(Treeselect, {
157 | attachToDocument: true,
158 | propsData: {
159 | options: [],
160 | alwaysOpen: true,
161 | },
162 | })
163 |
164 | wrapper.setProps({ appendToBody: true })
165 | })
166 | })
167 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@riophae/vue-treeselect",
3 | "version": "0.4.0",
4 | "description": "A multi-select component with nested options support for Vue.js",
5 | "author": "Riophae Lee ",
6 | "license": "MIT",
7 | "homepage": "https://vue-treeselect.js.org/",
8 | "repository": "riophae/vue-treeselect",
9 | "main": "dist/vue-treeselect.cjs.js",
10 | "unpkg": "dist/vue-treeselect.umd.min.js",
11 | "css": "dist/vue-treeselect.min.css",
12 | "files": [
13 | "src",
14 | "dist"
15 | ],
16 | "scripts": {
17 | "dev": "node build/dev-server.js",
18 | "build-library": "node build/build-library.js",
19 | "build-docs": "rimraf gh-pages && mkdir gh-pages && node build/build-docs.js",
20 | "preview-docs": "http-server gh-pages",
21 | "gh-pages": "npm run build-docs && gh-pages --dist gh-pages --branch gh-pages --dotfiles",
22 | "cleanup-cov": "rimraf test/unit/coverage",
23 | "unit": "npm run cleanup-cov && karma start test/unit/karma.conf.js --watch",
24 | "testonly": "npm run cleanup-cov && karma start test/unit/karma.config.js --single-run",
25 | "test": "npm run testonly",
26 | "pretest": "npm run lint",
27 | "lint:js": "eslint --ext .js --ext .vue --cache --cache-location node_modules/.cache/eslint --rule 'no-console: 2' .",
28 | "lint:css": "stylelint '**/*.less'",
29 | "lint": "npm run lint:js && npm run lint:css",
30 | "verify-builds": "size-limit && node build/verify-builds.js",
31 | "finish": "npm test && npm run build-library && npm run verify-builds"
32 | },
33 | "pre-commit": "lint",
34 | "dependencies": {
35 | "@babel/runtime": "^7.3.1",
36 | "babel-helper-vue-jsx-merge-props": "^2.0.3",
37 | "easings-css": "^1.0.0",
38 | "fuzzysearch": "^1.0.3",
39 | "is-promise": "^2.1.0",
40 | "lodash": "^4.0.0",
41 | "material-colors": "^1.2.6",
42 | "watch-size": "^2.0.0"
43 | },
44 | "devDependencies": {
45 | "@babel/core": "^7.0.0",
46 | "@babel/plugin-syntax-jsx": "^7.0.0",
47 | "@babel/plugin-transform-runtime": "^7.0.0",
48 | "@babel/preset-env": "^7.3.1",
49 | "@size-limit/preset-big-lib": "^2.0.2",
50 | "@vue/test-utils": "1.0.0-beta.16",
51 | "autoprefixer": "^9.4.6",
52 | "babel-eslint": "^10.0.1",
53 | "babel-loader": "^8.0.0",
54 | "babel-plugin-istanbul": "^5.0.1",
55 | "babel-plugin-transform-vue-jsx": "^4.0.1",
56 | "cache-loader": "^4.0.1",
57 | "chalk": "^2.3.2",
58 | "codecov": "^3.0.0",
59 | "connect-history-api-fallback": "^1.5.0",
60 | "copy-webpack-plugin": "^5.0.3",
61 | "css-loader": "^3.0.0",
62 | "entities": "^2.0.0",
63 | "eslint": "^6.1.0",
64 | "eslint-config-riophae": "^0.10.0",
65 | "eslint-friendly-formatter": "^4.0.0",
66 | "eslint-import-resolver-webpack": "^0.11.0",
67 | "eslint-loader": "^3.0.0",
68 | "eslint-plugin-import": "^2.15.0",
69 | "eslint-plugin-react": "^7.10.0",
70 | "eslint-plugin-unicorn": "^12.1.0",
71 | "eslint-plugin-vue": "^6.0.0",
72 | "eventsource-polyfill": "^0.9.6",
73 | "express": "^4.16.3",
74 | "friendly-errors-webpack-plugin": "^1.7.0",
75 | "gh-pages": "^2.0.0",
76 | "html-webpack-plugin": "^3.1.0",
77 | "http-server": "^0.11.1",
78 | "jasmine-core": "^3.1.0",
79 | "jstransformer-markdown-it": "^2.0.0",
80 | "karma": "^4.0.0",
81 | "karma-chrome-launcher": "^3.0.0",
82 | "karma-coverage": "^2.0.1",
83 | "karma-jasmine": "^2.0.0",
84 | "karma-jasmine-matchers": "^4.0.1",
85 | "karma-sourcemap-loader": "^0.3.7",
86 | "karma-spec-reporter": "0.0.32",
87 | "karma-webpack": "^4.0.2",
88 | "less": "^3.0.1",
89 | "less-loader": "^5.0.0",
90 | "mini-css-extract-plugin": "^0.8.0",
91 | "open": "^7.0.0",
92 | "optimize-css-assets-webpack-plugin": "^5.0.0",
93 | "ora": "^4.0.1",
94 | "postcss-loader": "^3.0.0",
95 | "pre-commit": "^1.2.2",
96 | "pug": "^2.0.0",
97 | "pug-loader": "^2.4.0",
98 | "puppeteer": "^2.0.0",
99 | "raw-loader": "^3.0.0",
100 | "regenerator-runtime": "^0.13.1",
101 | "rimraf": "^3.0.0",
102 | "run-series": "^1.1.8",
103 | "script-loader": "^0.7.2",
104 | "shallow-equal": "^1.0.0",
105 | "shelljs": "^0.8.1",
106 | "string.prototype.repeat": "^0.2.0",
107 | "strip-indent": "^3.0.0",
108 | "stylelint": "^11.0.0",
109 | "stylelint-config-xo-space": "^0.13.0",
110 | "terser-webpack-plugin": "^2.1.0",
111 | "url-loader": "^2.0.1",
112 | "vodal": "^2.3.3",
113 | "vue": "^2.2.0",
114 | "vue-loader": "^15.6.0",
115 | "vue-style-loader": "^4.0.2",
116 | "vue-template-compiler": "^2.4.4",
117 | "vuex": "^3.0.1",
118 | "webpack": "^4.6.0",
119 | "webpack-bundle-analyzer": "^3.0.2",
120 | "webpack-cdn-plugin": "^3.1.4",
121 | "webpack-dev-middleware": "^3.1.0",
122 | "webpack-hot-middleware": "^2.18.0",
123 | "webpack-merge": "^4.1.0",
124 | "webpack-node-externals": "^1.7.2",
125 | "yaku": "^0.19.3"
126 | },
127 | "peerDependencies": {
128 | "vue": "^2.2.0"
129 | },
130 | "keywords": [
131 | "vue",
132 | "component",
133 | "tree",
134 | "treeview",
135 | "select",
136 | "dropdown",
137 | "treeselect",
138 | "multiselect",
139 | "form",
140 | "control",
141 | "input",
142 | "ui"
143 | ]
144 | }
145 |
--------------------------------------------------------------------------------
/test/unit/specs/shared.js:
--------------------------------------------------------------------------------
1 | import sleep from 'yaku/lib/sleep'
2 | import Option from '@src/components/Option'
3 | import { INPUT_DEBOUNCE_DELAY } from '@src/constants'
4 |
5 | export function $(selector, context = document) {
6 | return context.querySelector(selector)
7 | }
8 |
9 | function createArray(len, fn) {
10 | const arr = []
11 | let i = 0
12 | while (i < len) arr.push(fn(i++))
13 | return arr
14 | }
15 |
16 | export function generateOptions(maxLevel) {
17 | const generate = (i, level) => {
18 | const id = String.fromCharCode(97 + i).repeat(level)
19 | const option = { id, label: id.toUpperCase() }
20 | if (level < maxLevel) option.children = [ generate(i, level + 1) ]
21 | return option
22 | }
23 |
24 | return createArray(maxLevel, i => generate(i, 1))
25 | }
26 |
27 | function createKeyObject(keyCode) {
28 | return { which: keyCode, keyCode }
29 | }
30 |
31 | export function leftClick(wrapper) {
32 | const MOUSE_BUTTON_LEFT = { button: 0 }
33 | wrapper.trigger('mousedown', MOUSE_BUTTON_LEFT)
34 | }
35 |
36 | export function pressBackspaceKey(wrapper) {
37 | const input = findInput(wrapper)
38 | const KEY_BACKSPACE = createKeyObject(8)
39 | input.trigger('keydown', KEY_BACKSPACE)
40 | }
41 |
42 | export function pressEnterKey(wrapper) {
43 | const input = findInput(wrapper)
44 | const KEY_ENTER = createKeyObject(13)
45 | input.trigger('keydown', KEY_ENTER)
46 | }
47 |
48 | export function pressEscapeKey(wrapper, modifierKey) {
49 | const input = findInput(wrapper)
50 | const KEY_ESCAPE = createKeyObject(27)
51 | let eventData = KEY_ESCAPE
52 | if (modifierKey) eventData = { ...KEY_ESCAPE, [modifierKey]: true }
53 | input.trigger('keydown', eventData)
54 | }
55 |
56 | export function pressEndKey(wrapper) {
57 | const input = findInput(wrapper)
58 | const KEY_END = createKeyObject(35)
59 | input.trigger('keydown', KEY_END)
60 | }
61 |
62 | export function pressHomeKey(wrapper) {
63 | const input = findInput(wrapper)
64 | const KEY_HOME = createKeyObject(36)
65 | input.trigger('keydown', KEY_HOME)
66 | }
67 |
68 | export function pressArrowLeft(wrapper) {
69 | const input = findInput(wrapper)
70 | const ARROW_LEFT = createKeyObject(37)
71 | input.trigger('keydown', ARROW_LEFT)
72 | }
73 |
74 | export function pressArrowUp(wrapper) {
75 | const input = findInput(wrapper)
76 | const ARROW_UP = createKeyObject(38)
77 | input.trigger('keydown', ARROW_UP)
78 | }
79 |
80 | export function pressArrowRight(wrapper) {
81 | const input = findInput(wrapper)
82 | const ARROW_RIGHT = createKeyObject(39)
83 | input.trigger('keydown', ARROW_RIGHT)
84 | }
85 |
86 | export function pressArrowDown(wrapper) {
87 | const input = findInput(wrapper)
88 | const ARROW_DOWN = createKeyObject(40)
89 | input.trigger('keydown', ARROW_DOWN)
90 | }
91 |
92 | export function pressDeleteKey(wrapper) {
93 | const input = findInput(wrapper)
94 | const KEY_DELETE = createKeyObject(46)
95 | input.trigger('keydown', KEY_DELETE)
96 | }
97 |
98 | export function pressAKey(wrapper) {
99 | const input = findInput(wrapper)
100 | const KEY_A = createKeyObject(65)
101 | input.trigger('keydown', KEY_A)
102 | }
103 |
104 | export async function typeSearchText(wrapper, text) {
105 | const $input = findInput(wrapper)
106 | $input.element.value = text
107 | $input.trigger('input')
108 | expect(wrapper.vm.$refs.control.$refs['value-container'].$refs.input.value).toBe(text)
109 | await sleep(INPUT_DEBOUNCE_DELAY + 1)
110 | expect(wrapper.vm.trigger.searchQuery).toBe(text)
111 | }
112 |
113 | export function findInputContainer(wrapper) {
114 | return wrapper.find('.vue-treeselect__input-container')
115 | }
116 |
117 | export function findInput(wrapper) {
118 | return wrapper.find('.vue-treeselect__input')
119 | }
120 |
121 | export function findMenuContainer(wrapper) {
122 | return wrapper.find('.vue-treeselect__menu-container')
123 | }
124 |
125 | export function findMenu(wrapper) {
126 | return wrapper.find('.vue-treeselect__menu')
127 | }
128 |
129 | export function findVisibleOptions(wrapper) {
130 | return wrapper.findAll('.vue-treeselect__option:not(.vue-treeselect__option--hide)')
131 | }
132 |
133 | export function findOptionByNodeId(wrapper, nodeId) {
134 | return wrapper.find(`.vue-treeselect__option[data-id="${nodeId}"]`)
135 | }
136 |
137 | export function findOptionArrowContainerByNodeId(wrapper, nodeId) {
138 | return findOptionByNodeId(wrapper, nodeId).find('.vue-treeselect__option-arrow-container')
139 | }
140 |
141 | export function findOptionArrowByNodeId(wrapper, nodeId) {
142 | return findOptionByNodeId(wrapper, nodeId).find('.vue-treeselect__option-arrow')
143 | }
144 |
145 | export function findCheckboxByNodeId(wrapper, nodeId) {
146 | return findOptionByNodeId(wrapper, nodeId).find('.vue-treeselect__checkbox')
147 | }
148 |
149 | export function findLabelContainerByNodeId(wrapper, nodeId) {
150 | return findOptionByNodeId(wrapper, nodeId).find('.vue-treeselect__label-container')
151 | }
152 |
153 | export function findLabelByNodeId(wrapper, nodeId) {
154 | return findOptionByNodeId(wrapper, nodeId).find('.vue-treeselect__label')
155 | }
156 |
157 | export function findChildrenOptionListByNodeId(wrapper, nodeId) {
158 | return wrapper.findAll(Option).wrappers
159 | .find(optionWrapper => optionWrapper.vm.node.id === nodeId)
160 | .find('.vue-treeselect__list')
161 | }
162 |
--------------------------------------------------------------------------------
/src/components/MenuPortal.vue:
--------------------------------------------------------------------------------
1 |
180 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue-treeselect
2 | [](https://www.npmjs.com/package/@riophae/vue-treeselect) [](https://circleci.com/gh/riophae/vue-treeselect/tree/master) [](https://codecov.io/gh/riophae/vue-treeselect?branch=master)
3 | 
4 |  [](https://snyk.io/test/npm/@riophae/vue-treeselect) 
5 |
6 | > A multi-select component with nested options support for Vue.js
7 |
8 | 
9 |
10 | ### Features
11 |
12 | - Single & multiple select with nested options support
13 | - Fuzzy matching
14 | - Async searching
15 | - Delayed loading (load data of deep level options only when needed)
16 | - Keyboard support (navigate using Arrow Up & Arrow Down keys, select option using Enter key, etc.)
17 | - Rich options & highly customizable
18 | - Supports a wide range of browsers (see [below](#browser-compatibility))
19 | - RTL support
20 |
21 | *Requires Vue 2.2+*
22 |
23 | ### Getting Started
24 |
25 | It's recommended to install vue-treeselect via npm, and build your app using a bundler like [webpack](https://webpack.js.org/).
26 |
27 | ```bash
28 | npm install --save @riophae/vue-treeselect
29 | ```
30 |
31 | This example shows how to integrate vue-treeselect with your [Vue SFCs](https://vuejs.org/v2/guide/single-file-components.html).
32 |
33 | ```vue
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
76 | ```
77 |
78 | If you just don't want to use webpack or any other bundlers, you can simply include the standalone UMD build in your page. In this way, make sure Vue as a dependency is included before vue-treeselect.
79 |
80 | ```html
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
124 |
125 | ```
126 |
127 | ### Documentation & Live Demo
128 |
129 | [Visit the website](https://vue-treeselect.js.org/)
130 |
131 | Note: please use a desktop browser since the website hasn't been optimized for mobile devices.
132 |
133 | ### Browser Compatibility
134 |
135 | - Chrome
136 | - Edge
137 | - Firefox
138 | - IE ≥ 9
139 | - Safari
140 |
141 | It should function well on IE9, but the style can be slightly broken due to the lack of support of some relatively new CSS features, such as `transition` and `animation`. Nevertheless it should look 90% same as on modern browsers.
142 |
143 | ### Bugs
144 |
145 | You can use this [pen](https://codepen.io/riophae/pen/MExgzP) to reproduce bugs and then [open an issue](https://github.com/riophae/vue-treeselect/issues/new).
146 |
147 | ### Contributing
148 |
149 | 1. Fork & clone the repo
150 | 2. Install dependencies by `yarn` or `npm install`
151 | 3. Check out a new branch
152 | 4. `npm run dev` & hack
153 | 5. Make sure `npm test` passes
154 | 6. Push your changes & file a pull request
155 |
156 | ### Credits
157 |
158 | This project is inspired by [vue-multiselect](https://github.com/monterail/vue-multiselect), [react-select](https://github.com/JedWatson/react-select) and [Ant Design](https://github.com/ant-design/ant-design/). Special thanks go to their respective authors!
159 |
160 | Some icons used in this project:
161 |
162 | - "link" icon made by [Smashicons](https://www.flaticon.com/authors/smashicons) is licensed under [CC 3.0 BY](https://creativecommons.org/licenses/by/3.0/)
163 | - "spinner" icon from [SpinKit](https://github.com/tobiasahlin/SpinKit) is licensed under the [MIT License](https://github.com/tobiasahlin/SpinKit/blob/master/LICENSE)
164 | - "caret" icon made by [Dave Gandy](https://www.flaticon.com/authors/dave-gandy) is licensed under [CC 3.0 BY](https://creativecommons.org/licenses/by/3.0/)
165 | - "delete" icon made by [Freepik](https://www.flaticon.com/authors/freepik) is licensed under [CC 3.0 BY](https://creativecommons.org/licenses/by/3.0/)
166 | - "checkmark symbol" & "minus symbol" icons made by [Catalin Fertu](https://www.flaticon.com/authors/catalin-fertu) are licensed under [CC 3.0 BY](https://creativecommons.org/licenses/by/3.0/)
167 |
168 | ### License
169 |
170 | Copyright (c) 2017-present [Riophae Lee](https://github.com/riophae).
171 |
172 | Released under the [MIT License](https://github.com/riophae/vue-treeselect/blob/master/LICENSE).
173 |
--------------------------------------------------------------------------------
/docs/partials/guides.pug:
--------------------------------------------------------------------------------
1 | +section('Guides')
2 |
3 | +subsection('Basic Features')
4 | :markdown-it
5 | This example demonstrates the most commonly-used features of vue-treeselect. Try the fuzzy matching functionality by typing a few letters.
6 | +demo('BasicFeatures')
7 | :markdown-it
8 | The first thing you need to learn is how to define options. There are two types of options: **a. folder options** that are foldable & *may* have children options, and **b. normal options** that aren't & don't. Here, I'd like to borrow the basic concepts from Computer Science and call the former as *branch nodes* & the latter as *leaf nodes*. These two kinds of nodes together compose the tree.
9 |
10 | Defining leaf nodes is quite simple:
11 |
12 | ```js
13 | {
14 | id: '', // used to identify the option within the tree so its value must be unique across all options
15 | label: '', // used to display the option
16 | }
17 | ```
18 |
19 | Defining branch nodes only needs an extra `children` property:
20 |
21 | ```js
22 | {
23 | id: '',
24 | label: '',
25 | children: [
26 | {
27 | id: '',
28 | label: '',
29 | },
30 | ...
31 | ],
32 | }
33 | ```
34 |
35 | Then you can pass an array of these nodes as the `options` prop. Note that, even if you assign an empty array to the `children` property, it's still considered to be a branch node. This is likely different from what you've learnt from Computer Science, in which a node with no children is commonly known as a leaf node.
36 |
37 | For information on all available properties in a `node` object, see [below](#node).
38 |
39 | +subsection('More Features')
40 | :markdown-it
41 | This demonstrates more features.
42 | +demo('MoreFeatures')
43 |
44 | +subsection('Delayed Loading')
45 | :markdown-it
46 | If you have a large number of deeply nested options, you might want to load options only of the most top levels on initial load, and load the rest only when needed. You can achieve that by following these steps:
47 |
48 | 1. Declare an *unloaded* branch node by setting `children: null`
49 | 2. Add `loadOptions` prop
50 | 3. Whenever an unloaded branch node gets expanded, `loadOptions({ action, parentNode, callback, instanceId })` will be called, then you can perform the job requesting data from a remote server
51 | +demo('DelayedLoading')
52 | :markdown-it
53 | It's also possible to have root level options to be delayed loaded. If no options have been initially registered (`options: null`), vue-treeselect will attempt to load root options by calling `loadOptions({ action, callback, instanceId })` after the component is mounted. In this example I have disabled the auto loading feature by setting `autoLoadRootOptions: false`, and the root options will be loaded when the menu is opened.
54 | +demo('DelayedRootOptions')
55 |
56 | +subsection('Async Searching')
57 | :markdown-it
58 | vue-treeselect supports dynamically loading & changing the entire options list as the user types. By default, vue-treeselect will cache the result of each AJAX request, thus the user could wait less.
59 | +demo('AsyncSearching')
60 |
61 | +subsection('Flat Mode & Sort Values')
62 | :markdown-it
63 | In all previous examples, we used the default non-flat mode of vue-treeselect, which means:
64 |
65 | 1. Whenever a branch node gets checked, all its children will be checked too
66 | 2. Whenever a branch node has all children checked, the branch node itself will be checked too
67 |
68 | Sometimes we don't need that mechanism, and want branch nodes & leaf nodes don't affect each other. In this case, flat mode should be used, as demonstrated in the following.
69 |
70 | If you want to control the order in which selected options to be displayed, use the `sortValueBy` prop. This prop has three options:
71 |
72 | - `"ORDER_SELECTED"` (default) - Order selected
73 | - `"LEVEL"` - Level of option: C 🡒 BB 🡒 AAA
74 | - `"INDEX"` - Index of option: AAA 🡒 BB 🡒 C
75 | +demo('FlatModeAndSortValues')
76 |
77 | +subsection('Prevent Value Combining')
78 | :markdown-it
79 | For non-flat & multi-select mode, if a branch node and its all descendants are checked, vue-treeselect will combine them into a single item in the value array, as demonstrated in the following example. By using `valueConsistsOf` prop you can change that behavior. This prop has four options:
80 |
81 | - `"ALL"` - Any node that is checked will be included in the `value` array
82 | - `"BRANCH_PRIORITY"` (default) - If a branch node is checked, all its descendants will be excluded in the `value` array
83 | - `"LEAF_PRIORITY"` - If a branch node is checked, this node itself and its branch descendants will be excluded from the `value` array but its leaf descendants will be included
84 | - `"ALL_WITH_INDETERMINATE"` - Any node that is checked will be included in the `value` array, plus indeterminate nodes
85 | +demo('PreventValueCombining')
86 |
87 | +subsection('Disable Branch Nodes')
88 | :markdown-it
89 | Set `disableBranchNodes: true` to make branch nodes uncheckable and treat them as collapsible folders only. You may also want to show a count next to the label of each branch node by setting `showCount: true`.
90 | +demo('DisableBranchNodes')
91 |
92 | +subsection('Flatten Search Results')
93 | :markdown-it
94 | Set `flattenSearchResults: true` to flatten the tree when searching. With this option set to `true`, only the results that match will be shown. With this set to `false` (default), its ancestors will also be displayed, even if they would not individually be included in the results.
95 | +demo('FlattenSearchResults')
96 |
97 | +subsection('Disable Item Selection')
98 | :markdown-it
99 | You can disable item selection by setting `isDisabled: true` on any leaf node or branch node. For non-flat mode, setting on a branch node will disable all its descendants as well.
100 | +demo('DisableItemSelection')
101 |
102 | +subsection('Nested Search')
103 | :markdown-it
104 | Sometimes we need the possibility to search options within a specific branch. For example your branches are different restaurants and the leafs are the foods they order. To search for the salad order of "McDonals" restaurant, just search for **"mc salad"**. You can also try searching **"salad"** to feel the difference.
105 |
106 | Concretely speaking, your search query gets split on spaces. If each splitted string is found within the path to the node, then we have a match.
107 | +demo('NestedSearch')
108 | p.tip
109 | :markdown-it(inline=true)
110 | Fuzzy matching functionality is disabled for this mode to avoid mismatching.
111 |
112 | +subsection('Customize Key Names')
113 | :markdown-it
114 | If your data of options is loaded via AJAX and have a different data structure than what vue-treeselect asks, e.g. your data has `name` property but vue-treeselect needs `label`, you may want to customize the key names. In this case, you can provide a function prop called `normalizer` which will be passed every node in the tree during data initialization. Use this function to create and return the transformed object.
115 | +demo('CustomizeKeyNames')
116 |
117 | +subsection('Customize Option Label')
118 | :markdown-it
119 | You can customize the label of each option. vue-treeselect utilizes Vue's [scoped slot](https://vuejs.org/v2/guide/components.html#Scoped-Slots) feature and provides some props you should use in your customized template:
120 |
121 | - `node` - a normalized node object (note that, this is differnt from what you return from `normalizer()` prop)
122 | - `count` & `shouldShowCount` - the count number and a boolean indicates whether the count should be displayed
123 | - `labelClassName` & `countClassName` - CSS classnames for making the style correct
124 | +demo('CustomizeOptionLabel')
125 |
126 | +subsection('Customize Value Label')
127 | :markdown-it
128 | You can customize the label of value item (each item in case of multi-select). vue-treeselect utilizes Vue's [scoped slot](https://vuejs.org/v2/guide/components.html#Scoped-Slots) feature and provides some props you should use in your customized template:
129 |
130 | - `node` - a normalized node object (note that, this is differnt from what you return from `normalizer()` prop)
131 | +demo('CustomizeValueLabel')
132 |
133 | +subsection('Vuex Support')
134 | :markdown-it
135 | In all previous examples, we used `v-model` to sync value between application state and vue-treeselect, a.k.a two-way data binding. If you are using Vuex, we could make use of `:value` and `@input`, since `v-model` is just a syntax sugar for them in Vue 2.
136 |
137 | Concretely speaking, we can bind `getter` to `:value` to make vue-treeselect reflect any changes in your Vuex state, and bind `action` or `mutation` to `@input` to update your Vuex state whenever the value changes.
138 | +demo('VuexSupport')
139 |
--------------------------------------------------------------------------------
/src/components/Input.vue:
--------------------------------------------------------------------------------
1 |
296 |
--------------------------------------------------------------------------------
/test/unit/specs/utils.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint import/namespace: 0 */
2 |
3 | import sleep from 'yaku/lib/sleep'
4 | import * as utils from '@src/utils'
5 |
6 | describe('Utils', () => {
7 | describe('Debugging Helpers', () => {
8 | describe('warning', () => {
9 | const { warning } = utils
10 | const WARNING_MSG = '$MESSAGE$'
11 |
12 | beforeEach(() => {
13 | spyOn(console, 'error')
14 | })
15 |
16 | it('when true', () => {
17 | warning(() => true, () => WARNING_MSG)
18 | expect(console.error).not.toHaveBeenCalled()
19 | })
20 |
21 | it('when false', () => {
22 | warning(() => false, () => WARNING_MSG)
23 | expect(console.error).toHaveBeenCalledWith('[Vue-Treeselect Warning]', WARNING_MSG)
24 | })
25 | })
26 | })
27 |
28 | describe('DOM Utilites', () => {
29 | describe('onLeftClick', () => {
30 | const { onLeftClick } = utils
31 | let spy
32 |
33 | beforeEach(() => {
34 | spy = jasmine.createSpy('onmousedown')
35 | })
36 |
37 | it('should invoke the function when left button has been clicked', () => {
38 | const eventObj = {
39 | type: 'mousedown',
40 | button: 0,
41 | }
42 | onLeftClick(spy)(eventObj)
43 | expect(spy).toHaveBeenCalledWith(eventObj)
44 | })
45 |
46 | it('should not invoke the function if wrong event type', () => {
47 | const eventObj = {
48 | type: 'mouseup',
49 | button: 0,
50 | }
51 | onLeftClick(spy)(eventObj)
52 | expect(spy).not.toHaveBeenCalled()
53 | })
54 |
55 | it('should not invoke the function if clicked with buttons other than left button', () => {
56 | const eventObj = {
57 | type: 'mousedown',
58 | button: 1,
59 | }
60 | onLeftClick(spy)(eventObj)
61 | expect(spy).not.toHaveBeenCalled()
62 | })
63 |
64 | it('should pass extra args', () => {
65 | const eventObj = {
66 | type: 'mousedown',
67 | button: 0,
68 | }
69 | const extraArg = {}
70 | onLeftClick(spy)(eventObj, extraArg)
71 | expect(spy).toHaveBeenCalledWith(eventObj, extraArg)
72 | })
73 | })
74 |
75 | it('scrollIntoView', () => {
76 | // TODO
77 | })
78 |
79 | it('debounce', () => {
80 | // vendor codes
81 | })
82 |
83 | describe('watchSize', () => {
84 | const { watchSize } = utils
85 |
86 | let $el
87 | let height
88 | let log
89 | const wait = 100
90 |
91 | const listener = (...args) => {
92 | log.push(args)
93 | }
94 | const enlarge = () => {
95 | $el.style.height = (height += 10) + 'px'
96 | }
97 | const reset = () => {
98 | $el = document.createElement('div')
99 | $el.style.height = (height = 100) + 'px'
100 | $el.style.position = 'relative'
101 | document.body.append($el)
102 | log = []
103 | }
104 | const cleanup = () => {
105 | $el.remove()
106 | }
107 | const test = async () => {
108 | reset()
109 |
110 | const unwatch = watchSize($el, listener)
111 | expect(log).toBeArrayOfSize(0)
112 |
113 | enlarge()
114 | await sleep(wait)
115 | expect(log).toBeArrayOfSize(1)
116 | expect(log[0][0].height).toBe(110)
117 |
118 | enlarge()
119 | await sleep(wait)
120 | expect(log).toBeArrayOfSize(2)
121 | expect(log[1][0].height).toBe(120)
122 |
123 | unwatch()
124 | cleanup()
125 | }
126 |
127 | it('for browsers other than IE9', async () => {
128 | await test()
129 | })
130 |
131 | it('for IE9', async () => {
132 | document.documentMode = 9
133 | await test()
134 | delete document.documentMode
135 | })
136 | })
137 |
138 | it('setupResizeAndScrollEventListeners', async () => {
139 | const { setupResizeAndScrollEventListeners } = utils
140 |
141 | let child, parent, grandparent, called
142 |
143 | const init = () => {
144 | grandparent = document.createElement('div')
145 | // eslint-disable-next-line unicorn/prefer-node-append
146 | parent = grandparent.appendChild(document.createElement('div'))
147 | parent.style.overflow = 'auto'
148 | parent.style.height = '100px'
149 | // eslint-disable-next-line unicorn/prefer-node-append
150 | child = parent.appendChild(document.createElement('div'))
151 | child.style.height = '99999px'
152 | document.body.append(grandparent)
153 | called = 0
154 | }
155 | const cleanup = () => {
156 | parent.remove()
157 | }
158 | const trigger = ($el, type) => {
159 | const event = document.createEvent('Event')
160 | event.initEvent(type, true, true)
161 | $el.dispatchEvent(event)
162 | }
163 | const test = async () => {
164 | init()
165 |
166 | const listener = () => called++
167 | const unwatch = setupResizeAndScrollEventListeners(child, listener)
168 |
169 | parent.scrollTop += 100
170 | await sleep(16)
171 | expect(called).toBe(1)
172 |
173 | parent.scrollTop -= 100
174 | await sleep(16)
175 | expect(called).toBe(2)
176 |
177 | trigger(window, 'scroll')
178 | await sleep(16)
179 | expect(called).toBe(3)
180 |
181 | trigger(window, 'resize')
182 | await sleep(16)
183 | expect(called).toBe(4)
184 |
185 | trigger(window, 'scroll')
186 | await sleep(16)
187 | expect(called).toBe(5)
188 |
189 | trigger(window, 'resize')
190 | await sleep(16)
191 | expect(called).toBe(6)
192 |
193 | unwatch()
194 |
195 | parent.scrollTop += 100
196 | await sleep(16)
197 | expect(called).toBe(6)
198 |
199 | trigger(window, 'scroll')
200 | await sleep(16)
201 | expect(called).toBe(6)
202 |
203 | trigger(window, 'resize')
204 | await sleep(16)
205 | expect(called).toBe(6)
206 |
207 | cleanup()
208 | }
209 |
210 | await test()
211 | })
212 | })
213 |
214 | describe('Language Helpers', () => {
215 | describe('isNaN', () => {
216 | const { isNaN } = utils
217 |
218 | it('check if value is NaN', () => {
219 | expect(isNaN(NaN)).toBe(true)
220 | expect(isNaN(0)).toBe(false)
221 | expect(isNaN(-1)).toBe(false)
222 | expect(isNaN(1)).toBe(false)
223 | expect(isNaN('NaN')).toBe(false)
224 | })
225 | })
226 |
227 | it('isPromise', () => {
228 | // vender codes
229 | })
230 |
231 | it('once', () => {
232 | // vender codes
233 | })
234 |
235 | it('noop', () => {
236 | // vender codes
237 | })
238 |
239 | it('identity', () => {
240 | // vender codes
241 | })
242 |
243 | it('constant', () => {
244 | // vender codes
245 | })
246 |
247 | describe('createMap', () => {
248 | const { createMap } = utils
249 |
250 | it('prototype should be null', () => {
251 | expect(Object.getPrototypeOf(createMap())).toBe(null)
252 | })
253 | })
254 |
255 | describe('deepExtend', () => {
256 | const { deepExtend } = utils
257 |
258 | it('should deep extend the target object', () => {
259 | expect(deepExtend({ b: 2 }, { a: 1, c: 3 })).toEqual({ a: 1, b: 2, c: 3 })
260 | })
261 |
262 | it('should work with undefined/null', () => {
263 | expect(deepExtend({}, undefined)).toEqual({})
264 | expect(deepExtend({}, null)).toEqual({})
265 | })
266 | })
267 |
268 | it('last', () => {
269 | // vendor codes
270 | })
271 |
272 | describe('includes', () => {
273 | const { includes } = utils
274 |
275 | it('string', () => {
276 | expect(includes('abc', 'ab')).toBe(true)
277 | expect(includes('xyz', 'bc')).toBe(false)
278 | })
279 |
280 | it('array', () => {
281 | expect(includes([ 'a', 'b', 'c' ], 'b')).toBe(true)
282 | expect(includes([ 'x', 'y', 'z' ], 'b')).toBe(false)
283 | })
284 | })
285 |
286 | describe('find', () => {
287 | const { find } = utils
288 |
289 | it('should return the element if matched', () => {
290 | expect(find([ 1, 2, 3 ], n => n % 2 === 0)).toBe(2)
291 | })
292 |
293 | it('should return undefined if not matched', () => {
294 | expect(find([ 1 ], n => n < 0)).toBe(undefined)
295 | })
296 | })
297 |
298 | it('removeFromArray', () => {
299 | const { removeFromArray } = utils
300 | const arr = [ 1, 2, 3 ]
301 | removeFromArray(arr, 2)
302 | expect(arr).toEqual([ 1, 3 ])
303 | removeFromArray(arr, 9)
304 | expect(arr).toEqual([ 1, 3 ])
305 | })
306 | })
307 |
308 | describe('Other Utilities', () => {
309 | it('quickDiff', () => {
310 | const { quickDiff } = utils
311 | const obj = {}
312 | expect(quickDiff([], [])).toBe(false)
313 | expect(quickDiff([ 1 ], [])).toBe(true)
314 | expect(quickDiff([ {} ], [ {} ])).toBe(true)
315 | expect(quickDiff([ obj ], [ obj ])).toBe(false)
316 | })
317 | })
318 | })
319 |
--------------------------------------------------------------------------------
/src/components/Option.vue:
--------------------------------------------------------------------------------
1 |
301 |
--------------------------------------------------------------------------------
/src/components/Menu.vue:
--------------------------------------------------------------------------------
1 |
314 |
--------------------------------------------------------------------------------
/docs/styles/docs.less:
--------------------------------------------------------------------------------
1 | /* stylelint-disable declaration-no-important */
2 |
3 | @import "~material-colors/dist/colors.less";
4 |
5 | /* Utils */
6 |
7 | .horizontal-padding(@value) {
8 | padding-left: @value;
9 | padding-right: @value;
10 | }
11 |
12 | .vertical-padding(@value) {
13 | padding-top: @value;
14 | padding-bottom: @value;
15 | }
16 |
17 | .clearfix {
18 | &::before,
19 | &::after {
20 | display: table;
21 | content: "";
22 | }
23 |
24 | &::after {
25 | clear: both;
26 | }
27 | }
28 |
29 | .nowrap {
30 | white-space: nowrap;
31 | }
32 |
33 | /* Global */
34 |
35 | html {
36 | -webkit-tap-highlight-color: transparent;
37 | }
38 |
39 | body {
40 | margin: 0;
41 | font-size: 14px;
42 | line-height: 1.5;
43 | font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
44 | color: @md-grey-700;
45 | -webkit-font-smoothing: antialiased;
46 | -moz-osx-font-smoothing: grayscale;
47 | }
48 |
49 | ::selection {
50 | background: fade(@md-light-blue-600, 10%);
51 | }
52 |
53 | dl, h1, h2, h3, h4, h5, h6, ol, p, pre, ul {
54 | margin-top: 0;
55 | }
56 |
57 | address, dl, ol, p, pre, ul {
58 | margin-bottom: 0.5em;
59 | }
60 |
61 | pre, code {
62 | font-family: Menlo, "Meslo LG M DZ", "Meslo LG M", Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
63 | }
64 |
65 | code:not([class*="language-"]) {
66 | padding: 2px 3px;
67 | background: rgba(0, 0, 0, 0.04);
68 | border-radius: 3px;
69 | font-size: 12px;
70 | }
71 |
72 | input[type="radio"] {
73 | margin: 0 3px 0 0;
74 | }
75 |
76 | #app {
77 | margin: 0 auto;
78 | }
79 |
80 | .container {
81 | .clearfix();
82 |
83 | max-width: 1020px;
84 | margin-left: auto;
85 | margin-right: auto;
86 | padding-left: 20px;
87 | padding-right: 20px;
88 | }
89 |
90 | a {
91 | text-decoration: none;
92 | color: @md-grey-700;
93 | }
94 |
95 | a img {
96 | border: 0;
97 | }
98 |
99 | h1, h2, h3, h4 {
100 | color: #2c3e50;
101 | letter-spacing: 0.5px;
102 | }
103 |
104 | h1, h2 {
105 | margin: 1em 0 0.45em;
106 | line-height: 1.2;
107 | }
108 |
109 | h1 {
110 | font-size: 28px;
111 | font-weight: 300;
112 | }
113 |
114 | h2 {
115 | font-size: 18px;
116 | font-weight: 400;
117 | }
118 |
119 | kbd {
120 | display: inline-block;
121 | padding: 3px 5px;
122 | font-size: 11px;
123 | line-height: 10px;
124 | color: #444d56;
125 | vertical-align: middle;
126 | background-color: #fafbfc;
127 | border: solid 1px #c6cbd1;
128 | border-bottom-color: #959da5;
129 | border-radius: 3px;
130 | box-shadow: inset 0 -1px 0 #959da5;
131 | }
132 |
133 | /* Layout */
134 |
135 | .site-header {
136 | .vertical-padding(15px);
137 |
138 | border-bottom: 1px solid #eee;
139 | }
140 |
141 | #header {
142 | width: 100%;
143 | display: table;
144 | }
145 |
146 | #main {
147 | display: table;
148 | width: 100%;
149 | table-layout: fixed;
150 | }
151 |
152 | #content,
153 | #sidebar {
154 | display: table-cell;
155 | vertical-align: top;
156 | }
157 |
158 | #content {
159 | width: 80%;
160 | padding-left: 20px;
161 |
162 | > :first-child {
163 | margin-top: 0;
164 | }
165 | }
166 |
167 | #content a,
168 | .site-footer a {
169 | color: @md-light-blue-600;
170 | font-weight: 600;
171 | }
172 |
173 | #sidebar {
174 | .horizontal-padding(16px);
175 |
176 | width: 20%;
177 | box-sizing: border-box;
178 | }
179 |
180 | .site-header a {
181 | text-decoration: none !important;
182 | }
183 |
184 | .site-header-logo {
185 | display: table-cell;
186 | vertical-align: middle;
187 | }
188 |
189 | .site-header-logo-link {
190 | padding-left: 2em;
191 | background: url("../../static/vue-logo.png") left center no-repeat;
192 | background-size: 1.5em 1.5em;
193 | font-size: 24px;
194 | font-family: Dosis, "Source Sans Pro", "Helvetica Neue", Arial, sans-serif;
195 | font-weight: 300;
196 | color: rgba(255, 255, 255, 0.9);
197 | color: #2c3e50;
198 | }
199 |
200 | .site-header-logo-component-name {
201 | font-weight: 500;
202 | }
203 |
204 | .site-header-nav {
205 | display: table-cell;
206 | line-height: 0;
207 | text-align: right;
208 | vertical-align: middle;
209 | }
210 |
211 | .site-header-nav-item {
212 | display: inline-block;
213 | margin-left: 1em;
214 | font-size: 0.875rem;
215 | color: #34495e;
216 | vertical-align: middle;
217 |
218 | // Github Star Button
219 | > iframe {
220 | display: block;
221 | }
222 | }
223 |
224 | .list-img {
225 | // remove extra blank spaces
226 | display: block;
227 | }
228 |
229 | /* Main */
230 |
231 | #main { // stylelint-disable-line no-duplicate-selectors
232 | padding-top: 3em;
233 |
234 | h1 {
235 | margin-left: -1px; // 为了对齐
236 |
237 | &:not(:first-of-type) {
238 | margin-top: 36px;
239 | }
240 | }
241 | }
242 |
243 | .anchor {
244 | display: inline-block;
245 | margin-left: 8px;
246 | text-decoration: none;
247 |
248 | svg {
249 | color: rgba(0, 0, 0, 0.25);
250 | }
251 |
252 | svg:hover {
253 | color: @md-light-blue-600;
254 | }
255 |
256 | h1 & svg {
257 | width: 16px;
258 | height: 16px;
259 | }
260 |
261 | h2 & svg {
262 | width: 12px;
263 | height: 12px;
264 | }
265 | }
266 |
267 | p.tip {
268 | @tip-color: @md-orange-500;
269 |
270 | position: relative;
271 | margin: 1em 0;
272 | padding: 12px 16px 12px 44px;
273 | background: #f8f8f8;
274 | border-radius: 5px;
275 |
276 | &::before {
277 | @size: 20px;
278 |
279 | content: "i";
280 | position: absolute;
281 | left: 16px;
282 | top: 12px;
283 | background: @tip-color;
284 | width: @size;
285 | height: @size;
286 | border-radius: 50%;
287 | color: #fff;
288 | text-align: center;
289 | font-weight: 400;
290 | line-height: @size;
291 | }
292 | }
293 |
294 | .example {
295 | display: table;
296 | table-layout: fixed;
297 | margin: 1em 0;
298 | width: 100%;
299 | border-radius: 5px;
300 | border: 1px solid #eee;
301 |
302 | p {
303 | margin: 5px 10px;
304 | }
305 |
306 | p.options label:not(:last-of-type) {
307 | margin-right: 6px;
308 | }
309 | }
310 |
311 | .example-demo,
312 | .example-code {
313 | display: table-cell;
314 | width: 50%;
315 | }
316 |
317 | .example-label {
318 | display: block;
319 | padding: 0.75em 1em;
320 | width: 100%;
321 | background: #f8f8f8;
322 | box-sizing: border-box;
323 | color: rgba(0, 0, 0, 0.6);
324 | font-weight: 700;
325 | font-size: 12px;
326 | line-height: 1;
327 | letter-spacing: 0.5px;
328 | text-transform: uppercase;
329 | }
330 |
331 | .example-code-lang-switch {
332 | margin-right: -8px;
333 | float: right;
334 | font-weight: 400;
335 | font-size: 0.9em;
336 | color: #aaa;
337 | letter-spacing: 0;
338 | }
339 |
340 | #content .example-code-lang {
341 | margin: 0 6px;
342 | cursor: pointer;
343 | color: #999;
344 | font-weight: 400;
345 |
346 | &.current {
347 | color: #666;
348 | font-weight: 600;
349 | text-decoration: none;
350 | cursor: default;
351 | }
352 | }
353 |
354 | .example-demo .example-inner,
355 | .example-code pre[class*="language-"] {
356 | padding: 15px;
357 | }
358 |
359 | .example-demo {
360 | pre.result {
361 | margin: 1em 0;
362 | padding: 1em 1.5em;
363 | background: rgba(0, 0, 0, 0.025);
364 | border: 0;
365 | border-radius: 5px;
366 | font-size: 12px;
367 | }
368 | }
369 |
370 | .example-code {
371 | border-left: 1px solid #eee;
372 |
373 | .example-inner {
374 | max-height: 300px;
375 | overflow-y: auto;
376 | margin-bottom: 1px; // Chrome 下面存在一个奇怪的 bug,下边框不能正常显示
377 | }
378 |
379 | pre[class*="language-"] {
380 | display: none;
381 | margin: 0;
382 | width: 100%;
383 | max-height: none;
384 | border: 0;
385 | background: none;
386 |
387 | &.current {
388 | display: block;
389 | }
390 |
391 | &::after {
392 | display: none;
393 | }
394 | }
395 | }
396 |
397 | pre[class*="language-"] {
398 | position: relative;
399 |
400 | &::after {
401 | font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif;
402 | position: absolute;
403 | right: 0;
404 | top: 0;
405 | padding: 5px 10px 0;
406 | color: #ccc;
407 | font-size: 0.9em;
408 | font-weight: 600;
409 | letter-spacing: 0.5px;
410 | }
411 | }
412 |
413 | pre.language-html::after {
414 | content: "HTML";
415 | }
416 |
417 | pre.language-js::after {
418 | content: "JS";
419 | }
420 |
421 | pre.language-vue::after {
422 | content: "Vue";
423 | }
424 |
425 | pre.language-bash::after {
426 | content: "Bash";
427 | }
428 |
429 | table {
430 | width: 100%;
431 | border-collapse: collapse;
432 | border-spacing: 0;
433 | margin-bottom: 1em;
434 |
435 | thead,
436 | &.striped tr:nth-of-type(2n) {
437 | background: #f8f8f8;
438 | }
439 |
440 | th {
441 | text-align: left;
442 | font-size: 15px;
443 | font-weight: 400;
444 | letter-spacing: 0.5px;
445 | }
446 |
447 | th,
448 | td {
449 | padding: 0.4em;
450 | border-bottom: 1px solid #eee;
451 | }
452 |
453 | tbody tr:hover {
454 | background: #f2f2f2 !important;
455 | }
456 | }
457 |
458 | th.name {
459 | width: 9em;
460 | }
461 |
462 | td.type {
463 | font-size: 0.9em;
464 | line-height: 1.8em;
465 | }
466 |
467 | th.desc {
468 | width: 28em;
469 | }
470 |
471 | label input[type="radio"],
472 | label input[type="checkbox"] {
473 | position: relative;
474 | top: 2px;
475 | margin-right: 4px;
476 | }
477 |
478 | /* Sidebar */
479 |
480 | .sidebar-nav {
481 | margin-top: 8px; // 和右边大标题文字对齐,因此应该仅限于大尺寸显示器
482 |
483 | &.sticky {
484 | position: fixed;
485 | top: 0;
486 | }
487 | }
488 |
489 | .sidebar-nav-section {
490 | margin-bottom: 1em;
491 |
492 | ul {
493 | margin-bottom: 0;
494 | }
495 | }
496 |
497 | .sidebar-nav-section-title {
498 | margin-bottom: 0.6em;
499 | font-size: 13px;
500 | font-weight: 600;
501 | text-transform: uppercase;
502 | }
503 |
504 | .sidebar-nav-list {
505 | padding-left: 8px;
506 | list-style: none;
507 | }
508 |
509 | .sidebar-nav-list-item {
510 | position: relative;
511 | display: block;
512 | font-size: 14px;
513 |
514 | a:hover,
515 | &.current a {
516 | color: @md-light-blue-600;
517 | font-weight: 600;
518 | }
519 | }
520 |
521 | /* Footer */
522 |
523 | .site-footer {
524 | .vertical-padding(30px);
525 |
526 | margin-top: 30px;
527 | border-top: 1px solid #eee;
528 | text-align: center;
529 | font-size: 13px;
530 | letter-spacing: 0.5px;
531 | }
532 |
--------------------------------------------------------------------------------
/test/unit/specs/Methods.spec.js:
--------------------------------------------------------------------------------
1 | import { mount } from '@vue/test-utils'
2 | import { typeSearchText } from './shared'
3 | import Treeselect from '@src/components/Treeselect'
4 |
5 | describe('Methods', () => {
6 | describe('toggleExpanded()', () => {
7 | it('basic', () => {
8 | const wrapper = mount(Treeselect, {
9 | propsData: {
10 | options: [ {
11 | id: 'a',
12 | label: 'a',
13 | children: [ {
14 | id: 'aa',
15 | label: 'aa',
16 | } ],
17 | } ],
18 | },
19 | })
20 | const { vm } = wrapper
21 | const { a } = vm.forest.nodeMap
22 |
23 | expect(a.isExpanded).toBe(false)
24 | wrapper.vm.toggleExpanded(a)
25 | expect(a.isExpanded).toBe(true)
26 | })
27 | })
28 |
29 | describe('clear()', () => {
30 | let wrapper, vm
31 |
32 | beforeEach(() => {
33 | wrapper = mount(Treeselect, {
34 | propsData: {
35 | options: [ {
36 | id: 'a',
37 | label: 'a',
38 | }, {
39 | id: 'b',
40 | label: 'b',
41 | }, {
42 | id: 'c',
43 | label: 'c',
44 | isDisabled: true,
45 | } ],
46 | },
47 | })
48 | vm = wrapper.vm
49 | })
50 |
51 | it('when multiple=false', () => {
52 | wrapper.setProps({ multiple: false })
53 |
54 | wrapper.setProps({ value: 'a' })
55 | vm.clear()
56 | expect(vm.internalValue).toEqual([])
57 |
58 | // Should clear disabled value.
59 | wrapper.setProps({ value: 'c' })
60 | vm.clear()
61 | expect(vm.internalValue).toEqual([])
62 | })
63 |
64 | it('when multiple=true', () => {
65 | wrapper.setProps({ multiple: true })
66 |
67 | wrapper.setProps({ value: [ 'a', 'b' ] })
68 | vm.clear()
69 | expect(vm.internalValue).toEqual([])
70 |
71 | // Should not clear disabled value.
72 | wrapper.setProps({ value: [ 'a', 'b', 'c' ] })
73 | vm.clear()
74 | expect(vm.internalValue).toEqual([ 'c' ])
75 | })
76 | })
77 |
78 | it('focusInput() & blurInput()', () => {
79 | const wrapper = mount(Treeselect, {
80 | attachToDocument: true,
81 | propsData: {
82 | options: [],
83 | disabled: false,
84 | searchable: true,
85 | autoFocus: false,
86 | },
87 | })
88 | const { vm } = wrapper
89 |
90 | expect(vm.trigger.isFocused).toBe(false)
91 | wrapper.vm.focusInput()
92 | expect(vm.trigger.isFocused).toBe(true)
93 | wrapper.vm.blurInput()
94 | expect(vm.trigger.isFocused).toBe(false)
95 | })
96 |
97 | describe('getMenu()', () => {
98 | let wrapper, vm
99 |
100 | const createInstance = appendToBody => {
101 | wrapper = mount(Treeselect, {
102 | sync: false,
103 | propsData: {
104 | appendToBody,
105 | options: [],
106 | },
107 | attachToDocument: true,
108 | })
109 | vm = wrapper.vm
110 | }
111 |
112 | afterEach(() => {
113 | wrapper.destroy()
114 | })
115 |
116 | it('when appendToBody=false', async () => {
117 | createInstance(false)
118 |
119 | vm.openMenu()
120 | await vm.$nextTick()
121 |
122 | expect(vm.getMenu().classList).toContain('vue-treeselect__menu')
123 | })
124 |
125 | it('when appendToBody=true', async () => {
126 | createInstance(true)
127 |
128 | vm.openMenu()
129 | await vm.$nextTick()
130 |
131 | expect(vm.getMenu().classList).toContain('vue-treeselect__menu')
132 | })
133 |
134 | it('when menu is closed', async () => {
135 | createInstance(false)
136 | expect(vm.getMenu()).toBe(null)
137 |
138 | vm.openMenu()
139 | await vm.$nextTick()
140 | expect(vm.getMenu().classList).toContain('vue-treeselect__menu')
141 |
142 | vm.closeMenu()
143 | await vm.$nextTick()
144 | expect(vm.getMenu()).toBe(null)
145 | })
146 | })
147 |
148 | describe('openMenu()', () => {
149 | let wrapper
150 |
151 | beforeEach(() => {
152 | wrapper = mount(Treeselect, {
153 | propsData: {
154 | options: [],
155 | },
156 | })
157 | expect(wrapper.vm.menu.isOpen).toBe(false)
158 | })
159 |
160 | it('should activate the menu', () => {
161 | wrapper.vm.openMenu()
162 | expect(wrapper.vm.menu.isOpen).toBe(true)
163 | })
164 |
165 | it('should ignore when disabled=true', () => {
166 | wrapper.setProps({ disabled: true })
167 | wrapper.vm.openMenu()
168 | expect(wrapper.vm.menu.isOpen).toBe(false)
169 | })
170 | })
171 |
172 | describe('closeMenu()', () => {
173 | it('should close the menu', () => {
174 | const wrapper = mount(Treeselect, {
175 | sync: false,
176 | propsData: {
177 | options: [],
178 | },
179 | })
180 | const { vm } = wrapper
181 |
182 | vm.openMenu()
183 | expect(wrapper.vm.menu.isOpen).toBe(true)
184 |
185 | vm.closeMenu()
186 | expect(wrapper.vm.menu.isOpen).toBe(false)
187 | })
188 | })
189 |
190 | describe('getNode()', () => {
191 | let wrapper
192 |
193 | beforeEach(() => {
194 | wrapper = mount(Treeselect, {
195 | propsData: {
196 | options: [ {
197 | id: 'a',
198 | label: 'a',
199 | } ],
200 | },
201 | })
202 | })
203 |
204 | it('should be able to obtain normalized node by id', () => {
205 | expect(wrapper.vm.getNode('a')).toEqual(jasmine.objectContaining({
206 | id: 'a',
207 | label: 'a',
208 | }))
209 | })
210 |
211 | it('should warn about invalid node id', () => {
212 | spyOn(console, 'error')
213 | wrapper.vm.getNode(null)
214 | expect(console.error).toHaveBeenCalledWith(
215 | '[Vue-Treeselect Warning]',
216 | 'Invalid node id: null',
217 | )
218 | })
219 | })
220 |
221 | describe('removeLastValue()', () => {
222 | it('single-select', () => {
223 | const wrapper = mount(Treeselect, {
224 | propsData: {
225 | value: 'a',
226 | multiple: false,
227 | options: [ {
228 | id: 'a',
229 | label: 'a',
230 | } ],
231 | },
232 | })
233 | const { vm } = wrapper
234 |
235 | vm.removeLastValue()
236 | expect(vm.internalValue).toEqual([])
237 | vm.removeLastValue()
238 | expect(vm.internalValue).toEqual([])
239 | })
240 |
241 | describe('multi-select', () => {
242 | describe('flat mode', () => {
243 | let wrapper, vm
244 |
245 | beforeEach(() => {
246 | wrapper = mount(Treeselect, {
247 | propsData: {
248 | flat: true,
249 | multiple: true,
250 | value: [ 'c', 'aaa', 'bb' ],
251 | options: [ 'a', 'b', 'c' ].map(letter => ({
252 | id: letter,
253 | label: letter,
254 | children: [ {
255 | id: letter.repeat(2),
256 | label: letter.repeat(2),
257 | children: [ {
258 | id: letter.repeat(3),
259 | label: letter.repeat(3),
260 | } ],
261 | } ],
262 | })),
263 | },
264 | })
265 | vm = wrapper.vm
266 | })
267 |
268 | it('when sortValueBy=ORDER_SELECTED', () => {
269 | wrapper.setProps({ sortValueBy: 'ORDER_SELECTED' })
270 | expect(vm.internalValue).toEqual([ 'c', 'aaa', 'bb' ])
271 | vm.removeLastValue()
272 | expect(vm.internalValue).toEqual([ 'c', 'aaa' ])
273 | vm.removeLastValue()
274 | expect(vm.internalValue).toEqual([ 'c' ])
275 | vm.removeLastValue()
276 | expect(vm.internalValue).toEqual([])
277 | })
278 |
279 | it('when sortValueBy=LEVEL', () => {
280 | wrapper.setProps({ sortValueBy: 'LEVEL' })
281 | expect(vm.internalValue).toEqual([ 'c', 'bb', 'aaa' ])
282 | vm.removeLastValue()
283 | expect(vm.internalValue).toEqual([ 'c', 'bb' ])
284 | vm.removeLastValue()
285 | expect(vm.internalValue).toEqual([ 'c' ])
286 | vm.removeLastValue()
287 | expect(vm.internalValue).toEqual([])
288 | })
289 |
290 | it('when sortValueBy=INDEX', () => {
291 | wrapper.setProps({ sortValueBy: 'INDEX' })
292 | expect(vm.internalValue).toEqual([ 'aaa', 'bb', 'c' ])
293 | vm.removeLastValue()
294 | expect(vm.internalValue).toEqual([ 'aaa', 'bb' ])
295 | vm.removeLastValue()
296 | expect(vm.internalValue).toEqual([ 'aaa' ])
297 | vm.removeLastValue()
298 | expect(vm.internalValue).toEqual([])
299 | })
300 | })
301 |
302 | describe('with `valueConsistsOf` prop', () => {
303 | let wrapper, vm
304 |
305 | beforeEach(() => {
306 | wrapper = mount(Treeselect, {
307 | propsData: {
308 | multiple: true,
309 | value: [ 'a' ],
310 | options: [ {
311 | id: 'a',
312 | label: 'a',
313 | children: [ {
314 | id: 'aa',
315 | label: 'aa',
316 | children: [ {
317 | id: 'aaa',
318 | label: 'aaa',
319 | }, {
320 | id: 'aab',
321 | label: 'aab',
322 | } ],
323 | }, {
324 | id: 'ab',
325 | label: 'ab',
326 | } ],
327 | } ],
328 | },
329 | })
330 | vm = wrapper.vm
331 | })
332 |
333 | it('when valueConsistsOf=ALL', () => {
334 | wrapper.setProps({ valueConsistsOf: 'ALL' })
335 | expect(vm.internalValue).toEqual([ 'a', 'aa', 'ab', 'aaa', 'aab' ])
336 | vm.removeLastValue()
337 | expect(vm.internalValue).toEqual([ 'ab', 'aaa' ])
338 | vm.removeLastValue()
339 | expect(vm.internalValue).toEqual([ 'ab' ])
340 | vm.removeLastValue()
341 | expect(vm.internalValue).toEqual([])
342 | })
343 |
344 | it('when valueConsistsOf=BRANCH_PRIORITY', () => {
345 | wrapper.setProps({ valueConsistsOf: 'BRANCH_PRIORITY' })
346 | expect(vm.internalValue).toEqual([ 'a' ])
347 | vm.removeLastValue()
348 | expect(vm.internalValue).toEqual([])
349 | })
350 |
351 | it('when valueConsistsOf=LEAF_PRIORITY', () => {
352 | wrapper.setProps({ valueConsistsOf: 'LEAF_PRIORITY' })
353 | expect(vm.internalValue).toEqual([ 'ab', 'aaa', 'aab' ])
354 | vm.removeLastValue()
355 | expect(vm.internalValue).toEqual([ 'ab', 'aaa' ])
356 | vm.removeLastValue()
357 | expect(vm.internalValue).toEqual([ 'ab' ])
358 | vm.removeLastValue()
359 | expect(vm.internalValue).toEqual([])
360 | })
361 |
362 | it('when valueConsistsOf=ALL_WITH_INDETERMINATE', () => {
363 | // TODO: the order is still strange
364 | wrapper.setProps({ valueConsistsOf: 'ALL_WITH_INDETERMINATE' })
365 | expect(vm.internalValue).toEqual([ 'a', 'aa', 'ab', 'aaa', 'aab' ])
366 | vm.removeLastValue()
367 | expect(vm.internalValue).toEqual([ 'ab', 'aaa', 'a', 'aa' ])
368 | vm.removeLastValue()
369 | expect(vm.internalValue).toEqual([ 'ab', 'a' ])
370 | vm.removeLastValue()
371 | expect(vm.internalValue).toEqual([])
372 | })
373 | })
374 | })
375 | })
376 |
377 | it('setCurrentHighlightedOption()', () => {
378 | // TODO
379 | })
380 |
381 | describe('resetCurrentHighlightedOption()', () => {
382 | let wrapper, vm
383 |
384 | beforeEach(() => {
385 | wrapper = mount(Treeselect, {
386 | propsData: {
387 | options: [ {
388 | id: 'a',
389 | label: 'a',
390 | }, {
391 | id: 'b',
392 | label: 'b',
393 | children: [ {
394 | id: 'ba',
395 | label: 'ba',
396 | }, {
397 | id: 'bb',
398 | label: 'bb',
399 | } ],
400 | } ],
401 | },
402 | })
403 | vm = wrapper.vm
404 | })
405 |
406 | it('when current=null', () => {
407 | expect(vm.menu.current).toBe(null)
408 | vm.resetHighlightedOptionWhenNecessary()
409 | expect(vm.menu.current).toBe('a')
410 | })
411 |
412 | it('when forceReset=true', () => {
413 | vm.setCurrentHighlightedOption(vm.forest.nodeMap.b)
414 | expect(vm.menu.current).toBe('b')
415 | vm.resetHighlightedOptionWhenNecessary()
416 | expect(vm.menu.current).toBe('b')
417 | vm.resetHighlightedOptionWhenNecessary(true)
418 | expect(vm.menu.current).toBe('a')
419 | })
420 |
421 | it('when current highlighted option not present in the list', async () => {
422 | vm.openMenu()
423 | await vm.$nextTick()
424 |
425 | expect(vm.menu.current).toBe('a')
426 |
427 | await typeSearchText(wrapper, 'bb')
428 | expect(vm.visibleOptionIds).toEqual([ 'b', 'bb' ])
429 | expect(vm.menu.current).toBe('b')
430 |
431 | vm.setCurrentHighlightedOption(vm.forest.nodeMap.bb)
432 | expect(vm.menu.current).toBe('bb')
433 |
434 | await typeSearchText(wrapper, 'a')
435 | // the previous highlighted option `bb` is not present in the list now
436 | expect(vm.visibleOptionIds).toEqual([ 'a', 'b', 'ba' ])
437 | expect(vm.menu.current).toBe('a')
438 | })
439 | })
440 | })
441 |
--------------------------------------------------------------------------------
/static/prism.min.js:
--------------------------------------------------------------------------------
1 | /* PrismJS 1.17.1
2 | https://prismjs.com/download.html#themes=prism&languages=markup+clike+javascript+bash */
3 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(u){var c=/\blang(?:uage)?-([\w-]+)\b/i,a=0;var _={manual:u.Prism&&u.Prism.manual,disableWorkerMessageHandler:u.Prism&&u.Prism.disableWorkerMessageHandler,util:{encode:function(e){return e instanceof L?new L(e.type,_.util.encode(e.content),e.alias):Array.isArray(e)?e.map(_.util.encode):e.replace(/&/g,"&").replace(/e.length)return;if(!(k instanceof L)){if(h&&y!=a.length-1){if(c.lastIndex=v,!(x=c.exec(e)))break;for(var b=x.index+(f&&x[1]?x[1].length:0),w=x.index+x[0].length,A=y,P=v,O=a.length;A"+n.content+""+n.tag+">"},!u.document)return u.addEventListener&&(_.disableWorkerMessageHandler||u.addEventListener("message",function(e){var a=JSON.parse(e.data),n=a.language,r=a.code,t=a.immediateClose;u.postMessage(_.highlight(r,_.languages[n],n)),t&&u.close()},!1)),_;var e=document.currentScript||[].slice.call(document.getElementsByTagName("script")).pop();if(e&&(_.filename=e.src,e.hasAttribute("data-manual")&&(_.manual=!0)),!_.manual){function n(){_.manual||_.highlightAll()}"loading"!==document.readyState?window.requestAnimationFrame?window.requestAnimationFrame(n):window.setTimeout(n,16):document.addEventListener("DOMContentLoaded",n)}return _}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism);
4 | Prism.languages.markup={comment://,prolog:/<\?[\s\S]+?\?>/,doctype://i,cdata://i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/i,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/i,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/i,inside:{punctuation:[/^=/,{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:/?[\da-z]{1,8};/i},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.hooks.add("wrap",function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))}),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^$/i;var n={"included-cdata":{pattern://i,inside:s}};n["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var i={};i[a]={pattern:RegExp("(<__[\\s\\S]*?>)(?:\\s*|[\\s\\S])*?(?=<\\/__>)".replace(/__/g,a),"i"),lookbehind:!0,greedy:!0,inside:n},Prism.languages.insertBefore("markup","cdata",i)}}),Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.html=Prism.languages.mathml=Prism.languages.svg=Prism.languages.vue=Prism.languages.markup;
5 | Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,boolean:/\b(?:true|false)\b/,function:/\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/};
6 | Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])[_$A-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\.(?:prototype|constructor))/,lookbehind:!0}],keyword:[{pattern:/((?:^|})\s*)(?:catch|finally)\b/,lookbehind:!0},{pattern:/(^|[^.])\b(?:as|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],number:/\b(?:(?:0[xX](?:[\dA-Fa-f](?:_[\dA-Fa-f])?)+|0[bB](?:[01](?:_[01])?)+|0[oO](?:[0-7](?:_[0-7])?)+)n?|(?:\d(?:_\d)?)+n|NaN|Infinity)\b|(?:\b(?:\d(?:_\d)?)+\.?(?:\d(?:_\d)?)*|\B\.(?:\d(?:_\d)?)+)(?:[Ee][+-]?(?:\d(?:_\d)?)+)?/,function:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,operator:/--|\+\+|\*\*=?|=>|&&|\|\||[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|[~?:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|interface|extends|implements|instanceof|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s])\s*)\/(\[(?:[^\]\\\r\n]|\\.)*]|\\.|[^/\\\[\r\n])+\/[gimyus]{0,6}(?=\s*($|[\r\n,.;})\]]))/,lookbehind:!0,greedy:!0},"function-variable":{pattern:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)?\s*\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*=>)/i,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*\s*)\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\[\s\S]|\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}|(?!\${)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\${|}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}}}),Prism.languages.markup&&Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.js=Prism.languages.javascript;
7 | !function(e){var t="\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b",n={environment:{pattern:RegExp("\\$"+t),alias:"constant"},variable:[{pattern:/\$?\(\([\s\S]+?\)\)/,greedy:!0,inside:{variable:[{pattern:/(^\$\(\([\s\S]+)\)\)/,lookbehind:!0},/^\$\(\(/],number:/\b0x[\dA-Fa-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:[Ee]-?\d+)?/,operator:/--?|-=|\+\+?|\+=|!=?|~|\*\*?|\*=|\/=?|%=?|<<=?|>>=?|<=?|>=?|==?|&&?|&=|\^=?|\|\|?|\|=|\?|:/,punctuation:/\(\(?|\)\)?|,|;/}},{pattern:/\$\((?:\([^)]+\)|[^()])+\)|`[^`]+`/,greedy:!0,inside:{variable:/^\$\(|^`|\)$|`$/}},{pattern:/\$\{[^}]+\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\/]|##?|%%?|\^\^?|,,?/,punctuation:/[\[\]]/,environment:{pattern:RegExp("(\\{)"+t),lookbehind:!0,alias:"constant"}}},/\$(?:\w+|[#?*!@$])/],entity:/\\(?:[abceEfnrtv\\"]|O?[0-7]{1,3}|x[0-9a-fA-F]{1,2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})/};e.languages.bash={shebang:{pattern:/^#!\s*\/.*/,alias:"important"},comment:{pattern:/(^|[^"{\\$])#.*/,lookbehind:!0},"function-name":[{pattern:/(\bfunction\s+)\w+(?=(?:\s*\(?:\s*\))?\s*\{)/,lookbehind:!0,alias:"function"},{pattern:/\b\w+(?=\s*\(\s*\)\s*\{)/,alias:"function"}],"for-or-select":{pattern:/(\b(?:for|select)\s+)\w+(?=\s+in\s)/,alias:"variable",lookbehind:!0},"assign-left":{pattern:/(^|[\s;|&]|[<>]\()\w+(?=\+?=)/,inside:{environment:{pattern:RegExp("(^|[\\s;|&]|[<>]\\()"+t),lookbehind:!0,alias:"constant"}},alias:"variable",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\s*)(\w+?)\s*(?:\r?\n|\r)(?:[\s\S])*?(?:\r?\n|\r)\2/,lookbehind:!0,greedy:!0,inside:n},{pattern:/((?:^|[^<])<<-?\s*)(["'])(\w+)\2\s*(?:\r?\n|\r)(?:[\s\S])*?(?:\r?\n|\r)\3/,lookbehind:!0,greedy:!0},{pattern:/(["'])(?:\\[\s\S]|\$\([^)]+\)|`[^`]+`|(?!\1)[^\\])*\1/,greedy:!0,inside:n}],environment:{pattern:RegExp("\\$?"+t),alias:"constant"},variable:n.variable,function:{pattern:/(^|[\s;|&]|[<>]\()(?:add|apropos|apt|aptitude|apt-cache|apt-get|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\s;|&]|[<>]\()(?:if|then|else|elif|fi|for|while|in|case|esac|function|select|do|done|until)(?=$|[)\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\s;|&]|[<>]\()(?:\.|:|break|cd|continue|eval|exec|exit|export|getopts|hash|pwd|readonly|return|shift|test|times|trap|umask|unset|alias|bind|builtin|caller|command|declare|echo|enable|help|let|local|logout|mapfile|printf|read|readarray|source|type|typeset|ulimit|unalias|set|shopt)(?=$|[)\s;|&])/,lookbehind:!0,alias:"class-name"},boolean:{pattern:/(^|[\s;|&]|[<>]\()(?:true|false)(?=$|[)\s;|&])/,lookbehind:!0},"file-descriptor":{pattern:/\B&\d\b/,alias:"important"},operator:{pattern:/\d?<>|>\||\+=|==?|!=?|=~|<<[<-]?|[&\d]?>>|\d?[<>]&?|&[>&]?|\|[&|]?|<=?|>=?/,inside:{"file-descriptor":{pattern:/^\d/,alias:"important"}}},punctuation:/\$?\(\(?|\)\)?|\.\.|[{}[\];\\]/,number:{pattern:/(^|\s)(?:[1-9]\d*|0)(?:[.,]\d+)?\b/,lookbehind:!0}};for(var a=["comment","function-name","for-or-select","assign-left","string","environment","function","keyword","builtin","boolean","file-descriptor","operator","punctuation","number"],r=n.variable[1].inside,s=0;s