From bdaae8108eff94061752f0a8a414c2dc3ed29310 Mon Sep 17 00:00:00 2001
From: Simona <36978416+SimonaliaChen@users.noreply.github.com>
Date: Wed, 29 May 2019 15:04:06 +0800
Subject: [PATCH] Cascader: refactor and add multiple mode. (#15611)
---
components.json | 3 +-
examples/docs/en-US/cascader.md | 1600 ++++++++++------
examples/docs/es/cascader.md | 1670 +++++++++++------
examples/docs/fr-FR/cascader.md | 1668 ++++++++++------
examples/docs/zh-CN/cascader.md | 1101 ++++++++---
packages/cascader-panel/index.js | 8 +
packages/cascader-panel/src/cascader-menu.vue | 138 ++
packages/cascader-panel/src/cascader-node.vue | 246 +++
.../cascader-panel/src/cascader-panel.vue | 354 ++++
packages/cascader-panel/src/node.js | 166 ++
packages/cascader-panel/src/store.js | 63 +
packages/cascader/index.js | 2 +-
packages/cascader/src/cascader.vue | 639 +++++++
packages/cascader/src/main.vue | 452 -----
packages/cascader/src/menu.vue | 375 ----
packages/theme-chalk/src/cascader-panel.scss | 124 ++
packages/theme-chalk/src/cascader.scss | 255 ++-
packages/theme-chalk/src/common/var.scss | 21 +-
packages/theme-chalk/src/index.scss | 2 +
packages/theme-chalk/src/infiniteScroll.scss | 0
src/index.js | 5 +-
src/locale/lang/af-ZA.js | 3 +-
src/locale/lang/ar.js | 3 +-
src/locale/lang/bg.js | 3 +-
src/locale/lang/ca.js | 3 +-
src/locale/lang/cs-CZ.js | 3 +-
src/locale/lang/da.js | 3 +-
src/locale/lang/de.js | 3 +-
src/locale/lang/ee.js | 3 +-
src/locale/lang/el.js | 3 +-
src/locale/lang/en.js | 3 +-
src/locale/lang/es.js | 3 +-
src/locale/lang/eu.js | 3 +-
src/locale/lang/fa.js | 3 +-
src/locale/lang/fi.js | 3 +-
src/locale/lang/fr.js | 3 +-
src/locale/lang/he.js | 3 +-
src/locale/lang/hr.js | 3 +-
src/locale/lang/hu.js | 3 +-
src/locale/lang/hy-AM.js | 3 +-
src/locale/lang/id.js | 3 +-
src/locale/lang/it.js | 3 +-
src/locale/lang/ja.js | 3 +-
src/locale/lang/kg.js | 3 +-
src/locale/lang/km.js | 3 +-
src/locale/lang/ko.js | 3 +-
src/locale/lang/ku.js | 3 +-
src/locale/lang/kz.js | 3 +-
src/locale/lang/lt.js | 3 +-
src/locale/lang/lv.js | 3 +-
src/locale/lang/mn.js | 3 +-
src/locale/lang/nb-NO.js | 3 +-
src/locale/lang/nl.js | 3 +-
src/locale/lang/pl.js | 3 +-
src/locale/lang/pt-br.js | 3 +-
src/locale/lang/pt.js | 3 +-
src/locale/lang/ro.js | 3 +-
src/locale/lang/ru-RU.js | 3 +-
src/locale/lang/sk.js | 3 +-
src/locale/lang/sl.js | 3 +-
src/locale/lang/sr.js | 3 +-
src/locale/lang/sv-SE.js | 3 +-
src/locale/lang/ta.js | 3 +-
src/locale/lang/th.js | 3 +-
src/locale/lang/tk.js | 3 +-
src/locale/lang/tr-TR.js | 3 +-
src/locale/lang/ua.js | 3 +-
src/locale/lang/ug-CN.js | 3 +-
src/locale/lang/vi.js | 3 +-
src/locale/lang/zh-CN.js | 3 +-
src/locale/lang/zh-TW.js | 3 +-
src/utils/aria-utils.js | 3 +-
src/utils/util.js | 73 +
test/unit/specs/cascader-panel.spec.js | 536 ++++++
test/unit/specs/cascader.spec.js | 1046 +++--------
types/cascader-panel.d.ts | 72 +
types/cascader.d.ts | 54 +-
77 files changed, 7116 insertions(+), 3710 deletions(-)
create mode 100644 packages/cascader-panel/index.js
create mode 100644 packages/cascader-panel/src/cascader-menu.vue
create mode 100644 packages/cascader-panel/src/cascader-node.vue
create mode 100644 packages/cascader-panel/src/cascader-panel.vue
create mode 100644 packages/cascader-panel/src/node.js
create mode 100644 packages/cascader-panel/src/store.js
create mode 100644 packages/cascader/src/cascader.vue
delete mode 100644 packages/cascader/src/main.vue
delete mode 100644 packages/cascader/src/menu.vue
create mode 100644 packages/theme-chalk/src/cascader-panel.scss
create mode 100644 packages/theme-chalk/src/infiniteScroll.scss
create mode 100644 test/unit/specs/cascader-panel.spec.js
create mode 100644 types/cascader-panel.d.ts
diff --git a/components.json b/components.json
index 07d33a97e..191b7bdc6 100644
--- a/components.json
+++ b/components.json
@@ -76,5 +76,6 @@
"calendar": "./packages/calendar/index.js",
"backtop": "./packages/backtop/index.js",
"infiniteScroll": "./packages/infiniteScroll/index.js",
- "page-header": "./packages/page-header/index.js"
+ "page-header": "./packages/page-header/index.js",
+ "cascader-panel": "./packages/cascader-panel/index.js"
}
diff --git a/examples/docs/en-US/cascader.md b/examples/docs/en-US/cascader.md
index dd0590c88..5374df8f1 100644
--- a/examples/docs/en-US/cascader.md
+++ b/examples/docs/en-US/cascader.md
@@ -6,30 +6,29 @@ If the options have a clear hierarchical structure, Cascader can be used to view
There are two ways to expand child option items.
-:::demo Assigning the `options` attribute to an array of options renders a Cascader. The `expand-trigger` attribute defines how child options are expanded. This example also demonstrates the `change` event, whose parameter is the value of Cascader, an array made up of the values of each selected level.
+:::demo Assigning the `options` attribute to an array of options renders a Cascader. The `props.expandTrigger` attribute defines how child options are expanded.
```html
Child options expand when clicked (default)
-
+ @change="handleChange">
Child options expand when hovered
-
+ :props="{ expandTrigger: 'hover' }"
+ @change="handleChange">
+```
+:::
+
### Display only the last level
The input can display only the last level instead of all levels.
:::demo The `show-all-levels` attribute defines if all levels are displayed. If it is `false`, only the last level is displayed.
```html
-
+
+```
+:::
+
+### Multiple Selection
+
+Set `props.multiple = true` to use multiple selection.
+
+:::demo When using multiple selection, all selected tags will display by default, You can set `collapse-tags = true` to fold selected tags.
+```html
+
+ Display all tags (default)
+
+ :props="props"
+ clearable>
+
+
+ Collapse tags
+
+
+
+
+```
+:::
+
+
+### Select any level of options
+
+In single selection, only the leaf nodes can be checked, and in multiple selection, check parent nodes will lead to leaf nodes be checked eventually. When enable this feature, it can make parent and child nodes unlinked and you can select any level of options.
+
+:::demo Set `props.checkStrictly = true` to make checked state of a node not affects its parent nodes and child nodes, and then you can select any level of options.
+```html
+
+ Select any level of options (Single selection)
+
+
+
+ Select any level of options (Multiple selection)
+
+
+
-```
-:::
-
-### Change on select
-
-Parent options can also be selected.
-
-:::demo By default only the options in the last level can be selected. By assigning `change-on-select` to `true`, options in parent levels can also be selected.
-```html
-
-
-```
-:::
-
-### Dynamically load child options
-
-Load child options when their parent option is clicked or hovered over.
-
-:::demo In this example, the options array does not have data of cities when initialized. With the `active-item-change` event, you can load the cities of a specific state dynamically. Besides, this example also demonstrates how `props` is used.
-```html
-
-
-
@@ -1157,24 +1250,22 @@ Load child options when their parent option is clicked or hovered over.
Search and select options with a keyword.
-:::demo Adding `filterable` to `el-cascader` enables filtering
+:::demo Adding `filterable` to `el-cascader` enables filtering. Cascader will match nodes whose label or parent's label (according to `show-all-levels`) includes input keyword. Of course, you can customize search logic by `filter-method` which accepts a function, the first parameter is `node`, the second is `keyword`, and need return a boolean value indicating whether it hits.
```html
- Only options of the last level can be selected
+ Filterable (Single selection)
+ filterable>
- Options of all levels can be selected
+ Filterable (Multiple selection)
+ :props="{ multiple: true }"
+ filterable>
+```
+:::
+
+### Cascader panel
+
+`CascaderPanel` is the core component of `Cascader` which has various of features such as single selection, multiple selection, dynamic loading and so on.
+
+:::demo Just like `el-cascader`, you can set alternative options by `options`, and enable other features by `props`, see the API form below for details.
+```html
+
+
+
+```
+:::
+
+### Cascader Attributes
+| Attribute | Description | Type | Accepted Values | Default |
+|---------- |-------- |---------- |------------- |-------- |
+| value / v-model | binding value | - | — | — |
+| options | data of the options,the key of `value` and `label` can be customize by `Props`.| array | — | — |
+| props | configuration options, see the following table. | object | — | — |
+| size | size of input | string | medium / small / mini | — |
+| placeholder | placeholder of input | string | — | Select |
+| disabled | whether Cascader is disabled | boolean | — | false |
+| clearable | whether selected value can be cleared | boolean | — | false |
| show-all-levels | whether to display all levels of the selected value in the input | boolean | — | true |
-| filterable | whether the options can be searched | boolean | — | — |
+| collapse-tags | whether to collapse tags in multiple selection mode | boolean | - | false |
+| separator | option label separator | string | — | ' / ' |
+| filterable | whether the options can be searched | boolean | — | — |
+| filter-method | customize search logic, the first parameter is `node`, the second is `keyword`, and need return a boolean value indicating whether it hits. | function(node, keyword) | - | - |
| debounce | debounce delay when typing filter keyword, in milliseconds | number | — | 300 |
-| change-on-select | whether selecting an option of any level is permitted | boolean | — | false |
-| size | size of Input | string | medium / small / mini | — |
| before-filter | hook function before filtering with the value to be filtered as its parameter. If `false` is returned or a `Promise` is returned and then is rejected, filtering will be aborted | function(value) | — | — |
+| popper-class | custom class name for Cascader's dropdown | string | — | — |
-### props
-| Attribute | Description | Type | Accepted Values | Default |
-| --------- | ----------------- | ------ | ------ | ------ |
-| label | specify which key of option object is used as the option's label | string | — | — |
-| value | specify which key of option object is used as the option's value | string | — | — |
-| children | specify which key of option object is used as the option's child options | string | — | — |
-| disabled | specify which key of option object indicates if the option is disabled | string | — | — |
-
-### Events
+### Cascader Events
| Event Name | Description | Parameters |
|---------- |-------- |---------- |
-| change | triggers when the binding value changes | value |
-| active-item-change | triggers when active option of its parent changes, only works when `change-on-select` is `false` | an array of active options |
+| change | triggers when the binding value changes | value |
+| expand-change | triggers when expand option changes | an array of the expanding node's parent nodes |
| blur | triggers when Cascader blurs | (event: Event) |
| focus | triggers when Cascader focuses | (event: Event) |
| visible-change | triggers when the dropdown appears/disappears | true when it appears, and false otherwise |
+| remove-tag | triggers when remove tag in multiple selection mode | the value of the tag which is removed |
+
+### Cascader Slots
+| Slot Name | Description |
+|---------|-------------|
+| - | the custom content of cascader node, the parameter is { node, data }, which are current Node object and node data respectively. |
+| empty | content when there is no matched options. |
+
+### CascaderPanel Attributes
+| Attribute | Description | Type | Accepted Values | Default |
+|---------- |-------- |---------- |------------- |-------- |
+| value / v-model | binding value | - | — | — |
+| options | data of the options,the key of `value` and `label` can be customize by `Props`.| array | — | — |
+| props | configuration options, see the following table. | object | — | — |
+
+### CascaderPanel Events
+| Event Name | Description | Parameters |
+|---------- |-------- |---------- |
+| change | triggers when the binding value changes | value |
+| expand-change | triggers when expand option changes | an array of the expanding node's parent nodes |
+
+### CascaderPanel Slots
+| Slot Name | Description |
+|---------|-------------|
+| - | the custom content of cascader node, the parameter is { node, data }, which are current Node object and node data respectively. |
+
+### Props
+| Attribute | Description | Type | Accepted Values | Default |
+| -------- | ----------------- | ------ | ------ | ------ |
+| expandTrigger | trigger mode of expanding options | string | click / hover | 'click' |
+| multiple | whether multiple selection is enabled | boolean | - | false |
+| checkStrictly | whether checked state of a node not affects its parent and child nodes | boolean | - | false |
+| emitPath | when checked nodes change, whether to emit an array of node's path, if false, only emit the value of node. | boolean | - | true |
+| lazy | whether to dynamic load child nodes, use with `lazyload` attribute | boolean | - | false |
+| lazyLoad | method for loading child nodes data, only works when `lazy` is true | function(node, resolve) | - | - |
+| value | specify which key of node object is used as the node's value | string | — | 'value' |
+| label | specify which key of node object is used as the node's label | string | — | 'label' |
+| children | specify which key of node object is used as the node's children | string | — | 'children' |
+| disabled | specify which key of node object is used as the node's disabled | string | — | 'disabled' |
+| leaf | specify which key of node object is used as the node's leaf field | string | — | 'leaf' |
diff --git a/examples/docs/es/cascader.md b/examples/docs/es/cascader.md
index 55228cbd9..5374df8f1 100644
--- a/examples/docs/es/cascader.md
+++ b/examples/docs/es/cascader.md
@@ -1,36 +1,34 @@
## Cascader
-Si las opciones tienen una estructura jerárquica clara, Cascader puede utilizarse para visualizarlas y seleccionarlas.
+If the options have a clear hierarchical structure, Cascader can be used to view and select them.
-### Uso básico
+### Basic usage
-Existen dos maneras de expandir los elementos hijo de la opción.
-
-:::demo Al asignar al atributo `options` un array de opciones, se genera un Cascader. El atributo `expand-trigger` define cómo se expanden las opciones hijo. Este ejemplo también muestra el evento `change` , cuyo parámetro es el valor de Cascader, un array formado por los valores de cada nivel seleccionado.
+There are two ways to expand child option items.
+:::demo Assigning the `options` attribute to an array of options renders a Cascader. The `props.expandTrigger` attribute defines how child options are expanded.
```html
Child options expand when clicked (default)
-
+ @change="handleChange">
Child options expand when hovered
-
+ :props="{ expandTrigger: 'hover' }"
+ @change="handleChange">
-```
-:::
-
-### Con valor por defecto
-
-:::demo El valor por defecto se puede definir con un array.
-```html
-
-
-```
-:::
-
-### Change on select
-
-También se pueden seleccionar las opciones del elemento padre.
-
-:::demo Por defecto sólo se pueden seleccionar las opciones del último nivel. Al asignar `change-on-select` a `true`, también se pueden seleccionar opciones en los niveles superiores.
-
-```html
-
-
-```
-:::
-
-### Carga dinamica de elementos hijos
-
-Se puede hacer una carga dinamica de elementos hijos cuando se hace clic en el elemento padre o se pasa el ratón sobre el.
-
-:::demo En este ejemplo, el array de opciones no tiene datos de ciudades cuando se inicializa. Con el evento `active-item-change`, puede cargar dinámicamente las ciudades de un estado específico. Además, este ejemplo también demuestra cómo se utilizan los`props`.
-
-```html
-
-
-
+```
+:::
+
+### Display only the last level
+
+The input can display only the last level instead of all levels.
+
+:::demo The `show-all-levels` attribute defines if all levels are displayed. If it is `false`, only the last level is displayed.
+```html
+
+
```
:::
-### Filtrable
+### Multiple Selection
-Buscar y seleccionar opciones con una palabra clave.
-
-:::demo Añadir `filterable` a `el-cascader` permite filtrar
+Set `props.multiple = true` to use multiple selection.
+:::demo When using multiple selection, all selected tags will display by default, You can set `collapse-tags = true` to fold selected tags.
```html
- Only options of the last level can be selected
+ Display all tags (default)
+ :props="props"
+ clearable>
- Options of all levels can be selected
+ Collapse tags
+ :props="props"
+ collapse-tags
+ clearable>
+
+
+
+```
+:::
+
+
+### Select any level of options
+
+In single selection, only the leaf nodes can be checked, and in multiple selection, check parent nodes will lead to leaf nodes be checked eventually. When enable this feature, it can make parent and child nodes unlinked and you can select any level of options.
+
+:::demo Set `props.checkStrictly = true` to make checked state of a node not affects its parent nodes and child nodes, and then you can select any level of options.
+```html
+
+ Select any level of options (Single selection)
+
+
+
+ Select any level of options (Multiple selection)
+
+```
+:::
+
+### Filterable
+
+Search and select options with a keyword.
+
+:::demo Adding `filterable` to `el-cascader` enables filtering. Cascader will match nodes whose label or parent's label (according to `show-all-levels`) includes input keyword. Of course, you can customize search logic by `filter-method` which accepts a function, the first parameter is `node`, the second is `keyword`, and need return a boolean value indicating whether it hits.
+```html
+
+ Filterable (Single selection)
+
+
+
+ Filterable (Multiple selection)
+
+
+
+
+```
+:::
+
+### Custom option content
+
+You can customize the content of cascader node.
+
+:::demo You can customize the content of cascader node by `scoped slot`. You'll have access to `node` and `data` in the scope, standing for the Node object and node data of the current node respectively。
+```html
+
+
+ {{ data.label }}
+ ({{ data.children.length }})
+
+
+
+
+```
+:::
+
+### Cascader panel
+
+`CascaderPanel` is the core component of `Cascader` which has various of features such as single selection, multiple selection, dynamic loading and so on.
+
+:::demo Just like `el-cascader`, you can set alternative options by `options`, and enable other features by `props`, see the API form below for details.
+```html
+
+
+
+```
+:::
+
+### Cascader Attributes
+| Attribute | Description | Type | Accepted Values | Default |
+|---------- |-------- |---------- |------------- |-------- |
+| value / v-model | binding value | - | — | — |
+| options | data of the options,the key of `value` and `label` can be customize by `Props`.| array | — | — |
+| props | configuration options, see the following table. | object | — | — |
+| size | size of input | string | medium / small / mini | — |
+| placeholder | placeholder of input | string | — | Select |
+| disabled | whether Cascader is disabled | boolean | — | false |
+| clearable | whether selected value can be cleared | boolean | — | false |
+| show-all-levels | whether to display all levels of the selected value in the input | boolean | — | true |
+| collapse-tags | whether to collapse tags in multiple selection mode | boolean | - | false |
+| separator | option label separator | string | — | ' / ' |
+| filterable | whether the options can be searched | boolean | — | — |
+| filter-method | customize search logic, the first parameter is `node`, the second is `keyword`, and need return a boolean value indicating whether it hits. | function(node, keyword) | - | - |
+| debounce | debounce delay when typing filter keyword, in milliseconds | number | — | 300 |
+| before-filter | hook function before filtering with the value to be filtered as its parameter. If `false` is returned or a `Promise` is returned and then is rejected, filtering will be aborted | function(value) | — | — |
+| popper-class | custom class name for Cascader's dropdown | string | — | — |
+
+### Cascader Events
+| Event Name | Description | Parameters |
+|---------- |-------- |---------- |
+| change | triggers when the binding value changes | value |
+| expand-change | triggers when expand option changes | an array of the expanding node's parent nodes |
+| blur | triggers when Cascader blurs | (event: Event) |
+| focus | triggers when Cascader focuses | (event: Event) |
+| visible-change | triggers when the dropdown appears/disappears | true when it appears, and false otherwise |
+| remove-tag | triggers when remove tag in multiple selection mode | the value of the tag which is removed |
+
+### Cascader Slots
+| Slot Name | Description |
+|---------|-------------|
+| - | the custom content of cascader node, the parameter is { node, data }, which are current Node object and node data respectively. |
+| empty | content when there is no matched options. |
+
+### CascaderPanel Attributes
+| Attribute | Description | Type | Accepted Values | Default |
+|---------- |-------- |---------- |------------- |-------- |
+| value / v-model | binding value | - | — | — |
+| options | data of the options,the key of `value` and `label` can be customize by `Props`.| array | — | — |
+| props | configuration options, see the following table. | object | — | — |
+
+### CascaderPanel Events
+| Event Name | Description | Parameters |
+|---------- |-------- |---------- |
+| change | triggers when the binding value changes | value |
+| expand-change | triggers when expand option changes | an array of the expanding node's parent nodes |
+
+### CascaderPanel Slots
+| Slot Name | Description |
+|---------|-------------|
+| - | the custom content of cascader node, the parameter is { node, data }, which are current Node object and node data respectively. |
+
+### Props
+| Attribute | Description | Type | Accepted Values | Default |
+| -------- | ----------------- | ------ | ------ | ------ |
+| expandTrigger | trigger mode of expanding options | string | click / hover | 'click' |
+| multiple | whether multiple selection is enabled | boolean | - | false |
+| checkStrictly | whether checked state of a node not affects its parent and child nodes | boolean | - | false |
+| emitPath | when checked nodes change, whether to emit an array of node's path, if false, only emit the value of node. | boolean | - | true |
+| lazy | whether to dynamic load child nodes, use with `lazyload` attribute | boolean | - | false |
+| lazyLoad | method for loading child nodes data, only works when `lazy` is true | function(node, resolve) | - | - |
+| value | specify which key of node object is used as the node's value | string | — | 'value' |
+| label | specify which key of node object is used as the node's label | string | — | 'label' |
+| children | specify which key of node object is used as the node's children | string | — | 'children' |
+| disabled | specify which key of node object is used as the node's disabled | string | — | 'disabled' |
+| leaf | specify which key of node object is used as the node's leaf field | string | — | 'leaf' |
diff --git a/examples/docs/fr-FR/cascader.md b/examples/docs/fr-FR/cascader.md
index 9510f1b1e..5374df8f1 100644
--- a/examples/docs/fr-FR/cascader.md
+++ b/examples/docs/fr-FR/cascader.md
@@ -1,35 +1,34 @@
## Cascader
-Si les options ont une structure hiérarchique claire, un Cascader peut être utilisé pour les afficher et les selectionner.
+If the options have a clear hierarchical structure, Cascader can be used to view and select them.
-### Usage
+### Basic usage
-Il y a deux manières de dérouler la liste des options.
+There are two ways to expand child option items.
-:::demo Assigner un tableau à l'attribut `options` génère un Cascader. L'attribut `expand-trigger` détermine comment les options suivantes sont affichées. Cet exemple utilises aussi l'évènement `change`, qui a pour paramètre la valeur du Cascader, c'est à dire un tableau constitué de chaque niveau jusqu'à la valeur selectionnée, comme un chemin.
+:::demo Assigning the `options` attribute to an array of options renders a Cascader. The `props.expandTrigger` attribute defines how child options are expanded.
```html
- Les options se déroulent après un clic (défaut)
+ Child options expand when clicked (default)
-
+ @change="handleChange">
- Les options se déroulent au passage de la souris
+ Child options expand when hovered
-
+ :props="{ expandTrigger: 'hover' }"
+ @change="handleChange">
-```
-:::
-
-### Valeur par défaut
-
-:::demo La valeur par défaut peut être définit grâce à un tableau.
-```html
-
-
-```
-:::
-
-### Changement après sélection
-
-Les options parentes peuvent aussi être sélectionnées.
-
-:::demo Par défaut seules les options de dernier niveau peuvent être sélectionnées. En réglant `change-on-select` à `true`, les options parentes peuvent aussi être sélectionnées.
-```html
-
-
-```
-:::
-
-### Charger les options dynamiquement
-
-Il est possible de charger dynamiquement les options quand on clique ou passe la souris sur leurs parent.
-
-:::demo Dans cet exemple, les optionsn'ont pas de données de villes au moment de l'initialisation. Grâce à l'évènement `active-item-change` vous pouvez charger les villes de manière dynamique. De plus, cet exemple montre comment `props` peut être utilisé.
-```html
-
-
-
+```
+:::
+
+### Display only the last level
+
+The input can display only the last level instead of all levels.
+
+:::demo The `show-all-levels` attribute defines if all levels are displayed. If it is `false`, only the last level is displayed.
+```html
+
+
```
:::
-### Filtres
+### Multiple Selection
-Recherchez une option particulière en entrant des mots-clé.
+Set `props.multiple = true` to use multiple selection.
-:::demo Ajouter `filterable` à `el-cascader` active le filtrage.
+:::demo When using multiple selection, all selected tags will display by default, You can set `collapse-tags = true` to fold selected tags.
```html
- Only options of the last level can be selected
+ Display all tags (default)
+ :props="props"
+ clearable>
- Options of all levels can be selected
+ Collapse tags
+ :props="props"
+ collapse-tags
+ clearable>
+
+
+
+```
+:::
+
+
+### Select any level of options
+
+In single selection, only the leaf nodes can be checked, and in multiple selection, check parent nodes will lead to leaf nodes be checked eventually. When enable this feature, it can make parent and child nodes unlinked and you can select any level of options.
+
+:::demo Set `props.checkStrictly = true` to make checked state of a node not affects its parent nodes and child nodes, and then you can select any level of options.
+```html
+
+ Select any level of options (Single selection)
+
+
+
+ Select any level of options (Multiple selection)
+
+```
+:::
+
+### Filterable
+
+Search and select options with a keyword.
+
+:::demo Adding `filterable` to `el-cascader` enables filtering. Cascader will match nodes whose label or parent's label (according to `show-all-levels`) includes input keyword. Of course, you can customize search logic by `filter-method` which accepts a function, the first parameter is `node`, the second is `keyword`, and need return a boolean value indicating whether it hits.
+```html
+
+ Filterable (Single selection)
+
+
+
+ Filterable (Multiple selection)
+
+
+
+
+```
+:::
+
+### Custom option content
+
+You can customize the content of cascader node.
+
+:::demo You can customize the content of cascader node by `scoped slot`. You'll have access to `node` and `data` in the scope, standing for the Node object and node data of the current node respectively。
+```html
+
+
+ {{ data.label }}
+ ({{ data.children.length }})
+
+
+
+
+```
+:::
+
+### Cascader panel
+
+`CascaderPanel` is the core component of `Cascader` which has various of features such as single selection, multiple selection, dynamic loading and so on.
+
+:::demo Just like `el-cascader`, you can set alternative options by `options`, and enable other features by `props`, see the API form below for details.
+```html
+
+
+
+```
+:::
+
+### Cascader Attributes
+| Attribute | Description | Type | Accepted Values | Default |
+|---------- |-------- |---------- |------------- |-------- |
+| value / v-model | binding value | - | — | — |
+| options | data of the options,the key of `value` and `label` can be customize by `Props`.| array | — | — |
+| props | configuration options, see the following table. | object | — | — |
+| size | size of input | string | medium / small / mini | — |
+| placeholder | placeholder of input | string | — | Select |
+| disabled | whether Cascader is disabled | boolean | — | false |
+| clearable | whether selected value can be cleared | boolean | — | false |
+| show-all-levels | whether to display all levels of the selected value in the input | boolean | — | true |
+| collapse-tags | whether to collapse tags in multiple selection mode | boolean | - | false |
+| separator | option label separator | string | — | ' / ' |
+| filterable | whether the options can be searched | boolean | — | — |
+| filter-method | customize search logic, the first parameter is `node`, the second is `keyword`, and need return a boolean value indicating whether it hits. | function(node, keyword) | - | - |
+| debounce | debounce delay when typing filter keyword, in milliseconds | number | — | 300 |
+| before-filter | hook function before filtering with the value to be filtered as its parameter. If `false` is returned or a `Promise` is returned and then is rejected, filtering will be aborted | function(value) | — | — |
+| popper-class | custom class name for Cascader's dropdown | string | — | — |
+
+### Cascader Events
+| Event Name | Description | Parameters |
|---------- |-------- |---------- |
-| change | Se déclecnhe lorsque la valeur change. | La valeur |
-| active-item-change | Se déclenche quand le parent de l'option active change, ne marche que si `change-on-select` est `false` | Un tableau des options actives |
-| blur | Se déclenche quand le Cascader perds le focus. | (event: Event) |
-| focus | Se déclenche quand le Cascader a le focus. | (event: Event) |
-| visible-change | Se déclenche quand le menu apparaît ou disparaît. | `true` quand il apparaît, `false` sinon. |
+| change | triggers when the binding value changes | value |
+| expand-change | triggers when expand option changes | an array of the expanding node's parent nodes |
+| blur | triggers when Cascader blurs | (event: Event) |
+| focus | triggers when Cascader focuses | (event: Event) |
+| visible-change | triggers when the dropdown appears/disappears | true when it appears, and false otherwise |
+| remove-tag | triggers when remove tag in multiple selection mode | the value of the tag which is removed |
+
+### Cascader Slots
+| Slot Name | Description |
+|---------|-------------|
+| - | the custom content of cascader node, the parameter is { node, data }, which are current Node object and node data respectively. |
+| empty | content when there is no matched options. |
+
+### CascaderPanel Attributes
+| Attribute | Description | Type | Accepted Values | Default |
+|---------- |-------- |---------- |------------- |-------- |
+| value / v-model | binding value | - | — | — |
+| options | data of the options,the key of `value` and `label` can be customize by `Props`.| array | — | — |
+| props | configuration options, see the following table. | object | — | — |
+
+### CascaderPanel Events
+| Event Name | Description | Parameters |
+|---------- |-------- |---------- |
+| change | triggers when the binding value changes | value |
+| expand-change | triggers when expand option changes | an array of the expanding node's parent nodes |
+
+### CascaderPanel Slots
+| Slot Name | Description |
+|---------|-------------|
+| - | the custom content of cascader node, the parameter is { node, data }, which are current Node object and node data respectively. |
+
+### Props
+| Attribute | Description | Type | Accepted Values | Default |
+| -------- | ----------------- | ------ | ------ | ------ |
+| expandTrigger | trigger mode of expanding options | string | click / hover | 'click' |
+| multiple | whether multiple selection is enabled | boolean | - | false |
+| checkStrictly | whether checked state of a node not affects its parent and child nodes | boolean | - | false |
+| emitPath | when checked nodes change, whether to emit an array of node's path, if false, only emit the value of node. | boolean | - | true |
+| lazy | whether to dynamic load child nodes, use with `lazyload` attribute | boolean | - | false |
+| lazyLoad | method for loading child nodes data, only works when `lazy` is true | function(node, resolve) | - | - |
+| value | specify which key of node object is used as the node's value | string | — | 'value' |
+| label | specify which key of node object is used as the node's label | string | — | 'label' |
+| children | specify which key of node object is used as the node's children | string | — | 'children' |
+| disabled | specify which key of node object is used as the node's disabled | string | — | 'disabled' |
+| leaf | specify which key of node object is used as the node's leaf field | string | — | 'leaf' |
diff --git a/examples/docs/zh-CN/cascader.md b/examples/docs/zh-CN/cascader.md
index f675e5892..d2b37bb2d 100644
--- a/examples/docs/zh-CN/cascader.md
+++ b/examples/docs/zh-CN/cascader.md
@@ -6,30 +6,29 @@
有两种触发子菜单的方式
-:::demo 只需为 Cascader 的`options`属性指定选项数组即可渲染出一个级联选择器。通过`expand-trigger`可以定义展开子级菜单的触发方式。本例还展示了`change`事件,它的参数为 Cascader 的绑定值:一个由各级菜单的值所组成的数组。
+:::demo 只需为 Cascader 的`options`属性指定选项数组即可渲染出一个级联选择器。通过`props.expandTrigger`可以定义展开子级菜单的触发方式。
```html
默认 click 触发子菜单
-
+ @change="handleChange">
hover 触发子菜单
-
+ :props="{ expandTrigger: 'hover' }"
+ @change="handleChange">
+```
+:::
+
### 仅显示最后一级
可以仅在输入框中显示选中项最后一级的标签,而不是选中项所在的完整路径。
:::demo 属性`show-all-levels`定义了是否显示完整的路径,将其赋值为`false`则仅显示最后一级
```html
-
+
+
@@ -1157,24 +1229,22 @@
可以快捷地搜索选项并选择。
-:::demo 将`filterable`赋值为`true`即可打开搜索功能。
+:::demo 将`filterable`赋值为`true`即可打开搜索功能,默认会匹配节点的`label`或所有父节点的`label`(由`show-all-levels`决定)中包含输入值的选项。你也可以用`filter-method`自定义搜索逻辑,接受一个函数,第一个参数是节点`node`,第二个参数是搜索关键词`keyword`,通过返回布尔值表示是否命中。
```html
- 只可选择最后一级菜单的选项
+ 单选可搜索
+ filterable>
- 可选择任意一级菜单的选项
+ 多选可搜索
+ :props="{ multiple: true }"
+ filterable>
+```
+:::
+
+### 级联面板
+
+级联面板是级联选择器的核心组件,与级联选择器一样,有单选、多选、动态加载等多种功能。
+
+:::demo 和级联选择器一样,通过`options`来指定选项,也可通过`props`来设置多选、动态加载等功能,具体详情见下方API表格。
+```html
+
+
+
+```
+:::
+
+### Cascader Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|---------- |-------- |---------- |------------- |-------- |
-| value / v-model | 选中项绑定值 | array | — | — |
-| options | 可选项数据源,键名可通过 `props` 属性配置 | array | — | — |
+| value / v-model | 选中项绑定值 | - | — | — |
+| options | 可选项数据源,键名可通过 `Props` 属性配置 | array | — | — |
| props | 配置选项,具体见下表 | object | — | — |
-| separator | 选项分隔符 | string | — | 斜杠'/' |
-| popper-class | 自定义浮层类名 | string | — | — |
+| size | 尺寸 | string | medium / small / mini | — |
| placeholder | 输入框占位文本 | string | — | 请选择 |
| disabled | 是否禁用 | boolean | — | false |
| clearable | 是否支持清空选项 | boolean | — | false |
-| expand-trigger | 次级菜单的展开方式 | string | click / hover | click |
| show-all-levels | 输入框中是否显示选中值的完整路径 | boolean | — | true |
+| collapse-tags | 多选模式下是否折叠Tag | boolean | - | false |
+| separator | 选项分隔符 | string | — | 斜杠' / ' |
| filterable | 是否可搜索选项 | boolean | — | — |
+| filter-method | 自定义搜索逻辑,第一个参数是节点`node`,第二个参数是搜索关键词`keyword`,通过返回布尔值表示是否命中 | function(node, keyword) | - | - |
| debounce | 搜索关键词输入的去抖延迟,毫秒 | number | — | 300 |
-| change-on-select | 是否允许选择任意一级的选项 | boolean | — | false |
-| size | 尺寸 | string | medium / small / mini | — |
| before-filter | 筛选之前的钩子,参数为输入的值,若返回 false 或者返回 Promise 且被 reject,则停止筛选 | function(value) | — | — |
+| popper-class | 自定义浮层类名 | string | — | — |
-### props
-| 参数 | 说明 | 类型 | 可选值 | 默认值 |
-| -------- | ----------------- | ------ | ------ | ------ |
-| value | 指定选项的值为选项对象的某个属性值 | string | — | — |
-| label | 指定选项标签为选项对象的某个属性值 | string | — | — |
-| children | 指定选项的子选项为选项对象的某个属性值 | string | — | — |
-| disabled | 指定选项的禁用为选项对象的某个属性值 | string | — | — |
-
-### Events
+### Cascader Events
| 事件名称 | 说明 | 回调参数 |
|---------- |-------- |---------- |
-| change | 当绑定值变化时触发的事件 | 当前值 |
-| active-item-change | 当父级选项变化时触发的事件,仅在 `change-on-select` 为 `false` 时可用 | 各父级选项组成的数组 |
-| blur | 在 Cascader 失去焦点时触发 | (event: Event) |
-| focus | 在 Cascader 获得焦点时触发 | (event: Event) |
+| change | 当选中节点变化时触发 | 选中节点的值 |
+| expand-change | 当展开节点发生变化时触发 | 各父级选项值组成的数组 |
+| blur | 当失去焦点时触发 | (event: Event) |
+| focus | 当获得焦点时触发 | (event: Event) |
| visible-change | 下拉框出现/隐藏时触发 | 出现则为 true,隐藏则为 false |
+| remove-tag | 在多选模式下,移除Tag时触发 | 移除的Tag对应的节点的值 |
+
+### Cascader Slots
+| 名称 | 说明 |
+|---------|-------------|
+| - | 自定义备选项的节点内容,参数为 { node, data },分别为当前节点的 Node 对象和数据 |
+| empty | 无匹配选项时的内容 |
+
+### CascaderPanel Attributes
+| 参数 | 说明 | 类型 | 可选值 | 默认值 |
+|---------- |-------- |---------- |------------- |-------- |
+| value / v-model | 选中项绑定值 | - | — | — |
+| options | 可选项数据源,键名可通过 `Props` 属性配置 | array | — | — |
+| props | 配置选项,具体见下表 | object | — | — |
+
+### CascaderPanel Events
+| 事件名称 | 说明 | 回调参数 |
+|---------- |-------- |---------- |
+| change | 当选中节点变化时触发 | 选中节点的值 |
+| expand-change | 当展开节点发生变化时触发 | 各父级选项值组成的数组 |
+
+### CascaderPanel Slots
+| 名称 | 说明 |
+|---------|-------------|
+| - | 自定义备选项的节点内容,参数为 { node, data },分别为当前节点的 Node 对象和数据 |
+
+### Props
+| 参数 | 说明 | 类型 | 可选值 | 默认值 |
+| -------- | ----------------- | ------ | ------ | ------ |
+| expandTrigger | 次级菜单的展开方式 | string | click / hover | 'click' |
+| multiple | 是否多选 | boolean | - | false |
+| checkStrictly | 是否严格的遵守父子节点不互相关联 | boolean | - | false |
+| emitPath | 在选中节点改变时,是否返回由该节点所在的各级菜单的值所组成的数组,若设置 false,则只返回该节点的值 | boolean | - | true |
+| lazy | 是否动态加载子节点,需与 lazyLoad 方法结合使用 | boolean | - | false |
+| lazyLoad | 加载动态数据的方法,仅在 lazy 为 true 时有效 | function(node, resolve),`node`为当前点击的节点,`resolve`为数据加载完成的回调(必须调用) | - | - |
+| value | 指定选项的值为选项对象的某个属性值 | string | — | 'value' |
+| label | 指定选项标签为选项对象的某个属性值 | string | — | 'label' |
+| children | 指定选项的子选项为选项对象的某个属性值 | string | — | 'children' |
+| disabled | 指定选项的禁用为选项对象的某个属性值 | string | — | 'disabled' |
+| leaf | 指定选项的叶子节点的标志位为选项对象的某个属性值 | string | — | 'leaf' |
diff --git a/packages/cascader-panel/index.js b/packages/cascader-panel/index.js
new file mode 100644
index 000000000..ceade94e7
--- /dev/null
+++ b/packages/cascader-panel/index.js
@@ -0,0 +1,8 @@
+import CascaderPanel from './src/cascader-panel';
+
+/* istanbul ignore next */
+CascaderPanel.install = function(Vue) {
+ Vue.component(CascaderPanel.name, CascaderPanel);
+};
+
+export default CascaderPanel;
diff --git a/packages/cascader-panel/src/cascader-menu.vue b/packages/cascader-panel/src/cascader-menu.vue
new file mode 100644
index 000000000..c7977e07f
--- /dev/null
+++ b/packages/cascader-panel/src/cascader-menu.vue
@@ -0,0 +1,138 @@
+
diff --git a/packages/cascader-panel/src/cascader-node.vue b/packages/cascader-panel/src/cascader-node.vue
new file mode 100644
index 000000000..d08564a9a
--- /dev/null
+++ b/packages/cascader-panel/src/cascader-node.vue
@@ -0,0 +1,246 @@
+
diff --git a/packages/cascader-panel/src/cascader-panel.vue b/packages/cascader-panel/src/cascader-panel.vue
new file mode 100644
index 000000000..3ac558f6a
--- /dev/null
+++ b/packages/cascader-panel/src/cascader-panel.vue
@@ -0,0 +1,354 @@
+
+
+
+
+
+
+
diff --git a/packages/cascader-panel/src/node.js b/packages/cascader-panel/src/node.js
new file mode 100644
index 000000000..1efd4f775
--- /dev/null
+++ b/packages/cascader-panel/src/node.js
@@ -0,0 +1,166 @@
+import { isEqual, capitalize } from 'element-ui/src/utils/util';
+import { isDef } from 'element-ui/src/utils/shared';
+
+let uid = 0;
+
+export default class Node {
+
+ constructor(data, config, parentNode) {
+ this.data = data;
+ this.config = config;
+ this.parent = parentNode || null;
+ this.level = !this.parent ? 1 : this.parent.level + 1;
+ this.uid = uid++;
+
+ this.initState();
+ this.initChildren();
+ }
+
+ initState() {
+ const { value: valueKey, label: labelKey } = this.config;
+
+ this.value = this.data[valueKey];
+ this.label = this.data[labelKey];
+ this.pathNodes = this.calculatePathNodes();
+ this.path = this.pathNodes.map(node => node.value);
+ this.pathLabels = this.pathNodes.map(node => node.label);
+
+ // lazy load
+ this.loading = false;
+ this.loaded = false;
+ }
+
+ initChildren() {
+ const { config } = this;
+ const childrenKey = config.children;
+ const childrenData = this.data[childrenKey];
+ this.hasChildren = Array.isArray(childrenData);
+ this.children = (childrenData || []).map(child => new Node(child, config, this));
+ }
+
+ get isDisabled() {
+ const { data, parent, config } = this;
+ const disabledKey = config.disabled;
+ const { checkStrictly } = config;
+ return data[disabledKey] ||
+ !checkStrictly && parent && parent.isDisabled;
+ }
+
+ get isLeaf() {
+ const { data, loaded, hasChildren, children } = this;
+ const { lazy, leaf: leafKey } = this.config;
+ if (lazy) {
+ const isLeaf = isDef(data[leafKey])
+ ? data[leafKey]
+ : (loaded ? !children.length : false);
+ this.hasChildren = !isLeaf;
+ return isLeaf;
+ }
+ return !hasChildren;
+ }
+
+ calculatePathNodes() {
+ const nodes = [this];
+ let parent = this.parent;
+
+ while (parent) {
+ nodes.unshift(parent);
+ parent = parent.parent;
+ }
+
+ return nodes;
+ }
+
+ getPath() {
+ return this.path;
+ }
+
+ getValue() {
+ return this.value;
+ }
+
+ getValueByOption() {
+ return this.config.emitPath
+ ? this.getPath()
+ : this.getValue();
+ }
+
+ getText(allLevels, separator) {
+ return allLevels ? this.pathLabels.join(separator) : this.label;
+ }
+
+ isSameNode(checkedValue) {
+ const value = this.getValueByOption();
+ return this.config.multiple && Array.isArray(checkedValue)
+ ? checkedValue.some(val => isEqual(val, value))
+ : isEqual(checkedValue, value);
+ }
+
+ broadcast(event, ...args) {
+ const handlerName = `onParent${capitalize(event)}`;
+
+ this.children.forEach(child => {
+ if (child) {
+ // bottom up
+ child.broadcast(event, ...args);
+ child[handlerName] && child[handlerName](...args);
+ }
+ });
+ }
+
+ emit(event, ...args) {
+ const { parent } = this;
+ const handlerName = `onChild${capitalize(event)}`;
+ if (parent) {
+ parent[handlerName] && parent[handlerName](...args);
+ parent.emit(event, ...args);
+ }
+ }
+
+ onParentCheck(checked) {
+ if (!this.isDisabled) {
+ this.setCheckState(checked);
+ }
+ }
+
+ onChildCheck() {
+ const { children } = this;
+ const validChildren = children.filter(child => !child.isDisabled);
+ const checked = validChildren.length
+ ? validChildren.every(child => child.checked)
+ : false;
+
+ this.setCheckState(checked);
+ }
+
+ setCheckState(checked) {
+ const totalNum = this.children.length;
+ const checkedNum = this.children.reduce((c, p) => {
+ const num = p.checked ? 1 : (p.indeterminate ? 0.5 : 0);
+ return c + num;
+ }, 0);
+
+ this.checked = checked;
+ this.indeterminate = checkedNum !== totalNum && checkedNum > 0;
+ }
+
+ syncCheckState(checkedValue) {
+ const value = this.getValueByOption();
+ const checked = this.isSameNode(checkedValue, value);
+
+ this.doCheck(checked);
+ }
+
+ doCheck(checked) {
+ if (this.checked !== checked) {
+ if (this.config.checkStrictly) {
+ this.checked = checked;
+ } else {
+ // bottom up to unify the calculation of the indeterminate state
+ this.broadcast('check', checked);
+ this.setCheckState(checked);
+ this.emit('check');
+ }
+ }
+ }
+}
diff --git a/packages/cascader-panel/src/store.js b/packages/cascader-panel/src/store.js
new file mode 100644
index 000000000..19f930dd1
--- /dev/null
+++ b/packages/cascader-panel/src/store.js
@@ -0,0 +1,63 @@
+import Node from './node';
+import { coerceTruthyValueToArray } from 'element-ui/src/utils/util';
+
+const flatNodes = (data, leafOnly) => {
+ return data.reduce((res, node) => {
+ if (node.isLeaf) {
+ res.push(node);
+ } else {
+ !leafOnly && res.push(node);
+ res = res.concat(flatNodes(node.children, leafOnly));
+ }
+ return res;
+ }, []);
+};
+
+export default class Store {
+
+ constructor(data, config) {
+ this.config = config;
+ this.initNodes(data);
+ }
+
+ initNodes(data) {
+ data = coerceTruthyValueToArray(data);
+ this.nodes = data.map(nodeData => new Node(nodeData, this.config));
+ this.flattedNodes = this.getFlattedNodes(false, false);
+ this.leafNodes = this.getFlattedNodes(true, false);
+ }
+
+ appendNode(nodeData, parentNode) {
+ const node = new Node(nodeData, this.config, parentNode);
+ const children = parentNode ? parentNode.children : this.nodes;
+
+ children.push(node);
+ }
+
+ appendNodes(nodeDataList, parentNode) {
+ nodeDataList = coerceTruthyValueToArray(nodeDataList);
+ nodeDataList.forEach(nodeData => this.appendNode(nodeData, parentNode));
+ }
+
+ getNodes() {
+ return this.nodes;
+ }
+
+ getFlattedNodes(leafOnly, cached = true) {
+ const cachedNodes = leafOnly ? this.leafNodes : this.flattedNodes;
+ return cached
+ ? cachedNodes
+ : flatNodes(this.nodes, leafOnly);
+ }
+
+ getNodeByValue(value) {
+ if (value) {
+ value = Array.isArray(value) ? value[value.length - 1] : value;
+ const nodes = this.getFlattedNodes(false, !this.config.lazy)
+ .filter(node => node.value === value);
+ return nodes && nodes.length ? nodes[0] : null;
+ }
+ return null;
+ }
+
+}
diff --git a/packages/cascader/index.js b/packages/cascader/index.js
index 68163ac5e..b3d860fc1 100644
--- a/packages/cascader/index.js
+++ b/packages/cascader/index.js
@@ -1,4 +1,4 @@
-import Cascader from './src/main';
+import Cascader from './src/cascader';
/* istanbul ignore next */
Cascader.install = function(Vue) {
diff --git a/packages/cascader/src/cascader.vue b/packages/cascader/src/cascader.vue
new file mode 100644
index 000000000..15d56edea
--- /dev/null
+++ b/packages/cascader/src/cascader.vue
@@ -0,0 +1,639 @@
+
+ toggleDropDownVisible(readonly ? undefined : true)"
+ @keydown="handleKeyDown">
+
+
+
+
+
+
+
+
+
+
+ {{ tag.text }}
+
+ handleInput(inputValue, e)"
+ @click.stop="toggleDropDownVisible(true)"
+ @keydown.delete="handleDelete">
+
+
+
+
+
+
+
+
+ {{ item.text }}
+
+
+
+
+ {{ t('el.cascader.noMatch') }}
+
+
+
+
+
+
+
+
+
diff --git a/packages/cascader/src/main.vue b/packages/cascader/src/main.vue
deleted file mode 100644
index 3d8e98f40..000000000
--- a/packages/cascader/src/main.vue
+++ /dev/null
@@ -1,452 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- {{ label }}
- {{ separator }}
-
-
-
- {{ currentLabels[currentLabels.length - 1] }}
-
-
-
-
-
-
diff --git a/packages/cascader/src/menu.vue b/packages/cascader/src/menu.vue
deleted file mode 100644
index 3eff3cd08..000000000
--- a/packages/cascader/src/menu.vue
+++ /dev/null
@@ -1,375 +0,0 @@
-
diff --git a/packages/theme-chalk/src/cascader-panel.scss b/packages/theme-chalk/src/cascader-panel.scss
new file mode 100644
index 000000000..d2061e247
--- /dev/null
+++ b/packages/theme-chalk/src/cascader-panel.scss
@@ -0,0 +1,124 @@
+@import "mixins/mixins";
+@import "common/var";
+@import "./checkbox";
+@import "./radio";
+@import "./scrollbar";
+
+@include b(cascader-panel) {
+ display: flex;
+ border-radius: $--cascader-menu-radius;
+ font-size: $--cascader-menu-font-size;
+
+ @include when(bordered) {
+ border: $--cascader-menu-border;
+ border-radius: $--cascader-menu-radius;
+ }
+}
+
+@include b(cascader-menu) {
+ min-width: 180px;
+ box-sizing: border-box;
+ color: $--cascader-menu-font-color;
+ border-right: $--cascader-menu-border;
+
+ &:last-child {
+ border-right: none;
+ .el-cascader-node {
+ padding-right: 20px;
+ }
+ }
+
+ @include e(wrap) {
+ height: 204px;
+ }
+
+ @include e(list) {
+ position: relative;
+ min-height: 100%;
+ margin: 0;
+ padding: 6px 0;
+ list-style: none;
+ box-sizing: border-box;
+ }
+
+ @include e(hover-zone) {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ }
+
+ @include e(empty-text) {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ text-align: center;
+ color: $--cascader-color-empty;
+ }
+}
+
+@include b(cascader-node) {
+ position: relative;
+ display: flex;
+ align-items: center;
+ padding: 0 30px 0 20px;
+ height: 34px;
+ line-height: 34px;
+ outline: none;
+
+ &.is-selectable.in-active-path {
+ color: $--cascader-menu-font-color;
+ }
+
+ &.in-active-path,
+ &.is-selectable.in-checked-path,
+ &.is-active {
+ color: $--cascader-menu-selected-font-color;
+ font-weight: bold;
+ }
+
+ &:not(.is-disabled) {
+ cursor: pointer;
+ &:hover, &:focus {
+ background: $--cascader-node-background-hover;
+ }
+ }
+
+ @include when(disabled) {
+ color: $--cascader-node-color-disabled;
+ cursor: not-allowed;
+ }
+
+ @include e(prefix) {
+ position: absolute;
+ left: 10px;
+ }
+
+ @include e(postfix) {
+ position: absolute;
+ right: 10px;
+ }
+
+ @include e(label) {
+ flex: 1;
+ padding: 0 10px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ > .el-checkbox {
+ margin-right: 0;
+ }
+
+ > .el-radio {
+ margin-right: 0;
+
+ .el-radio__label {
+ padding-left: 0;
+ }
+ }
+}
diff --git a/packages/theme-chalk/src/cascader.scss b/packages/theme-chalk/src/cascader.scss
index b3b10210e..5c883ea9c 100644
--- a/packages/theme-chalk/src/cascader.scss
+++ b/packages/theme-chalk/src/cascader.scss
@@ -1,7 +1,9 @@
@import "mixins/mixins";
@import "common/var";
-@import "./input.scss";
+@import "./input";
@import "./popper";
+@import "./tag";
+@import "./cascader-panel";
@include b(cascader) {
display: inline-block;
@@ -9,76 +11,57 @@
font-size: $--font-size-base;
line-height: $--input-height;
- .el-input,
- .el-input__inner {
- cursor: pointer;
- }
-
- .el-input.is-focus .el-input__inner {
- border-color: $--input-focus-border;
- }
-
- .el-input__icon {
- transition: none;
- }
-
- .el-icon-arrow-down {
- transition: transform .3s;
- font-size: 14px;
-
- @include when(reverse) {
- transform: rotateZ(180deg);
+ &:not(.is-disabled):hover {
+ .el-input__inner {
+ cursor: pointer;
+ border-color: $--input-hover-border;
}
}
- .el-icon-circle-close {
- z-index: #{$--index-normal + 1};
- transition: $--color-transition-base;
-
- &:hover {
- color: $--color-text-secondary;
- }
- }
-
- @include e(clearIcon) {
- z-index: 2;
- position: relative;
- }
-
- @include e(label) {
- position: absolute;
- left: 0;
- top: 0;
- height: 100%;
- padding: 0 25px 0 15px;
- color: $--cascader-menu-font-color;
- width: 100%;
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow: hidden;
- box-sizing: border-box;
+ .el-input {
cursor: pointer;
- text-align: left;
- font-size: inherit;
- span {
- color: $--color-black;
+ .el-input__inner {
+ text-overflow: ellipsis;
+
+ &:focus {
+ border-color: $--input-focus-border;
+ }
+ }
+
+ .el-icon-arrow-down {
+ transition: transform .3s;
+ font-size: 14px;
+
+ @include when(reverse) {
+ transform: rotateZ(180deg);
+ }
+ }
+
+ .el-icon-circle-close:hover {
+ color: $--input-clear-hover-color;
+ }
+
+ @include when(focus) {
+ .el-input__inner {
+ border-color: $--input-focus-border;
+ }
}
}
@include m(medium) {
font-size: $--input-medium-font-size;
- line-height: #{$--input-medium-height};
+ line-height: $--input-medium-height;
}
@include m(small) {
font-size: $--input-small-font-size;
- line-height: #{$--input-small-height};
+ line-height: $--input-small-height;
}
@include m(mini) {
font-size: $--input-mini-font-size;
- line-height: #{$--input-mini-height};
+ line-height: $--input-mini-height;
}
@include when(disabled) {
@@ -87,99 +70,113 @@
color: $--disabled-color-base;
}
}
-}
-@include b(cascader-menus) {
- white-space: nowrap;
- background: #fff;
- position: absolute;
- margin: 5px 0;
- z-index: #{$--index-normal + 1};
- border: $--select-dropdown-border;
- border-radius: $--border-radius-small;
- box-shadow: $--select-dropdown-shadow;
-}
-
-@include b(cascader-menu) {
- display: inline-block;
- vertical-align: top;
- height: 204px;
- overflow: auto;
- border-right: $--select-dropdown-border;
- background-color: $--select-dropdown-background;
- box-sizing: border-box;
- margin: 0;
- padding: 6px 0;
- min-width: 160px;
-
- &:last-child {
- border-right: 0;
+ @include e(dropdown) {
+ margin: 5px 0;
+ font-size: $--cascader-menu-font-size;
+ background: $--cascader-menu-fill;
+ border: $--cascader-menu-border;
+ border-radius: $--cascader-menu-radius;
+ box-shadow: $--cascader-menu-shadow;
}
- @include e(item) {
- font-size: $--select-font-size;
- padding: 8px 20px;
- position: relative;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- color: $--select-option-color;
- height: $--select-option-height;
- line-height: 1.5;
+ @include e(tags) {
+ position: absolute;
+ left: 0;
+ right: 30px;
+ top: 50%;
+ transform: translateY(-50%);
+ display: flex;
+ flex-wrap: wrap;
+ line-height: normal;
+ text-align: left;
box-sizing: border-box;
- cursor: pointer;
+
+ .el-tag {
+ display: inline-flex;
+ align-items: center;
+ max-width: 100%;
+ margin: 2px 0 2px 6px;
+ text-overflow: ellipsis;
+ background: $--cascader-tag-background;
+
+ &:not(.is-hit) {
+ border-color: transparent;
+ }
+
+ > span {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .el-icon-close {
+ flex: none;
+ background-color: $--color-text-placeholder;
+ color: $--color-white;
+
+ &:hover {
+ background-color: $--color-text-secondary;
+ }
+ }
+ }
+ }
+
+ @include e(suggestion-panel) {
+ border-radius: $--cascader-menu-radius;
+ }
+
+ @include e(suggestion-list) {
+ max-height: 204px;
+ margin: 0;
+ padding: 6px 0;
+ font-size: $--font-size-base;
+ color: $--cascader-menu-font-color;
+ text-align: center;
+ }
+
+ @include e(suggestion-item) {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ height: 34px;
+ padding: 0 15px;
+ text-align: left;
outline: none;
+ cursor: pointer;
- span {
- padding-right: 10px;
+ &:hover, &:focus {
+ background: $--cascader-node-background-hover;
}
- @include m(extensible) {
- &:after {
- font-family: 'element-icons';
- content: "\e6e0";
- font-size: 14px;
- color: rgb(191, 203, 217);
- position: absolute;
- right: 15px;
- }
- }
-
- @include when(disabled) {
- color: $--select-option-disabled-color;
- background-color: $--select-option-disabled-background;
- cursor: not-allowed;
-
- &:hover {
- background-color: $--color-white;
- }
- }
-
- @include when(active) {
+ &.is-checked {
color: $--cascader-menu-selected-font-color;
+ font-weight: bold;
}
- &:hover, &:focus:not(:active) {
- background-color: $--select-option-hover-background;
- }
-
- &.selected {
- color: $--color-white;
- background-color: $--select-option-selected-hover;
+ > span {
+ margin-right: 10px;
}
}
- @include e(item__keyword) {
- font-weight: bold;
+ @include e(empty-text) {
+ margin: 10px 0;
+ color: $--cascader-color-empty;
}
- @include m(flexible) {
- height: auto;
- max-height: 180px;
- overflow: auto;
+ @include e(search-input) {
+ flex: 1;
+ height: 24px;
+ min-width: 60px;
+ margin: 2px 0 2px 15px;
+ padding: 0;
+ color: $--cascader-menu-font-color;
+ border: none;
+ outline: none;
+ box-sizing: border-box;
- .el-cascader-menu__item {
- overflow: visible;
+ &::placeholder {
+ color: $--color-text-placeholder;
}
}
}
diff --git a/packages/theme-chalk/src/common/var.scss b/packages/theme-chalk/src/common/var.scss
index 1559fc859..38550c03e 100644
--- a/packages/theme-chalk/src/common/var.scss
+++ b/packages/theme-chalk/src/common/var.scss
@@ -474,19 +474,14 @@ $--cascader-menu-font-color: $--color-text-regular !default;
/// color||Color|0
$--cascader-menu-selected-font-color: $--color-primary !default;
$--cascader-menu-fill: $--fill-base !default;
-$--cascader-menu-border: $--border-base !default;
-$--cascader-menu-border-width: $--border-width-base !default;
-$--cascader-menu-color: $--color-text-regular !default;
-$--cascader-menu-option-color-active: $--color-text-secondary !default;
-$--cascader-menu-option-fill-active: rgba($--color-text-secondary, 0.12) !default;
-$--cascader-menu-option-color-hover: $--color-text-regular !default;
-$--cascader-menu-option-fill-hover: rgba($--color-text-primary, 0.06) !default;
-$--cascader-menu-option-color-disabled: #999 !default;
-$--cascader-menu-option-fill-disabled: rgba($--color-black, 0.06) !default;
-$--cascader-menu-option-empty-color: #666 !default;
-$--cascader-menu-shadow: 0 1px 2px rgba($--color-black, 0.14), 0 0 3px rgba($--color-black, 0.14) !default;
-$--cascader-menu-option-pinyin-color: #999 !default;
-$--cascader-menu-submenu-shadow: 1px 1px 2px rgba($--color-black, 0.14), 1px 0 2px rgba($--color-black, 0.14) !default;
+$--cascader-menu-font-size: $--font-size-base !default;
+$--cascader-menu-radius: $--border-radius-base !default;
+$--cascader-menu-border: solid 1px $--border-color-light !default;
+$--cascader-menu-shadow: $--box-shadow-light !default;
+$--cascader-node-background-hover: $--background-color-base !default;
+$--cascader-node-color-disabled:$--color-text-placeholder !default;
+$--cascader-color-empty:$--color-text-placeholder !default;
+$--cascader-tag-background: #f0f2f5;
/* Group
-------------------------- */
diff --git a/packages/theme-chalk/src/index.scss b/packages/theme-chalk/src/index.scss
index f54249b79..6ba2f6c8e 100644
--- a/packages/theme-chalk/src/index.scss
+++ b/packages/theme-chalk/src/index.scss
@@ -72,4 +72,6 @@
@import "./image.scss";
@import "./calendar.scss";
@import "./backtop.scss";
+@import "./infiniteScroll.scss";
@import "./page-header.scss";
+@import "./cascader-panel.scss";
diff --git a/packages/theme-chalk/src/infiniteScroll.scss b/packages/theme-chalk/src/infiniteScroll.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/index.js b/src/index.js
index 0133c4b9b..f764fe8d9 100644
--- a/src/index.js
+++ b/src/index.js
@@ -78,6 +78,7 @@ import Calendar from '../packages/calendar/index.js';
import Backtop from '../packages/backtop/index.js';
import InfiniteScroll from '../packages/infiniteScroll/index.js';
import PageHeader from '../packages/page-header/index.js';
+import CascaderPanel from '../packages/cascader-panel/index.js';
import locale from 'element-ui/src/locale';
import CollapseTransition from 'element-ui/src/transitions/collapse-transition';
@@ -155,6 +156,7 @@ const components = [
Calendar,
Backtop,
PageHeader,
+ CascaderPanel,
CollapseTransition
];
@@ -272,5 +274,6 @@ export default {
Calendar,
Backtop,
InfiniteScroll,
- PageHeader
+ PageHeader,
+ CascaderPanel
};
diff --git a/src/locale/lang/af-ZA.js b/src/locale/lang/af-ZA.js
index f207eba8c..a26af24c8 100644
--- a/src/locale/lang/af-ZA.js
+++ b/src/locale/lang/af-ZA.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Geen toepaslike data',
loading: 'Laai',
- placeholder: 'Kies'
+ placeholder: 'Kies',
+ noData: 'Geen data'
},
pagination: {
goto: 'Gaan na',
diff --git a/src/locale/lang/ar.js b/src/locale/lang/ar.js
index 8af92a5ec..40a8c14ff 100644
--- a/src/locale/lang/ar.js
+++ b/src/locale/lang/ar.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'لايوجد بيانات مطابقة',
loading: 'جار التحميل',
- placeholder: 'أختر'
+ placeholder: 'أختر',
+ noData: 'لايوجد بيانات'
},
pagination: {
goto: 'أذهب إلى',
diff --git a/src/locale/lang/bg.js b/src/locale/lang/bg.js
index d8b561452..b4cbbdc47 100644
--- a/src/locale/lang/bg.js
+++ b/src/locale/lang/bg.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Няма намерени',
loading: 'Зареждане',
- placeholder: 'Избери'
+ placeholder: 'Избери',
+ noData: 'Няма данни'
},
pagination: {
goto: 'Иди на',
diff --git a/src/locale/lang/ca.js b/src/locale/lang/ca.js
index fd06ce8df..7fe5be262 100644
--- a/src/locale/lang/ca.js
+++ b/src/locale/lang/ca.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'No hi ha dades que coincideixin',
loading: 'Carregant',
- placeholder: 'Seleccionar'
+ placeholder: 'Seleccionar',
+ noData: 'Sense Dades'
},
pagination: {
goto: 'Anar a',
diff --git a/src/locale/lang/cs-CZ.js b/src/locale/lang/cs-CZ.js
index 12beb2cb2..f15f421e5 100644
--- a/src/locale/lang/cs-CZ.js
+++ b/src/locale/lang/cs-CZ.js
@@ -69,7 +69,8 @@ export default {
cascader: {
noMatch: 'Žádná shoda',
loading: 'Načítání',
- placeholder: 'Vybrat'
+ placeholder: 'Vybrat',
+ noData: 'Žádná data'
},
pagination: {
goto: 'Jít na',
diff --git a/src/locale/lang/da.js b/src/locale/lang/da.js
index 2d4991473..44ef40769 100644
--- a/src/locale/lang/da.js
+++ b/src/locale/lang/da.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Ingen matchende data',
loading: 'Henter',
- placeholder: 'Vælg'
+ placeholder: 'Vælg',
+ noData: 'Ingen data'
},
pagination: {
goto: 'Gå til',
diff --git a/src/locale/lang/de.js b/src/locale/lang/de.js
index 196d6911c..14542795f 100644
--- a/src/locale/lang/de.js
+++ b/src/locale/lang/de.js
@@ -69,7 +69,8 @@ export default {
cascader: {
noMatch: 'Nichts gefunden.',
loading: 'Lädt.',
- placeholder: 'Daten wählen'
+ placeholder: 'Daten wählen',
+ noData: 'Keine Daten'
},
pagination: {
goto: 'Gehe zu',
diff --git a/src/locale/lang/ee.js b/src/locale/lang/ee.js
index 640612907..da65b1f09 100644
--- a/src/locale/lang/ee.js
+++ b/src/locale/lang/ee.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Sobivad andmed puuduvad',
loading: 'Laadimine',
- placeholder: 'Vali'
+ placeholder: 'Vali',
+ noData: 'Andmed puuduvad'
},
pagination: {
goto: 'Mine lehele',
diff --git a/src/locale/lang/el.js b/src/locale/lang/el.js
index f111bfc6e..03d8e1cc8 100644
--- a/src/locale/lang/el.js
+++ b/src/locale/lang/el.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Δεν βρέθηκαν αποτελέσματα',
loading: 'Φόρτωση',
- placeholder: 'Επιλογή'
+ placeholder: 'Επιλογή',
+ noData: 'Χωρίς δεδομένα'
},
pagination: {
goto: 'Μετάβαση σε',
diff --git a/src/locale/lang/en.js b/src/locale/lang/en.js
index c2ea43832..3e45ee637 100644
--- a/src/locale/lang/en.js
+++ b/src/locale/lang/en.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'No matching data',
loading: 'Loading',
- placeholder: 'Select'
+ placeholder: 'Select',
+ noData: 'No data'
},
pagination: {
goto: 'Go to',
diff --git a/src/locale/lang/es.js b/src/locale/lang/es.js
index 8fb737ca9..7810038b1 100644
--- a/src/locale/lang/es.js
+++ b/src/locale/lang/es.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'No hay datos que coincidan',
loading: 'Cargando',
- placeholder: 'Seleccionar'
+ placeholder: 'Seleccionar',
+ noData: 'Sin datos'
},
pagination: {
goto: 'Ir a',
diff --git a/src/locale/lang/eu.js b/src/locale/lang/eu.js
index 9f5f182d3..481b3d060 100644
--- a/src/locale/lang/eu.js
+++ b/src/locale/lang/eu.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Bat datorren daturik ez',
loading: 'Kargatzen',
- placeholder: 'Hautatu'
+ placeholder: 'Hautatu',
+ noData: 'Daturik ez'
},
pagination: {
goto: 'Joan',
diff --git a/src/locale/lang/fa.js b/src/locale/lang/fa.js
index b70a45c92..84ad5b660 100644
--- a/src/locale/lang/fa.js
+++ b/src/locale/lang/fa.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'هیچ دادهای پیدا نشد',
loading: 'بارگیری',
- placeholder: 'انتخاب کنید'
+ placeholder: 'انتخاب کنید',
+ noData: 'اطلاعاتی وجود ندارد'
},
pagination: {
goto: 'برو به',
diff --git a/src/locale/lang/fi.js b/src/locale/lang/fi.js
index 5b3c5ef3c..b836c90c8 100644
--- a/src/locale/lang/fi.js
+++ b/src/locale/lang/fi.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Ei vastaavia tietoja',
loading: 'Lataa',
- placeholder: 'Valitse'
+ placeholder: 'Valitse',
+ noData: 'Ei tietoja'
},
pagination: {
goto: 'Mene',
diff --git a/src/locale/lang/fr.js b/src/locale/lang/fr.js
index 10a456461..3421e2a58 100644
--- a/src/locale/lang/fr.js
+++ b/src/locale/lang/fr.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Aucune correspondance',
loading: 'Chargement',
- placeholder: 'Choisir'
+ placeholder: 'Choisir',
+ noData: 'Aucune donnée'
},
pagination: {
goto: 'Aller à',
diff --git a/src/locale/lang/he.js b/src/locale/lang/he.js
index f27b99bfe..09650186a 100644
--- a/src/locale/lang/he.js
+++ b/src/locale/lang/he.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'ללא נתונים מתאימים',
loading: 'טוען',
- placeholder: 'בחר'
+ placeholder: 'בחר',
+ noData: 'ללא נתונים'
},
pagination: {
goto: 'עבור ל',
diff --git a/src/locale/lang/hr.js b/src/locale/lang/hr.js
index a508e7d3f..dbb1b0764 100644
--- a/src/locale/lang/hr.js
+++ b/src/locale/lang/hr.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Nema pronađenih podataka',
loading: 'Učitavanje',
- placeholder: 'Izaberi'
+ placeholder: 'Izaberi',
+ noData: 'Nema podataka'
},
pagination: {
goto: 'Idi na',
diff --git a/src/locale/lang/hu.js b/src/locale/lang/hu.js
index 295c3105c..0086727d4 100644
--- a/src/locale/lang/hu.js
+++ b/src/locale/lang/hu.js
@@ -66,7 +66,8 @@ export default {
cascader: {
noMatch: 'Nincs találat',
loading: 'Betöltés',
- placeholder: 'Válassz'
+ placeholder: 'Válassz',
+ noData: 'Nincs adat'
},
pagination: {
goto: 'Ugrás',
diff --git a/src/locale/lang/hy-AM.js b/src/locale/lang/hy-AM.js
index 6b3c63918..7f2c79375 100644
--- a/src/locale/lang/hy-AM.js
+++ b/src/locale/lang/hy-AM.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Համապատասխան տուեալներ չկան',
loading: 'Բեռնում',
- placeholder: 'Ընտրել'
+ placeholder: 'Ընտրել',
+ noData: 'Տվյալներ չկան'
},
pagination: {
goto: 'Անցնել',
diff --git a/src/locale/lang/id.js b/src/locale/lang/id.js
index 5875a86c8..ce3bedf60 100644
--- a/src/locale/lang/id.js
+++ b/src/locale/lang/id.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Tidak ada data yg cocok',
loading: 'Memuat',
- placeholder: 'Pilih'
+ placeholder: 'Pilih',
+ noData: 'Tidak ada data'
},
pagination: {
goto: 'Pergi ke',
diff --git a/src/locale/lang/it.js b/src/locale/lang/it.js
index 850829fc6..b9be34bb0 100644
--- a/src/locale/lang/it.js
+++ b/src/locale/lang/it.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Nessuna corrispondenza',
loading: 'Caricamento',
- placeholder: 'Seleziona'
+ placeholder: 'Seleziona',
+ noData: 'Nessun dato'
},
pagination: {
goto: 'Vai a',
diff --git a/src/locale/lang/ja.js b/src/locale/lang/ja.js
index 16a635adc..e449f01f4 100644
--- a/src/locale/lang/ja.js
+++ b/src/locale/lang/ja.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'データなし',
loading: 'ロード中',
- placeholder: '選択してください'
+ placeholder: '選択してください',
+ noData: 'データなし'
},
pagination: {
goto: '',
diff --git a/src/locale/lang/kg.js b/src/locale/lang/kg.js
index 5498ac7ff..b6a715a7d 100644
--- a/src/locale/lang/kg.js
+++ b/src/locale/lang/kg.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Дал келген маалыматтар',
loading: 'Жүктөлүүдө',
- placeholder: 'тандоо'
+ placeholder: 'тандоо',
+ noData: 'маалымат жок'
},
pagination: {
goto: 'Мурунку',
diff --git a/src/locale/lang/km.js b/src/locale/lang/km.js
index a45cbfc78..185ab0bc8 100644
--- a/src/locale/lang/km.js
+++ b/src/locale/lang/km.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'គ្មានទិន្ន័យដូច',
loading: 'កំពុងផ្ទុក',
- placeholder: 'ជ្រើសរើស'
+ placeholder: 'ជ្រើសរើស',
+ noData: 'គ្មានទិន្ន័យ'
},
pagination: {
goto: 'ទៅកាន់',
diff --git a/src/locale/lang/ko.js b/src/locale/lang/ko.js
index 4cadf21f8..71fe33889 100644
--- a/src/locale/lang/ko.js
+++ b/src/locale/lang/ko.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: '맞는 데이터가 없습니다',
loading: '불러오는 중',
- placeholder: '선택'
+ placeholder: '선택',
+ noData: '데이터 없음'
},
pagination: {
goto: '이동',
diff --git a/src/locale/lang/ku.js b/src/locale/lang/ku.js
index 57d4480f0..ff57fcf60 100644
--- a/src/locale/lang/ku.js
+++ b/src/locale/lang/ku.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Li hembere ve agahî tune',
loading: 'Bardibe',
- placeholder: 'Bibijêre'
+ placeholder: 'Bibijêre',
+ noData: 'Agahî tune'
},
pagination: {
goto: 'Biçe',
diff --git a/src/locale/lang/kz.js b/src/locale/lang/kz.js
index a4579d4ad..7cdba7530 100644
--- a/src/locale/lang/kz.js
+++ b/src/locale/lang/kz.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Сәйкес деректер жоқ',
loading: 'Жүктелуде',
- placeholder: 'Таңдаңыз'
+ placeholder: 'Таңдаңыз',
+ noData: 'Деректер жоқ'
},
pagination: {
goto: 'Бару',
diff --git a/src/locale/lang/lt.js b/src/locale/lang/lt.js
index 3e3ba3092..7f5e10f79 100644
--- a/src/locale/lang/lt.js
+++ b/src/locale/lang/lt.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Duomenų nerasta',
loading: 'Kraunasi',
- placeholder: 'Pasirink'
+ placeholder: 'Pasirink',
+ noData: 'Nėra duomenų'
},
pagination: {
goto: 'Eiti į',
diff --git a/src/locale/lang/lv.js b/src/locale/lang/lv.js
index 18f99cda7..975b9174a 100644
--- a/src/locale/lang/lv.js
+++ b/src/locale/lang/lv.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Nav atbilstošu datu',
loading: 'Ielādē',
- placeholder: 'Izvēlēties'
+ placeholder: 'Izvēlēties',
+ noData: 'Nav datu'
},
pagination: {
goto: 'Iet uz',
diff --git a/src/locale/lang/mn.js b/src/locale/lang/mn.js
index 47280d88f..5c3aaf27a 100644
--- a/src/locale/lang/mn.js
+++ b/src/locale/lang/mn.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Тохирох өгөгдөл байхгүй',
loading: 'Ачаалж байна',
- placeholder: 'Сонгох'
+ placeholder: 'Сонгох',
+ noData: 'Өгөгдөл байхгүй'
},
pagination: {
goto: 'Очих',
diff --git a/src/locale/lang/nb-NO.js b/src/locale/lang/nb-NO.js
index c32b54d3f..2674d286f 100644
--- a/src/locale/lang/nb-NO.js
+++ b/src/locale/lang/nb-NO.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Ingen samsvarende data',
loading: 'Laster',
- placeholder: 'Velg'
+ placeholder: 'Velg',
+ noData: 'Ingen data'
},
pagination: {
goto: 'Gå til',
diff --git a/src/locale/lang/nl.js b/src/locale/lang/nl.js
index fb4df9cc8..37a97379b 100644
--- a/src/locale/lang/nl.js
+++ b/src/locale/lang/nl.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Geen overeenkomende resultaten',
loading: 'Laden',
- placeholder: 'Selecteer'
+ placeholder: 'Selecteer',
+ noData: 'Geen data'
},
pagination: {
goto: 'Ga naar',
diff --git a/src/locale/lang/pl.js b/src/locale/lang/pl.js
index e837edac2..b0eaf8936 100644
--- a/src/locale/lang/pl.js
+++ b/src/locale/lang/pl.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Brak dopasowań',
loading: 'Ładowanie',
- placeholder: 'Wybierz'
+ placeholder: 'Wybierz',
+ noData: 'Brak danych'
},
pagination: {
goto: 'Idź do',
diff --git a/src/locale/lang/pt-br.js b/src/locale/lang/pt-br.js
index 2902fc358..bb756347f 100644
--- a/src/locale/lang/pt-br.js
+++ b/src/locale/lang/pt-br.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Sem resultados',
loading: 'Carregando',
- placeholder: 'Selecione'
+ placeholder: 'Selecione',
+ noData: 'Sem dados'
},
pagination: {
goto: 'Ir para',
diff --git a/src/locale/lang/pt.js b/src/locale/lang/pt.js
index 16bc2053e..1cc88a656 100644
--- a/src/locale/lang/pt.js
+++ b/src/locale/lang/pt.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Sem correspondência',
loading: 'A carregar',
- placeholder: 'Selecione'
+ placeholder: 'Selecione',
+ noData: 'Sem dados'
},
pagination: {
goto: 'Ir para',
diff --git a/src/locale/lang/ro.js b/src/locale/lang/ro.js
index 76610749f..21134defa 100644
--- a/src/locale/lang/ro.js
+++ b/src/locale/lang/ro.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Nu există date potrivite',
loading: 'Se încarcă',
- placeholder: 'Selectează'
+ placeholder: 'Selectează',
+ noData: 'Nu există date'
},
pagination: {
goto: 'Go to',
diff --git a/src/locale/lang/ru-RU.js b/src/locale/lang/ru-RU.js
index ef6824f8e..03cf91d8b 100644
--- a/src/locale/lang/ru-RU.js
+++ b/src/locale/lang/ru-RU.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Совпадений не найдено',
loading: 'Загрузка',
- placeholder: 'Выбрать'
+ placeholder: 'Выбрать',
+ noData: 'Нет данных'
},
pagination: {
goto: 'Перейти',
diff --git a/src/locale/lang/sk.js b/src/locale/lang/sk.js
index 0897e76ed..82e89dc73 100644
--- a/src/locale/lang/sk.js
+++ b/src/locale/lang/sk.js
@@ -69,7 +69,8 @@ export default {
cascader: {
noMatch: 'Žiadna zhoda',
loading: 'Načítavanie',
- placeholder: 'Vybrať'
+ placeholder: 'Vybrať',
+ noData: 'Žiadne dáta'
},
pagination: {
goto: 'Choď na',
diff --git a/src/locale/lang/sl.js b/src/locale/lang/sl.js
index 4361d62e4..e2e8eccd8 100644
--- a/src/locale/lang/sl.js
+++ b/src/locale/lang/sl.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Ni ustreznih podatkov',
loading: 'Nalaganje',
- placeholder: 'Izberi'
+ placeholder: 'Izberi',
+ noData: 'Ni podatkov'
},
pagination: {
goto: 'Pojdi na',
diff --git a/src/locale/lang/sr.js b/src/locale/lang/sr.js
index b40afc612..2b54ac135 100644
--- a/src/locale/lang/sr.js
+++ b/src/locale/lang/sr.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Нема резултата',
loading: 'Учитавање',
- placeholder: 'Изабери'
+ placeholder: 'Изабери',
+ noData: 'Нема података'
},
pagination: {
goto: 'Иди на',
diff --git a/src/locale/lang/sv-SE.js b/src/locale/lang/sv-SE.js
index 72ad41b2d..5977f87b7 100644
--- a/src/locale/lang/sv-SE.js
+++ b/src/locale/lang/sv-SE.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Hittade inget',
loading: 'Laddar',
- placeholder: 'Välj'
+ placeholder: 'Välj',
+ noData: 'Ingen data'
},
pagination: {
goto: 'Gå till',
diff --git a/src/locale/lang/ta.js b/src/locale/lang/ta.js
index 2e802d028..897b30ba0 100644
--- a/src/locale/lang/ta.js
+++ b/src/locale/lang/ta.js
@@ -66,7 +66,8 @@ export default {
cascader: {
noMatch: 'பொருத்தமான தரவு கிடைக்கவில்லை',
loading: 'தயாராகிக்கொண்டிருக்கிறது',
- placeholder: 'தேர்வு செய்'
+ placeholder: 'தேர்வு செய்',
+ noData: 'தரவு இல்லை'
},
pagination: {
goto: 'தேவையான் பகுதிக்கு செல்',
diff --git a/src/locale/lang/th.js b/src/locale/lang/th.js
index c83cfcfbd..52b95b4bb 100644
--- a/src/locale/lang/th.js
+++ b/src/locale/lang/th.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'ไม่พบข้อมูลที่ตรงกัน',
loading: 'กำลังโหลด',
- placeholder: 'เลือก'
+ placeholder: 'เลือก',
+ noData: 'ไม่พบข้อมูล'
},
pagination: {
goto: 'ไปที่',
diff --git a/src/locale/lang/tk.js b/src/locale/lang/tk.js
index b1b602665..77ece7d62 100644
--- a/src/locale/lang/tk.js
+++ b/src/locale/lang/tk.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Hiçzat tapylmady',
loading: 'Indirilýär',
- placeholder: 'Saýlaň'
+ placeholder: 'Saýlaň',
+ noData: 'Hiçzat ýok'
},
pagination: {
goto: 'Git',
diff --git a/src/locale/lang/tr-TR.js b/src/locale/lang/tr-TR.js
index ebbfca8a9..4e5eab83e 100644
--- a/src/locale/lang/tr-TR.js
+++ b/src/locale/lang/tr-TR.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Eşleşen veri bulunamadı',
loading: 'Yükleniyor',
- placeholder: 'Seç'
+ placeholder: 'Seç',
+ noData: 'Veri yok'
},
pagination: {
goto: 'Git',
diff --git a/src/locale/lang/ua.js b/src/locale/lang/ua.js
index 39d10c2dc..6ae9df8b0 100644
--- a/src/locale/lang/ua.js
+++ b/src/locale/lang/ua.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Співпадінь не знайдено',
loading: 'Завантаження',
- placeholder: 'Обрати'
+ placeholder: 'Обрати',
+ noData: 'Немає даних'
},
pagination: {
goto: 'Перейти',
diff --git a/src/locale/lang/ug-CN.js b/src/locale/lang/ug-CN.js
index 4381b9c8f..2dc32f77d 100644
--- a/src/locale/lang/ug-CN.js
+++ b/src/locale/lang/ug-CN.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'ئۇچۇر تېپىلمىدى',
loading: 'يۈكلىنىۋاتىدۇ',
- placeholder: 'تاللاڭ'
+ placeholder: 'تاللاڭ',
+ noData: 'ئۇچۇر يوق'
},
pagination: {
goto: 'كىيىنكى بەت',
diff --git a/src/locale/lang/vi.js b/src/locale/lang/vi.js
index 14d85e21b..5f143424d 100644
--- a/src/locale/lang/vi.js
+++ b/src/locale/lang/vi.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: 'Dữ liệu không phù hợp',
loading: 'Đang tải',
- placeholder: 'Chọn'
+ placeholder: 'Chọn',
+ noData: 'Không tìm thấy dữ liệu'
},
pagination: {
goto: 'Nhảy tới',
diff --git a/src/locale/lang/zh-CN.js b/src/locale/lang/zh-CN.js
index 0466da446..1e586162c 100644
--- a/src/locale/lang/zh-CN.js
+++ b/src/locale/lang/zh-CN.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: '无匹配数据',
loading: '加载中',
- placeholder: '请选择'
+ placeholder: '请选择',
+ noData: '暂无数据'
},
pagination: {
goto: '前往',
diff --git a/src/locale/lang/zh-TW.js b/src/locale/lang/zh-TW.js
index 1e978b1a4..519d044b1 100644
--- a/src/locale/lang/zh-TW.js
+++ b/src/locale/lang/zh-TW.js
@@ -67,7 +67,8 @@ export default {
cascader: {
noMatch: '無匹配資料',
loading: '加載中',
- placeholder: '請選擇'
+ placeholder: '請選擇',
+ noData: '無資料'
},
pagination: {
goto: '前往',
diff --git a/src/utils/aria-utils.js b/src/utils/aria-utils.js
index 2e7f84ce6..5bccfab1c 100644
--- a/src/utils/aria-utils.js
+++ b/src/utils/aria-utils.js
@@ -115,7 +115,8 @@ aria.Utils.keys = {
left: 37,
up: 38,
right: 39,
- down: 40
+ down: 40,
+ esc: 27
};
export default aria.Utils;
diff --git a/src/utils/util.js b/src/utils/util.js
index 16043af5f..1ca8373b1 100644
--- a/src/utils/util.js
+++ b/src/utils/util.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { isString, isObject } from 'element-ui/src/utils/types';
const hasOwnProperty = Object.prototype.hasOwnProperty;
@@ -143,3 +144,75 @@ export const kebabCase = function(str) {
.replace(hyphenateRE, '$1-$2')
.toLowerCase();
};
+
+export const capitalize = function(str) {
+ if (!isString(str)) return str;
+ return str.charAt(0).toUpperCase() + str.slice(1);
+};
+
+export const looseEqual = function(a, b) {
+ const isObjectA = isObject(a);
+ const isObjectB = isObject(b);
+ if (isObjectA && isObjectB) {
+ return JSON.stringify(a) === JSON.stringify(b);
+ } else if (!isObjectA && !isObjectB) {
+ return String(a) === String(b);
+ } else {
+ return false;
+ }
+};
+
+export const arrayEquals = function(arrayA, arrayB) {
+ arrayA = arrayA || [];
+ arrayB = arrayB || [];
+
+ if (arrayA.length !== arrayB.length) {
+ return false;
+ }
+
+ for (let i = 0; i < arrayA.length; i++) {
+ if (!looseEqual(arrayA[i], arrayB[i])) {
+ return false;
+ }
+ }
+
+ return true;
+};
+
+export const isEqual = function(value1, value2) {
+ if (Array.isArray(value1) && Array.isArray(value2)) {
+ return arrayEquals(value1, value2);
+ }
+ return looseEqual(value1, value2);
+};
+
+export const isEmpty = function(val) {
+ // null or undefined
+ if (val == null) return true;
+
+ if (typeof val === 'boolean') return false;
+
+ if (typeof val === 'number') return !val;
+
+ if (val instanceof Error) return val.message === '';
+
+ switch (Object.prototype.toString.call(val)) {
+ // String or Array
+ case '[object String]':
+ case '[object Array]':
+ return !val.length;
+
+ // Map or Set or File
+ case '[object File]':
+ case '[object Map]':
+ case '[object Set]': {
+ return !val.size;
+ }
+ // Plain Object
+ case '[object Object]': {
+ return !Object.keys(val).length;
+ }
+ }
+
+ return false;
+};
diff --git a/test/unit/specs/cascader-panel.spec.js b/test/unit/specs/cascader-panel.spec.js
new file mode 100644
index 000000000..a57200fd5
--- /dev/null
+++ b/test/unit/specs/cascader-panel.spec.js
@@ -0,0 +1,536 @@
+import {
+ createTest,
+ createVue,
+ destroyVM,
+ waitImmediate,
+ wait,
+ triggerEvent
+} from '../util';
+import CascaderPanel from 'packages/cascader-panel';
+
+const selectedValue = ['zhejiang', 'hangzhou', 'xihu'];
+
+const options = [{
+ value: 'zhejiang',
+ label: 'Zhejiang',
+ children: [{
+ value: 'hangzhou',
+ label: 'Hangzhou',
+ children: [{
+ value: 'xihu',
+ label: 'West Lake'
+ }, {
+ value: 'binjiang',
+ label: 'Bin Jiang'
+ }]
+ }, {
+ value: 'ningbo',
+ label: 'NingBo',
+ children: [{
+ value: 'jiangbei',
+ label: 'Jiang Bei'
+ }, {
+ value: 'jiangdong',
+ label: 'Jiang Dong',
+ disabled: true
+ }]
+ }]
+}, {
+ value: 'jiangsu',
+ label: 'Jiangsu',
+ disabled: true,
+ children: [{
+ value: 'nanjing',
+ label: 'Nanjing',
+ children: [{
+ value: 'zhonghuamen',
+ label: 'Zhong Hua Men'
+ }]
+ }]
+}];
+
+const options2 = [{
+ id: 'zhejiang',
+ name: 'Zhejiang',
+ areas: [{
+ id: 'hangzhou',
+ name: 'Hangzhou',
+ areas: [{
+ id: 'xihu',
+ name: 'West Lake'
+ }, {
+ id: 'binjiang',
+ name: 'Bin Jiang'
+ }]
+ }, {
+ id: 'ningbo',
+ name: 'NingBo',
+ areas: [{
+ id: 'jiangbei',
+ label: 'Jiang Bei'
+ }, {
+ id: 'jiangdong',
+ name: 'Jiang Dong',
+ invalid: true
+ }]
+ }]
+}, {
+ id: 'jiangsu',
+ name: 'Jiangsu',
+ invalid: true,
+ areas: [{
+ id: 'nanjing',
+ name: 'Nanjing',
+ areas: [{
+ id: 'zhonghuamen',
+ name: 'Zhong Hua Men'
+ }]
+ }]
+}];
+
+const getMenus = el => el.querySelectorAll('.el-cascader-menu');
+const getOptions = (el, menuIndex) => getMenus(el)[menuIndex].querySelectorAll('.el-cascader-node');
+const getValidOptions = (el, menuIndex) => getMenus(el)[menuIndex].querySelectorAll('.el-cascader-node[tabindex="-1"]');
+const getLabel = el => el.querySelector('.el-cascader-node__label').textContent;
+
+describe('CascaderPanel', () => {
+ let vm;
+ afterEach(() => {
+ destroyVM(vm);
+ });
+
+ it('create', () => {
+ vm = createTest(CascaderPanel, true);
+ expect(vm.$el).to.exist;
+ });
+
+ it('expand and check', async() => {
+ vm = createVue({
+ template: `
+
+ `,
+ data() {
+ return {
+ value: [],
+ options
+ };
+ }
+ }, true);
+
+ const el = vm.$el;
+ const expandHandler = sinon.spy();
+ const changeHandler = sinon.spy();
+ vm.$refs.panel.$on('expand-change', expandHandler);
+ vm.$refs.panel.$on('change', changeHandler);
+
+ expect(getMenus(el).length).to.equal(1);
+ expect(getOptions(el, 0).length).to.equal(2);
+
+ const firstOption = getOptions(el, 0)[0];
+ expect(getLabel(firstOption)).to.equal('Zhejiang');
+ firstOption.click();
+ await waitImmediate();
+ expect(expandHandler.calledOnceWith(['zhejiang'])).to.be.true;
+ expect(getMenus(el).length).to.equal(2);
+
+ getOptions(el, 1)[0].click();
+ await waitImmediate();
+ expect(getMenus(el).length).to.equal(3);
+
+ getOptions(el, 2)[0].click();
+ await waitImmediate();
+ expect(changeHandler.calledOnceWith(selectedValue)).to.be.true;
+ expect(vm.value).to.deep.equal(selectedValue);
+ });
+
+ it('with default value', async() => {
+ vm = createVue({
+ template: `
+
+ `,
+ data() {
+ return {
+ value: selectedValue,
+ options
+ };
+ }
+ }, true);
+
+ const el = vm.$el;
+
+ await waitImmediate();
+ expect(getMenus(el).length).to.equal(3);
+ expect(getOptions(el, 0)[0].className).to.includes('in-active-path');
+ expect(getOptions(el, 2)[0].className).to.includes('is-active');
+ expect(getOptions(el, 2)[0].querySelector('.el-icon-check')).to.exist;
+ });
+
+ it('disabled options', async() => {
+ vm = createVue({
+ template: `
+
+ `,
+ data() {
+ return {
+ value: [],
+ options
+ };
+ }
+ }, true);
+
+ const el = vm.$el;
+ const expandHandler = sinon.spy();
+ vm.$refs.panel.$on('expand-change', expandHandler);
+
+ expect(getOptions(el, 0).length).to.equal(2);
+ expect(getValidOptions(el, 0).length).to.equal(1);
+
+ const secondOption = getOptions(el, 0)[1];
+ expect(secondOption.className).to.includes('is-disabled');
+ secondOption.click();
+
+ await waitImmediate();
+ expect(expandHandler.called).to.be.false;
+ expect(getMenus(el).length).to.equal(1);
+ });
+
+ it('expand by hover', async() => {
+ vm = createVue({
+ template: `
+
+ `,
+ data() {
+ return {
+ options,
+ props: {
+ expandTrigger: 'hover'
+ }
+ };
+ }
+ }, true);
+
+ const el = vm.$el;
+ triggerEvent(getOptions(el, 0)[1], 'mouseenter');
+ await waitImmediate();
+ expect(getMenus(el).length).to.equal(1);
+ triggerEvent(getOptions(el, 0)[0], 'mouseenter');
+ await waitImmediate();
+ expect(getMenus(el).length).to.equal(2);
+ triggerEvent(getOptions(el, 1)[0], 'mouseenter');
+ await waitImmediate();
+ expect(getMenus(el).length).to.equal(3);
+ });
+
+ it('emit value only', async() => {
+ vm = createVue({
+ template: `
+
+ `,
+ data() {
+ return {
+ value: 'xihu',
+ options,
+ props: {
+ emitPath: false
+ }
+ };
+ }
+ }, true);
+
+ const el = vm.$el;
+
+ await waitImmediate();
+ expect(getMenus(el).length).to.equal(3);
+ expect(getOptions(el, 2)[0].querySelector('.el-icon-check')).to.exist;
+
+ getOptions(el, 1)[1].click();
+ await waitImmediate();
+ getOptions(el, 2)[0].click();
+ await waitImmediate();
+ expect(vm.value).to.equal('jiangbei');
+ });
+
+ it('multiple mode', async() => {
+ vm = createVue({
+ template: `
+
+ `,
+ data() {
+ return {
+ value: [],
+ options: options,
+ props: {
+ multiple: true
+ }
+ };
+ }
+ }, true);
+
+ const el = vm.$el;
+ const checkbox = getOptions(el, 0)[0].querySelector('.el-checkbox');
+ expect(checkbox).to.exist;
+ expect(checkbox.querySelector('.el-checkbox__input').className).to.not.includes('is-checked');
+ checkbox.querySelector('input').click();
+
+ await waitImmediate();
+ expect(checkbox.querySelector('.el-checkbox__input').className).to.includes('is-checked');
+ expect(vm.value.length).to.equal(3);
+ });
+
+ it('multiple mode with disabled default value', async() => {
+ vm = createVue({
+ template: `
+
+ `,
+ data() {
+ return {
+ value: [['zhejiang', 'ningbo', 'jiangdong']],
+ options: options,
+ props: {
+ multiple: true
+ }
+ };
+ }
+ }, true);
+
+ const el = vm.$el;
+ const checkbox = getOptions(el, 0)[0].querySelector('.el-checkbox');
+
+ await waitImmediate();
+ expect(checkbox).to.exist;
+ expect(checkbox.querySelector('.el-checkbox__input').className).to.includes('is-indeterminate');
+ checkbox.querySelector('input').click();
+
+ await waitImmediate();
+ expect(checkbox.querySelector('.el-checkbox__input').className).to.includes('is-checked');
+ expect(vm.value.length).to.equal(4);
+
+ getOptions(el, 1)[1].click();
+ await waitImmediate();
+ getOptions(el, 2)[1].querySelector('input').click();
+ await waitImmediate();
+ expect(vm.value.length).to.equal(4);
+ });
+
+ it('check strictly in single mode', async() => {
+ vm = createVue({
+ template: `
+
+ `,
+ data() {
+ return {
+ value: ['zhejiang'],
+ options: options,
+ props: {
+ checkStrictly: true
+ }
+ };
+ }
+ }, true);
+
+ const el = vm.$el;
+ const radio = getOptions(el, 0)[0].querySelector('.el-radio');
+
+ await waitImmediate();
+ expect(radio).to.exist;
+ expect(radio.className).to.includes('is-checked');
+
+ getOptions(el, 0)[0].click();
+ await waitImmediate();
+ getOptions(el, 1)[0].querySelector('input').click();
+ await waitImmediate();
+ expect(vm.value).to.deep.equal(['zhejiang', 'hangzhou']);
+ expect(getOptions(el, 0)[1].querySelector('.el-radio').className).to.includes('is-disabled');
+ });
+
+ it('check strictly in multiple mode', async() => {
+ vm = createVue({
+ template: `
+
+ `,
+ data() {
+ return {
+ value: [['zhejiang']],
+ options: options,
+ props: {
+ multiple: true,
+ checkStrictly: true,
+ emitPath: false
+ }
+ };
+ }
+ }, true);
+
+ const el = vm.$el;
+ const checkbox = getOptions(el, 0)[0].querySelector('.el-checkbox');
+
+ await waitImmediate();
+ expect(checkbox).to.exist;
+ expect(checkbox.className).to.includes('is-checked');
+
+ getOptions(el, 0)[0].click();
+ await waitImmediate();
+ expect(getOptions(el, 1)[0].querySelector('.el-checkbox').className).to.not.includes('is-checked');
+ getOptions(el, 1)[0].querySelector('input').click();
+ await waitImmediate();
+ expect(vm.value).to.deep.equal(['zhejiang', 'hangzhou']);
+ expect(getOptions(el, 0)[1].querySelector('.el-checkbox').className).to.includes('is-disabled');
+ });
+
+ it('custom props', async() => {
+ vm = createVue({
+ template: `
+
+ `,
+ data() {
+ return {
+ value: [],
+ options: options2,
+ props: {
+ value: 'id',
+ label: 'name',
+ children: 'areas',
+ disabled: 'invalid'
+ }
+ };
+ }
+ }, true);
+
+ const el = vm.$el;
+
+ expect(getMenus(el).length).to.equal(1);
+ expect(getOptions(el, 0).length).to.equal(2);
+ expect(getValidOptions(el, 0).length).to.equal(1);
+
+ const firstOption = getOptions(el, 0)[0];
+ expect(getLabel(firstOption)).to.equal('Zhejiang');
+ firstOption.click();
+ await waitImmediate();
+ expect(getMenus(el).length).to.equal(2);
+
+ getOptions(el, 1)[0].click();
+ await waitImmediate();
+ expect(getMenus(el).length).to.equal(3);
+
+ getOptions(el, 2)[0].click();
+ await waitImmediate();
+ expect(vm.value).to.deep.equal(selectedValue);
+ });
+
+ it('value key is same as label key', async() => {
+ vm = createVue({
+ template: `
+
+ `,
+ data() {
+ return {
+ value: [],
+ options,
+ props: {
+ label: 'value'
+ }
+ };
+ }
+ }, true);
+
+ const el = vm.$el;
+
+ expect(getMenus(el).length).to.equal(1);
+ expect(getOptions(el, 0).length).to.equal(2);
+ expect(getValidOptions(el, 0).length).to.equal(1);
+
+ const firstOption = getOptions(el, 0)[0];
+ expect(getLabel(firstOption)).to.equal('zhejiang');
+ firstOption.click();
+ await waitImmediate();
+ expect(getMenus(el).length).to.equal(2);
+
+ getOptions(el, 1)[0].click();
+ await waitImmediate();
+ expect(getMenus(el).length).to.equal(3);
+
+ getOptions(el, 2)[0].click();
+ await waitImmediate();
+ expect(vm.value).to.deep.equal(selectedValue);
+ });
+
+ it('dynamic loading', async() => {
+ vm = createVue({
+ template: `
+
+ `,
+ data() {
+ let id = 0;
+ return {
+ value: [],
+ props: {
+ lazy: true,
+ lazyLoad(node, resolve) {
+ const { level } = node;
+ setTimeout(() => {
+ const nodes = Array.from({ length: level + 1 })
+ .map(() => ({
+ value: ++id,
+ label: `选项${id}`,
+ leaf: level >= 2
+ }));
+ resolve(nodes);
+ }, 1000);
+ }
+ }
+ };
+ }
+ }, true);
+
+ const el = vm.$el;
+ await wait(1000);
+ const firstOption = getOptions(el, 0)[0];
+ firstOption.click();
+ await waitImmediate();
+ expect(firstOption.querySelector('i').className).to.includes('el-icon-loading');
+ await wait(1000);
+ expect(firstOption.querySelector('i').className).to.includes('el-icon-arrow-right');
+ expect(getMenus(el).length).to.equal(2);
+ getOptions(el, 1)[0].click();
+ await wait(1000);
+ getOptions(el, 2)[0].click();
+ await waitImmediate();
+ expect(vm.value.length).to.equal(3);
+ });
+});
+
diff --git a/test/unit/specs/cascader.spec.js b/test/unit/specs/cascader.spec.js
index 42b326e2d..97cab396f 100644
--- a/test/unit/specs/cascader.spec.js
+++ b/test/unit/specs/cascader.spec.js
@@ -1,4 +1,55 @@
-import { createVue, destroyVM, triggerEvent, triggerClick } from '../util';
+import {
+ createTest,
+ createVue,
+ destroyVM,
+ waitImmediate,
+ wait,
+ triggerEvent
+} from '../util';
+import Cascader from 'packages/cascader';
+
+const options = [{
+ value: 'zhejiang',
+ label: 'Zhejiang',
+ children: [{
+ value: 'hangzhou',
+ label: 'Hangzhou',
+ children: [{
+ value: 'xihu',
+ label: 'West Lake'
+ }, {
+ value: 'binjiang',
+ label: 'Bin Jiang'
+ }]
+ }, {
+ value: 'ningbo',
+ label: 'NingBo',
+ children: [{
+ value: 'jiangbei',
+ label: 'Jiang Bei'
+ }, {
+ value: 'jiangdong',
+ label: 'Jiang Dong',
+ disabled: true
+ }]
+ }]
+}, {
+ value: 'jiangsu',
+ label: 'Jiangsu',
+ disabled: true,
+ children: [{
+ value: 'nanjing',
+ label: 'Nanjing',
+ children: [{
+ value: 'zhonghuamen',
+ label: 'Zhong Hua Men'
+ }]
+ }]
+}];
+
+const getMenus = el => el.querySelectorAll('.el-cascader-menu');
+const getOptions = (el, menuIndex) => getMenus(el)[menuIndex].querySelectorAll('.el-cascader-node');
+const selectedValue = ['zhejiang', 'hangzhou', 'xihu'];
describe('Cascader', () => {
let vm;
@@ -6,847 +57,352 @@ describe('Cascader', () => {
destroyVM(vm);
});
- it('create', done => {
- vm = createVue({
- template: `
-
- `,
- data() {
- return {
- options: [{
- value: 'zhejiang',
- label: 'Zhejiang',
- children: [{
- value: 'hangzhou',
- label: 'Hangzhou',
- children: [{
- value: 'xihu',
- label: 'West Lake'
- }]
- }, {
- value: 'ningbo',
- label: 'NingBo',
- children: [{
- value: 'jiangbei',
- label: 'Jiang Bei'
- }]
- }]
- }, {
- value: 'jiangsu',
- label: 'Jiangsu',
- children: [{
- value: 'nanjing',
- label: 'Nanjing',
- children: [{
- value: 'zhonghuamen',
- label: 'Zhong Hua Men'
- }]
- }]
- }],
- selectedOptions: []
- };
- }
- }, true);
- expect(vm.$el).to.be.exist;
+ it('create', () => {
+ vm = createTest(Cascader, true);
+ expect(vm.$el).to.exist;
+ });
+
+ it('toggle dropdown visible', async() => {
+ vm = createTest(Cascader, true);
+ expect(vm.$refs.popper.style.display).to.equal('none');
vm.$el.click();
- setTimeout(_ => {
- expect(document.body.querySelector('.el-cascader-menus')).to.be.exist;
-
- const menu = vm.$refs.cascader.menu;
- const menuElm = menu.$el;
- const item1 = menuElm.querySelector('.el-cascader-menu__item');
-
- item1.click();
- menu.$nextTick(_ => {
- expect(menuElm.children.length).to.be.equal(3); // two menus and an arrow
- expect(item1.classList.contains('is-active')).to.be.true;
-
- const item2 = menuElm.children[2].querySelector('.el-cascader-menu__item');
- item2.click();
-
- menu.$nextTick(_ => {
- expect(menuElm.children.length).to.be.equal(4);
- expect(item2.classList.contains('is-active')).to.be.true;
-
- const item3 = menuElm.children[3].querySelector('.el-cascader-menu__item');
- item3.click();
-
- setTimeout(_ => {
- expect(document.body.querySelector('.el-cascader-menus').style.display).to.be.equal('none');
- expect(vm.selectedOptions[0]).to.be.equal('zhejiang');
- expect(vm.selectedOptions[1]).to.be.equal('hangzhou');
- expect(vm.selectedOptions[2]).to.be.equal('xihu');
- expect(vm.$refs.cascader.$el.querySelector('.el-input__inner').value).to.be.equal('');
-
- triggerEvent(vm.$refs.cascader.$el, 'mouseenter');
- vm.$nextTick(_ => {
- vm.$refs.cascader.$el.querySelector('.el-cascader__clearIcon').click();
- vm.$nextTick(_ => {
- expect(vm.selectedOptions.length).to.be.equal(0);
- done();
- });
- });
- }, 500);
- });
- });
- }, 300);
- });
- // Github issue #3470
- it('should work with zero', done => {
- vm = createVue({
- template: `
-
- `,
- data() {
- return {
- options: [{
- value: 'zhejiang',
- label: 'Zhejiang',
- children: [{
- value: 0,
- label: 'Hangzhou',
- children: [{
- value: 'xihu',
- label: 'West Lake'
- }]
- }, {
- value: 'ningbo',
- label: 'NingBo',
- children: [{
- value: 'jiangbei',
- label: 'Jiang Bei'
- }]
- }]
- }, {
- value: 'jiangsu',
- label: 'Jiangsu',
- children: [{
- value: 'nanjing',
- label: 'Nanjing',
- children: [{
- value: 'zhonghuamen',
- label: 'Zhong Hua Men'
- }]
- }]
- }],
- selectedOptions: []
- };
- }
- }, true);
- expect(vm.$el).to.be.exist;
+ await waitImmediate();
+ expect(vm.$refs.popper.style.display).to.includes('');
vm.$el.click();
- setTimeout(_ => {
- expect(document.body.querySelector('.el-cascader-menus')).to.be.exist;
-
- const menu = vm.$refs.cascader.menu;
- const menuElm = menu.$el;
- const item1 = menuElm.querySelector('.el-cascader-menu__item');
-
- item1.click();
- menu.$nextTick(_ => {
- expect(menuElm.children.length).to.be.equal(3);
- expect(item1.classList.contains('is-active')).to.be.true;
-
- const item2 = menuElm.children[2].querySelector('.el-cascader-menu__item');
- item2.click();
-
- menu.$nextTick(_ => {
- expect(menuElm.children.length).to.be.equal(4);
- expect(item2.classList.contains('is-active')).to.be.true;
-
- const item3 = menuElm.children[3].querySelector('.el-cascader-menu__item');
- item3.click();
-
- setTimeout(_ => {
- expect(document.body.querySelector('.el-cascader-menus').style.display).to.be.equal('none');
- expect(vm.selectedOptions[0]).to.be.equal('zhejiang');
- expect(vm.selectedOptions[1]).to.be.equal(0);
- expect(vm.selectedOptions[2]).to.be.equal('xihu');
-
- triggerEvent(vm.$refs.cascader.$el, 'mouseenter');
- vm.$nextTick(_ => {
- vm.$refs.cascader.$el.querySelector('.el-cascader__clearIcon').click();
- vm.$nextTick(_ => {
- expect(vm.selectedOptions.length).to.be.equal(0);
- done();
- });
- });
- }, 500);
- });
- });
- }, 300);
+ await wait(500);
+ expect(vm.$refs.popper.style.display).to.includes('none');
});
- it('not allow clearable', done => {
- vm = createVue({
+
+ it('expand and check', async() => {
+ vm = createTest({
template: `
+ v-model="value"
+ :options="options">
`,
data() {
return {
- options: [{
- value: 'zhejiang',
- label: 'Zhejiang',
- children: [{
- value: 'hangzhou',
- label: 'Hangzhou',
- children: [{
- value: 'xihu',
- label: 'West Lake'
- }]
- }, {
- value: 'ningbo',
- label: 'NingBo',
- children: [{
- value: 'jiangbei',
- label: 'Jiang Bei'
- }]
- }]
- }, {
- value: 'jiangsu',
- label: 'Jiangsu',
- children: [{
- value: 'nanjing',
- label: 'Nanjing',
- children: [{
- value: 'zhonghuamen',
- label: 'Zhong Hua Men'
- }]
- }]
- }],
- selectedOptions: []
+ value: [],
+ options
};
}
}, true);
- expect(vm.$el).to.be.exist;
- triggerEvent(vm.$refs.cascader.$el, 'mouseenter');
- vm.$nextTick(_ => {
- expect(vm.$refs.cascader.$el.querySelector('.el-cascader__clearIcon')).to.not.exist;
- done();
- });
+
+ const { body } = document;
+ const expandHandler = sinon.spy();
+ const changeHandler = sinon.spy();
+
+ vm.$refs.cascader.$on('expand-change', expandHandler);
+ vm.$refs.cascader.$on('change', changeHandler);
+
+ getOptions(body, 0)[0].click();
+ await waitImmediate();
+ expect(expandHandler.calledOnceWith(['zhejiang'])).to.be.true;
+ getOptions(body, 1)[0].click();
+ await waitImmediate();
+ const checkedOption = getOptions(body, 2)[0];
+ checkedOption.click();
+ await waitImmediate();
+ expect(changeHandler.calledOnceWith(selectedValue)).to.be.true;
+ expect(vm.value).to.deep.equal(selectedValue);
+ expect(checkedOption.querySelector('i.el-icon-check')).to.exist;
+ expect(vm.$el.querySelector('input').value).to.equal('Zhejiang / Hangzhou / West Lake');
});
- it('disabled options', done => {
- vm = createVue({
- template: `
-
- `,
- data() {
- return {
- options: [{
- value: 'zhejiang',
- label: 'Zhejiang',
- disabled: true,
- children: [{
- value: 'hangzhou',
- label: 'Hangzhou',
- children: [{
- value: 'xihu',
- label: 'West Lake'
- }]
- }, {
- value: 'ningbo',
- label: 'NingBo',
- children: [{
- value: 'jiangbei',
- label: 'Jiang Bei'
- }]
- }]
- }, {
- value: 'jiangsu',
- label: 'Jiangsu',
- children: [{
- value: 'nanjing',
- label: 'Nanjing',
- children: [{
- value: 'zhonghuamen',
- label: 'Zhong Hua Men'
- }]
- }]
- }],
- selectedOptions: []
- };
- }
+
+ it('disabled', async() => {
+ vm = createTest(Cascader, {
+ disabled: true
}, true);
- expect(vm.$el).to.be.exist;
+ expect(vm.$el.className).to.includes('is-disabled');
vm.$el.click();
- setTimeout(_ => {
- expect(document.body.querySelector('.el-cascader-menus')).to.be.exist;
-
- const menu = vm.$refs.cascader.menu;
- const menuElm = menu.$el;
- const item1 = menuElm.querySelector('.el-cascader-menu__item');
-
- item1.click();
- menu.$nextTick(_ => {
- expect(menuElm.children.length).to.be.equal(2);
- expect(item1.classList.contains('is-active')).to.be.false;
- done();
- });
- }, 300);
+ await waitImmediate();
+ expect(vm.$refs.popper.style.display).to.includes('none');
});
- it('default value', done => {
+
+ it('with default value', async() => {
vm = createVue({
template: `
+ v-model="value"
+ :options="options">
`,
data() {
return {
- options: [{
- value: 'zhejiang',
- label: 'Zhejiang',
- children: [{
- value: 'hangzhou',
- label: 'Hangzhou',
- children: [{
- value: 'xihu',
- label: 'West Lake'
- }]
- }, {
- value: 'ningbo',
- label: 'NingBo',
- children: [{
- value: 'jiangbei',
- label: 'Jiang Bei'
- }]
- }]
- }, {
- value: 'jiangsu',
- label: 'Jiangsu',
- children: [{
- value: 'nanjing',
- label: 'Nanjing',
- children: [{
- value: 'zhonghuamen',
- label: 'Zhong Hua Men'
- }]
- }]
- }],
- selectedOptions: ['zhejiang', 'hangzhou', 'xihu']
+ value: selectedValue,
+ options
};
}
}, true);
- expect(vm.$el).to.be.exist;
- vm.$el.click();
- setTimeout(_ => {
- expect(document.body.querySelector('.el-cascader-menus')).to.be.exist;
- const menu = vm.$refs.cascader.menu;
- const menuElm = menu.$el;
- const item1 = menuElm.children[1].querySelector('.el-cascader-menu__item');
- const item2 = menuElm.children[2].querySelector('.el-cascader-menu__item');
- const item3 = menuElm.children[3].querySelector('.el-cascader-menu__item');
- expect(menuElm.children.length).to.be.equal(4);
- expect(item1.classList.contains('is-active')).to.be.true;
- expect(item2.classList.contains('is-active')).to.be.true;
- expect(item3.classList.contains('is-active')).to.be.true;
- triggerClick(document, 'mouseup');
- setTimeout(_ => {
- expect(document.body.querySelector('.el-cascader-menus').style.display).to.be.equal('none');
- done();
- }, 500);
- }, 300);
+ const el = vm.$el;
+ await waitImmediate();
+ expect(getMenus(el).length).to.equal(3);
+ expect(getOptions(el, 2)[0].querySelector('i').className).to.includes('el-icon-check');
+ expect(vm.$el.querySelector('input').value).to.equal('Zhejiang / Hangzhou / West Lake');
});
- it('expand by hover', done => {
+
+ it('async set selected value', async() => {
vm = createVue({
template: `
+ v-model="value"
+ :options="options">
`,
data() {
return {
- options: [{
- value: 'zhejiang',
- label: 'Zhejiang',
- children: [{
- value: 'hangzhou',
- label: 'Hangzhou',
- children: [{
- value: 'xihu',
- label: 'West Lake'
- }]
- }, {
- value: 'ningbo',
- label: 'NingBo',
- children: [{
- value: 'jiangbei',
- label: 'Jiang Bei'
- }]
- }]
- }, {
- value: 'jiangsu',
- label: 'Jiangsu',
- children: [{
- value: 'nanjing',
- label: 'Nanjing',
- children: [{
- value: 'zhonghuamen',
- label: 'Zhong Hua Men'
- }]
- }]
- }],
- selectedOptions: []
+ value: [],
+ options
};
}
}, true);
- expect(vm.$el).to.be.exist;
- vm.$el.click();
- setTimeout(_ => {
- expect(document.body.querySelector('.el-cascader-menus')).to.be.exist;
- const menu = vm.$refs.cascader.menu;
- const menuElm = menu.$el;
- const item1 = menuElm.querySelector('.el-cascader-menu__item');
-
- triggerEvent(item1, 'mouseenter');
- menu.$nextTick(_ => {
- expect(menuElm.children.length).to.be.equal(3);
- expect(item1.classList.contains('is-active')).to.be.true;
-
- const item2 = menuElm.children[2].querySelector('.el-cascader-menu__item');
- triggerEvent(item2, 'mouseenter');
-
- menu.$nextTick(_ => {
- expect(menuElm.children.length).to.be.equal(4);
- expect(item2.classList.contains('is-active')).to.be.true;
-
- const item3 = menuElm.children[3].querySelector('.el-cascader-menu__item');
- item3.click();
-
- setTimeout(_ => {
- expect(document.body.querySelector('.el-cascader-menus').style.display).to.be.equal('none');
- expect(vm.selectedOptions[0]).to.be.equal('zhejiang');
- expect(vm.selectedOptions[1]).to.be.equal('hangzhou');
- expect(vm.selectedOptions[2]).to.be.equal('xihu');
- done();
- }, 500);
- });
- });
- }, 300);
+ const el = vm.$el;
+ vm.value = selectedValue;
+ await waitImmediate();
+ expect(getMenus(el).length).to.equal(3);
+ expect(getOptions(el, 2)[0].querySelector('i').className).to.includes('el-icon-check');
+ expect(vm.$el.querySelector('input').value).to.equal('Zhejiang / Hangzhou / West Lake');
});
- it('change on select', done => {
+
+ it('default value with async options', async() => {
vm = createVue({
template: `
+ v-model="value"
+ :options="options">
`,
data() {
return {
- options: [{
- value: 'zhejiang',
- label: 'Zhejiang',
- children: [{
- value: 'hangzhou',
- label: 'Hangzhou',
- children: [{
- value: 'xihu',
- label: 'West Lake'
- }]
- }, {
- value: 'ningbo',
- label: 'NingBo',
- children: [{
- value: 'jiangbei',
- label: 'Jiang Bei'
- }]
- }]
- }, {
- value: 'jiangsu',
- label: 'Jiangsu',
- children: [{
- value: 'nanjing',
- label: 'Nanjing',
- children: [{
- value: 'zhonghuamen',
- label: 'Zhong Hua Men'
- }]
- }]
- }],
- selectedOptions: []
+ value: selectedValue,
+ options: []
};
}
}, true);
- expect(vm.$el).to.be.exist;
- vm.$el.click();
- setTimeout(_ => {
- expect(document.body.querySelector('.el-cascader-menus')).to.be.exist;
- const menu = vm.$refs.cascader.menu;
- const menuElm = menu.$el;
- const item1 = menuElm.querySelector('.el-cascader-menu__item');
-
- item1.click();
- menu.$nextTick(_ => {
- expect(menuElm.children.length).to.be.equal(3);
- expect(item1.classList.contains('is-active')).to.be.true;
- expect(vm.selectedOptions[0]).to.be.equal('zhejiang');
-
- const item2 = menuElm.children[2].querySelector('.el-cascader-menu__item');
- item2.click();
-
- menu.$nextTick(_ => {
- expect(menuElm.children.length).to.be.equal(4);
- expect(item2.classList.contains('is-active')).to.be.true;
- expect(vm.selectedOptions[1]).to.be.equal('hangzhou');
-
- const item3 = menuElm.children[3].querySelector('.el-cascader-menu__item');
- item3.click();
-
- setTimeout(_ => {
- expect(document.body.querySelector('.el-cascader-menus').style.display).to.be.equal('none');
- expect(vm.selectedOptions[0]).to.be.equal('zhejiang');
- expect(vm.selectedOptions[1]).to.be.equal('hangzhou');
- expect(vm.selectedOptions[2]).to.be.equal('xihu');
- done();
- }, 500);
- });
- });
- }, 300);
+ const el = vm.$el;
+ vm.options = options;
+ await waitImmediate();
+ expect(getMenus(el).length).to.equal(3);
+ expect(getOptions(el, 2)[0].querySelector('i').className).to.includes('el-icon-check');
+ expect(vm.$el.querySelector('input').value).to.equal('Zhejiang / Hangzhou / West Lake');
});
- it('hover and select', done => {
+
+ it('clearable', async() => {
vm = createVue({
template: `
+ clearable>
`,
data() {
return {
- options: [{
- value: 'zhejiang',
- label: 'Zhejiang',
- children: [{
- value: 'hangzhou',
- label: 'Hangzhou',
- children: [{
- value: 'xihu',
- label: 'West Lake'
- }]
- }, {
- value: 'ningbo',
- label: 'NingBo',
- children: [{
- value: 'jiangbei',
- label: 'Jiang Bei'
- }]
- }]
- }, {
- value: 'jiangsu',
- label: 'Jiangsu',
- children: [{
- value: 'nanjing',
- label: 'Nanjing',
- children: [{
- value: 'zhonghuamen',
- label: 'Zhong Hua Men'
- }]
- }]
- }],
- selectedOptions: []
+ value: selectedValue,
+ options
};
}
}, true);
- vm.$el.click();
- vm.$nextTick(() => {
- const menu = vm.$refs.cascader.menu;
- const menuElm = menu.$el;
- const item1 = menuElm.querySelector('.el-cascader-menu__item');
- triggerEvent(item1, 'mouseenter');
- menu.$nextTick(() => {
- expect(vm.selectedOptions[0]).to.be.equal('zhejiang');
-
- const spy = sinon.spy();
- menu.$on('closeInside', spy);
- item1.click();
-
- menu.$nextTick(() => {
- expect(spy.calledWith(true)).to.be.true;
- expect(menu.visible).to.be.false;
- done();
- });
- });
- });
+ triggerEvent(vm.$el, 'mouseenter');
+ await waitImmediate();
+ const closeBtn = vm.$el.querySelector('i.el-input__icon');
+ expect(closeBtn).to.exist;
+ closeBtn.click();
+ await waitImmediate();
+ expect(vm.value).to.deep.equal([]);
});
- it('filterable', done => {
+
+ it('show last level label', async() => {
vm = createVue({
template: `
+ :show-all-levels="false">
`,
data() {
return {
- options: [{
- value: 'zhejiang',
- label: 'Zhejiang',
- children: [{
- value: 'hangzhou',
- label: 'Hangzhou',
- children: [{
- value: 'xihu',
- label: 'West Lake'
- }]
- }, {
- value: 'ningbo',
- label: 'NingBo',
- children: [{
- value: 'jiangbei',
- label: 'Jiang Bei'
- }]
- }]
- }, {
- value: 'jiangsu',
- label: 'Jiangsu',
- children: [{
- value: 'nanjing',
- label: 'Nanjing',
- children: [{
- value: 'zhonghuamen',
- label: 'Zhong Hua Men'
- }]
- }]
- }],
- selectedOptions: []
+ value: selectedValue,
+ options
};
}
}, true);
- expect(vm.$el).to.be.exist;
- vm.$el.click();
- vm.$nextTick(_ => {
- vm.$refs.cascader.handleInputChange('z');
- setTimeout(_ => {
- expect(document.body.querySelector('.el-cascader-menus')).to.be.exist;
- const menu = vm.$refs.cascader.menu;
- const menuElm = menu.$el;
- const item1 = menuElm.querySelector('.el-cascader-menu__item');
-
- expect(menuElm.children.length).to.be.equal(2);
- expect(menuElm.children[1].children.length).to.be.equal(3);
-
- item1.click();
-
- setTimeout(_ => {
- expect(document.body.querySelector('.el-cascader-menus').style.display).to.be.equal('none');
- expect(vm.selectedOptions[0]).to.be.equal('zhejiang');
- expect(vm.selectedOptions[1]).to.be.equal('hangzhou');
- expect(vm.selectedOptions[2]).to.be.equal('xihu');
- done();
- }, 500);
- }, 300);
- });
+ const el = vm.$el;
+ await waitImmediate();
+ expect(getMenus(el).length).to.equal(3);
+ expect(getOptions(el, 2)[0].querySelector('i').className).to.includes('el-icon-check');
+ expect(vm.$el.querySelector('input').value).to.equal('West Lake');
});
- it('props', done => {
+
+ it('multiple mode', async() => {
vm = createVue({
template: `
+ `,
+ data() {
+ return {
+ value: [],
+ options,
+ props: {
+ multiple: true
+ }
+ };
+ }
+ }, true);
+
+ getOptions(document.body, 0)[0].querySelector('.el-checkbox input').click();
+ await waitImmediate();
+ expect(vm.value.length).to.equal(3);
+
+ const tags = vm.$el.querySelectorAll('.el-tag');
+ const closeBtn = tags[0].querySelector('.el-tag__close');
+ expect(tags.length).to.equal(3);
+ expect(closeBtn).to.exist;
+ closeBtn.click();
+ await waitImmediate();
+ expect(vm.value.length).to.equal(2);
+ expect(vm.$el.querySelectorAll('.el-tag').length).to.equal(2);
+ });
+
+ it('clearable in multiple mode', async() => {
+ vm = createVue({
+ template: `
+
+ clearable>
`,
data() {
return {
- options: [{
- label: 'Zhejiang',
- cities: [{
- label: 'Hangzhou'
- }, {
- label: 'NingBo'
- }]
- }, {
- label: 'Jiangsu',
- cities: [{
- label: 'Nanjing'
- }]
- }],
+ value: [],
+ options,
props: {
- value: 'label',
- children: 'cities'
- },
- selectedOptions: []
+ multiple: true,
+ emitPath: false
+ }
};
}
}, true);
- vm.$el.click();
- setTimeout(_ => {
- expect(document.body.querySelector('.el-cascader-menus')).to.be.exist;
-
- const menu = vm.$refs.cascader.menu;
- const menuElm = menu.$el;
- let items = menuElm.querySelectorAll('.el-cascader-menu__item');
- expect(items.length).to.equal(2);
- items[0].click();
- setTimeout(_ => {
- items = menuElm.querySelectorAll('.el-cascader-menu__item');
- expect(items.length).to.equal(4);
- expect(items[items.length - 1].innerText).to.equal('NingBo');
- done();
- }, 100);
- }, 100);
+ vm.value = ['xihu', 'binjiang', 'jiangbei', 'jiangdong'];
+ await waitImmediate();
+ expect(getOptions(document.body, 0)[0].querySelector('.el-checkbox.is-checked')).to.exist;
+ triggerEvent(vm.$el, 'mouseenter');
+ await waitImmediate();
+ const closeBtn = vm.$el.querySelector('i.el-input__icon');
+ expect(closeBtn).to.exist;
+ closeBtn.click();
+ await waitImmediate();
+ expect(vm.value.length).to.equal(1);
});
- it('show last level', done => {
+
+ it('collapse tags', async() => {
vm = createVue({
template: `
+ :props="props"
+ collapse-tags>
`,
data() {
return {
- options: [{
- value: 'zhejiang',
- label: 'Zhejiang',
- children: [{
- value: 'hangzhou',
- label: 'Hangzhou',
- children: [{
- value: 'xihu',
- label: 'West Lake'
- }]
- }, {
- value: 'ningbo',
- label: 'NingBo',
- children: [{
- value: 'jiangbei',
- label: 'Jiang Bei'
- }]
- }]
- }, {
- value: 'jiangsu',
- label: 'Jiangsu',
- children: [{
- value: 'nanjing',
- label: 'Nanjing',
- children: [{
- value: 'zhonghuamen',
- label: 'Zhong Hua Men'
- }]
- }]
- }],
- selectedOptions: ['zhejiang', 'ningbo', 'jiangbei']
+ value: ['xihu', 'binjiang', 'jiangbei', 'jiangdong'],
+ options,
+ props: {
+ multiple: true,
+ emitPath: false
+ }
};
}
}, true);
- setTimeout(_ => {
- const span = vm.$el.querySelector('.el-cascader__label');
- expect(span.innerText).to.equal('Jiang Bei');
- done();
- }, 100);
+ await waitImmediate();
+ const tags = vm.$el.querySelectorAll('.el-tag');
+ expect(tags.length).to.equal(2);
+ expect(tags[0].querySelector('.el-tag__close')).to.exist;
+ expect(tags[1].querySelector('.el-tag__close')).to.be.null;
+ tags[0].querySelector('.el-tag__close').click();
+ expect(tags[1].textContent).to.equal('+ 3');
+ await waitImmediate();
+ expect(vm.value.length).to.equal(3);
+ vm.$el.querySelector('.el-tag .el-tag__close').click();
+ await waitImmediate();
+ vm.$el.querySelector('.el-tag .el-tag__close').click();
+ await waitImmediate();
+ expect(vm.$el.querySelector('.el-tag')).to.exist;
+ // disabled tag can not be closed
+ expect(vm.$el.querySelector('.el-tag .el-tag__close')).to.be.null;
});
- describe('Cascader Events', () => {
- it('event:focus & blur', done => {
- vm = createVue({
- template: `
-
- `,
- data() {
- return {
- options: [{
- value: 'zhejiang',
- label: 'Zhejiang',
- children: [{
- value: 'hangzhou',
- label: 'Hangzhou',
- children: [{
- value: 'xihu',
- label: 'West Lake'
- }]
- }, {
- value: 'ningbo',
- label: 'NingBo',
- children: [{
- value: 'jiangbei',
- label: 'Jiang Bei'
- }]
- }]
- }, {
- value: 'jiangsu',
- label: 'Jiangsu',
- children: [{
- value: 'nanjing',
- label: 'Nanjing',
- children: [{
- value: 'zhonghuamen',
- label: 'Zhong Hua Men'
- }]
- }]
- }],
- selectedOptions: []
- };
+
+ it('filterable', async() => {
+ vm = createVue({
+ template: `
+
+ `,
+ data() {
+ return {
+ value: [],
+ options
+ };
+ }
+ }, true);
+ const el = vm.$el;
+ const { body } = document;
+ const input = el.querySelector('input');
+ el.click();
+ await waitImmediate();
+ input.value = 'Zhejiang';
+ triggerEvent(input, 'input');
+ await wait(300);
+ expect(body.querySelector('.el-cascader__suggestion-list')).to.exist;
+ expect(body.querySelectorAll('.el-cascader__suggestion-item').length).to.equal(3);
+ body.querySelectorAll('.el-cascader__suggestion-item')[0].click();
+ await waitImmediate();
+ expect(vm.value).to.deep.equal(selectedValue);
+ });
+
+ it('filter method', async() => {
+ vm = createVue({
+ template: `
+
+ `,
+ data() {
+ return {
+ value: [],
+ options
+ };
+ },
+ methods: {
+ filterMethod(node, keyword) {
+ const { text, path } = node;
+ return text.includes(keyword) || path.includes(keyword);
}
- }, true);
-
- const spyFocus = sinon.spy();
- const spyBlur = sinon.spy();
-
- vm.$refs.cascader.$on('focus', spyFocus);
- vm.$refs.cascader.$on('blur', spyBlur);
- vm.$el.querySelector('input').focus();
- vm.$el.querySelector('input').blur();
-
- vm.$nextTick(_ => {
- expect(spyFocus.calledOnce).to.be.true;
- expect(spyBlur.calledOnce).to.be.true;
- done();
- });
- });
+ }
+ }, true);
+ const el = vm.$el;
+ const { body } = document;
+ const input = el.querySelector('input');
+ el.click();
+ await waitImmediate();
+ input.value = 'Zhejiang';
+ triggerEvent(input, 'input');
+ await wait(300);
+ expect(body.querySelectorAll('.el-cascader__suggestion-item').length).to.equal(3);
+ input.value = 'xihu';
+ triggerEvent(input, 'input');
+ await wait(300);
+ expect(body.querySelector('.el-cascader__suggestion-item').textContent).to.equal('Zhejiang / Hangzhou / West Lake');
});
});
diff --git a/types/cascader-panel.d.ts b/types/cascader-panel.d.ts
new file mode 100644
index 000000000..5c2049c29
--- /dev/null
+++ b/types/cascader-panel.d.ts
@@ -0,0 +1,72 @@
+import { VNode, CreateElement } from 'vue';
+import { ElementUIComponent } from './component'
+
+/** Trigger mode of expanding current item */
+export type ExpandTrigger = 'click' | 'hover'
+
+/** Cascader Option */
+export interface CascaderOption {
+ label: string,
+ value: any,
+ children?: CascaderOption[],
+ disabled?: boolean,
+ leaf?: boolean
+}
+
+/** Cascader Props */
+export interface CascaderProps {
+ expandTrigger?: ExpandTrigger,
+ multiple?: boolean,
+ checkStrictly?: boolean,
+ emitPath?: boolean,
+ lazy?: boolean,
+ lazyLoad?: (node: CascaderNode, resolve: Resolve) => void,
+ value?: string,
+ label?: string,
+ children?: string,
+ disabled?: string
+ leaf?: string
+}
+
+/** Cascader Node */
+export interface CascaderNode {
+ uid: number,
+ data: D,
+ value: V,
+ label: string,
+ level: number,
+ isDisabled: boolean,
+ isLeaf: boolean,
+ parent: CascaderNode | null,
+ children: CascaderNode[]
+ config: CascaderProps
+}
+
+type Resolve = (dataList?: D[]) => void
+
+export interface CascaderPanelSlots {
+ /** Custom label content */
+ default: VNode[]
+
+ [key: string]: VNode[]
+}
+
+/** CascaderPanel Component */
+export declare class ElCascaderPanel extends ElementUIComponent {
+ /** Selected value */
+ value: V | V[]
+
+ /** Data of the options */
+ options: D[]
+
+ /** Configuration options */
+ props: CascaderProps
+
+ /** Whether to add border */
+ border: boolean
+
+ /** Render function of custom label content */
+ renderLabel: (h: CreateElement, context: { node: CascaderNode; data: D }) => VNode
+
+ $slots: CascaderPanelSlots
+}
diff --git a/types/cascader.d.ts b/types/cascader.d.ts
index f7a844fcc..baf07c2a0 100644
--- a/types/cascader.d.ts
+++ b/types/cascader.d.ts
@@ -1,29 +1,36 @@
+import { VNode } from 'vue';
import { ElementUIComponent, ElementUIComponentSize } from './component'
+import { CascaderOption, CascaderProps, CascaderNode } from './cascader-panel';
-/** Trigger mode of expanding current item */
-export type ExpandTrigger = 'click' | 'hover'
+export type CascaderOption = CascaderOption
-/** Cascader Option */
-export interface CascaderOption {
- label: string,
- value: any,
- children?: CascaderOption[],
- disabled?: boolean
+export type CascaderProps = CascaderProps
+
+export type CascaderNode = CascaderNode
+
+export interface CascaderSlots {
+ /** Custom label content */
+ default: VNode[],
+
+ /** Empty content when no option matches */
+ empty: VNode[]
+
+ [key: string]: VNode[]
}
/** Cascader Component */
-export declare class ElCascader extends ElementUIComponent {
+export declare class ElCascader extends ElementUIComponent {
/** Data of the options */
options: CascaderOption[]
/** Configuration options */
- props: object
+ props: CascaderProps
/** Selected value */
- value: any[]
+ value: V | V[]
- /** Custom class name for Cascader's dropdown */
- popperClass: string
+ /** Size of Input */
+ size: ElementUIComponentSize
/** Input placeholder */
placeholder: string
@@ -34,24 +41,29 @@ export declare class ElCascader extends ElementUIComponent {
/** Whether selected value can be cleared */
clearable: boolean
- /** Trigger mode of expanding current item */
- expandTrigger: ExpandTrigger
-
/** Whether to display all levels of the selected value in the input */
showAllLevels: boolean
+ /** Whether to collapse selected tags in multiple selection mode */
+ collapseTags: boolean
+
+ /** Separator of option labels */
+ separator: string
+
/** Whether the options can be searched */
filterable: boolean
+ /** filter method to match options according to input keyword */
+ filterMethod: (node: CascaderNode, keyword: string) => boolean
+
/** Debounce delay when typing filter keyword, in millisecond */
debounce: number
- /** Whether selecting an option of any level is permitted */
- changeOnSelect: boolean
-
- /** Size of Input */
- size: ElementUIComponentSize
+ /** Custom class name for Cascader's dropdown */
+ popperClass: string
/** Hook function before filtering with the value to be filtered as its parameter */
beforeFilter: (value: string) => boolean | Promise
+
+ $slots: CascaderSlots
}