style: use prettier (#3228)

* style: use prettier

* style: just prettier format, no code changes

* style: eslint fix
object-shorthand, prefer-const

* style: fix no-void

* style: no-console
This commit is contained in:
三咲智子 2021-09-04 19:29:28 +08:00 committed by GitHub
parent a4146608db
commit 55348b30b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
823 changed files with 55587 additions and 42248 deletions

View File

@ -12,72 +12,52 @@ module.exports = {
browser: true,
node: true,
},
plugins: [
'@typescript-eslint',
],
plugins: ['@typescript-eslint', 'prettier'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:vue/vue3-recommended',
'prettier',
],
overrides: [
{
files: ['*.ts'],
rules: {
'no-undef': 'off',
},
},
],
rules: {
// style
'block-spacing': 'error',
'eol-last': 'error',
'no-trailing-spaces': 'error',
'comma-style': ['error', 'last'],
'comma-dangle': ['error', 'always-multiline'],
'no-multi-spaces': 'error',
semi: ['error', 'never'],
'arrow-parens':['error', 'as-needed'],
'array-bracket-spacing': ['error', 'never'],
'indent': 'off',
'@typescript-eslint/indent':['error', 2, { SwitchCase: 1 }],
'object-curly-spacing': 'off',
'@typescript-eslint/object-curly-spacing': ['error', 'always'],
quotes: 'off',
'@typescript-eslint/quotes': ['error', 'single', { avoidEscape: true, allowTemplateLiterals: true }],
'space-infix-ops': 'off',
'@typescript-eslint/space-infix-ops': ['error', { 'int32Hint': false }],
'@typescript-eslint/type-annotation-spacing': ['error', {}],
// js/ts
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
'no-redeclare': 'off',
'@typescript-eslint/no-redeclare': 'error',
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-restricted-syntax': ['error', 'LabeledStatement', 'WithStatement'],
camelcase: ['error', { properties: 'never' }],
'no-var': 'error',
'prefer-const': [
'warn',
{ destructuring: 'all', ignoreReadBeforeAssign: true },
],
'object-shorthand': [
'error',
'always',
{ ignoreConstructors: false, avoidQuotes: true },
],
'block-scoped-var': 'error',
complexity: ['off', 11],
'no-with': 'error',
'no-void': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/member-delimiter-style': [
'error',
{
multiline: {
delimiter: 'none',
requireLast: false,
},
singleline: {
delimiter: 'semi',
requireLast: true,
},
},
],
// vue
'vue/no-v-html': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/html-self-closing': ['error', {
html: {
void: 'never',
normal: 'never',
component: 'always',
},
}],
'vue/max-attributes-per-line': ['error', {
singleline: 3,
multiline: 1,
}],
'vue/require-default-prop': 'off',
'vue/html-closing-bracket-spacing': 'error',
'prettier/prettier': 'warn',
},
}

View File

@ -31,4 +31,3 @@ We are excited that you are interested in contributing to Element Plus. Before s
- If your PR fixes a bug, please provide a description about the related bug.
- Merging a PR takes two maintainers: one approves the changes after reviewing, and then the other reviews and merges.

View File

@ -6,7 +6,6 @@
Estamos orgullosos de que usted esta interesado en contribuir al proyecto `Element Plus`. Antes de someter sus contribuciones, por favor tome un momentito para leer estas simples guías para contribuidores.
## Guía Para Reportar Problemas (“Issues”)
- Preguntas de otro tipo corren el riesgo de ser cerradas inmediatamente. Sí tiene preguntas sobre el uso de `Element`, vea [Discord](https://discord.link/ElementPlus) para más ayuda.
@ -15,7 +14,6 @@ Estamos orgullosos de que usted esta interesado en contribuir al proyecto `Eleme
- Por favor especifique que versión de `Element Plus` y `Vue` que esta utilizando, y que versión de sistema operativo y que versión de navegador web que está utilizando. [JSFiddle](https://jsfiddle.net/) esta recomendado para crear un entorno para reproducir el problema claramente.
## Guías para un “Pull Request (PR)”
- Crea una bifurcación (“fork”) del repositorio a su propia cuenta en github.com. Por favor no crea ramas nuevas aquí.
@ -33,4 +31,3 @@ Estamos orgullosos de que usted esta interesado en contribuir al proyecto `Eleme
- Si su PR arregla un error técnico, por favor, haga referencia al error especifico.
- Fusión de un PR requiere dos mantenedores: el primero aprueba los cambios después de revisar, y entonces el segundo mantenedor revisa los cambios y hace la fusión.

View File

@ -31,4 +31,3 @@ Nous sommes ravis que vous souhaitiez contribuer à Element Plus. Avant de soume
- Si votre PR corrige un bug, veuillez fournir une description du bug en question.
- La fusion d'un PR nécessite deux responsables: l'un approuve les modifications après révision, puis l'autre les révise et les fusionne.

View File

@ -7,6 +7,7 @@ Element Plus 是一套为开发者、设计师和产品经理准备的开源组
Element Plus 的成长离不开大家的支持,如果你愿意为 Element Plus 贡献代码或提供建议,请阅读以下内容。
## Issue 规范
- issue 仅用于提交 Bug 或 Feature 以及设计相关的内容,其它内容可能会被直接关闭。如果你在使用时产生了疑问,请到 Slack 或 [Discord](https://discord.link/ElementPlus) 里咨询。
- 在提交 issue 之前,请搜索相关内容是否已被提出。
@ -14,6 +15,7 @@ Element Plus 的成长离不开大家的支持,如果你愿意为 Element Plus
- 请说明 Element Plus 和 Vue 的版本号,并提供操作系统和浏览器信息。推荐使用 [JSFiddle](https://jsfiddle.net/) 生成在线 demo这能够更直观地重现问题。
## Pull Request 规范
- 请先 fork 一份到自己的项目下,不要直接在仓库下建分支。
- commit 信息要以`[组件名]: 描述信息` 的形式填写,例如 `Button: fix xxx bug`

View File

@ -1,5 +1,5 @@
Please make sure these boxes are checked before submitting your PR, thank you!
* [ ] Make sure you follow Element's contributing guide [English](https://github.com/element-plus/element-plus/blob/master/.github/CONTRIBUTING.en-US.md) | ([中文](https://github.com/element-plus/element-plus/blob/master/.github/CONTRIBUTING.zh-CN.md) | [Español](https://github.com/element-plus/element-plus/blob/master/.github/CONTRIBUTING.es.md) | [Français](https://github.com/element-plus/element-plus/blob/master/.github/CONTRIBUTING.fr-FR.md)).
* [ ] Make sure you are merging your commits to `dev` branch.
* [ ] Add some descriptions and refer to relative issues for your PR.
- [ ] Make sure you follow Element's contributing guide [English](https://github.com/element-plus/element-plus/blob/master/.github/CONTRIBUTING.en-US.md) | ([中文](https://github.com/element-plus/element-plus/blob/master/.github/CONTRIBUTING.zh-CN.md) | [Español](https://github.com/element-plus/element-plus/blob/master/.github/CONTRIBUTING.es.md) | [Français](https://github.com/element-plus/element-plus/blob/master/.github/CONTRIBUTING.fr-FR.md)).
- [ ] Make sure you are merging your commits to `dev` branch.
- [ ] Add some descriptions and refer to relative issues for your PR.

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
dist
node_modules
packages/*/es
packages/*/lib

8
.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"printWidth": 80,
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"endOfLine": "lf",
"trailingComma": "es5"
}

View File

@ -1,16 +0,0 @@
module.exports = {
semi: false,
trailingComma: 'all',
singleQuote: true,
printWidth: 80,
tabWidth: 2,
endOfLine: 'auto',
overrides: [
{
files: '*.scss',
options: {
parser: 'scss',
},
},
],
}

View File

@ -2,7 +2,7 @@
### 1.1.0-beta.8
*2021-08-31*
_2021-08-31_
#### Breaking changes:
@ -41,7 +41,7 @@
### 1.1.0-beta.7
*2021-08-26*
_2021-08-26_
#### Bug fixes
@ -51,7 +51,7 @@
### 1.1.0-beta.6
*2021-08-26*
_2021-08-26_
#### Bug fixes
@ -63,7 +63,7 @@
### 1.1.0-beta.5
*2021-08-25*
_2021-08-25_
#### Features
@ -81,7 +81,7 @@
### 1.1.0-beta.4
*2021-08-25*
_2021-08-25_
#### Bug fixes
@ -96,7 +96,7 @@
### 1.1.0-beta.3
*2021-08-24*
_2021-08-24_
#### Bug fixes
@ -104,17 +104,16 @@
### 1.1.0-beta.2
*2021-08-24*
_2021-08-24_
#### Bug fixes
- Build upgrade vue to 3.2.x (#3031 by @JeremyWuuuuu)
- Build: update browserslist (#3027 by @kooriookami)
### 1.1.0-beta.1
*2021-08-24*
_2021-08-24_
#### Features
@ -143,7 +142,7 @@
### 1.0.2-beta.71
*2021-08-18*
_2021-08-18_
#### Features
@ -176,7 +175,7 @@
### 1.0.2-beta.70
*2021-08-05*
_2021-08-05_
#### Bug fixes
@ -186,7 +185,7 @@
### 1.0.2-beta.69
*2021-08-04*
_2021-08-04_
#### Bug fixes
@ -194,7 +193,8 @@
### 1.0.2-beta.68
*2021-08-04*
_2021-08-04_
#### Bug fixes
- Dropdown styles error (#2823 by @tolking)
@ -203,7 +203,7 @@
### 1.0.2-beta.67
*2021-08-04*
_2021-08-04_
#### Bug fixes
@ -215,7 +215,7 @@
### 1.0.2-beta.66
*2021-08-03*
_2021-08-03_
#### Features
@ -229,7 +229,7 @@
- Var map merge default var (#2727 by @YunYouJun)
- Scrollbar the problem of ScrollbarDirKey being deleted (#2722 by @msidolphin)
- Input no trigger input event when clear value (#2723 by @msidolphin)
- Message using iconClass causes "el-message__icon" to be lost (#2709 by @GaliMu)
- Message using iconClass causes "el-message\_\_icon" to be lost (#2709 by @GaliMu)
- Locale inject locale for modal like components (#2737 by @JeremyWuuuuu)
- Tooltip styles error (#2763 by @tolking)
- Input Improve inputStyle reference (#2780 by @adaex)
@ -251,7 +251,7 @@
### 1.0.2-beta.65
*2021-07-28*
_2021-07-28_
#### Bug fixes
@ -268,20 +268,22 @@
### 1.0.2-beta.64
#### Bug fixes
- fix(var): map merge default var (#2727)
- fix(scrollbar): the problem of ScrollbarDirKey being deleted (#2722)
- fix(input): no trigger input event when clear value (#2723) …
- fix(message): using iconClass causes "el-message__icon" to be lost (#… …
- fix(message): using iconClass causes "el-message\_\_icon" to be lost (#… …
- fix(typing): fixed Nullable type for RefElement (#2730)
- fix(locale): inject locale for modal like components (#2737)
#### Refactors
- refactor(backtop): migrate css var (#2711)
- refactor(var): simplify checkbox var & move transition to root (#2729)
### 1.0.2-beta.63
*2021-07-27*
_2021-07-27_
#### Refactor
@ -302,7 +304,7 @@
### 1.0.2-beta.62
*2021-07-26*
_2021-07-26_
#### Bug fixes
@ -310,7 +312,7 @@
### 1.0.2-beta.61
*2021-07-26*
_2021-07-26_
#### Bug fixes
@ -319,7 +321,7 @@
### 1.0.2-beta.60
*2021-07-26*
_2021-07-26_
#### Bug fixes
@ -327,7 +329,7 @@
### 1.0.2-beta.59
*2021-07-26*
_2021-07-26_
#### Breaking change
@ -363,7 +365,7 @@
### 1.0.2-beta.58
*2021-07-24*
_2021-07-24_
#### Bug fixes
@ -371,7 +373,7 @@
### 1.0.2-beta.57
*2021-07-23*
_2021-07-23_
#### Bug fixes
@ -379,7 +381,7 @@
### 1.0.2-beta.56
*2021-07-23*
_2021-07-23_
#### Features
@ -449,7 +451,7 @@
### 1.0.2-beta.55
*2021-07-09*
_2021-07-09_
#### Bug fixes
@ -475,7 +477,7 @@
### 1.0.2-beta.54
*2021-07-02*
_2021-07-02_
#### Bug fixes
@ -491,7 +493,7 @@
### 1.0.2-beta.53
*2021-06-25*
_2021-06-25_
#### Bug fixes
@ -506,7 +508,7 @@
### 1.0.2-beta.52
*2021-06-24*
_2021-06-24_
#### Bug fixes
@ -514,7 +516,7 @@
### 1.0.2-beta.49
*2021-06-23*
_2021-06-23_
#### Bug fixes
@ -536,7 +538,7 @@
### 1.0.2-beta.47
*2021-06-11*
_2021-06-11_
#### Features
@ -580,7 +582,7 @@
### 1.0.2-beta.46
*2021-06-04*
_2021-06-04_
#### Features
@ -588,6 +590,7 @@
- Feat Input: add input-style prop (#2117)
#### Bugfixes
- Fix Col: should hidden when span is zero (#1769)
- Fix Table: hot reload (#2077) (#2097)
- Fix Popper: in dialog isn't enough space to position problem (#2102)
@ -597,17 +600,21 @@
- Fix Cascader: size issue when filterable and multiple are enabled (#2123)
#### Chore
- Chore Website: update radio VCA doc (#2111)
- Chore Project: esbuild for single component build (#2112)
- Chore Website: use built-in throwError function. (#2131)
### 1.0.2-beta.45
*2021-05-28*
_2021-05-28_
#### Features
- Feat VClickOutside: Expose mousedown and mouseup event object (#2038)
#### Bugfixes
- Fix Website: 'textContent' of undefined and text wrapping (#1973)
- Fix DatePicker: attribute 'type' add ts declare (#1979)
- Fix Table: fix sorting error #1919 (#1983)
@ -622,7 +629,9 @@
- Fix Website: algolia search wrong search parameter (#2065)
- Fix ScrollBar: perf behavior of bar click (#2066)
- Fix DateTimePicker: allow clearable to control both of the clear button (#2072)
#### Chore
- Chore Tag: add specific type declaration (#1939)
- Chore Docs: drawer.md add modal-class attribute (#1974)
- Chore Transfer: remove useless button style (#1975)
@ -642,26 +651,28 @@
- Chore Project: bump sass from 1.26.10 to 1.34.0 (#2060)
- Chore Docs: value -> model-value in vue 3 (#2067)
### 1.0.2-beta.44
*2021-05-11*
_2021-05-11_
#### Bug fixes
- Hot fix for #1980 (#1981)
- Hot fix for #1980 (#1981)
### 1.0.2-beta.43
*2021-05-10*
_2021-05-10_
#### Bug fixes
- Fix DateTimePicker positioning issue (#1980, #1981)
### 1.0.2-beta.42
*2021-05-09*
_2021-05-09_
#### Bug fixes
- Fix Rate: remove the confusing this (#1910)
- Fix Drawer: add modal false div class (#1932)
- Fix Button: fix incorrect style rule when hovering disabled plain button (#1937)
@ -673,6 +684,7 @@
- Fix Switch: fix switch component onMounted condition error (#1966)
#### Chore
- Chore Notification: Remove magic numbers (#1928)
- Chore Tag: keep the same with property name (#1940)
- Chore Pagination: update Pagination documentation with .sync modifier (#1920 #1957)
@ -680,11 +692,9 @@
- Chore website: update ad (#1968)
- Chore ThemeChalk: add missing `!default` modifiers for global variables (#1969)
### 1.0.2-beta.41
*2021-04-26*
_2021-04-26_
#### Features
@ -708,7 +718,7 @@
### 1.0.2-beta.40
*2021-04-14*
_2021-04-14_
#### Bug fixes
@ -726,6 +736,7 @@
- Fix docs footer links error (#1827)
- Fix table fix table column rerendered whenever table updates
- Fix time-picker time picker change event bug (#1828)
#### Features
- Result: new componet Result (#1818)
@ -736,20 +747,21 @@
### 1.0.2-beta.39
*2021-04-09*
_2021-04-09_
#### Bug fixes
- Fix `packages/utils` path issue (#1792)
### 1.0.2-beta.38
*2021-04-08*
_2021-04-08_
- Fix `packages/utils` build issue caused `setConfig` not working (#1788)
### 1.0.2-beta.37
*2021-04-08*
_2021-04-08_
#### Bug fixes
@ -767,7 +779,7 @@
### 1.0.2-beta.36
*2021-03-28*
_2021-03-28_
#### Bug fixes
@ -806,16 +818,16 @@
### 1.0.2-beta.35
*2021-03-15*
_2021-03-15_
- Fix type generator error
### 1.0.2-beta.34
*2021-03-12*
_2021-03-12_
#### Bug fixes
- Fix slider warning of modelValue (#1622)
- Fix tree auto expand parent on set current key and node (#1502)
- Fix avatar watch invalid when src is missing from props (#1615)
@ -839,7 +851,7 @@
### 1.0.2-beta.33
*2021-03-03*
_2021-03-03_
#### Bug fixes
@ -862,7 +874,7 @@
### 1.0.2-beta.32
*2021-01-31*
_2021-01-31_
#### Bug fixes
@ -897,7 +909,7 @@
### 1.0.2-beta.31
*2021-01-31*
_2021-01-31_
#### Bug fixes
@ -928,7 +940,7 @@
### 1.0.2-beta.30
*2021-01-25*
_2021-01-25_
#### Bug fixes
@ -938,7 +950,7 @@
### 1.0.2-beta.29
*2021-01-25*
_2021-01-25_
#### New feature
@ -958,7 +970,7 @@
### 1.0.2-beta.28
*2021-01-20*
_2021-01-20_
#### New feature
@ -980,7 +992,7 @@
### 1.0.1-beta.27
*2021-01-15*
_2021-01-15_
#### Bug fixes
@ -990,7 +1002,7 @@
### 1.0.1-beta.26
*2021-01-14*
_2021-01-14_
#### New feature
@ -1007,7 +1019,7 @@
### 1.0.1-beta.24
*2021-01-11*
_2021-01-11_
#### Bug fixes
@ -1020,7 +1032,7 @@
### 1.0.1-beta.23
*2021-01-07*
_2021-01-07_
#### New feature
@ -1037,7 +1049,7 @@
### 1.0.1-beta.22
*2021-01-06*
_2021-01-06_
#### Bug fixes
@ -1050,7 +1062,7 @@
### 1.0.1-beta.21
*2021-01-05*
_2021-01-05_
#### Bug fixes
@ -1064,7 +1076,7 @@
### 1.0.1-beta.19
*2021-01-02*
_2021-01-02_
#### Bug fixes
@ -1074,7 +1086,7 @@
### 1.0.1-beta.18
*2020-12-31*
_2020-12-31_
#### Bug fixes
@ -1087,7 +1099,7 @@
### 1.0.1-beta.15
*2020-12-27*
_2020-12-27_
#### Bug fixes
@ -1098,7 +1110,7 @@
### 1.0.1-beta.14
*2020-12-24*
_2020-12-24_
#### Bug fixes
@ -1114,7 +1126,7 @@
### 1.0.1-beta.11
*2020-12-21*
_2020-12-21_
#### New features
@ -1129,7 +1141,7 @@
### 1.0.1-beta.10
*2020-12-18*
_2020-12-18_
#### New features
@ -1142,7 +1154,7 @@
### 1.0.1-beta.9
*2020-12-16*
_2020-12-16_
#### Bug fixes
@ -1157,7 +1169,7 @@
### 1.0.1-beta.8
*2020-12-12*
_2020-12-12_
#### Bug fixes
@ -1171,7 +1183,7 @@
### 1.0.1-beta.7
*2020-12-10*
_2020-12-10_
#### Bug fixes
@ -1185,7 +1197,7 @@
### 1.0.1-beta.6
*2020-12-09*
_2020-12-09_
#### Bug fixes
@ -1201,7 +1213,7 @@
### 1.0.1-beta.5
*2020-12-07*
_2020-12-07_
#### Bug fixes
@ -1219,7 +1231,7 @@
### 1.0.1-beta.4
*2020-12-05*
_2020-12-05_
#### Bug fixes
@ -1236,7 +1248,7 @@
### 1.0.1-beta.3
*2020-12-03*
_2020-12-03_
#### Bug fixes
@ -1248,7 +1260,7 @@
### 1.0.1-beta.2
*2020-12-02*
_2020-12-02_
#### Bug fixes
@ -1256,7 +1268,7 @@
### 1.0.1-beta.1
*2020-12-01*
_2020-12-01_
#### Bug fixes

View File

@ -14,21 +14,21 @@ appearance, race, religion, or sexual identity and orientation.
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities

View File

@ -14,8 +14,8 @@
<p align="center">Element Plus - A Vue.js 3.0 UI library</p>
* 💪 Vue 3.0 Composition API
* 🔥 Written in TypeScript
- 💪 Vue 3.0 Composition API
- 🔥 Written in TypeScript
## Status: Beta
@ -48,6 +48,7 @@ This project is still under heavy development. Feel free to join us and make you
---
## Documentation
You can find for more details, API, and other docs on [https://element-plus.org](https://element-plus.org/)
国内[加速镜像站点](https://element-plus.gitee.io/)
@ -59,25 +60,34 @@ Join our [Discord](https://discord.link/ElementPlus) to start communicating with
You can find the breaking change list here: [Breaking Change List](https://github.com/element-plus/element-plus/issues/162).
## Bootstrap project
With command
```bash
$ yarn bootstrap
```
the project will install all dependencies and run `lerna bootstrap` to initialize the project
## Website preview
With command
```bash
$ yarn website-dev
```
the project will launch website for you to preview all existing component
You can also use this command to start a blank page to debug
```bash
$ yarn website-dev:play
//source file: ./website/play/index.vue
```
## Component migration process
1. Convert the item in https://github.com/element-plus/element-plus/projects/1 to an issue
2. Assign yourself to the issue
3. Author your component by generating new component command below
@ -85,7 +95,9 @@ $ yarn website-dev:play
5. Open a new pull request, fill in the component issue link in 1
## Generate new component
With command
```bash
$ yarn gen component-name
```
@ -93,12 +105,15 @@ $ yarn gen component-name
Note the `component-name` must be in `kebab-case`, combining words by replacing each space with a dash.
## Commit template
With command
```bash
yarn cz
```
Example
```
[TYPE](SCOPE):DESCRIPTION#[ISSUE]
# example feat(components): add type 'button' for form usage #1234
@ -109,7 +124,6 @@ Example
ElementPlus is open source software licensed as
[MIT](https://github.com/element-plus/element-plus/blob/master/LICENSE).
## Contributors
This project wouldn't exist without our amazing contributors
@ -117,6 +131,3 @@ This project wouldn't exist without our amazing contributors
<a href="https://github.com/element-plus/element-plus/graphs/contributors">
<img src="https://contrib.rocks/image?repo=element-plus/element-plus" />
</a>

View File

@ -22,17 +22,12 @@ module.exports = {
overrides: [
{
test: /\.vue$/,
plugins: [
'@babel/transform-typescript',
],
plugins: ['@babel/transform-typescript'],
},
],
env: {
utils: {
ignore: [
'**/*.test.ts',
'**/*.spec.ts',
],
ignore: ['**/*.test.ts', '**/*.spec.ts'],
presets: [
[
'@babel/env',

View File

@ -3,13 +3,10 @@ import helper from 'components-helper'
import path from 'path'
import { epRoot } from './paths'
const { name, version } = require(path.resolve(
epRoot,
'./package.json',
))
const { name, version } = require(path.resolve(epRoot, './package.json'))
import icon from '../website/icon.json'
const icons = icon.map(item => 'el-icon-' + item).join('/')
const icons = icon.map((item) => 'el-icon-' + item).join('/')
const tagVer = process.env.TAG_VERSION
const _version = tagVer
? tagVer.startsWith('v')

View File

@ -25,13 +25,13 @@ const langs = {
'fr-FR': 'element-fr',
jp: 'element-jp',
}
;['zh-CN', 'en-US', 'es', 'fr-FR', 'jp'].forEach(lang => {
;['zh-CN', 'en-US', 'es', 'fr-FR', 'jp'].forEach((lang) => {
const indexName = langs[lang]
const index = client.initIndex(indexName)
index.clearObjects().then(() => {
const files = fg.sync(`website/docs/${lang}/*.md`)
let indices: Index[] = []
files.forEach(file => {
files.forEach((file) => {
const regExp = new RegExp(`website\/docs\/${lang}\/(.*).md`)
const pathContent = file.match(regExp)
const path = pathContent[1]
@ -43,13 +43,13 @@ const langs = {
.replace(/:::[\s\S]*?:::/g, '')
.replace(/```[\s\S]*?```/g, '')
.match(/#{2,4}[^#]*/g)
.map(match =>
.map((match) =>
match
.replace(/\n+/g, '\n')
.split('\n')
.filter(part => !!part),
.filter((part) => !!part)
)
.map(match => {
.map((match) => {
const length = match.length
if (length > 2) {
const desc = match.slice(1, length).join('')
@ -59,7 +59,7 @@ const langs = {
})
let i = 0
indices = indices.concat(
matches.map(match => {
matches.map((match) => {
const title = match[0].replace(/#{2,4}/, '').trim()
const index = { component, title } as Index
index.anchor = slugify(title)
@ -67,7 +67,7 @@ const langs = {
index.path = path
index.sort = i++
return index
}),
})
)
})

View File

@ -8,12 +8,14 @@ const localePath = resolve(__dirname, '../packages/locale/lang')
const fileList = fs.readdirSync(localePath)
const transform = function (filename, name, cb) {
require('@babel/core').transformFile(resolve(localePath, filename), {
plugins: [
'@babel/plugin-transform-modules-umd',
],
require('@babel/core').transformFile(
resolve(localePath, filename),
{
plugins: ['@babel/plugin-transform-modules-umd'],
moduleId: name,
}, cb)
},
cb
)
}
fileList
@ -29,13 +31,15 @@ fileList
} else {
const code = result.code
const transformedCode = code
.replace('define(\"', 'define(\"element/locale/')
.replace('define("', 'define("element/locale/')
.replace(
/global\.(\S*) = mod.exports/,
'global.ElementPlus.lang = global.ElementPlus.lang || {};\n global.ElementPlus.lang.$1 = mod.exports.default'
)
save(resolve(buildOutput, 'element-plus/dist/locale', `${name}.js`)).write(transformedCode)
save(
resolve(buildOutput, 'element-plus/dist/locale', `${name}.js`)
).write(transformedCode)
}
})
})

View File

@ -22,8 +22,8 @@ async function getComponents() {
// filter out package.json since under packages/components we only got this file
//
return raw
.filter(f => f !== 'package.json' && f !== 'index.ts')
.map(f => ({ path: path.resolve(compRoot, f), name: f }))
.filter((f) => f !== 'package.json' && f !== 'index.ts')
.map((f) => ({ path: path.resolve(compRoot, f), name: f }))
}
const plugins = [
@ -42,9 +42,11 @@ const externals = getDeps(pathToPkgJson)
const excludes = ['icons']
const pathsRewriter = id => {
if (id.startsWith(`${EP_PREFIX}/components`)) return id.replace(`${EP_PREFIX}/components`, '..')
if (id.startsWith(EP_PREFIX) && excludes.every(e => !id.endsWith(e))) return id.replace(EP_PREFIX, '../..')
const pathsRewriter = (id) => {
if (id.startsWith(`${EP_PREFIX}/components`))
return id.replace(`${EP_PREFIX}/components`, '..')
if (id.startsWith(EP_PREFIX) && excludes.every((e) => !id.endsWith(e)))
return id.replace(EP_PREFIX, '../..')
return id
}
@ -61,10 +63,12 @@ const pathsRewriter = id => {
yellow('Start building entry file')
await buildEntry()
green('Entry built successfully')
})().then(() => {
})()
.then(() => {
console.log('Individual component build finished')
process.exit(0)
}).catch((e) => {
})
.catch((e) => {
console.error(e.message)
process.exit(1)
})
@ -72,18 +76,18 @@ const pathsRewriter = id => {
async function buildComponents() {
const componentPaths = await getComponents()
const builds = componentPaths.map(async ({
path: p,
name: componentName,
}) => {
const builds = componentPaths.map(
async ({ path: p, name: componentName }) => {
const entry = path.resolve(p, './index.ts')
if (!fs.existsSync(entry)) return
const external = (id) => {
return id.startsWith(VUE_REGEX)
|| id.startsWith(VUE_MONO)
|| id.startsWith(EP_PREFIX)
|| externals.some(i => id.startsWith(i))
return (
id.startsWith(VUE_REGEX) ||
id.startsWith(VUE_MONO) ||
id.startsWith(EP_PREFIX) ||
externals.some((i) => id.startsWith(i))
)
}
const esm = {
format: 'es',
@ -91,7 +95,7 @@ async function buildComponents() {
plugins: [
filesize({
reporter,
})
}),
],
paths: pathsRewriter,
}
@ -103,7 +107,7 @@ async function buildComponents() {
plugins: [
filesize({
reporter,
})
}),
],
paths: pathsRewriter,
}
@ -115,12 +119,10 @@ async function buildComponents() {
const bundle = await rollup.rollup(rollupConfig)
await bundle.write(esm as any)
await bundle.write(cjs as any)
})
try {
await Promise.all(
builds
}
)
try {
await Promise.all(builds)
} catch (e) {
logAndShutdown(e)
}
@ -131,7 +133,7 @@ async function buildEntry() {
const config = {
input: entry,
plugins,
external: _ => true,
external: (_) => true,
}
try {
@ -142,8 +144,8 @@ async function buildEntry() {
plugins: [
filesize({
reporter,
})
]
}),
],
})
await bundle.write({
@ -152,7 +154,7 @@ async function buildEntry() {
plugins: [
filesize({
reporter,
})
}),
],
})
} catch (e) {

View File

@ -11,9 +11,7 @@ import genDts from './gen-entry-dts'
import RollupResolveEntryPlugin from './rollup.plugin.entry'
import { epRoot, buildOutput } from './paths'
import { EP_PREFIX, excludes } from './constants'
;(async () => {
const config = {
input: path.resolve(epRoot, './index.ts'),
plugins: [
@ -73,20 +71,25 @@ import { EP_PREFIX, excludes } from './constants'
const entryFiles = await fs.promises.readdir(epRoot, { withFileTypes: true })
const entryPoints = entryFiles.filter(f => f.isFile()).filter(f => {
const entryPoints = entryFiles
.filter((f) => f.isFile())
.filter((f) => {
return f.name !== 'package.json' && f.name !== 'README.md'
}).map(f => path.resolve(epRoot, f.name))
})
.map((f) => path.resolve(epRoot, f.name))
const entryBundle = await rollup.rollup({
...config,
input: entryPoints,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
external: _ => true,
external: (_) => true,
})
const rewriter = id => {
if (id.startsWith(`${EP_PREFIX}/components`)) return id.replace(`${EP_PREFIX}/components`, './components')
if (id.startsWith(EP_PREFIX) && excludes.every(e => !id.endsWith(e))) return id.replace(EP_PREFIX, '.')
const rewriter = (id) => {
if (id.startsWith(`${EP_PREFIX}/components`))
return id.replace(`${EP_PREFIX}/components`, './components')
if (id.startsWith(EP_PREFIX) && excludes.every((e) => !id.endsWith(e)))
return id.replace(EP_PREFIX, '.')
}
console.log(chalk.yellow('Generating cjs entry'))
@ -117,5 +120,4 @@ import { EP_PREFIX, excludes } from './constants'
await genDts()
console.log(chalk.green('Entry file definitions generated'))
})()

View File

@ -17,12 +17,15 @@ const excludedFiles = [
'css',
'.DS_Store',
]
const exclude = (path: string) => !excludedFiles.some(f => path.includes(f))
const exclude = (path: string) => !excludedFiles.some((f) => path.includes(f))
/**
* fork = require( https://github.com/egoist/vue-dts-gen/blob/main/src/index.ts
*/
const genVueTypes = async (root, outDir = path.resolve(__dirname, '../dist/types')) => {
const genVueTypes = async (
root,
outDir = path.resolve(__dirname, '../dist/types')
) => {
const project = new Project({
compilerOptions: {
allowJs: true,
@ -45,13 +48,13 @@ const genVueTypes = async (root, outDir = path.resolve(__dirname, '../dist/types
const filePaths = klawSync(root, {
nodir: true,
})
.map(item => item.path)
.filter(path => !DEMO_RE.test(path))
.filter(path => !TEST_RE.test(path))
.map((item) => item.path)
.filter((path) => !DEMO_RE.test(path))
.filter((path) => !TEST_RE.test(path))
.filter(exclude)
await Promise.all(
filePaths.map(async file => {
filePaths.map(async (file) => {
if (file.endsWith('.vue')) {
const content = await fs.promises.readFile(file, 'utf-8')
const sfc = vueCompiler.parse(content)
@ -72,7 +75,7 @@ const genVueTypes = async (root, outDir = path.resolve(__dirname, '../dist/types
}
const sourceFile = project.createSourceFile(
path.relative(process.cwd(), file) + (isTS ? '.ts' : '.js'),
content,
content
)
sourceFiles.push(sourceFile)
}
@ -80,7 +83,7 @@ const genVueTypes = async (root, outDir = path.resolve(__dirname, '../dist/types
const sourceFile = project.addSourceFileAtPath(file)
sourceFiles.push(sourceFile)
}
}),
})
)
const diagnostics = project.getPreEmitDiagnostics()
@ -95,10 +98,8 @@ const genVueTypes = async (root, outDir = path.resolve(__dirname, '../dist/types
console.log(
chalk.yellow(
'Generating definition for file: ' +
chalk.bold(
sourceFile.getBaseName(),
),
),
chalk.bold(sourceFile.getBaseName())
)
)
const emitOutput = sourceFile.getEmitOutput()
@ -109,21 +110,27 @@ const genVueTypes = async (root, outDir = path.resolve(__dirname, '../dist/types
recursive: true,
})
await fs.promises.writeFile(filepath,
await fs.promises.writeFile(
filepath,
outputFile
.getText()
.replace(new RegExp('@element-plus/components', 'g'), 'element-plus/es')
.replace(new RegExp('@element-plus/theme-chalk', 'g'), 'element-plus/theme-chalk')
.replace(
new RegExp('@element-plus/components', 'g'),
'element-plus/es'
)
.replace(
new RegExp('@element-plus/theme-chalk', 'g'),
'element-plus/theme-chalk'
)
.replace(new RegExp('@element-plus', 'g'), 'element-plus/es'),
'utf8')
'utf8'
)
console.log(
chalk.green(
'Definition for file: ' +
chalk.bold(
sourceFile.getBaseName(),
) +
' generated',
),
chalk.bold(sourceFile.getBaseName()) +
' generated'
)
)
}
}

View File

@ -26,16 +26,15 @@ const gen = async () => {
skipAddingFilesFromTsConfig: true,
})
const sourceFiles = []
files.map(f => {
files.map((f) => {
const sourceFile = project.addSourceFileAtPath(f)
sourceFiles.push(sourceFile)
})
for (const sourceFile of sourceFiles) {
console.log(chalk.yellow(
`Emitting file: ${chalk.bold(sourceFile.getFilePath())}`,
))
console.log(
chalk.yellow(`Emitting file: ${chalk.bold(sourceFile.getFilePath())}`)
)
await sourceFile.emit()
const emitOutput = sourceFile.getEmitOutput()
for (const outputFile of emitOutput.getOutputFiles()) {
@ -44,24 +43,21 @@ const gen = async () => {
await fs.promises.mkdir(path.dirname(filepath), {
recursive: true,
})
await fs.promises.writeFile(filepath,
outputFile
.getText()
.replace(new RegExp('@element-plus', 'g'), '.'),
await fs.promises.writeFile(
filepath,
outputFile.getText().replace(new RegExp('@element-plus', 'g'), '.'),
// .replaceAll('@element-plus/theme-chalk', 'element-plus/theme-chalk'),
'utf8')
'utf8'
)
console.log(
chalk.green(
'Definition for file: ' +
chalk.bold(
sourceFile.getBaseName(),
) +
' generated',
),
chalk.bold(sourceFile.getBaseName()) +
' generated'
)
)
}
}
}
export default gen

View File

@ -13,5 +13,5 @@ if (tagVer) {
fs.writeFileSync(
path.resolve(__dirname, '../packages/element-plus/version.ts'),
`export const version = '${version}'
`,
`
)

View File

@ -4,7 +4,7 @@
* @param {PathLike} path path to dependencies
* @returns {Array<string>}
*/
export default path => {
export default (path) => {
const pkgJson = require(path)
const { dependencies } = pkgJson

View File

@ -1,4 +1,4 @@
/* eslint-disable */
import { getPackagesSync } from '@lerna/project';
import { getPackagesSync } from '@lerna/project'
export default getPackagesSync();
export default getPackagesSync()

View File

@ -5,9 +5,7 @@ const rewriter = (rewriteTo = '../..') => {
const compIdentifier = new RegExp('@element-plus', 'g')
file.contents = Buffer.from(
file.contents
.toString()
.replace(compIdentifier, rewriteTo),
file.contents.toString().replace(compIdentifier, rewriteTo)
)
cb(null, file)
})

View File

@ -22,7 +22,7 @@ const rewriter = () => {
file.contents
.toString()
.replace(compIdentifier, compReplacer)
.replace(themeIdentifier, themeReplacer),
.replace(themeIdentifier, themeReplacer)
)
cb(null, file)
})
@ -48,7 +48,7 @@ function compileCjs() {
target: 'ESNEXT',
skipLibCheck: true,
module: 'ESNEXT',
})(),
})()
)
.pipe(gulp.dest(path.resolve(output, 'es')))
}

View File

@ -11,7 +11,6 @@ const epRoot = path.resolve(pkgRoot, './element-plus')
const utilRoot = path.resolve(pkgRoot, './utils')
const buildOutput = path.resolve(projRoot, './dist')
export {
projRoot,
pkgRoot,

View File

@ -17,15 +17,16 @@ const getOutFile = (name, dir = 'lib') => {
const deps = Object.keys(pkg.dependencies)
const inputs = getPackagesSync()
.map(pkg => pkg.name)
.filter(name =>
name.includes('@element-plus') &&
!name.includes('utils'),
)
.map((pkg) => pkg.name)
.filter((name) => name.includes('@element-plus') && !name.includes('utils'))
export default inputs.map(name => ({
input: path.resolve(__dirname, `../packages/${name.split('@element-plus/')[1]}/index.ts`),
output: [{
export default inputs.map((name) => ({
input: path.resolve(
__dirname,
`../packages/${name.split('@element-plus/')[1]}/index.ts`
),
output: [
{
format: 'es',
file: getOutFile(name, 'es'),
paths(id) {
@ -34,7 +35,8 @@ export default inputs.map(name => ({
return id.replace('@element-plus/', '../el-')
}
},
}, {
},
{
format: 'cjs',
file: getOutFile(name, 'lib'),
exports: 'named',
@ -44,7 +46,8 @@ export default inputs.map(name => ({
return id.replace('@element-plus/', '../el-')
}
},
}],
},
],
plugins: [
css(),
vue({
@ -55,8 +58,10 @@ export default inputs.map(name => ({
esbuild(),
],
external(id) {
return /^vue/.test(id)
|| /^@element-plus/.test(id)
|| deps.some(k => new RegExp('^' + k).test(id))
return (
/^vue/.test(id) ||
/^@element-plus/.test(id) ||
deps.some((k) => new RegExp('^' + k).test(id))
)
},
}))

View File

@ -11,8 +11,8 @@ export default function entryPlugin(): Plugin {
/@element-plus\//g,
`${path.relative(
path.dirname(id),
path.resolve(__dirname, '../packages'),
)}/`,
path.resolve(__dirname, '../packages')
)}/`
),
map: null,
}

View File

@ -1,6 +1,5 @@
import chalk from 'chalk'
export default async function reporter(opt, outputOptions, info) {
const values = [
// ...(outputOptions.file || outputOptions.dest
@ -10,9 +9,7 @@ export default async function reporter(opt, outputOptions, info) {
// )}`,
// ]
// :
info.fileName
? [`${outputOptions.file.split('packages/').pop()}`]
: [],
info.fileName ? [`${outputOptions.file.split('packages/').pop()}`] : [],
// )
// ...(info.bundleSizeBefore
// ? [
@ -27,8 +24,7 @@ export default async function reporter(opt, outputOptions, info) {
[`${info.bundleSize}`],
// ),
...(info.minSize
?
// info.minSizeBefore
? // info.minSizeBefore
// ? [
// `${title('Minified Size: ')} ${value(info.minSize)} (was ${value(
// info.minSizeBefore,
@ -66,12 +62,7 @@ export default async function reporter(opt, outputOptions, info) {
// : []),
]
return `${
chalk.cyan(chalk.bold(values[0]))
}: bundle size ${
chalk.yellow(values[1])
} -> minified ${
chalk.green(values[2])
}`
return `${chalk.cyan(chalk.bold(values[0]))}: bundle size ${chalk.yellow(
values[1]
)} -> minified ${chalk.green(values[2])}`
}

View File

@ -8,20 +8,21 @@ const gitHead = process.env.GIT_HEAD
if (!tagVersion || !gitHead) {
console.log(
chalk.red(
'No tag version or git head were found, make sure that you set the environment variable $TAG_VERSION \n',
),
'No tag version or git head were found, make sure that you set the environment variable $TAG_VERSION \n'
)
)
process.exit(1)
}
console.log(chalk.cyan('Start updating version'))
console.log(chalk.cyan([
'NOTICE:',
`$TAG_VERSION: ${tagVersion}`,
`$GIT_HEAD: ${gitHead}`,
].join('\n')))
console.log(
chalk.cyan(
['NOTICE:', `$TAG_VERSION: ${tagVersion}`, `$GIT_HEAD: ${gitHead}`].join(
'\n'
)
)
)
;(async () => {
console.log(chalk.yellow(`Updating package.json for element-plus`))

View File

@ -23,15 +23,17 @@ const plugins = [
const entry = path.resolve(__dirname, '../packages/element-plus/index.ts')
if (!isFullMode) {
externals.push({
externals.push(
{
'@popperjs/core': '@popperjs/core',
'async-validator': 'async-validator',
'mitt': 'mitt',
mitt: 'mitt',
'normalize-wheel': 'normalize-wheel',
'resize-observer-polyfill': 'resize-observer-polyfill',
},
/^dayjs.*/,
/^lodash.*/)
/^lodash.*/
)
}
const config = {
@ -44,7 +46,7 @@ const config = {
libraryTarget: 'umd',
library: 'ElementPlus',
umdNamedDefine: true,
globalObject: 'typeof self !== \'undefined\' ? self : this',
globalObject: "typeof self !== 'undefined' ? self : this",
},
module: {
rules: [

View File

@ -6,7 +6,6 @@ const { Project } = require('ts-morph')
const chalk = require('chalk')
const vueCompiler = require('@vue/compiler-sfc')
const TSCONFIG_PATH = path.resolve(process.cwd(), 'tsconfig.dts.json')
;(async () => {
@ -17,7 +16,9 @@ const TSCONFIG_PATH = path.resolve(process.cwd(), 'tsconfig.dts.json')
messagePort.postMessage({
type: 'log',
message: chalk.yellow(`Worker: ${chalk.bold(workerData.workerId)} started`),
message: chalk.yellow(
`Worker: ${chalk.bold(workerData.workerId)} started`
),
})
const project = new Project({
@ -30,7 +31,8 @@ const TSCONFIG_PATH = path.resolve(process.cwd(), 'tsconfig.dts.json')
const sourceFiles = []
await Promise.all(filePaths.map(async file => {
await Promise.all(
filePaths.map(async (file) => {
if (file.endsWith('.vue')) {
const content = await fs.promises.readFile(file, 'utf-8')
const sfc = vueCompiler.parse(content)
@ -51,7 +53,7 @@ const TSCONFIG_PATH = path.resolve(process.cwd(), 'tsconfig.dts.json')
}
const sourceFile = project.createSourceFile(
path.relative(process.cwd(), file) + (isTS ? '.ts' : '.js'),
content,
content
)
sourceFiles.push(sourceFile)
}
@ -59,7 +61,8 @@ const TSCONFIG_PATH = path.resolve(process.cwd(), 'tsconfig.dts.json')
const sourceFile = project.addSourceFileAtPath(file)
sourceFiles.push(sourceFile)
}
}))
})
)
const diagnostics = project.getPreEmitDiagnostics()
await project.emit({
@ -72,28 +75,28 @@ const TSCONFIG_PATH = path.resolve(process.cwd(), 'tsconfig.dts.json')
})
for (const sourceFile of sourceFiles) {
messagePort.postMessage({
type: 'log',
message: chalk.yellow(
'Generating definition for file: ' +
chalk.bold(
sourceFile.getFilePath(),
),
chalk.bold(sourceFile.getFilePath())
),
})
// console.log(sourceFile.getStructure())
const ElementPlusSign = '@element-plus/'
sourceFile.getImportDeclarations(dec => dec.getModuleSpecifierValue().startsWith(ElementPlusSign)).map(modifySpecifier)
sourceFile
.getImportDeclarations((dec) =>
dec.getModuleSpecifierValue().startsWith(ElementPlusSign)
)
.map(modifySpecifier)
function modifySpecifier(d) {
const replaceTo = 'element-plus/es/' + d.getModuleSpecifierValue().slice(ElementPlusSign.length)
d.setModuleSpecifier(
replaceTo,
)
const replaceTo =
'element-plus/es/' +
d.getModuleSpecifierValue().slice(ElementPlusSign.length)
d.setModuleSpecifier(replaceTo)
}
// console.log(sourceFile.getFilePath())
@ -114,19 +117,14 @@ const TSCONFIG_PATH = path.resolve(process.cwd(), 'tsconfig.dts.json')
type: 'log',
message: chalk.green(
'Definition for file: ' +
chalk.bold(
sourceFile.getBaseName(),
) +
' generated',
chalk.bold(sourceFile.getBaseName()) +
' generated'
),
})
}
messagePort.postMessage({ type: 'fulfill', message: workerId })
}
})
// parentPort.emit
})()

View File

@ -10,10 +10,10 @@ function getPackages(context) {
const project = new Project(cwd)
return project.getPackages()
})
.then(packages => {
.then((packages) => {
return packages
.map(pkg => pkg.name)
.map(name => (name.charAt(0) === '@' ? name.split('/')[1] : name))
.map((pkg) => pkg.name)
.map((name) => (name.charAt(0) === '@' ? name.split('/')[1] : name))
})
}
@ -35,8 +35,8 @@ const scopes = [
module.exports = {
rules: {
'scope-enum': ctx =>
getPackages(ctx).then(packages => [
'scope-enum': (ctx) =>
getPackages(ctx).then((packages) => [
2,
'always',
[...packages, ...scopes],

View File

@ -11,7 +11,8 @@ module.exports = {
transform: {
'^.+\\.vue$': 'vue-jest',
'^.+\\.(t|j)sx?$': [
'babel-jest', {
'babel-jest',
{
presets: [
[
'@babel/preset-env',

View File

@ -1,7 +1,5 @@
{
"packages": [
"packages/*"
],
"packages": ["packages/*"],
"version": "independent",
"npmClient": "yarn",
"useWorkspaces": true

View File

@ -4,13 +4,13 @@
"cz": "npx git-cz",
"test": "jest",
"gen": "bash ./scripts/gc.sh",
"bootstrap": "yarn --frozen-lockfile && npx lerna bootstrap && yarn gen:version",
"gen:version": "esno build/gen-version.ts",
"update:version": "esno build/update-version.ts",
"bootstrap": "yarn --frozen-lockfile && npx lerna bootstrap && yarn gen:version",
"clean:lib": "rimraf lib && rimraf es && rimraf dist",
"build:locale-umd": "esno ./build/build-locale.ts",
"build:helper": "esno build/build-helper.ts",
"build:indices": "esno build/build-indices.ts",
"update:version": "esno build/update-version.ts",
"build:comps": "rimraf dist/components && esno build/components.ts",
"build:style": "gulp --cwd ./build",
"build:prod": "sh scripts/monorepo.sh",
@ -21,10 +21,9 @@
"build:utils": "cd packages/utils && yarn clean && yarn build",
"build:tokens": "cd packages/tokens && yarn clean && yarn build",
"build:full-bundle": "esno build/full-bundle.ts",
"format": "yarn format:scss",
"format:scss": "prettier --write **/*.scss",
"lint": "eslint ./packages --ext .vue,.js,.ts",
"lint-fix": "eslint --fix ./packages --ext .vue,.js,.ts",
"format": "prettier --write '**/*.{vue,js,ts,jsx,tsx,scss,json,yaml,md,html}'",
"lint": "eslint ./packages --ext .vue,.js,.ts,.jsx,.tsx && prettier --check '**/*.{scss,json,yaml,md,html}'",
"lint:fix": "eslint --fix ./packages --ext .vue,.js,.ts,.jsx,.tsx && prettier --write '**/*.{scss,json,yaml,md,html}'",
"website-build": "rimraf website-dist && cross-env NODE_ENV=production webpack --config ./website/webpack.config.js",
"website-dev": "webpack-dev-server --config ./website/webpack.config.js",
"website-dev:play": "cross-env PLAY_ENV=true yarn website-dev"
@ -68,6 +67,8 @@
"cz-conventional-changelog": "^3.3.0",
"esbuild": "^0.12.5",
"eslint": "^7.7.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^7.0.0-beta.0",
"esno": "^0.9.1",
"fast-glob": "^3.2.7",
@ -89,6 +90,7 @@
"markdown-it-container": "^3.0.0",
"mini-css-extract-plugin": "^0.11.2",
"ora": "^5.4.1",
"prettier": "^2.3.2",
"progress-bar-webpack-plugin": "^2.1.0",
"rimraf": "^3.0.2",
"rollup": "^2.28.2",
@ -133,8 +135,8 @@
}
},
"lint-staged": {
"*.{js,ts,vue}": "eslint --fix",
"*.{scss}": "prettier --write"
"*.{vue,js,ts,jsx,tsx}": "eslint --fix",
"*.{scss,json,yaml,md,html}": "prettier --write"
},
"workspaces": [
"packages/*"

View File

@ -1,4 +1,4 @@
module.exports = jest.fn(fn => {
module.exports = jest.fn((fn) => {
fn.cancel = jest.fn()
fn.flush = jest.fn()
return fn

View File

@ -1,4 +1,4 @@
module.exports = jest.fn(fn => {
module.exports = jest.fn((fn) => {
fn.cancel = jest.fn()
fn.flush = jest.fn()
return fn

View File

@ -4,17 +4,26 @@ import Affix from '../src/index.vue'
let clientHeightRestore = null
const _mount = (template: string) => mount({
const _mount = (template: string) =>
mount(
{
components: {
'el-affix': Affix,
},
template,
}, { attachTo: document.body })
},
{ attachTo: document.body }
)
const AXIOM = 'Rem is the best girl'
beforeAll(() => {
clientHeightRestore = defineGetter(window.HTMLElement.prototype, 'clientHeight', 1000, 0)
clientHeightRestore = defineGetter(
window.HTMLElement.prototype,
'clientHeight',
1000,
0
)
})
afterAll(() => {
@ -27,13 +36,17 @@ describe('Affix.vue', () => {
<el-affix>${AXIOM}</el-affix>
`)
expect(wrapper.text()).toEqual(AXIOM)
const mockAffixRect = jest.spyOn(wrapper.find('.el-affix').element, 'getBoundingClientRect').mockReturnValue({
const mockAffixRect = jest
.spyOn(wrapper.find('.el-affix').element, 'getBoundingClientRect')
.mockReturnValue({
height: 40,
width: 1000,
top: -100,
bottom: -80,
} as DOMRect)
const mockDocumentRect = jest.spyOn(document.documentElement, 'getBoundingClientRect').mockReturnValue({
const mockDocumentRect = jest
.spyOn(document.documentElement, 'getBoundingClientRect')
.mockReturnValue({
height: 200,
width: 1000,
top: 0,
@ -50,13 +63,17 @@ describe('Affix.vue', () => {
const wrapper = _mount(`
<el-affix :offset="30">${AXIOM}</el-affix>
`)
const mockAffixRect = jest.spyOn(wrapper.find('.el-affix').element, 'getBoundingClientRect').mockReturnValue({
const mockAffixRect = jest
.spyOn(wrapper.find('.el-affix').element, 'getBoundingClientRect')
.mockReturnValue({
height: 40,
width: 1000,
top: -100,
bottom: -80,
} as DOMRect)
const mockDocumentRect = jest.spyOn(document.documentElement, 'getBoundingClientRect').mockReturnValue({
const mockDocumentRect = jest
.spyOn(document.documentElement, 'getBoundingClientRect')
.mockReturnValue({
height: 200,
width: 1000,
top: 0,
@ -64,7 +81,9 @@ describe('Affix.vue', () => {
} as DOMRect)
await makeScroll(document.documentElement, 'scrollTop', 200)
expect(wrapper.find('.el-affix--fixed').exists()).toBe(true)
expect(wrapper.find('.el-affix--fixed').attributes('style')).toContain('top: 30px;')
expect(wrapper.find('.el-affix--fixed').attributes('style')).toContain(
'top: 30px;'
)
mockAffixRect.mockRestore()
mockDocumentRect.mockRestore()
})
@ -74,13 +93,17 @@ describe('Affix.vue', () => {
<el-affix position="bottom" :offset="20">${AXIOM}</el-affix>
`)
const mockAffixRect = jest.spyOn(wrapper.find('.el-affix').element, 'getBoundingClientRect').mockReturnValue({
const mockAffixRect = jest
.spyOn(wrapper.find('.el-affix').element, 'getBoundingClientRect')
.mockReturnValue({
height: 40,
width: 1000,
top: 2000,
bottom: 2040,
} as DOMRect)
const mockDocumentRect = jest.spyOn(document.documentElement, 'getBoundingClientRect').mockReturnValue({
const mockDocumentRect = jest
.spyOn(document.documentElement, 'getBoundingClientRect')
.mockReturnValue({
height: 200,
width: 1000,
top: 0,
@ -88,7 +111,9 @@ describe('Affix.vue', () => {
} as DOMRect)
await makeScroll(document.documentElement, 'scrollTop', 0)
expect(wrapper.find('.el-affix--fixed').exists()).toBe(true)
expect(wrapper.find('.el-affix--fixed').attributes('style')).toContain('bottom: 20px;')
expect(wrapper.find('.el-affix--fixed').attributes('style')).toContain(
'bottom: 20px;'
)
mockAffixRect.mockRestore()
mockDocumentRect.mockRestore()
})
@ -101,13 +126,17 @@ describe('Affix.vue', () => {
<div style="height: 1000px"></div>
`)
const mockAffixRect = jest.spyOn(wrapper.find('.el-affix').element, 'getBoundingClientRect').mockReturnValue({
const mockAffixRect = jest
.spyOn(wrapper.find('.el-affix').element, 'getBoundingClientRect')
.mockReturnValue({
height: 40,
width: 1000,
top: -100,
bottom: -60,
} as DOMRect)
const mockTargetRect = jest.spyOn(wrapper.find('.target').element, 'getBoundingClientRect').mockReturnValue({
const mockTargetRect = jest
.spyOn(wrapper.find('.target').element, 'getBoundingClientRect')
.mockReturnValue({
height: 200,
width: 1000,
top: -100,
@ -137,13 +166,17 @@ describe('Affix.vue', () => {
const wrapper = _mount(`
<el-affix :z-index="1000">${AXIOM}</el-affix>
`)
const mockAffixRect = jest.spyOn(wrapper.find('.el-affix').element, 'getBoundingClientRect').mockReturnValue({
const mockAffixRect = jest
.spyOn(wrapper.find('.el-affix').element, 'getBoundingClientRect')
.mockReturnValue({
height: 40,
width: 1000,
top: -100,
bottom: -80,
} as DOMRect)
const mockDocumentRect = jest.spyOn(document.documentElement, 'getBoundingClientRect').mockReturnValue({
const mockDocumentRect = jest
.spyOn(document.documentElement, 'getBoundingClientRect')
.mockReturnValue({
height: 200,
width: 1000,
top: 0,
@ -151,7 +184,9 @@ describe('Affix.vue', () => {
} as DOMRect)
await makeScroll(document.documentElement, 'scrollTop', 200)
expect(wrapper.find('.el-affix--fixed').exists()).toBe(true)
expect(wrapper.find('.el-affix--fixed').attributes('style')).toContain('z-index: 1000;')
expect(wrapper.find('.el-affix--fixed').attributes('style')).toContain(
'z-index: 1000;'
)
mockAffixRect.mockRestore()
mockDocumentRect.mockRestore()
})

View File

@ -4,7 +4,6 @@ import type { App } from 'vue'
import type { SFCWithInstall } from '@element-plus/utils/types'
Affix.install = (app: App): void => {
app.component(Affix.name, Affix)
}

View File

@ -6,9 +6,20 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import {
computed,
defineComponent,
onBeforeUnmount,
onMounted,
reactive,
ref,
watch,
} from 'vue'
import { getScrollContainer, off, on } from '@element-plus/utils/dom'
import { addResizeListener, removeResizeListener } from '@element-plus/utils/resize-event'
import {
addResizeListener,
removeResizeListener,
} from '@element-plus/utils/resize-event'
import type { PropType } from 'vue'
@ -61,14 +72,16 @@ export default defineComponent({
return
}
const offset = props.offset ? `${props.offset}px` : 0
const transform = state.transform ? `translateY(${state.transform}px)` : ''
const transform = state.transform
? `translateY(${state.transform}px)`
: ''
return {
height: `${state.height}px`,
width: `${state.width}px`,
top: props.position === 'top' ? offset : '',
bottom: props.position === 'bottom' ? offset : '',
transform: transform,
transform,
zIndex: props.zIndex,
}
})
@ -78,7 +91,10 @@ export default defineComponent({
const targetRect = target.value.getBoundingClientRect()
state.height = rootRect.height
state.width = rootRect.width
state.scrollTop = scrollContainer.value === window ? document.documentElement.scrollTop : scrollContainer.value.scrollTop
state.scrollTop =
scrollContainer.value === window
? document.documentElement.scrollTop
: scrollContainer.value.scrollTop
state.clientHeight = document.documentElement.clientHeight
if (props.position === 'top') {
@ -91,8 +107,11 @@ export default defineComponent({
}
} else {
if (props.target) {
const difference = state.clientHeight - targetRect.top - props.offset - state.height
state.fixed = state.clientHeight - props.offset < rootRect.bottom && state.clientHeight > targetRect.top
const difference =
state.clientHeight - targetRect.top - props.offset - state.height
state.fixed =
state.clientHeight - props.offset < rootRect.bottom &&
state.clientHeight > targetRect.top
state.transform = difference < 0 ? -difference : 0
} else {
state.fixed = state.clientHeight - props.offset < rootRect.bottom
@ -109,9 +128,12 @@ export default defineComponent({
})
}
watch(() => state.fixed, () => {
watch(
() => state.fixed,
() => {
emit('change', state.fixed)
})
}
)
onMounted(() => {
if (props.target) {

View File

@ -24,7 +24,9 @@ describe('Alert.vue', () => {
},
})
expect(wrapper.find('.el-alert').classes()).toContain('el-alert--success')
expect(wrapper.find('.el-alert__icon').classes()).toContain('el-icon-success')
expect(wrapper.find('.el-alert__icon').classes()).toContain(
'el-icon-success'
)
})
test('description', () => {

View File

@ -6,9 +6,17 @@
:class="[typeClass, center ? 'is-center' : '', 'is-' + effect]"
role="alert"
>
<i v-if="showIcon" class="el-alert__icon" :class="[ iconClass, isBigIcon ]"></i>
<i
v-if="showIcon"
class="el-alert__icon"
:class="[iconClass, isBigIcon]"
></i>
<div class="el-alert__content">
<span v-if="title || $slots.title" class="el-alert__title" :class="[ isBoldTitle ]">
<span
v-if="title || $slots.title"
class="el-alert__title"
:class="[isBoldTitle]"
>
<slot name="title">{{ title }}</slot>
</span>
<p v-if="$slots.default || !!description" class="el-alert__description">
@ -19,7 +27,10 @@
<i
v-if="closable"
class="el-alert__closebtn"
:class="{ 'is-customed': closeText !== '', 'el-icon-close': closeText === '' }"
:class="{
'is-customed': closeText !== '',
'el-icon-close': closeText === '',
}"
@click="close"
>
{{ closeText }}
@ -28,15 +39,15 @@
</div>
</transition>
</template>
<script lang='ts'>
<script lang="ts">
import { defineComponent, computed, ref } from 'vue'
import type { PropType } from 'vue'
const TYPE_CLASSES_MAP = {
'success': 'el-icon-success',
'warning': 'el-icon-warning',
'error': 'el-icon-error',
success: 'el-icon-success',
warning: 'el-icon-warning',
error: 'el-icon-error',
}
export default defineComponent({
@ -67,7 +78,8 @@ export default defineComponent({
effect: {
type: String,
default: 'light',
validator: (value: string): boolean => ['light', 'dark'].indexOf(value) > -1,
validator: (value: string): boolean =>
['light', 'dark'].indexOf(value) > -1,
},
},
emits: ['close'],
@ -77,12 +89,18 @@ export default defineComponent({
// computed
const typeClass = computed(() => `el-alert--${props.type}`)
const iconClass = computed(() => TYPE_CLASSES_MAP[props.type] || 'el-icon-info')
const isBigIcon = computed(() => props.description || ctx.slots.default ? 'is-big' : '')
const isBoldTitle = computed(() => props.description || ctx.slots.default ? 'is-bold' : '')
const iconClass = computed(
() => TYPE_CLASSES_MAP[props.type] || 'el-icon-info'
)
const isBigIcon = computed(() =>
props.description || ctx.slots.default ? 'is-big' : ''
)
const isBoldTitle = computed(() =>
props.description || ctx.slots.default ? 'is-bold' : ''
)
// methods
const close = evt => {
const close = (evt) => {
visible.value = false
ctx.emit('close', evt)
}

View File

@ -6,7 +6,8 @@ jest.unmock('lodash/debounce')
import Autocomplete from '../src/index.vue'
const _mount = (payload = {}) => mount({
const _mount = (payload = {}) =>
mount({
components: {
'el-autocomplete': Autocomplete,
},
@ -24,7 +25,13 @@ const _mount = (payload = {}) => mount({
},
methods: {
querySearch(queryString, cb) {
cb(queryString ? this.list.filter(i => i.value.indexOf(queryString.toLowerCase()) === 0) : this.list)
cb(
queryString
? this.list.filter(
(i) => i.value.indexOf(queryString.toLowerCase()) === 0
)
: this.list
)
},
},
template: `
@ -76,11 +83,17 @@ describe('Autocomplete.vue', () => {
const wrapper = _mount()
await wrapper.setProps({ popperClass: 'error' })
expect(document.body.querySelector('.el-popper').classList.contains('error')).toBe(true)
expect(
document.body.querySelector('.el-popper').classList.contains('error')
).toBe(true)
await wrapper.setProps({ popperClass: 'success' })
expect(document.body.querySelector('.el-popper').classList.contains('error')).toBe(false)
expect(document.body.querySelector('.el-popper').classList.contains('success')).toBe(true)
expect(
document.body.querySelector('.el-popper').classList.contains('error')
).toBe(false)
expect(
document.body.querySelector('.el-popper').classList.contains('success')
).toBe(true)
})
test('popperAppendToBody', async () => {

View File

@ -54,7 +54,10 @@
<template #default>
<div
ref="regionRef"
:class="['el-autocomplete-suggestion', suggestionLoading && 'is-loading']"
:class="[
'el-autocomplete-suggestion',
suggestionLoading && 'is-loading',
]"
:style="{ width: dropdownWidth, outline: 'none' }"
role="region"
>
@ -71,7 +74,7 @@
v-for="(item, index) in suggestions"
:id="`${id}-item-${index}`"
:key="index"
:class="{'highlighted': highlightedIndex === index}"
:class="{ highlighted: highlightedIndex === index }"
role="option"
:aria-selected="highlightedIndex === index"
@click="select(item)"
@ -87,9 +90,13 @@
<script lang="ts">
import {
defineComponent, ref, computed,
onMounted, onUpdated,
nextTick, watch,
defineComponent,
ref,
computed,
onMounted,
onUpdated,
nextTick,
watch,
} from 'vue'
import { NOOP } from '@vue/shared'
import debounce from 'lodash/debounce'
@ -131,12 +138,21 @@ export default defineComponent({
placement: {
type: String as PropType<Placement>,
validator: (val: string): boolean => {
return ['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end'].includes(val)
return [
'top',
'top-start',
'top-end',
'bottom',
'bottom-start',
'bottom-end',
].includes(val)
},
default: 'bottom-start',
},
fetchSuggestions: {
type: Function as PropType<(queryString: string, cb: (data: any[]) => void) => void>,
type: Function as PropType<
(queryString: string, cb: (data: any[]) => void) => void
>,
default: NOOP,
},
popperClass: {
@ -164,7 +180,15 @@ export default defineComponent({
default: false,
},
},
emits: [UPDATE_MODEL_EVENT, 'input', 'change', 'focus', 'blur', 'clear', 'select'],
emits: [
UPDATE_MODEL_EVENT,
'input',
'change',
'focus',
'blur',
'clear',
'select',
],
setup(props, ctx) {
const attrs = useAttrs()
const suggestions = ref([])
@ -181,7 +205,8 @@ export default defineComponent({
return `el-autocomplete-${generateId()}`
})
const suggestionVisible = computed(() => {
const isValidData = isArray(suggestions.value) && suggestions.value.length > 0
const isValidData =
isArray(suggestions.value) && suggestions.value.length > 0
return (isValidData || loading.value) && activated.value
})
const suggestionLoading = computed(() => {
@ -200,21 +225,26 @@ export default defineComponent({
inputRef.value.inputOrTextarea.setAttribute('role', 'textbox')
inputRef.value.inputOrTextarea.setAttribute('aria-autocomplete', 'list')
inputRef.value.inputOrTextarea.setAttribute('aria-controls', 'id')
inputRef.value.inputOrTextarea.setAttribute('aria-activedescendant', `${id.value}-item-${highlightedIndex.value}`)
const $ul = regionRef.value.querySelector('.el-autocomplete-suggestion__list')
inputRef.value.inputOrTextarea.setAttribute(
'aria-activedescendant',
`${id.value}-item-${highlightedIndex.value}`
)
const $ul = regionRef.value.querySelector(
'.el-autocomplete-suggestion__list'
)
$ul.setAttribute('role', 'listbox')
$ul.setAttribute('id', id.value)
})
onUpdated(updatePopperPosition)
const getData = queryString => {
const getData = (queryString) => {
if (suggestionDisabled.value) {
return
}
loading.value = true
updatePopperPosition()
props.fetchSuggestions(queryString, suggestionsArg => {
props.fetchSuggestions(queryString, (suggestionsArg) => {
loading.value = false
if (suggestionDisabled.value) {
return
@ -223,12 +253,15 @@ export default defineComponent({
suggestions.value = suggestionsArg
highlightedIndex.value = props.highlightFirstItem ? 0 : -1
} else {
throwError('ElAutocomplete', 'autocomplete suggestions must be an array')
throwError(
'ElAutocomplete',
'autocomplete suggestions must be an array'
)
}
})
}
const debouncedGetData = debounce(getData, props.debounce)
const handleInput = value => {
const handleInput = (value) => {
ctx.emit('input', value)
ctx.emit(UPDATE_MODEL_EVENT, value)
suggestionDisabled.value = false
@ -239,17 +272,17 @@ export default defineComponent({
}
debouncedGetData(value)
}
const handleChange = value => {
const handleChange = (value) => {
ctx.emit('change', value)
}
const handleFocus = e => {
const handleFocus = (e) => {
activated.value = true
ctx.emit('focus', e)
if (props.triggerOnFocus) {
debouncedGetData(props.modelValue)
}
}
const handleBlur = e => {
const handleBlur = (e) => {
ctx.emit('blur', e)
}
const handleClear = () => {
@ -258,9 +291,10 @@ export default defineComponent({
ctx.emit('clear')
}
const handleKeyEnter = () => {
if (suggestionVisible.value
&& highlightedIndex.value >= 0
&& highlightedIndex.value < suggestions.value.length
if (
suggestionVisible.value &&
highlightedIndex.value >= 0 &&
highlightedIndex.value < suggestions.value.length
) {
select(suggestions.value[highlightedIndex.value])
} else if (props.selectWhenUnmatched) {
@ -277,7 +311,7 @@ export default defineComponent({
const focus = () => {
inputRef.value.focus()
}
const select = item => {
const select = (item) => {
ctx.emit('input', item[props.valueKey])
ctx.emit(UPDATE_MODEL_EVENT, item[props.valueKey])
ctx.emit('select', item)
@ -286,7 +320,7 @@ export default defineComponent({
highlightedIndex.value = -1
})
}
const highlight = index => {
const highlight = (index) => {
if (!suggestionVisible.value || loading.value) {
return
}
@ -297,24 +331,29 @@ export default defineComponent({
if (index >= suggestions.value.length) {
index = suggestions.value.length - 1
}
const suggestion = regionRef.value.querySelector('.el-autocomplete-suggestion__wrap')
const suggestionList = suggestion.querySelectorAll('.el-autocomplete-suggestion__list li')
const suggestion = regionRef.value.querySelector(
'.el-autocomplete-suggestion__wrap'
)
const suggestionList = suggestion.querySelectorAll(
'.el-autocomplete-suggestion__list li'
)
const highlightItem = suggestionList[index]
const scrollTop = suggestion.scrollTop
const { offsetTop, scrollHeight } = highlightItem
if (offsetTop + scrollHeight > (scrollTop + suggestion.clientHeight)) {
if (offsetTop + scrollHeight > scrollTop + suggestion.clientHeight) {
suggestion.scrollTop += scrollHeight
}
if (offsetTop < scrollTop) {
suggestion.scrollTop -= scrollHeight
}
highlightedIndex.value = index
inputRef.value.inputOrTextarea.setAttribute('aria-activedescendant', `${id.value}-item-${highlightedIndex.value}`)
inputRef.value.inputOrTextarea.setAttribute(
'aria-activedescendant',
`${id.value}-item-${highlightedIndex.value}`
)
}
return {
Effect,

View File

@ -1,6 +1,10 @@
import { nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import { IMAGE_SUCCESS, IMAGE_FAIL, mockImageEvent } from '@element-plus/test-utils'
import {
IMAGE_SUCCESS,
IMAGE_FAIL,
mockImageEvent,
} from '@element-plus/test-utils'
import Avatar from '../src/index.vue'
@ -66,7 +70,9 @@ describe('Avatar.vue', () => {
const wrapper = mount(Avatar, {
props: { fit, src: IMAGE_SUCCESS },
})
expect(wrapper.find('img').attributes('style')).toContain(`object-fit: ${fit};`)
expect(wrapper.find('img').attributes('style')).toContain(
`object-fit: ${fit};`
)
}
})
@ -84,4 +90,3 @@ describe('Avatar.vue', () => {
expect(wrapper.find('img').exists()).toBe(true)
})
})

View File

@ -7,7 +7,7 @@
:srcset="srcSet"
:style="fitStyle"
@error="handleError"
>
/>
<i v-else-if="icon" :class="icon"></i>
<slot v-else></slot>
</span>
@ -77,23 +77,31 @@ export default defineComponent({
const sizeStyle = computed(() => {
const { size } = props
return typeof size === 'number' ? {
return typeof size === 'number'
? {
height: `${size}px`,
width: `${size}px`,
lineHeight: `${size}px`,
} : {}
}
: {}
})
const fitStyle = computed(() => ({
const fitStyle = computed(
() =>
({
objectFit: props.fit,
}) as CSSProperties)
} as CSSProperties)
)
function handleError(e: Event) {
hasLoadError.value = true
emit(ERROR_EVENT, e)
}
return {
hasLoadError, avatarClass, sizeStyle, handleError,
hasLoadError,
avatarClass,
sizeStyle,
handleError,
fitStyle,
}
},

View File

@ -1,12 +1,16 @@
import { mount } from '@vue/test-utils'
import Backtop from '../src/index.vue'
const _mount = (template: string) => mount({
const _mount = (template: string) =>
mount(
{
components: {
'el-backtop': Backtop,
},
template,
}, { attachTo: document.body })
},
{ attachTo: document.body }
)
describe('Backtop.vue', () => {
test('render', async () => {
@ -22,7 +26,9 @@ describe('Backtop.vue', () => {
await wrapper.trigger('scroll')
expect(wrapper.find('.el-backtop').exists()).toBe(true)
expect(wrapper.find('.el-backtop').attributes('style')).toBe('right: 100px; bottom: 200px;')
expect(wrapper.find('.el-backtop').attributes('style')).toBe(
'right: 100px; bottom: 200px;'
)
expect(wrapper.find('.el-icon-caret-top').exists()).toBe(true)
await wrapper.trigger('click')

View File

@ -3,8 +3,8 @@
<div
v-if="visible"
:style="{
'right': styleRight,
'bottom': styleBottom
right: styleRight,
bottom: styleBottom,
}"
class="el-backtop"
@click.stop="handleClick"
@ -62,7 +62,8 @@ export default defineComponent({
const scrollToTop = () => {
const beginTime = Date.now()
const beginValue = el.value.scrollTop
const rAF = window.requestAnimationFrame || (func => setTimeout(func, 16))
const rAF =
window.requestAnimationFrame || ((func) => setTimeout(func, 16))
const frameFunc = () => {
const progress = (Date.now() - beginTime) / 500
if (progress < 1) {
@ -77,7 +78,7 @@ export default defineComponent({
const onScroll = () => {
visible.value = el.value.scrollTop >= props.visibilityHeight
}
const handleClick = event => {
const handleClick = (event) => {
scrollToTop()
ctx.emit('click', event)
}

View File

@ -9,7 +9,7 @@
isDot ? 'is-dot' : 'el-badge__content--' + type,
{
'is-fixed': $slots.default,
}
},
]"
v-text="content"
>

View File

@ -2,19 +2,23 @@ import { mount } from '@vue/test-utils'
import Breadcrumb from '../src/index.vue'
import BreadcrumbItem from '../src/item.vue'
const _mount = (template: string) => mount({
const _mount = (template: string) =>
mount(
{
components: {
'el-breadcrumb': Breadcrumb,
'el-breadcrumb-item': BreadcrumbItem,
},
template,
}, {
},
{
global: {
provide: {
breadcrumb: {},
},
},
})
}
)
describe('Breadcrumb.vue', () => {
test('separator', () => {
@ -33,7 +37,9 @@ describe('Breadcrumb.vue', () => {
</el-breadcrumb>
`)
expect(wrapper.find('.el-breadcrumb__separator').text()).toBe('')
expect(wrapper.find('.el-breadcrumb__separator').classes()).toContain('test')
expect(wrapper.find('.el-breadcrumb__separator').classes()).toContain(
'test'
)
})
test('to', () => {

View File

@ -74,7 +74,6 @@ describe('Button.vue', () => {
})
await wrapper.trigger('click')
expect(wrapper.emitted()).toBeDefined()
})
test('handle click inside', async () => {
@ -106,7 +105,6 @@ describe('Button.vue', () => {
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toBeUndefined()
})
})
describe('Button Group', () => {
it('create', () => {
@ -128,7 +126,8 @@ describe('Button Group', () => {
const size = ref('small')
const wrapper = mount({
setup() {
return () => h(ButtonGroup, { size: size.value }, () => [
return () =>
h(ButtonGroup, { size: size.value }, () => [
h(Button, { type: 'primary' }, () => 'Prev'),
h(Button, { type: 'primary' }, () => 'Next'),
h(Button, { type: 'primary', size: 'mini' }, () => 'Mini'),
@ -136,13 +135,21 @@ describe('Button Group', () => {
},
})
expect(wrapper.classes()).toContain('el-button-group')
expect(wrapper.findAll('.el-button-group button.el-button--small').length).toBe(2)
expect(wrapper.findAll('.el-button-group button.el-button--mini').length).toBe(1)
expect(
wrapper.findAll('.el-button-group button.el-button--small').length
).toBe(2)
expect(
wrapper.findAll('.el-button-group button.el-button--mini').length
).toBe(1)
size.value = 'medium'
await nextTick()
expect(wrapper.findAll('.el-button-group button.el-button--medium').length).toBe(2)
expect(wrapper.findAll('.el-button-group button.el-button--mini').length).toBe(1)
expect(
wrapper.findAll('.el-button-group button.el-button--medium').length
).toBe(2)
expect(
wrapper.findAll('.el-button-group button.el-button--mini').length
).toBe(1)
})
})

View File

@ -20,9 +20,12 @@ export default defineComponent({
},
},
setup(props) {
provide(elButtonGroupKey, reactive({
provide(
elButtonGroupKey,
reactive({
size: toRef(props, 'size'),
}))
})
)
},
})
</script>

View File

@ -9,8 +9,8 @@
'is-loading': loading,
'is-plain': plain,
'is-round': round,
'is-circle': circle
}
'is-circle': circle,
},
]"
:disabled="buttonDisabled || loading"
:autofocus="autofocus"
@ -23,7 +23,7 @@
</button>
</template>
<script lang='ts'>
<script lang="ts">
import { computed, inject, defineComponent } from 'vue'
import { elFormKey, elFormItemKey } from '@element-plus/tokens'
import { useGlobalConfig } from '@element-plus/utils/util'

View File

@ -1,2 +1,9 @@
export type ButtonType = 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'text' | 'default'
export type ButtonType =
| 'primary'
| 'success'
| 'warning'
| 'danger'
| 'info'
| 'text'
| 'default'
export type ButtonNativeType = 'button' | 'submit' | 'reset'

View File

@ -2,7 +2,8 @@ import { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
import Calendar from '../src/index.vue'
const _mount = (template: string, data?, otherObj?) => mount({
const _mount = (template: string, data?, otherObj?) =>
mount({
components: {
'el-calendar': Calendar,
},
@ -13,23 +14,32 @@ const _mount = (template: string, data?, otherObj?) => mount({
describe('Calendar.vue', () => {
it('create', async () => {
const wrapper = _mount(`
const wrapper = _mount(
`
<el-calendar v-model="value"></el-calendar>
`, () => ({ value: new Date('2019-04-01') }))
`,
() => ({ value: new Date('2019-04-01') })
)
const titleEl = wrapper.find('.el-calendar__title')
expect(/2019.*April/.test((titleEl.element as HTMLElement).innerHTML)).toBeTruthy()
expect(
/2019.*April/.test((titleEl.element as HTMLElement).innerHTML)
).toBeTruthy()
expect(wrapper.element.querySelectorAll('thead th').length).toBe(7)
const rows = wrapper.element.querySelectorAll('.el-calendar-table__row')
expect(rows.length).toBe(6);
(rows[5].firstElementChild as HTMLElement).click()
expect(rows.length).toBe(6)
;(rows[5].firstElementChild as HTMLElement).click()
await nextTick()
expect(/2019.*May/.test((titleEl.element as HTMLElement).innerHTML)).toBeTruthy()
expect(
/2019.*May/.test((titleEl.element as HTMLElement).innerHTML)
).toBeTruthy()
const vm = wrapper.vm as any
const date = vm.value
expect(date.getFullYear()).toBe(2019)
expect(date.getMonth()).toBe(4)
expect((wrapper.find('.is-selected span').element as HTMLElement).innerHTML).toBe('5')
expect(
(wrapper.find('.is-selected span').element as HTMLElement).innerHTML
).toBe('5')
})
it('range', () => {
@ -37,10 +47,14 @@ describe('Calendar.vue', () => {
<el-calendar :range="[new Date(2019, 2, 4), new Date(2019, 2, 24)]"></el-calendar>
`)
const titleEl = wrapper.find('.el-calendar__title')
expect(/2019.*March/.test((titleEl.element as HTMLElement).innerHTML)).toBeTruthy()
expect(
/2019.*March/.test((titleEl.element as HTMLElement).innerHTML)
).toBeTruthy()
const rows = wrapper.element.querySelectorAll('.el-calendar-table__row')
expect(rows.length).toBe(4)
expect(wrapper.element.querySelector('.el-calendar__button-group')).toBeNull()
expect(
wrapper.element.querySelector('.el-calendar__button-group')
).toBeNull()
})
// https://github.com/element-plus/element-plus/issues/3155
@ -49,10 +63,14 @@ describe('Calendar.vue', () => {
<el-calendar :range="[new Date(2021, 1, 2), new Date(2021, 1, 28)]"></el-calendar>
`)
const titleEl = wrapper.find('.el-calendar__title')
expect(/2021.*January/.test((titleEl.element as HTMLElement).innerHTML)).toBeTruthy()
expect(
/2021.*January/.test((titleEl.element as HTMLElement).innerHTML)
).toBeTruthy()
const rows = wrapper.element.querySelectorAll('.el-calendar-table__row')
expect(rows.length).toBe(5)
expect(wrapper.element.querySelector('.el-calendar__button-group')).toBeNull()
expect(
wrapper.element.querySelector('.el-calendar__button-group')
).toBeNull()
})
it('range tow monthes', async () => {
@ -60,17 +78,23 @@ describe('Calendar.vue', () => {
<el-calendar :range="[new Date(2019, 3, 14), new Date(2019, 4, 18)]"></el-calendar>
`)
const titleEl = wrapper.find('.el-calendar__title')
expect(/2019.*April/.test((titleEl.element as HTMLElement).innerHTML)).toBeTruthy()
const dateTables = wrapper.element.querySelectorAll('.el-calendar-table.is-range')
expect(
/2019.*April/.test((titleEl.element as HTMLElement).innerHTML)
).toBeTruthy()
const dateTables = wrapper.element.querySelectorAll(
'.el-calendar-table.is-range'
)
expect(dateTables.length).toBe(2)
const rows = wrapper.element.querySelectorAll('.el-calendar-table__row')
expect(rows.length).toBe(5)
const cell = rows[rows.length - 1].firstElementChild;
(cell as HTMLElement).click()
const cell = rows[rows.length - 1].firstElementChild
;(cell as HTMLElement).click()
await nextTick()
expect(/2019.*May/.test((titleEl.element as HTMLElement).innerHTML)).toBeTruthy()
expect(
/2019.*May/.test((titleEl.element as HTMLElement).innerHTML)
).toBeTruthy()
expect(cell.classList.contains('is-selected')).toBeTruthy()
})
@ -80,37 +104,51 @@ describe('Calendar.vue', () => {
<el-calendar :range="[new Date(2021, 1, 2), new Date(2021, 2, 21)]"></el-calendar>
`)
const titleEl = wrapper.find('.el-calendar__title')
expect(/2021.*January/.test((titleEl.element as HTMLElement).innerHTML)).toBeTruthy()
const dateTables = wrapper.element.querySelectorAll('.el-calendar-table.is-range')
expect(
/2021.*January/.test((titleEl.element as HTMLElement).innerHTML)
).toBeTruthy()
const dateTables = wrapper.element.querySelectorAll(
'.el-calendar-table.is-range'
)
expect(dateTables.length).toBe(3)
const rows = wrapper.element.querySelectorAll('.el-calendar-table__row')
expect(rows.length).toBe(8)
const cell = rows[rows.length - 1].firstElementChild;
(cell as HTMLElement).click()
const cell = rows[rows.length - 1].firstElementChild
;(cell as HTMLElement).click()
await nextTick()
expect(/2021.*March/.test((titleEl.element as HTMLElement).innerHTML)).toBeTruthy()
expect(
/2021.*March/.test((titleEl.element as HTMLElement).innerHTML)
).toBeTruthy()
expect(cell.classList.contains('is-selected')).toBeTruthy()
})
it('firstDayOfWeek', async () => {
// default en locale, weekStart 0 Sunday
const wrapper = _mount(`
const wrapper = _mount(
`
<el-calendar v-model="value"></el-calendar>
`, () => ({ value: new Date('2019-04-01') }))
`,
() => ({ value: new Date('2019-04-01') })
)
const head = wrapper.element.querySelector('.el-calendar-table thead')
expect((head.firstElementChild as HTMLElement).innerHTML).toBe('Sun')
expect((head.lastElementChild as HTMLElement).innerHTML).toBe('Sat')
const firstRow = wrapper.element.querySelector('.el-calendar-table__row')
expect((firstRow.firstElementChild as HTMLElement).innerHTML).toContain('31')
expect((firstRow.firstElementChild as HTMLElement).innerHTML).toContain(
'31'
)
expect((firstRow.lastElementChild as HTMLElement).innerHTML).toContain('6')
})
it('firstDayOfWeek in range mode', async () => {
const wrapper = _mount(`
const wrapper = _mount(
`
<el-calendar v-model="value" :first-day-of-week="7" :range="[new Date(2019, 1, 3), new Date(2019, 2, 23)]"></el-calendar>
`, () => ({ value: new Date('2019-03-04') }))
`,
() => ({ value: new Date('2019-03-04') })
)
const head = wrapper.element.querySelector('.el-calendar-table thead')
expect((head.firstElementChild as HTMLElement).innerHTML).toBe('Sun')
expect((head.lastElementChild as HTMLElement).innerHTML).toBe('Sat')

View File

@ -2,7 +2,7 @@
<table
:class="{
'el-calendar-table': true,
'is-range': isInRange
'is-range': isInRange,
}"
cellspacing="0"
cellpadding="0"
@ -16,7 +16,7 @@
:key="index"
:class="{
'el-calendar-table__row': true,
'el-calendar-table__row--hide-border': index === 0 && hideHeader
'el-calendar-table__row--hide-border': index === 0 && hideHeader,
}"
>
<td
@ -26,10 +26,7 @@
@click="pickDay(cell)"
>
<div class="el-calendar-day">
<slot
name="dateCell"
:data="getSlotData(cell)"
>
<slot name="dateCell" :data="getSlotData(cell)">
<span>{{ cell.text }}</span>
</slot>
</div>
@ -40,11 +37,7 @@
</template>
<script lang="ts">
import {
computed,
defineComponent,
ref,
} from 'vue'
import { computed, defineComponent, ref } from 'vue'
import { PropType } from 'vue'
import dayjs, { Dayjs } from 'dayjs'
import localeData from 'dayjs/plugin/localeData'
@ -80,14 +73,16 @@ export default defineComponent({
emits: ['pick'],
setup(props, ctx) {
const { lang } = useLocaleInject()
const WEEK_DAYS = ref(dayjs().locale(lang.value).localeData().weekdaysShort())
const WEEK_DAYS = ref(
dayjs().locale(lang.value).localeData().weekdaysShort()
)
const now = dayjs().locale(lang.value)
// todo better way to get Day.js locale object
const firstDayOfWeek = (now as any).$locale().weekStart || 0
const toNestedArr = days => {
const toNestedArr = (days) => {
return rangeArr(days.length / 7).map((_, index) => {
const start = index * 7
return days.slice(start, start + 7)
@ -143,12 +138,12 @@ export default defineComponent({
let days = []
if (isInRange.value) {
const [start, end] = props.range
const currentMonthRange = rangeArr(
end.date() - start.date() + 1,
).map((_, index) => ({
const currentMonthRange = rangeArr(end.date() - start.date() + 1).map(
(_, index) => ({
text: start.date() + index,
type: 'current',
}))
})
)
let remaining = currentMonthRange.length % 7
remaining = remaining === 0 ? 0 : 7 - remaining
@ -161,12 +156,12 @@ export default defineComponent({
const firstDay = props.date.startOf('month').day() || 7
const prevMonthDays = getPrevMonthLastDays(
props.date,
firstDay - firstDayOfWeek,
).map(day => ({
firstDay - firstDayOfWeek
).map((day) => ({
text: day,
type: 'prev',
}))
const currentMonthDays = getMonthDays(props.date).map(day => ({
const currentMonthDays = getMonthDays(props.date).map((day) => ({
text: day,
type: 'current',
}))
@ -202,5 +197,4 @@ export default defineComponent({
}
},
})
</script>

View File

@ -4,32 +4,20 @@
<div class="el-calendar__title">{{ i18nDate }}</div>
<div v-if="validatedRange.length === 0" class="el-calendar__button-group">
<el-button-group>
<el-button
size="mini"
@click="selectDate('prev-month')"
>
<el-button size="mini" @click="selectDate('prev-month')">
{{ t('el.datepicker.prevMonth') }}
</el-button>
<el-button size="mini" @click="selectDate('today')">
{{
t('el.datepicker.today')
}}
{{ t('el.datepicker.today') }}
</el-button>
<el-button
size="mini"
@click="selectDate('next-month')"
>
<el-button size="mini" @click="selectDate('next-month')">
{{ t('el.datepicker.nextMonth') }}
</el-button>
</el-button-group>
</div>
</div>
<div v-if="validatedRange.length === 0" class="el-calendar__body">
<date-table
:date="date"
:selected-day="realSelectedDay"
@pick="pickDay"
>
<date-table :date="date" :selected-day="realSelectedDay" @pick="pickDay">
<template v-if="$slots.dateCell" #dateCell="data">
<slot name="dateCell" v-bind="data"></slot>
</template>
@ -54,11 +42,7 @@
</template>
<script lang="ts">
import {
ref,
computed,
defineComponent,
} from 'vue'
import { ref, computed, defineComponent } from 'vue'
import type { PropType, ComputedRef } from 'vue'
import dayjs from 'dayjs'
@ -90,10 +74,7 @@ export default defineComponent({
validator: (range: Date): boolean => {
if (Array.isArray(range)) {
return (
range.length === 2 &&
range.every(
item => item instanceof Date,
)
range.length === 2 && range.every((item) => item instanceof Date)
)
}
return false
@ -152,7 +133,10 @@ export default defineComponent({
// https://github.com/element-plus/element-plus/issues/3155
// Calculate the validate date range according to the start and end dates
const calculateValidatedDateRange = (startDayjs: dayjs.Dayjs,endDayjs: dayjs.Dayjs) => {
const calculateValidatedDateRange = (
startDayjs: dayjs.Dayjs,
endDayjs: dayjs.Dayjs
) => {
const firstDay = startDayjs.startOf('week')
const lastDay = endDayjs.endOf('week')
const firstMonth = firstDay.get('month')
@ -164,30 +148,43 @@ export default defineComponent({
}
// Two adjacent months
else if (firstMonth + 1 === lastMonth) {
const firstMonthLastDay = firstDay.endOf('month')
const lastMonthFirstDay = lastDay.startOf('month')
// Whether the last day of the first month and the first day of the last month is in the same week
const isSameWeek = firstMonthLastDay.isSame(lastMonthFirstDay, 'week')
const lastMonthStartDay = isSameWeek ? lastMonthFirstDay.add(1, 'week') : lastMonthFirstDay
const lastMonthStartDay = isSameWeek
? lastMonthFirstDay.add(1, 'week')
: lastMonthFirstDay
return [[firstDay, firstMonthLastDay], [lastMonthStartDay.startOf('week'), lastDay]]
return [
[firstDay, firstMonthLastDay],
[lastMonthStartDay.startOf('week'), lastDay],
]
}
// Three consecutive months (compatible: 2021-01-30 to 2021-02-28)
else if (firstMonth + 2 === lastMonth) {
const firstMonthLastDay = firstDay.endOf('month')
const secondMonthFisrtDay = firstDay.add(1, 'month').startOf('month')
// Whether the last day of the first month and the second month is in the same week
const secondMonthStartDay = firstMonthLastDay.isSame(secondMonthFisrtDay, 'week') ? secondMonthFisrtDay.add(1, 'week') : secondMonthFisrtDay
const secondMonthStartDay = firstMonthLastDay.isSame(
secondMonthFisrtDay,
'week'
)
? secondMonthFisrtDay.add(1, 'week')
: secondMonthFisrtDay
const secondMonthLastDay = secondMonthStartDay.endOf('month')
const lastMonthFirstDay = lastDay.startOf('month')
// Whether the last day of the second month and the last day of the last month is in the same week
const lastMonthStartDay = secondMonthLastDay.isSame(lastMonthFirstDay, 'week') ? lastMonthFirstDay.add(1, 'week') : lastMonthFirstDay
const lastMonthStartDay = secondMonthLastDay.isSame(
lastMonthFirstDay,
'week'
)
? lastMonthFirstDay.add(1, 'week')
: lastMonthFirstDay
return [
[firstDay, firstMonthLastDay],
@ -197,7 +194,10 @@ export default defineComponent({
}
// Other cases
else {
warn('ElCalendar', 'start time and end time interval must not exceed two months')
warn(
'ElCalendar',
'start time and end time interval must not exceed two months'
)
return []
}
}
@ -205,7 +205,7 @@ export default defineComponent({
// if range is valid, we get a two-digit array
const validatedRange = computed(() => {
if (!props.range) return []
const rangeArrDayjs = props.range.map(_ => dayjs(_).locale(lang.value))
const rangeArrDayjs = props.range.map((_) => dayjs(_).locale(lang.value))
const [startDayjs, endDayjs] = rangeArrDayjs
if (startDayjs.isAfter(endDayjs)) {
warn('ElCalendar', 'end time should be greater than start time')
@ -217,7 +217,10 @@ export default defineComponent({
} else {
// two months
if (startDayjs.add(1, 'month').month() !== endDayjs.month()) {
warn('ElCalendar', 'start time and end time interval must not exceed two months')
warn(
'ElCalendar',
'start time and end time interval must not exceed two months'
)
return []
}
return calculateValidatedDateRange(startDayjs, endDayjs)

View File

@ -65,7 +65,9 @@ describe('Card.vue', () => {
bodyStyle: style,
},
})
expect(wrapper.find('.el-card__body').attributes('style')).toBe('font-size: 14px;')
expect(wrapper.find('.el-card__body').attributes('style')).toBe(
'font-size: 14px;'
)
})
test('body style with array', () => {
@ -78,7 +80,9 @@ describe('Card.vue', () => {
bodyStyle: style,
},
})
expect(wrapper.find('.el-card__body').attributes('style').replace(/[ ]/g, '')).toBe('font-size:14px;color:blue;')
expect(
wrapper.find('.el-card__body').attributes('style').replace(/[ ]/g, '')
).toBe('font-size:14px;color:blue;')
})
test('shadow', () => {

View File

@ -1,5 +1,8 @@
<template>
<div class="el-card" :class="shadow ? 'is-' + shadow + '-shadow' : 'is-always-shadow'">
<div
class="el-card"
:class="shadow ? 'is-' + shadow + '-shadow' : 'is-always-shadow'"
>
<div v-if="$slots.header || header" class="el-card__header">
<slot name="header">{{ header }}</slot>
</div>
@ -8,7 +11,7 @@
</div>
</div>
</template>
<script lang='ts'>
<script lang="ts">
import { defineComponent } from 'vue'
import type { PropType } from 'vue'
import type { StyleValue } from '@element-plus/utils/types'

View File

@ -4,7 +4,7 @@ import Carousel from '../src/main.vue'
import CarouselItem from '../src/item.vue'
const wait = (ms = 100) =>
new Promise(resolve => setTimeout(() => resolve(0), ms))
new Promise((resolve) => setTimeout(() => resolve(0), ms))
const _mount = (template: string, data?: () => void, methods?: any) =>
mount({
@ -26,14 +26,14 @@ describe('Carousel', () => {
<el-carousel-item v-for="item in 3" :key="item"></el-carousel-item>
</el-carousel>
</div>
`,
`
)
expect(wrapper.vm.$refs.carousel.direction).toBe('horizontal')
expect(wrapper.findAll('.el-carousel__item').length).toEqual(3)
})
it('auto play', async done => {
it('auto play', async (done) => {
const wrapper = _mount(`
<div>
<el-carousel :interval="50">
@ -51,7 +51,7 @@ describe('Carousel', () => {
done()
})
it('initial index', async done => {
it('initial index', async (done) => {
const wrapper = _mount(`
<div>
<el-carousel :autoplay="false" :initial-index="1">
@ -66,12 +66,12 @@ describe('Carousel', () => {
expect(
wrapper.vm.$el
.querySelectorAll('.el-carousel__item')[1]
.classList.contains('is-active'),
.classList.contains('is-active')
).toBeTruthy()
done()
})
it('reset timer', async done => {
it('reset timer', async (done) => {
const wrapper = _mount(`
<div>
<el-carousel :interval="500">
@ -91,7 +91,7 @@ describe('Carousel', () => {
done()
})
it('change', async done => {
it('change', async (done) => {
const wrapper = _mount(
`
<div>
@ -111,7 +111,7 @@ describe('Carousel', () => {
this.val = val
this.oldVal = oldVal
},
},
}
)
await nextTick()
@ -121,7 +121,7 @@ describe('Carousel', () => {
done()
})
it('label', async done => {
it('label', async (done) => {
const wrapper = _mount(`
<div>
<el-carousel>
@ -135,7 +135,7 @@ describe('Carousel', () => {
})
describe('manual control', () => {
it('hover', async done => {
it('hover', async (done) => {
const wrapper = _mount(`
<div>
<el-carousel :autoplay="false">
@ -152,13 +152,13 @@ describe('Carousel', () => {
expect(
wrapper.vm.$el
.querySelectorAll('.el-carousel__item')[1]
.classList.contains('is-active'),
.classList.contains('is-active')
).toBeTruthy()
done()
})
})
it('card', async done => {
it('card', async (done) => {
const wrapper = _mount(`
<div>
<el-carousel :autoplay="false" type="card">
@ -198,7 +198,7 @@ describe('Carousel', () => {
expect(items[0].style.transform.indexOf('translateY') !== -1).toBeTruthy()
})
it('pause auto play on hover', async done => {
it('pause auto play on hover', async (done) => {
const wrapper = _mount(`
<div>
<el-carousel :interval="50" :pause-on-hover="false">

View File

@ -31,7 +31,8 @@ export interface ICarouselProps {
pauseOnHover: boolean
}
export type UnionCarouselItemData = ICarouselItemProps & ToRefs<ICarouselItemData>
export type UnionCarouselItemData = ICarouselItemProps &
ToRefs<ICarouselItemData>
export interface CarouselItem extends UnionCarouselItemData {
uid: number

View File

@ -31,9 +31,7 @@ import {
getCurrentInstance,
onUnmounted,
} from 'vue'
import {
autoprefixer,
} from '@element-plus/utils/util'
import { autoprefixer } from '@element-plus/utils/util'
import type { CSSProperties } from 'vue'
import type { InjectCarouselScope, ICarouselItemProps } from './carousel'
@ -65,7 +63,7 @@ export default defineComponent({
// inject
const injectCarouselScope: InjectCarouselScope = inject(
'injectCarouselScope',
'injectCarouselScope'
)
// computed
@ -112,14 +110,17 @@ export default defineComponent({
}
function calcTranslate(index, activeIndex, isVertical) {
const distance = (isVertical ? injectCarouselScope.root.value?.offsetHeight : injectCarouselScope.root.value?.offsetWidth) || 0
const distance =
(isVertical
? injectCarouselScope.root.value?.offsetHeight
: injectCarouselScope.root.value?.offsetWidth) || 0
return distance * (index - activeIndex)
}
const translateItem = (
index: number,
activeIndex: number,
oldIndex: number,
oldIndex: number
) => {
const parentType = injectCarouselScope.type
const length = injectCarouselScope.items.value.length
@ -132,7 +133,7 @@ export default defineComponent({
if (parentType === 'card') {
if (parentDirection.value === 'vertical') {
console.warn(
'[Element Warn][Carousel]vertical direction is not supported in card mode',
'[Element Warn][Carousel]vertical direction is not supported in card mode'
)
}
data.inStage = Math.round(Math.abs(index - activeIndex)) <= 1
@ -150,7 +151,7 @@ export default defineComponent({
function handleItemClick() {
if (injectCarouselScope && injectCarouselScope.type === 'card') {
const index = injectCarouselScope.items.value
.map(d => d.uid)
.map((d) => d.uid)
.indexOf(instance.uid)
injectCarouselScope.setActiveItem(index)
}

View File

@ -76,7 +76,11 @@ import {
removeResizeListener,
} from '@element-plus/utils/resize-event'
import type { ICarouselProps, CarouselItem, InjectCarouselScope } from './carousel'
import type {
ICarouselProps,
CarouselItem,
InjectCarouselScope,
} from './carousel'
export default defineComponent({
name: 'ElCarousel',
@ -145,11 +149,11 @@ export default defineComponent({
// computed
const arrowDisplay = computed(
() => props.arrow !== 'never' && props.direction !== 'vertical',
() => props.arrow !== 'never' && props.direction !== 'vertical'
)
const hasLabel = computed(() => {
return items.value.some(item => item.label.toString().length > 0)
return items.value.some((item) => item.label.toString().length > 0)
})
const carouselClasses = computed(() => {
@ -176,14 +180,14 @@ export default defineComponent({
// methods
const throttledArrowClick = throttle(
index => {
(index) => {
setActiveItem(index)
},
300,
{ trailing: true },
{ trailing: true }
)
const throttledIndicatorHover = throttle(index => {
const throttledIndicatorHover = throttle((index) => {
handleIndicatorHover(index)
}, 300)
@ -209,7 +213,7 @@ export default defineComponent({
function setActiveItem(index) {
if (typeof index === 'string') {
const filteredItems = items.value.filter(item => item.name === index)
const filteredItems = items.value.filter((item) => item.name === index)
if (filteredItems.length > 0) {
index = items.value.indexOf(filteredItems[0])
}
@ -219,7 +223,7 @@ export default defineComponent({
console.warn('[Element Warn][Carousel]index must be an integer.')
return
}
let length = items.value.length
const length = items.value.length
const oldIndex = data.activeIndex
if (index < 0) {
data.activeIndex = props.loop ? length - 1 : 0
@ -244,7 +248,7 @@ export default defineComponent({
}
function removeItem(uid) {
const index = items.value.findIndex(item => item.uid === uid)
const index = items.value.findIndex((item) => item.uid === uid)
if (index !== -1) {
items.value.splice(index, 1)
if (data.activeIndex === index) next()
@ -294,7 +298,7 @@ export default defineComponent({
function handleButtonLeave() {
if (props.direction === 'vertical') return
items.value.forEach(item => {
items.value.forEach((item) => {
item.hover = false
})
}
@ -325,19 +329,19 @@ export default defineComponent({
if (prev > -1) {
emit('change', current, prev)
}
},
}
)
watch(
() => props.autoplay,
current => {
(current) => {
current ? startTimer() : pauseTimer()
},
}
)
watch(
() => props.loop,
() => {
setActiveItem(data.activeIndex)
},
}
)
// lifecycle

View File

@ -97,7 +97,8 @@ const RADIO = '.el-radio__input'
let id = 0
const _mount: typeof mount = options => mount({
const _mount: typeof mount = (options) =>
mount({
components: {
CascaderPanel,
},
@ -107,8 +108,7 @@ const _mount: typeof mount = options => mount({
const lazyLoad = (node, resolve) => {
const { level } = node
setTimeout(() => {
const nodes = Array.from({ length: level + 1 })
.map(() => ({
const nodes = Array.from({ length: level + 1 }).map(() => ({
value: ++id,
label: `option${id}`,
leaf: level >= 1,
@ -380,7 +380,10 @@ describe('CascaderPanel.vue', () => {
await nbCheckbox.find('input').trigger('click')
expect(zjCheckbox.classes('is-checked')).toBe(true)
expect(wrapper.vm.value).toEqual([['zhejiang', 'hangzhou'], ['zhejiang', 'ningbo']])
expect(wrapper.vm.value).toEqual([
['zhejiang', 'hangzhou'],
['zhejiang', 'ningbo'],
])
await zjCheckbox.find('input').trigger('click')
expect(zjCheckbox.classes('is-checked')).toBe(false)
@ -586,8 +589,7 @@ describe('CascaderPanel.vue', () => {
lazyLoad(node, resolve) {
const { level } = node
setTimeout(() => {
const nodes = Array.from({ length: level + 1 })
.map(() => ({
const nodes = Array.from({ length: level + 1 }).map(() => ({
value: { id: ++id },
label: `option${id}`,
leaf: level >= 1,
@ -622,8 +624,7 @@ describe('CascaderPanel.vue', () => {
lazyLoad(node, resolve) {
const { level } = node
setTimeout(() => {
const nodes = Array.from({ length: level + 1 })
.map(() => {
const nodes = Array.from({ length: level + 1 }).map(() => {
++id
return {
value: id,
@ -653,9 +654,13 @@ describe('CascaderPanel.vue', () => {
expect(firstMenu.find(CHECKBOX).classes('is-checked')).toBe(false)
expect(firstMenu.find(CHECKBOX).classes('is-indeterminate')).toBe(true)
expect(secondMenu.findAll(CHECKBOX)[0].classes('is-checked')).toBe(false)
expect(secondMenu.findAll(CHECKBOX)[0].classes('is-indeterminate')).toBe(false)
expect(secondMenu.findAll(CHECKBOX)[0].classes('is-indeterminate')).toBe(
false
)
expect(secondMenu.findAll(CHECKBOX)[1].classes('is-checked')).toBe(true)
expect(secondMenu.findAll(CHECKBOX)[1].classes('is-indeterminate')).toBe(false)
expect(secondMenu.findAll(CHECKBOX)[1].classes('is-indeterminate')).toBe(
false
)
})
test('getCheckedNodes and clearCheckedNodes', () => {

View File

@ -2,7 +2,6 @@ import { App } from 'vue'
import type { SFCWithInstall } from '@element-plus/utils/types'
import CascaderPanel from './src/index.vue'
CascaderPanel.install = (app: App): void => {
app.component(CascaderPanel.name, CascaderPanel)
}

View File

@ -14,7 +14,7 @@ export const CommonProps = {
modelValue: [Number, String, Array] as PropType<CascaderValue>,
options: {
type: Array as PropType<CascaderOption[]>,
default: () => ([] as CascaderOption[]),
default: () => [] as CascaderOption[],
},
props: {
type: Object as PropType<CascaderProps>,
@ -37,7 +37,7 @@ export const DefaultProps: CascaderConfig = {
hoverThreshold: 500,
}
export const useCascaderConfig = (props: { props: CascaderProps; }) => {
export const useCascaderConfig = (props: { props: CascaderProps }) => {
return computed(() => ({
...DefaultProps,
...props.props,

View File

@ -1,27 +1,30 @@
<template>
<div
:class="[
'el-cascader-panel',
border && 'is-bordered'
]"
:class="['el-cascader-panel', border && 'is-bordered']"
@keydown="handleKeyDown"
>
<el-cascader-menu
v-for="(menu, index) in menus"
:key="index"
:ref="item => menuList[index] = item"
:ref="(item) => (menuList[index] = item)"
:index="index"
:nodes="menu"
/>
</div>
</template>
<script lang='ts'>
<script lang="ts">
import {
computed, defineComponent, nextTick,
onBeforeUpdate, onMounted,
provide, reactive,
Ref, ref, watch,
computed,
defineComponent,
nextTick,
onBeforeUpdate,
onMounted,
provide,
reactive,
Ref,
ref,
watch,
} from 'vue'
import isEqual from 'lodash/isEqual'
import { EVENT_CODE } from '@element-plus/utils/aria'
@ -76,12 +79,7 @@ export default defineComponent({
renderLabel: Function as PropType<RenderLabel>,
},
emits: [
UPDATE_MODEL_EVENT,
CHANGE_EVENT,
'close',
'expand-change',
],
emits: [UPDATE_MODEL_EVENT, CHANGE_EVENT, 'close', 'expand-change'],
setup(props, { emit, slots }) {
let initialLoaded = true
@ -97,7 +95,9 @@ export default defineComponent({
const expandingNode: Ref<Nullable<CascaderNode>> = ref(null)
const checkedNodes: Ref<CascaderNode[]> = ref([])
const isHoverMenu = computed(() => config.value.expandTrigger === ExpandTrigger.HOVER)
const isHoverMenu = computed(
() => config.value.expandTrigger === ExpandTrigger.HOVER
)
const renderLabelFn = computed(() => props.renderLabel || slots.default)
const initStore = () => {
@ -155,7 +155,11 @@ export default defineComponent({
}
}
const handleCheckChange: ElCascaderPanelContext['handleCheckChange'] = (node, checked, emitClose = true) => {
const handleCheckChange: ElCascaderPanelContext['handleCheckChange'] = (
node,
checked,
emitClose = true
) => {
const { checkStrictly, multiple } = config.value
const oldNode = checkedNodes.value[0]
manualChecked = true
@ -171,12 +175,11 @@ export default defineComponent({
}
const getCheckedNodes = (leafOnly: boolean) => {
return getFlattedNodes(leafOnly)
.filter(node => node.checked !== false)
return getFlattedNodes(leafOnly).filter((node) => node.checked !== false)
}
const clearCheckedNodes = () => {
checkedNodes.value.forEach(node => node.doCheck(false))
checkedNodes.value.forEach((node) => node.doCheck(false))
calculateCheckedValue()
}
@ -186,9 +189,9 @@ export default defineComponent({
const newNodes = getCheckedNodes(!checkStrictly)
// ensure the original order
const nodes = sortByOriginalOrder(oldNodes, newNodes)
const values = nodes.map(node => node.valueByOption)
const values = nodes.map((node) => node.valueByOption)
checkedNodes.value = nodes
checkedValue.value = multiple ? values : (values[0] ?? null)
checkedValue.value = multiple ? values : values[0] ?? null
}
const syncCheckedValue = (loaded = false, forced = false) => {
@ -199,44 +202,58 @@ export default defineComponent({
if (
!initialLoaded ||
manualChecked ||
!forced && isEqual(modelValue, checkedValue.value)
) return
(!forced && isEqual(modelValue, checkedValue.value))
)
return
if (lazy && !loaded) {
const values: CascaderNodeValue[] = deduplicate(arrayFlat(coerceTruthyValueToArray(modelValue)))
const nodes = values.map(val => store.value.getNodeByValue(val))
.filter(node => !!node && !node.loaded && !node.loading)
const values: CascaderNodeValue[] = deduplicate(
arrayFlat(coerceTruthyValueToArray(modelValue))
)
const nodes = values
.map((val) => store.value.getNodeByValue(val))
.filter((node) => !!node && !node.loaded && !node.loading)
if (nodes.length) {
nodes.forEach(node => {
nodes.forEach((node) => {
lazyLoad(node, () => syncCheckedValue(false, forced))
})
} else {
syncCheckedValue(true, forced)
}
} else {
const values = multiple ? coerceTruthyValueToArray(modelValue) : [modelValue]
const nodes = deduplicate(values.map(val => store.value.getNodeByValue(val, leafOnly)))
const values = multiple
? coerceTruthyValueToArray(modelValue)
: [modelValue]
const nodes = deduplicate(
values.map((val) => store.value.getNodeByValue(val, leafOnly))
)
syncMenuState(nodes, false)
checkedValue.value = modelValue
}
}
const syncMenuState = (newCheckedNodes: CascaderNode[], reserveExpandingState = true) => {
const syncMenuState = (
newCheckedNodes: CascaderNode[],
reserveExpandingState = true
) => {
const { checkStrictly } = config.value
const oldNodes = checkedNodes.value
const newNodes = newCheckedNodes.filter(node => !!node && (checkStrictly || node.isLeaf))
const newNodes = newCheckedNodes.filter(
(node) => !!node && (checkStrictly || node.isLeaf)
)
const oldExpandingNode = store.value.getSameNode(expandingNode.value)
const newExpandingNode = reserveExpandingState && oldExpandingNode || newNodes[0]
const newExpandingNode =
(reserveExpandingState && oldExpandingNode) || newNodes[0]
if (newExpandingNode) {
newExpandingNode.pathNodes.forEach(node => expandNode(node, true))
newExpandingNode.pathNodes.forEach((node) => expandNode(node, true))
} else {
expandingNode.value = null
}
oldNodes.forEach(node => node.doCheck(false))
newNodes.forEach(node => node.doCheck(true))
oldNodes.forEach((node) => node.doCheck(false))
newNodes.forEach((node) => node.doCheck(true))
checkedNodes.value = newNodes
nextTick(scrollToExpandingNode)
@ -245,11 +262,12 @@ export default defineComponent({
const scrollToExpandingNode = () => {
if (isServer) return
menuList.value.forEach(menu => {
menuList.value.forEach((menu) => {
const menuElement = menu?.$el
if (menuElement) {
const container = menuElement.querySelector('.el-scrollbar__wrap')
const activeNode = menuElement.querySelector('.el-cascader-node.is-active') ||
const activeNode =
menuElement.querySelector('.el-cascader-node.is-active') ||
menuElement.querySelector('.el-cascader-node.in-active-path')
scrollIntoView(container, activeNode)
}
@ -268,12 +286,16 @@ export default defineComponent({
break
case EVENT_CODE.left:
const preMenu = menuList.value[getMenuIndex(target) - 1]
const expandedNode = preMenu?.$el.querySelector('.el-cascader-node[aria-expanded="true"]')
const expandedNode = preMenu?.$el.querySelector(
'.el-cascader-node[aria-expanded="true"]'
)
focusNode(expandedNode)
break
case EVENT_CODE.right:
const nextMenu = menuList.value[getMenuIndex(target) + 1]
const firstNode = nextMenu?.$el.querySelector('.el-cascader-node[tabindex="-1"]')
const firstNode = nextMenu?.$el.querySelector(
'.el-cascader-node[tabindex="-1"]'
)
focusNode(firstNode)
break
case EVENT_CODE.enter:
@ -286,7 +308,9 @@ export default defineComponent({
}
}
provide(CASCADER_PANEL_INJECTION_KEY, reactive({
provide(
CASCADER_PANEL_INJECTION_KEY,
reactive({
config,
expandingNode,
checkedNodes,
@ -295,27 +319,30 @@ export default defineComponent({
lazyLoad,
expandNode,
handleCheckChange,
}))
watch(
[config, () => props.options],
initStore,
{ deep: true, immediate: true },
})
)
watch(() => props.modelValue, () => {
manualChecked = false
syncCheckedValue()
watch([config, () => props.options], initStore, {
deep: true,
immediate: true,
})
watch(checkedValue, val => {
watch(
() => props.modelValue,
() => {
manualChecked = false
syncCheckedValue()
}
)
watch(checkedValue, (val) => {
if (!isEqual(val, props.modelValue)) {
emit(UPDATE_MODEL_EVENT, val)
emit(CHANGE_EVENT, val)
}
})
onBeforeUpdate(() => menuList.value = [])
onBeforeUpdate(() => (menuList.value = []))
onMounted(() => !isEmpty(props.modelValue) && syncCheckedValue())

View File

@ -5,10 +5,7 @@
role="menu"
class="el-cascader-menu"
wrap-class="el-cascader-menu__wrap"
:view-class="[
'el-cascader-menu__list',
isEmpty && 'is-empty'
]"
:view-class="['el-cascader-menu__list', isEmpty && 'is-empty']"
@mousemove="handleMouseMove"
@mouseleave="clearHoverZone"
>
@ -19,10 +16,7 @@
:menu-id="menuId"
@expand="handleExpand"
/>
<div
v-if="isEmpty"
class="el-cascader-menu__empty-text"
>
<div v-if="isEmpty" class="el-cascader-menu__empty-text">
{{ t('el.cascader.noData') }}
</div>
<svg
@ -34,18 +28,13 @@
</template>
<script lang="ts">
import {
computed, defineComponent, getCurrentInstance,
inject, ref,
} from 'vue'
import { computed, defineComponent, getCurrentInstance, inject, ref } from 'vue'
import ElScrollbar from '@element-plus/components/scrollbar'
import { useLocaleInject } from '@element-plus/hooks'
import { generateId } from '@element-plus/utils/util'
import ElCascaderNode from './node.vue'
import { default as CascaderNode } from './node'
import {
CASCADER_PANEL_INJECTION_KEY,
} from './types'
import { CASCADER_PANEL_INJECTION_KEY } from './types'
import type { PropType } from 'vue'
import type { TimeoutHandle, Nullable } from '@element-plus/utils/types'
@ -105,7 +94,10 @@ export default defineComponent({
<path style="pointer-events: auto;" fill="transparent" d="M${startX} ${bottom} L${offsetWidth} ${offsetHeight} V${bottom} Z" />
`
} else if (!hoverTimer) {
hoverTimer = window.setTimeout(clearHoverZone, panel.config.hoverThreshold)
hoverTimer = window.setTimeout(
clearHoverZone,
panel.config.hoverThreshold
)
}
}
@ -133,6 +125,4 @@ export default defineComponent({
}
},
})
</script>

View File

@ -9,7 +9,7 @@ export default defineComponent({
return h(
'span',
{ class: 'el-cascader-node__label' },
renderLabelFn ? renderLabelFn({ node, data }) : label,
renderLabelFn ? renderLabelFn({ node, data }) : label
)
},
})

View File

@ -1,21 +1,26 @@
import { isFunction } from '@vue/shared'
import { capitalize, isUndefined, isEmpty } from '@element-plus/utils/util'
import type { VNode } from 'vue'
export type CascaderNodeValue = string | number
export type CascaderNodePathValue = CascaderNodeValue[]
export type CascaderValue = CascaderNodeValue | CascaderNodePathValue | (CascaderNodeValue | CascaderNodePathValue)[]
export type CascaderValue =
| CascaderNodeValue
| CascaderNodePathValue
| (CascaderNodeValue | CascaderNodePathValue)[]
export type CascaderConfig = Required<CascaderProps>
export enum ExpandTrigger {
CLICK = 'click',
HOVER = 'hover'
HOVER = 'hover',
}
export type isDisabled = (data: CascaderOption, node: Node) => boolean
export type isLeaf = (data: CascaderOption, node: Node) => boolean
export type Resolve = (dataList?: CascaderOption[]) => void
export type LazyLoad = (node: Node, resolve: Resolve) => void
export type RenderLabel = ({ node: Node, data: CascaderOption }) => VNode | VNode[]
export type RenderLabel = ({
node: Node,
data: CascaderOption,
}) => VNode | VNode[]
export interface CascaderOption extends Record<string, unknown> {
label?: string
value?: CascaderNodeValue
@ -78,7 +83,7 @@ class Node {
readonly data: Nullable<CascaderOption>,
readonly config: CascaderConfig,
readonly parent?: Node,
readonly root = false,
readonly root = false
) {
const { value: valueKey, label: labelKey, children: childrenKey } = config
@ -89,18 +94,22 @@ class Node {
this.value = data[valueKey] as CascaderNodeValue
this.label = data[labelKey] as string
this.pathNodes = pathNodes
this.pathValues = pathNodes.map(node => node.value)
this.pathLabels = pathNodes.map(node => node.label)
this.pathValues = pathNodes.map((node) => node.value)
this.pathLabels = pathNodes.map((node) => node.label)
this.childrenData = childrenData
this.children = (childrenData || []).map(child => new Node(child, config, this))
this.children = (childrenData || []).map(
(child) => new Node(child, config, this)
)
this.loaded = !config.lazy || this.isLeaf || !isEmpty(childrenData)
}
get isDisabled(): boolean {
const { data, parent, config } = this
const { disabled, checkStrictly } = config
const isDisabled = isFunction(disabled) ? disabled(data, this) : !!data[disabled]
return isDisabled || !checkStrictly && parent?.isDisabled
const isDisabled = isFunction(disabled)
? disabled(data, this)
: !!data[disabled]
return isDisabled || (!checkStrictly && parent?.isDisabled)
}
get isLeaf(): boolean {
@ -109,7 +118,9 @@ class Node {
const isLeaf = isFunction(leaf) ? leaf(data, this) : data[leaf]
return isUndefined(isLeaf)
? lazy && !loaded ? false : !Array.isArray(childrenData)
? lazy && !loaded
? false
: !Array.isArray(childrenData)
: !!isLeaf
}
@ -140,7 +151,7 @@ class Node {
broadcast(event: string, ...args: unknown[]) {
const handlerName = `onParent${capitalize(event)}`
this.children.forEach(child => {
this.children.forEach((child) => {
if (child) {
// bottom up
child.broadcast(event, ...args)
@ -166,9 +177,9 @@ class Node {
onChildCheck() {
const { children } = this
const validChildren = children.filter(child => !child.isDisabled)
const validChildren = children.filter((child) => !child.isDisabled)
const checked = validChildren.length
? validChildren.every(child => child.checked)
? validChildren.every((child) => child.checked)
: false
this.setCheckState(checked)
@ -177,12 +188,16 @@ class Node {
setCheckState(checked: boolean) {
const totalNum = this.children.length
const checkedNum = this.children.reduce((c, p) => {
const num = p.checked ? 1 : (p.indeterminate ? 0.5 : 0)
const num = p.checked ? 1 : p.indeterminate ? 0.5 : 0
return c + num
}, 0)
this.checked = this.loaded && this.children.every(child => child.loaded && child.checked) && checked
this.indeterminate = this.loaded && checkedNum !== totalNum && checkedNum > 0
this.checked =
this.loaded &&
this.children.every((child) => child.loaded && child.checked) &&
checked
this.indeterminate =
this.loaded && checkedNum !== totalNum && checkedNum > 0
}
doCheck(checked: boolean) {

View File

@ -12,7 +12,7 @@
inExpandingPath && 'in-active-path',
inCheckedPath && 'in-checked-path',
node.checked && 'is-active',
!expandable && 'is-disabled'
!expandable && 'is-disabled',
]"
@mouseenter="handleHoverExpand"
@focus="handleHoverExpand"
@ -41,14 +41,20 @@
-->
<span></span>
</el-radio>
<i v-else-if="isLeaf && node.checked" class="el-icon-check el-cascader-node__prefix"></i>
<i
v-else-if="isLeaf && node.checked"
class="el-icon-check el-cascader-node__prefix"
></i>
<!-- content -->
<node-content />
<!-- postfix -->
<template v-if="!isLeaf">
<i v-if="node.loading" class="el-icon-loading el-cascader-node__postfix"></i>
<i
v-if="node.loading"
class="el-icon-loading el-cascader-node__postfix"
></i>
<i v-else class="el-icon-arrow-right el-cascader-node__postfix"></i>
</template>
</li>
@ -60,9 +66,7 @@ import ElCheckbox from '@element-plus/components/checkbox'
import ElRadio from '@element-plus/components/radio'
import NodeContent from './node-content'
import type { default as CascaderNode } from './node'
import {
CASCADER_PANEL_INJECTION_KEY,
} from './types'
import { CASCADER_PANEL_INJECTION_KEY } from './types'
import type { PropType } from 'vue'
@ -94,10 +98,14 @@ export default defineComponent({
const checkedNodeId = computed(() => panel.checkedNodes[0]?.uid)
const isDisabled = computed(() => props.node.isDisabled)
const isLeaf = computed(() => props.node.isLeaf)
const expandable = computed(() => checkStrictly.value && !isLeaf.value || !isDisabled.value)
const expandable = computed(
() => (checkStrictly.value && !isLeaf.value) || !isDisabled.value
)
const inExpandingPath = computed(() => isInPath(panel.expandingNode))
// only useful in check-strictly mode
const inCheckedPath = computed(() => checkStrictly.value && panel.checkedNodes.some(isInPath))
const inCheckedPath = computed(
() => checkStrictly.value && panel.checkedNodes.some(isInPath)
)
const isInPath = (node: CascaderNode) => {
const { level, uid } = props.node
@ -137,7 +145,12 @@ export default defineComponent({
const handleClick = () => {
if (isHoverMenu.value && !isLeaf.value) return
if (isLeaf.value && !isDisabled.value && !checkStrictly.value && !multiple.value) {
if (
isLeaf.value &&
!isDisabled.value &&
!checkStrictly.value &&
!multiple.value
) {
handleCheck(true)
} else {
handleExpand()

View File

@ -9,7 +9,6 @@ import type {
CascaderConfig,
} from './node'
const flatNodes = (nodes: Node[], leafOnly: boolean) => {
return nodes.reduce((res, node) => {
if (node.isLeaf) {
@ -28,7 +27,9 @@ export default class Store {
readonly leafNodes: Node[]
constructor(data: CascaderOption[], readonly config: CascaderConfig) {
const nodes = (data || []).map(nodeData => new Node(nodeData, this.config))
const nodes = (data || []).map(
(nodeData) => new Node(nodeData, this.config)
)
this.nodes = nodes
this.allNodes = flatNodes(nodes, false)
this.leafNodes = flatNodes(nodes, true)
@ -54,15 +55,19 @@ export default class Store {
}
appendNodes(nodeDataList: CascaderOption[], parentNode: Node) {
nodeDataList.forEach(nodeData => this.appendNode(nodeData, parentNode))
nodeDataList.forEach((nodeData) => this.appendNode(nodeData, parentNode))
}
// when checkStrictly, leaf node first
getNodeByValue (value: CascaderNodeValue | CascaderNodePathValue, leafOnly = false): Nullable<Node> {
getNodeByValue(
value: CascaderNodeValue | CascaderNodePathValue,
leafOnly = false
): Nullable<Node> {
if (!value && value !== 0) return null
const nodes = this.getFlattedNodes(leafOnly)
.filter(node => isEqual(node.value, value) || isEqual(node.pathValues, value))
const nodes = this.getFlattedNodes(leafOnly).filter(
(node) => isEqual(node.value, value) || isEqual(node.pathValues, value)
)
return nodes[0] || null
}
@ -70,10 +75,10 @@ export default class Store {
getSameNode(node: Node): Nullable<Node> {
if (!node) return null
const nodes = this.getFlattedNodes(false)
.filter(({ value, level }) => isEqual(node.value, value) && node.level === level)
const nodes = this.getFlattedNodes(false).filter(
({ value, level }) => isEqual(node.value, value) && node.level === level
)
return nodes[0] || null
}
}

View File

@ -1,4 +1,3 @@
import type { VNode, InjectionKey } from 'vue'
import type { Nullable } from '@element-plus/utils/types'
import type { default as CascaderNode } from './node'
@ -7,17 +6,23 @@ export type { CascaderNode }
export type CascaderNodeValue = string | number
export type CascaderNodePathValue = CascaderNodeValue[]
export type CascaderValue = CascaderNodeValue | CascaderNodePathValue | (CascaderNodeValue | CascaderNodePathValue)[]
export type CascaderValue =
| CascaderNodeValue
| CascaderNodePathValue
| (CascaderNodeValue | CascaderNodePathValue)[]
export type CascaderConfig = Required<CascaderProps>
export type isDisabled = (data: CascaderOption, node: CascaderNode) => boolean
export type isLeaf = (data: CascaderOption, node: CascaderNode) => boolean
export type Resolve = (dataList?: CascaderOption[]) => void
export type LazyLoad = (node: CascaderNode, resolve: Resolve) => void
export type RenderLabel = ({ node: CascaderNode, data: CascaderOption }) => VNode | VNode[]
export type RenderLabel = ({
node: CascaderNode,
data: CascaderOption,
}) => VNode | VNode[]
export enum ExpandTrigger {
CLICK = 'click',
HOVER = 'hover'
HOVER = 'hover',
}
export interface CascaderOption extends Record<string, unknown> {
@ -57,9 +62,17 @@ export interface ElCascaderPanelContext {
checkedNodes: CascaderNode[]
isHoverMenu: boolean
renderLabelFn: RenderLabel
lazyLoad: (node?: CascaderNode, cb?: (dataList: CascaderOption[]) => void) => void
lazyLoad: (
node?: CascaderNode,
cb?: (dataList: CascaderOption[]) => void
) => void
expandNode: (node: CascaderNode, silent?: boolean) => void
handleCheckChange: (node: CascaderNode, checked: boolean, emitClose?: boolean) => void
handleCheckChange: (
node: CascaderNode,
checked: boolean,
emitClose?: boolean
) => void
}
export const CASCADER_PANEL_INJECTION_KEY: InjectionKey<ElCascaderPanelContext> = Symbol()
export const CASCADER_PANEL_INJECTION_KEY: InjectionKey<ElCascaderPanelContext> =
Symbol()

View File

@ -1,15 +1,19 @@
import type { Nullable } from '@element-plus/utils/types'
import type { default as CascaderNode } from './node'
export const isLeaf = (el: HTMLElement) => !el.getAttribute('aria-owns')
export const getSibling = (el: HTMLElement, distance: number): Nullable<Element> => {
export const getSibling = (
el: HTMLElement,
distance: number
): Nullable<Element> => {
const { parentNode } = el
if (!parentNode) return null
const siblings = parentNode.querySelectorAll('.el-cascader-node[tabindex="-1"]')
const siblings = parentNode.querySelectorAll(
'.el-cascader-node[tabindex="-1"]'
)
const index = Array.prototype.indexOf.call(siblings, el)
return siblings[index + distance] || null
}
@ -20,13 +24,13 @@ export const getMenuIndex = (el: HTMLElement) => {
return Number(pieces[pieces.length - 2])
}
export const focusNode = el => {
export const focusNode = (el) => {
if (!el) return
el.focus()
!isLeaf(el) && el.click()
}
export const checkNode = el => {
export const checkNode = (el) => {
if (!el) return
const input = el.querySelector('input')
@ -37,9 +41,12 @@ export const checkNode = el => {
}
}
export const sortByOriginalOrder = (oldNodes: CascaderNode[], newNodes: CascaderNode[]) => {
export const sortByOriginalOrder = (
oldNodes: CascaderNode[],
newNodes: CascaderNode[]
) => {
const newNodesCopy = newNodes.slice(0)
const newIds = newNodesCopy.map(node => node.uid)
const newIds = newNodesCopy.map((node) => node.uid)
const res = oldNodes.reduce((acc, item) => {
const index = newIds.indexOf(item.uid)
if (index > -1) {

View File

@ -30,14 +30,18 @@ const TAG = '.el-tag'
const SUGGESTION_ITEM = '.el-cascader__suggestion-item'
const CHECK_ICON = '.el-icon-check'
const _mount: typeof mount = options => mount({
const _mount: typeof mount = (options) =>
mount(
{
components: {
Cascader,
},
...options,
}, {
},
{
attachTo: 'body',
})
}
)
afterEach(() => {
document.body.innerHTML = ''
@ -198,7 +202,10 @@ describe('Cascader.vue', () => {
return {
options: OPTIONS,
props: { multiple: true },
value: [['zhejiang', 'hangzhou'], ['zhejiang', 'ningbo']],
value: [
['zhejiang', 'hangzhou'],
['zhejiang', 'ningbo'],
],
}
},
})
@ -219,7 +226,10 @@ describe('Cascader.vue', () => {
options: OPTIONS,
props: { multiple: true },
collapseTags: true,
modelValue: [['zhejiang', 'hangzhou'], ['zhejiang', 'ningbo']],
modelValue: [
['zhejiang', 'hangzhou'],
['zhejiang', 'ningbo'],
],
},
})
await nextTick()
@ -249,7 +259,9 @@ describe('Cascader.vue', () => {
const dropdown = document.querySelector(DROPDOWN)
input.element.value = 'Ha'
await input.trigger('input')
const suggestions = dropdown.querySelectorAll(SUGGESTION_ITEM) as NodeListOf<HTMLElement>
const suggestions = dropdown.querySelectorAll(
SUGGESTION_ITEM
) as NodeListOf<HTMLElement>
const hzSuggestion = suggestions[0]
expect(suggestions.length).toBe(1)
expect(hzSuggestion.textContent).toBe('Zhejiang / Hangzhou')

View File

@ -10,4 +10,3 @@ const _Cascader = Cascader as SFCWithInstall<typeof Cascader>
export default _Cascader
export const ElCascader = _Cascader

View File

@ -21,7 +21,7 @@
:class="[
'el-cascader',
realSize && `el-cascader--${realSize}`,
{ 'is-disabled': isDisabled }
{ 'is-disabled': isDisabled },
]"
@click="() => togglePopperVisible(readonly ? undefined : true)"
@keydown="handleKeyDown"
@ -37,8 +37,8 @@
:validate-event="false"
:size="realSize"
:class="{ 'is-focus': popperVisible }"
@focus="e => $emit('focus', e)"
@blur="e => $emit('blur', e)"
@focus="(e) => $emit('focus', e)"
@blur="(e) => $emit('blur', e)"
@input="handleInput"
>
<template #suffix>
@ -54,7 +54,7 @@
:class="[
'el-input__icon',
'el-icon-arrow-down',
popperVisible && 'is-reverse'
popperVisible && 'is-reverse',
]"
@click.stop="togglePopperVisible()"
></i>
@ -80,10 +80,10 @@
type="text"
class="el-cascader__search-input"
:placeholder="presentText ? '' : inputPlaceholder"
@input="e => handleInput(searchInputValue, e)"
@input="(e) => handleInput(searchInputValue, e)"
@click.stop="togglePopperVisible(true)"
@keydown.delete="handleDelete"
>
/>
</div>
</div>
</template>
@ -114,7 +114,7 @@
:key="item.uid"
:class="[
'el-cascader__suggestion-item',
item.checked && 'is-checked'
item.checked && 'is-checked',
]"
:tabindex="-1"
@click="handleSuggestionClick(item)"
@ -124,24 +124,33 @@
</li>
</template>
<slot v-else name="empty">
<li class="el-cascader__empty-text">{{ t('el.cascader.noMatch') }}</li>
<li class="el-cascader__empty-text">
{{ t('el.cascader.noMatch') }}
</li>
</slot>
</el-scrollbar>
</template>
</el-popper>
</template>
<script lang='ts'>
<script lang="ts">
import {
computed, defineComponent,
inject, nextTick,
onMounted, onBeforeUnmount,
Ref, ref, watch,
computed,
defineComponent,
inject,
nextTick,
onMounted,
onBeforeUnmount,
Ref,
ref,
watch,
} from 'vue'
import { isPromise } from '@vue/shared'
import debounce from 'lodash/debounce'
import ElCascaderPanel, { CommonProps } from '@element-plus/components/cascader-panel'
import ElCascaderPanel, {
CommonProps,
} from '@element-plus/components/cascader-panel'
import ElInput from '@element-plus/components/input'
import ElPopper from '@element-plus/components/popper'
import ElScrollbar from '@element-plus/components/scrollbar'
@ -155,13 +164,20 @@ import { EVENT_CODE } from '@element-plus/utils/aria'
import { UPDATE_MODEL_EVENT, CHANGE_EVENT } from '@element-plus/utils/constants'
import isServer from '@element-plus/utils/isServer'
import { useGlobalConfig } from '@element-plus/utils/util'
import { addResizeListener, removeResizeListener } from '@element-plus/utils/resize-event'
import {
addResizeListener,
removeResizeListener,
} from '@element-plus/utils/resize-event'
import { isValidComponentSize } from '@element-plus/utils/validators'
import { Effect, Options } from '@element-plus/components/popper'
import type { ComputedRef, PropType } from 'vue'
import type { ElFormContext, ElFormItemContext } from '@element-plus/tokens'
import type { CascaderValue, CascaderNode, Tag } from '@element-plus/components/cascader-panel'
import type {
CascaderValue,
CascaderNode,
Tag,
} from '@element-plus/components/cascader-panel'
import type { ComponentSize } from '@element-plus/utils/types'
const DEFAULT_INPUT_HEIGHT = 40
@ -216,8 +232,11 @@ export default defineComponent({
clearable: Boolean,
filterable: Boolean,
filterMethod: {
type: Function as PropType<(node: CascaderNode, keyword: string) => boolean>,
default: (node: CascaderNode, keyword: string) => node.text.includes(keyword),
type: Function as PropType<
(node: CascaderNode, keyword: string) => boolean
>,
default: (node: CascaderNode, keyword: string) =>
node.text.includes(keyword),
},
separator: {
type: String,
@ -279,20 +298,31 @@ export default defineComponent({
const suggestions: Ref<CascaderNode[]> = ref([])
const isDisabled = computed(() => props.disabled || elForm.disabled)
const inputPlaceholder = computed(() => props.placeholder || t('el.cascader.placeholder'))
const realSize: ComputedRef<ComponentSize> = computed(() => props.size || elFormItem.size || $ELEMENT.size)
const tagSize = computed(() => ['small', 'mini'].includes(realSize.value) ? 'mini' : 'small')
const inputPlaceholder = computed(
() => props.placeholder || t('el.cascader.placeholder')
)
const realSize: ComputedRef<ComponentSize> = computed(
() => props.size || elFormItem.size || $ELEMENT.size
)
const tagSize = computed(() =>
['small', 'mini'].includes(realSize.value) ? 'mini' : 'small'
)
const multiple = computed(() => !!props.props.multiple)
const readonly = computed(() => !props.filterable || multiple.value)
const searchKeyword = computed(() => multiple.value ? searchInputValue.value : inputValue.value)
const checkedNodes: ComputedRef<CascaderNode[]> = computed(() => panel.value?.checkedNodes || [])
const searchKeyword = computed(() =>
multiple.value ? searchInputValue.value : inputValue.value
)
const checkedNodes: ComputedRef<CascaderNode[]> = computed(
() => panel.value?.checkedNodes || []
)
const clearBtnVisible = computed(() => {
if (
!props.clearable ||
isDisabled.value ||
filtering.value ||
!inputHover.value
) return false
)
return false
return !!checkedNodes.value.length
})
@ -300,7 +330,9 @@ export default defineComponent({
const { showAllLevels, separator } = props
const nodes = checkedNodes.value
return nodes.length
? multiple.value ? ' ' : nodes[0].calcText(showAllLevels, separator)
? multiple.value
? ' '
: nodes[0].calcText(showAllLevels, separator)
: ''
})
@ -387,7 +419,7 @@ export default defineComponent({
closable: false,
})
} else {
rest.forEach(node => tags.push(genTag(node)))
rest.forEach((node) => tags.push(genTag(node)))
}
}
}
@ -397,15 +429,16 @@ export default defineComponent({
const calculateSuggestions = () => {
const { filterMethod, showAllLevels, separator } = props
const res = panel.value.getFlattedNodes(!props.props.checkStrictly)
.filter(node => {
const res = panel.value
.getFlattedNodes(!props.props.checkStrictly)
.filter((node) => {
if (node.isDisabled) return false
node.calcText(showAllLevels, separator)
return filterMethod(node, searchKeyword.value)
})
if (multiple.value) {
presentTags.value.forEach(tag => {
presentTags.value.forEach((tag) => {
tag.hitState = false
})
}
@ -419,9 +452,13 @@ export default defineComponent({
let firstNode = null
if (filtering.value && suggestionPanel.value) {
firstNode = suggestionPanel.value.$el.querySelector('.el-cascader__suggestion-item')
firstNode = suggestionPanel.value.$el.querySelector(
'.el-cascader__suggestion-item'
)
} else {
firstNode = panel.value?.$el.querySelector('.el-cascader-node[tabindex="-1"]')
firstNode = panel.value?.$el.querySelector(
'.el-cascader-node[tabindex="-1"]'
)
}
if (firstNode) {
@ -438,13 +475,18 @@ export default defineComponent({
if (isServer || !inputInner) return
if (suggestionPanelEl) {
const suggestionList = suggestionPanelEl.querySelector('.el-cascader__suggestion-list')
const suggestionList = suggestionPanelEl.querySelector(
'.el-cascader__suggestion-list'
)
suggestionList.style.minWidth = inputInner.offsetWidth + 'px'
}
if (tagWrapperEl) {
const { offsetHeight } = tagWrapperEl
const height = presentTags.value.length > 0 ? Math.max(offsetHeight + 6, inputInitialHeight) + 'px' : `${inputInitialHeight}px`
const height =
presentTags.value.length > 0
? Math.max(offsetHeight + 6, inputInitialHeight) + 'px'
: `${inputInitialHeight}px`
inputInner.style.height = height
updatePopperPosition()
}
@ -514,8 +556,9 @@ export default defineComponent({
const passed = props.beforeFilter(value)
if (isPromise(passed)) {
passed.then(calculateSuggestions)
.catch(() => { /* prevent log error */ })
passed.then(calculateSuggestions).catch(() => {
/* prevent log error */
})
} else if (passed !== false) {
calculateSuggestions()
} else {
@ -537,15 +580,14 @@ export default defineComponent({
watch(presentTags, () => nextTick(updateStyle))
watch(
presentText,
val => inputValue.value = val,
{ immediate: true },
)
watch(presentText, (val) => (inputValue.value = val), { immediate: true })
onMounted(() => {
const inputEl = input.value.$el
inputInitialHeight = inputEl?.offsetHeight || INPUT_HEIGHT_MAP[realSize.value] || DEFAULT_INPUT_HEIGHT
inputInitialHeight =
inputEl?.offsetHeight ||
INPUT_HEIGHT_MAP[realSize.value] ||
DEFAULT_INPUT_HEIGHT
addResizeListener(inputEl, updateStyle)
})

View File

@ -15,9 +15,9 @@ describe('CheckTag.vue', () => {
expect(wrapper.classes()).toContain('el-check-tag')
})
test('functionality', async () => {
const wrapper = mount({
const wrapper = mount(
{
template: `<el-check-tag @change="checked = !checked" :checked="checked">
${AXIOM}
</el-check-tag>`,
@ -29,11 +29,13 @@ describe('CheckTag.vue', () => {
checked: false,
}
},
}, {
},
{
slots: {
default: AXIOM,
},
})
}
)
expect(wrapper.text()).toEqual(AXIOM)
await wrapper.find('.el-check-tag').trigger('click')
@ -43,8 +45,5 @@ describe('CheckTag.vue', () => {
await wrapper.find('.el-check-tag').trigger('click')
expect(wrapper.vm.checked).toBe(false)
})
})

View File

@ -9,7 +9,7 @@
<slot></slot>
</span>
</template>
<script lang='ts'>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'ElCheckTag',
@ -18,7 +18,6 @@ export default defineComponent({
},
emits: ['change'],
setup(props, { emit }) {
const onChange = () => {
emit('change', !props.checked)
}

View File

@ -4,7 +4,12 @@ import Checkbox from '../src/checkbox.vue'
import CheckboxButton from '../src/checkbox-button.vue'
import CheckboxGroup from '../src/checkbox-group.vue'
const _mount = <D>(template: string, data: () => D, otherObj?: Record<string, unknown>) => mount<D>({
const _mount = <D>(
template: string,
data: () => D,
otherObj?: Record<string, unknown>
) =>
mount<D>({
components: {
'el-checkbox': Checkbox,
'el-checkbox-group': CheckboxGroup,
@ -17,7 +22,10 @@ const _mount = <D>(template: string, data: () => D, otherObj?: Record<string, un
describe('Checkbox', () => {
test('create', async () => {
const wrapper = _mount('<el-checkbox v-model="checkbox" label="a"/>', () => ({ checkbox: false }))
const wrapper = _mount(
'<el-checkbox v-model="checkbox" label="a"/>',
() => ({ checkbox: false })
)
expect(wrapper.classes()).toContain('el-checkbox')
await wrapper.trigger('click')
expect(wrapper.classes()).toContain('is-checked')
@ -26,12 +34,17 @@ describe('Checkbox', () => {
})
test('no v-model', async () => {
const wrapper = _mount('<el-checkbox label="a"/>', () => ({ checkbox: false }))
const wrapper = _mount('<el-checkbox label="a"/>', () => ({
checkbox: false,
}))
expect(wrapper.classes('is-checked')).toBe(false)
})
test('disabled', async () => {
const wrapper = _mount('<el-checkbox v-model="checkbox" disabled label="a"/>', () => ({ checkbox: false }))
const wrapper = _mount(
'<el-checkbox v-model="checkbox" disabled label="a"/>',
() => ({ checkbox: false })
)
expect(wrapper.classes()).toContain('is-disabled')
await wrapper.trigger('click')
expect(wrapper.classes()).toContain('is-disabled')
@ -50,7 +63,7 @@ describe('Checkbox', () => {
this.data = val
},
},
},
}
)
const vm = wrapper.vm
@ -68,7 +81,7 @@ describe('Checkbox', () => {
<el-checkbox label="d" ref="d"></el-checkbox>
</el-checkbox-group>
`,
() => ({ checkList: [] }),
() => ({ checkList: [] })
)
const vm = wrapper.vm
expect(vm.checkList.length).toBe(0)
@ -96,7 +109,7 @@ describe('Checkbox', () => {
this.data = val
},
},
},
}
)
const vm = wrapper.vm
await wrapper.findComponent({ ref: 'a' }).trigger('click')
@ -117,7 +130,7 @@ describe('Checkbox', () => {
</div>
</el-checkbox-group>
`,
() => ({ checkList: [] }),
() => ({ checkList: [] })
)
const vm = wrapper.vm
expect(vm.checkList.length).toBe(0)
@ -130,7 +143,7 @@ describe('Checkbox', () => {
`<el-checkbox true-label="a" :false-label="3" v-model="checked"></el-checkbox>`,
() => ({
checked: 'a',
}),
})
)
const vm = wrapper.vm
await wrapper.trigger('click')
@ -150,7 +163,7 @@ describe('Checkbox', () => {
() => ({
checked: false,
checklist: [],
}),
})
) as any
expect(wrapper.vm.checked).toBe(true)
expect(wrapper.vm.checklist).toEqual(['a'])
@ -170,7 +183,7 @@ describe('Checkbox', () => {
() => ({
checked: false,
checklist: [],
}),
})
)
const checkbox = wrapper.find('.el-checkbox')
await checkbox.trigger('click')
@ -180,7 +193,10 @@ describe('Checkbox', () => {
describe('check-button', () => {
test('create', async () => {
const wrapper = _mount('<el-checkbox-button v-model="checkbox" label="a"/>', () => ({ checkbox: false }))
const wrapper = _mount(
'<el-checkbox-button v-model="checkbox" label="a"/>',
() => ({ checkbox: false })
)
expect(wrapper.classes()).toContain('el-checkbox-button')
await wrapper.trigger('click')
expect(wrapper.classes()).toContain('is-checked')
@ -189,7 +205,10 @@ describe('check-button', () => {
})
test('disabled', async () => {
const wrapper = _mount('<el-checkbox-button v-model="checkbox" disabled label="a"/>', () => ({ checkbox: false }))
const wrapper = _mount(
'<el-checkbox-button v-model="checkbox" disabled label="a"/>',
() => ({ checkbox: false })
)
expect(wrapper.classes()).toContain('is-disabled')
await wrapper.trigger('click')
expect(wrapper.classes()).toContain('is-disabled')
@ -210,7 +229,7 @@ describe('check-button', () => {
this.data = val
},
},
},
}
)
const vm = wrapper.vm
@ -238,7 +257,7 @@ describe('check-button', () => {
this.data = val
},
},
},
}
)
const vm = wrapper.vm
await wrapper.findComponent({ ref: 'a' }).trigger('click')
@ -259,12 +278,15 @@ describe('check-button', () => {
<el-checkbox-button label="d" ref="d"></el-checkbox-button>
</el-checkbox-group>
`,
() => ({ checkList: ['a', 'b'] }),
() => ({ checkList: ['a', 'b'] })
)
const vm = wrapper.vm
expect(vm.checkList.length).toBe(2)
expect((vm.$refs.a as any).$el.classList.contains('is-checked')).toBe(true)
expect((vm.$refs.a as any).$el.querySelector('.el-checkbox-button__inner').style.borderColor).toEqual('#ff0000')
expect(
(vm.$refs.a as any).$el.querySelector('.el-checkbox-button__inner').style
.borderColor
).toEqual('#ff0000')
})
test('button group min and max', async () => {
@ -284,7 +306,7 @@ describe('check-button', () => {
() => ({
checkList: ['a'],
lastEvent: null,
}),
})
)
const vm = wrapper.vm
expect(vm.checkList.length).toBe(1)
@ -296,8 +318,12 @@ describe('check-button', () => {
await wrapper.findComponent({ ref: 'c' }).trigger('click')
expect(vm.checkList.length).toBe(2)
expect(vm.checkList).toEqual(['a', 'b'])
expect((wrapper.findComponent({ ref: 'c' }).vm as any).isDisabled).toBe(true)
expect((wrapper.findComponent({ ref: 'd' }).vm as any).isDisabled).toBe(true)
expect((wrapper.findComponent({ ref: 'c' }).vm as any).isDisabled).toBe(
true
)
expect((wrapper.findComponent({ ref: 'd' }).vm as any).isDisabled).toBe(
true
)
})
})
@ -313,7 +339,7 @@ describe('check-button', () => {
</div>
</el-checkbox-group>
`,
() => ({ checkList: [] }),
() => ({ checkList: [] })
)
const vm = wrapper.vm
expect(vm.checkList.length).toBe(0)
@ -326,7 +352,7 @@ describe('check-button', () => {
`<el-checkbox-button true-label="a" :false-label="3" v-model="checked" />`,
() => ({
checked: 'a',
}),
})
)
const vm = wrapper.vm
await wrapper.trigger('click')
@ -346,17 +372,16 @@ describe('check-button', () => {
() => ({
checked: false,
checklist: [],
}),
})
)
expect(wrapper.vm.checked).toBe(true)
expect(wrapper.vm.checklist).toEqual(['a'])
})
test('checked', () => {
const wrapper = _mount(
`<el-checkbox checked />`,
() => ({}))
expect(wrapper.find('.el-checkbox').classes().toString()).toMatch('is-checked')
const wrapper = _mount(`<el-checkbox checked />`, () => ({}))
expect(wrapper.find('.el-checkbox').classes().toString()).toMatch(
'is-checked'
)
})
})

View File

@ -23,7 +23,7 @@
@change="handleChange"
@focus="focus = true"
@blur="focus = false"
>
/>
<input
v-else
v-model="model"
@ -35,7 +35,7 @@
@change="handleChange"
@focus="focus = true"
@blur="focus = false"
>
/>
<span
v-if="$slots.default || label"
@ -44,14 +44,10 @@
>
<slot>{{ label }}</slot>
</span>
</label>
</template>
<script lang='ts'>
import {
defineComponent,
computed,
} from 'vue'
<script lang="ts">
import { defineComponent, computed } from 'vue'
import { UPDATE_MODEL_EVENT } from '@element-plus/utils/constants'
import { useCheckbox, useCheckboxGroup, useCheckboxProps } from './useCheckbox'
@ -60,7 +56,8 @@ export default defineComponent({
props: useCheckboxProps,
emits: [UPDATE_MODEL_EVENT, 'change'],
setup(props) {
const { focus, isChecked, isDisabled, size, model, handleChange } = useCheckbox(props)
const { focus, isChecked, isDisabled, size, model, handleChange } =
useCheckbox(props)
const { checkboxGroup } = useCheckboxGroup()
const activeStyle = computed(() => {
@ -73,7 +70,6 @@ export default defineComponent({
}
})
return {
focus,
isChecked,

View File

@ -5,7 +5,14 @@
</template>
<script lang="ts">
import { defineComponent, computed, watch, provide, nextTick, toRefs } from 'vue'
import {
defineComponent,
computed,
watch,
provide,
nextTick,
toRefs,
} from 'vue'
import { UPDATE_MODEL_EVENT } from '@element-plus/utils/constants'
import { isValidComponentSize } from '@element-plus/utils/validators'
import { useCheckboxGroup } from './useCheckbox'
@ -48,9 +55,11 @@ export default defineComponent({
setup(props, ctx) {
const { elFormItem, elFormItemSize, ELEMENT } = useCheckboxGroup()
const checkboxGroupSize = computed(() => props.size || elFormItemSize.value || ELEMENT.size)
const checkboxGroupSize = computed(
() => props.size || elFormItemSize.value || ELEMENT.size
)
const changeEvent = value => {
const changeEvent = (value) => {
ctx.emit(UPDATE_MODEL_EVENT, value)
nextTick(() => {
ctx.emit('change', value)
@ -74,9 +83,12 @@ export default defineComponent({
changeEvent,
})
watch(() => props.modelValue, val => {
watch(
() => props.modelValue,
(val) => {
elFormItem.formItemMitt?.emit('el.form.change', [val])
})
}
)
},
})
</script>

View File

@ -6,7 +6,7 @@
checkboxSize ? 'el-checkbox--' + checkboxSize : '',
{ 'is-disabled': isDisabled },
{ 'is-bordered': border },
{ 'is-checked': isChecked }
{ 'is-checked': isChecked },
]"
:aria-controls="indeterminate ? controls : null"
>
@ -16,7 +16,7 @@
'is-disabled': isDisabled,
'is-checked': isChecked,
'is-indeterminate': indeterminate,
'is-focus': focus
'is-focus': focus,
}"
:tabindex="indeterminate ? 0 : undefined"
:role="indeterminate ? 'checkbox' : undefined"
@ -36,7 +36,7 @@
@change="handleChange"
@focus="focus = true"
@blur="focus = false"
>
/>
<input
v-else
v-model="model"
@ -49,7 +49,7 @@
@change="handleChange"
@focus="focus = true"
@blur="focus = false"
>
/>
</span>
<span v-if="$slots.default || label" class="el-checkbox__label">
<slot></slot>
@ -57,10 +57,8 @@
</span>
</label>
</template>
<script lang='ts'>
import {
defineComponent,
} from 'vue'
<script lang="ts">
import { defineComponent } from 'vue'
import { UPDATE_MODEL_EVENT } from '@element-plus/utils/constants'
import { isValidComponentSize } from '@element-plus/utils/validators'
import { useCheckbox } from './useCheckbox'

View File

@ -1,10 +1,4 @@
import {
ref,
computed,
inject,
getCurrentInstance,
watch,
} from 'vue'
import { ref, computed, inject, getCurrentInstance, watch } from 'vue'
import { toTypeString } from '@vue/shared'
import { UPDATE_MODEL_EVENT } from '@element-plus/utils/constants'
import { useGlobalConfig } from '@element-plus/utils/util'
@ -48,7 +42,9 @@ export const useCheckboxGroup = () => {
const elForm = inject(elFormKey, {} as ElFormContext)
const elFormItem = inject(elFormItemKey, {} as ElFormItemContext)
const checkboxGroup = inject<ICheckboxGroupInstance>('CheckboxGroup', {})
const isGroup = computed(() => checkboxGroup && checkboxGroup?.name === 'ElCheckboxGroup')
const isGroup = computed(
() => checkboxGroup && checkboxGroup?.name === 'ElCheckboxGroup'
)
const elFormItemSize = computed(() => {
return elFormItem.size
})
@ -67,22 +63,28 @@ const useModel = (props: IUseCheckboxProps) => {
const { emit } = getCurrentInstance()
const { isGroup, checkboxGroup } = useCheckboxGroup()
const isLimitExceeded = ref(false)
const store = computed(() => checkboxGroup ? checkboxGroup.modelValue?.value : props.modelValue)
const store = computed(() =>
checkboxGroup ? checkboxGroup.modelValue?.value : props.modelValue
)
const model = computed({
get() {
return isGroup.value
? store.value
: props.modelValue ?? selfModel.value
return isGroup.value ? store.value : props.modelValue ?? selfModel.value
},
set(val: unknown) {
if (isGroup.value && Array.isArray(val)) {
isLimitExceeded.value = false
if (checkboxGroup.min !== undefined && val.length < checkboxGroup.min.value) {
if (
checkboxGroup.min !== undefined &&
val.length < checkboxGroup.min.value
) {
isLimitExceeded.value = true
}
if (checkboxGroup.max !== undefined && val.length > checkboxGroup.max.value) {
if (
checkboxGroup.max !== undefined &&
val.length > checkboxGroup.max.value
) {
isLimitExceeded.value = true
}
@ -100,10 +102,18 @@ const useModel = (props: IUseCheckboxProps) => {
}
}
const useCheckboxStatus = (props: IUseCheckboxProps, { model }: PartialReturnType<typeof useModel>) => {
const useCheckboxStatus = (
props: IUseCheckboxProps,
{ model }: PartialReturnType<typeof useModel>
) => {
const { isGroup, checkboxGroup, elFormItemSize, ELEMENT } = useCheckboxGroup()
const focus = ref(false)
const size = computed<string | undefined>(() => checkboxGroup?.checkboxGroupSize?.value || elFormItemSize.value || ELEMENT.size)
const size = computed<string | undefined>(
() =>
checkboxGroup?.checkboxGroupSize?.value ||
elFormItemSize.value ||
ELEMENT.size
)
const isChecked = computed<boolean>(() => {
const value = model.value
if (toTypeString(value) === '[object Boolean]') {
@ -133,14 +143,20 @@ const useCheckboxStatus = (props: IUseCheckboxProps, { model }: PartialReturnTyp
const useDisabled = (
props: IUseCheckboxProps,
{ model, isChecked }: PartialReturnType<typeof useModel> & PartialReturnType<typeof useCheckboxStatus>,
{
model,
isChecked,
}: PartialReturnType<typeof useModel> &
PartialReturnType<typeof useCheckboxStatus>
) => {
const { elForm, isGroup, checkboxGroup } = useCheckboxGroup()
const isLimitDisabled = computed(() => {
const max = checkboxGroup.max?.value
const min = checkboxGroup.min?.value
return !!(max || min) && (model.value.length >= max && !isChecked.value) ||
return (
(!!(max || min) && model.value.length >= max && !isChecked.value) ||
(model.value.length <= min && isChecked.value)
)
})
const isDisabled = computed(() => {
const disabled = props.disabled || elForm.disabled
@ -155,12 +171,12 @@ const useDisabled = (
}
}
const setStoreValue = (props: IUseCheckboxProps, { model }: PartialReturnType<typeof useModel>) => {
const setStoreValue = (
props: IUseCheckboxProps,
{ model }: PartialReturnType<typeof useModel>
) => {
function addToStore() {
if (
Array.isArray(model.value) &&
!model.value.includes(props.label)
) {
if (Array.isArray(model.value) && !model.value.includes(props.label)) {
model.value.push(props.label)
} else {
model.value = props.trueLabel || true
@ -169,7 +185,10 @@ const setStoreValue = (props: IUseCheckboxProps, { model }: PartialReturnType<ty
props.checked && addToStore()
}
const useEvent = (props: IUseCheckboxProps, { isLimitExceeded }: PartialReturnType<typeof useModel>) => {
const useEvent = (
props: IUseCheckboxProps,
{ isLimitExceeded }: PartialReturnType<typeof useModel>
) => {
const { elFormItem } = useCheckboxGroup()
const { emit } = getCurrentInstance()
function handleChange(e: InputEvent) {
@ -182,9 +201,12 @@ const useEvent = (props: IUseCheckboxProps, { isLimitExceeded }: PartialReturnTy
emit('change', value, e)
}
watch(() => props.modelValue, val => {
watch(
() => props.modelValue,
(val) => {
elFormItem.formItemMitt?.emit('el.form.change', [val])
})
}
)
return {
handleChange,
@ -193,7 +215,9 @@ const useEvent = (props: IUseCheckboxProps, { isLimitExceeded }: PartialReturnTy
export const useCheckbox = (props: IUseCheckboxProps) => {
const { model, isLimitExceeded } = useModel(props)
const { focus, size, isChecked, checkboxSize } = useCheckboxStatus(props, { model })
const { focus, size, isChecked, checkboxSize } = useCheckboxStatus(props, {
model,
})
const { isDisabled } = useDisabled(props, { model, isChecked })
const { handleChange } = useEvent(props, { isLimitExceeded })

View File

@ -51,15 +51,19 @@ describe('Col', () => {
const App = {
setup() {
return () => {
return h(Row, {
return h(
Row,
{
gutter: outer.value,
ref: 'row',
}, [
},
[
h(Col, {
span: 12,
ref: 'col',
}),
])
]
)
}
},
}
@ -129,4 +133,3 @@ describe('Row', () => {
expect(wrapper.classes()).toContain('is-align-bottom')
})
})

View File

@ -64,7 +64,7 @@ const ElCol = defineComponent({
const classList = computed(() => {
const ret: string[] = []
const pos = ['span', 'offset', 'pull', 'push'] as const
pos.forEach(prop => {
pos.forEach((prop) => {
const size = props[prop]
if (typeof size === 'number') {
if (prop === 'span') ret.push(`el-col-${props[prop]}`)
@ -72,14 +72,16 @@ const ElCol = defineComponent({
}
})
const sizes = ['xs', 'sm', 'md', 'lg', 'xl'] as const
sizes.forEach(size => {
sizes.forEach((size) => {
if (typeof props[size] === 'number') {
ret.push(`el-col-${size}-${props[size]}`)
} else if (typeof props[size] === 'object') {
const sizeProps = props[size]
Object.keys(sizeProps).forEach(prop => {
Object.keys(sizeProps).forEach((prop) => {
ret.push(
prop !== 'span' ? `el-col-${size}-${prop}-${sizeProps[prop]}` : `el-col-${size}-${sizeProps[prop]}`,
prop !== 'span'
? `el-col-${size}-${prop}-${sizeProps[prop]}`
: `el-col-${size}-${sizeProps[prop]}`
)
})
}
@ -92,13 +94,14 @@ const ElCol = defineComponent({
return ret
})
return () => h(
return () =>
h(
props.tag,
{
class: ['el-col', classList.value],
style: style.value,
},
slots.default?.(),
slots.default?.()
)
},
})

View File

@ -6,7 +6,9 @@ CollapseTransition.install = (app: App): void => {
app.component(CollapseTransition.name, CollapseTransition)
}
const _CollapseTransition = CollapseTransition as SFCWithInstall<typeof CollapseTransition>
const _CollapseTransition = CollapseTransition as SFCWithInstall<
typeof CollapseTransition
>
export default _CollapseTransition
export const ElCollapseTransition = _CollapseTransition

View File

@ -36,7 +36,9 @@ describe('Collapse.vue', () => {
const vm = wrapper.vm
const collapseWrapper = wrapper.findComponent(Collapse)
const collapseItemWrappers = collapseWrapper.findAllComponents(CollapseItem)
const collapseItemHeaderEls = vm.$el.querySelectorAll('.el-collapse-item__header')
const collapseItemHeaderEls = vm.$el.querySelectorAll(
'.el-collapse-item__header'
)
expect(collapseItemWrappers[0].vm.isActive).toBe(true)
collapseItemHeaderEls[2].click()
@ -80,7 +82,9 @@ describe('Collapse.vue', () => {
const vm = wrapper.vm
const collapseWrapper = wrapper.findComponent(Collapse)
const collapseItemWrappers = collapseWrapper.findAllComponents(CollapseItem)
const collapseItemHeaderEls = vm.$el.querySelectorAll('.el-collapse-item__header')
const collapseItemHeaderEls = vm.$el.querySelectorAll(
'.el-collapse-item__header'
)
expect(collapseItemWrappers[0].vm.isActive).toBe(true)
collapseItemHeaderEls[2].click()
@ -129,7 +133,9 @@ describe('Collapse.vue', () => {
const vm = wrapper.vm
const collapseWrapper = wrapper.findComponent(Collapse)
const collapseItemWrappers = collapseWrapper.findAllComponents(CollapseItem)
const collapseItemHeaderEls = vm.$el.querySelectorAll('.el-collapse-item__header')
const collapseItemHeaderEls = vm.$el.querySelectorAll(
'.el-collapse-item__header'
)
expect(collapseItemWrappers[0].vm.isActive).toBe(true)
expect(vm.activeNames).toEqual(['1'])
expect(onChange).not.toHaveBeenCalled()

View File

@ -15,8 +15,8 @@
role="button"
:tabindex="disabled ? -1 : 0"
:class="{
'focusing': focusing,
'is-active': isActive
focusing: focusing,
'is-active': isActive,
}"
@click="handleHeaderClick"
@keyup.space.enter.stop="handleEnterClick"
@ -47,7 +47,7 @@
</el-collapse-transition>
</div>
</template>
<script lang='ts'>
<script lang="ts">
import { defineComponent, PropType, inject, computed, ref } from 'vue'
import { CollapseProvider } from './collapse'
import { generateId } from '@element-plus/utils/util'

View File

@ -5,13 +5,7 @@
</template>
<script lang="ts">
import {
defineComponent,
ref,
watch,
provide,
onUnmounted,
} from 'vue'
import { defineComponent, ref, watch, provide, onUnmounted } from 'vue'
import type { PropType, Ref } from 'vue'
import mitt, { Emitter } from 'mitt'
import { UPDATE_MODEL_EVENT, CHANGE_EVENT } from '@element-plus/utils/constants'
@ -36,20 +30,20 @@ export default defineComponent({
const activeNames = ref([].concat(props.modelValue))
const collapseMitt: Emitter = mitt()
const setActiveNames = _activeNames => {
const setActiveNames = (_activeNames) => {
activeNames.value = [].concat(_activeNames)
const value = props.accordion ? activeNames.value[0] : activeNames.value
emit(UPDATE_MODEL_EVENT, value)
emit(CHANGE_EVENT, value)
}
const handleItemClick = name => {
const handleItemClick = (name) => {
if (props.accordion) {
setActiveNames(
(activeNames.value[0] || activeNames.value[0] === 0) &&
activeNames.value[0] === name
? ''
: name,
: name
)
} else {
const _activeNames = activeNames.value.slice(0)
@ -68,7 +62,7 @@ export default defineComponent({
() => props.modelValue,
() => {
activeNames.value = [].concat(props.modelValue)
},
}
)
collapseMitt.on('item-click', handleItemClick)

View File

@ -4,7 +4,7 @@ import ColorPicker from '../src/index.vue'
import type { Nullable } from '@element-plus/utils/types'
const _mount = (template: string, data: () => ({ [key: string]: any; })) => {
const _mount = (template: string, data: () => { [key: string]: any }) => {
const Component = defineComponent({
components: {
ElColorPicker: ColorPicker,
@ -24,8 +24,7 @@ type ColorPickerVM = ComponentPublicInstance<{
clientY: number
}) => void
thumbTop: number
handleDrag: (opt: { clientX: number; clientY: number; }) => void
handleDrag: (opt: { clientX: number; clientY: number }) => void
}>
describe('Color-picker', () => {
@ -39,7 +38,7 @@ describe('Color-picker', () => {
`<el-color-picker v-model="color" :show-alpha="true"></el-color-picker>`,
() => ({
color: '#20A0FF',
}),
})
)
await wrapper.find('.el-color-picker__trigger').trigger('click')
const alphaSlider = document.querySelector('.el-color-alpha-slider')
@ -51,11 +50,13 @@ describe('Color-picker', () => {
`<el-color-picker v-model="color"></el-color-picker>`,
() => ({
color: '#20A0FF',
}),
})
)
await wrapper.find('.el-color-picker__trigger').trigger('click')
await nextTick()
const input = document.querySelector<HTMLInputElement>('.el-color-dropdown__value input')
const input = document.querySelector<HTMLInputElement>(
'.el-color-dropdown__value input'
)
expect(input.value.trim().toUpperCase()).toEqual('#20A0FF')
wrapper.unmount()
})
@ -64,7 +65,7 @@ describe('Color-picker', () => {
`<el-color-picker v-model="color"></el-color-picker>`,
() => ({
color: null,
}),
})
)
await wrapper.find('.el-color-picker__trigger').trigger('click')
@ -79,17 +80,26 @@ describe('Color-picker', () => {
`<el-color-picker v-model="color"></el-color-picker>`,
() => ({
color: '#0F0',
}),
})
)
const colorPickerWrapper = wrapper.findComponent(ColorPicker)
const hueSlideWrapper = colorPickerWrapper.findComponent({ ref: 'hue' })
const hueSlideDom = hueSlideWrapper.element as HTMLElement
const thumbDom = hueSlideWrapper.find<HTMLElement>('.el-color-hue-slider__thumb').element
const mockHueSlideHeight = jest.spyOn(hueSlideDom, 'offsetHeight', 'get').mockImplementation(() => 200)
const mockThumbDom = jest.spyOn(thumbDom, 'offsetHeight', 'get').mockImplementation(() => 4)
const thumbDom = hueSlideWrapper.find<HTMLElement>(
'.el-color-hue-slider__thumb'
).element
const mockHueSlideHeight = jest
.spyOn(hueSlideDom, 'offsetHeight', 'get')
.mockImplementation(() => 200)
const mockThumbDom = jest
.spyOn(thumbDom, 'offsetHeight', 'get')
.mockImplementation(() => 4)
await wrapper.find('.el-color-picker__trigger').trigger('click')
await nextTick()
expect((hueSlideWrapper.vm as ComponentPublicInstance<{ thumbTop: number; }>).thumbTop > 10).toBeTruthy()
expect(
(hueSlideWrapper.vm as ComponentPublicInstance<{ thumbTop: number }>)
.thumbTop > 10
).toBeTruthy()
mockHueSlideHeight.mockRestore()
mockThumbDom.mockRestore()
wrapper.unmount()
@ -99,7 +109,7 @@ describe('Color-picker', () => {
`<el-color-picker v-model="color"></el-color-picker>`,
() => ({
color: '#0F0',
}),
})
)
await wrapper.find('.el-color-picker__trigger').trigger('click')
const dropdown = document.querySelector('.el-color-dropdown')
@ -111,10 +121,12 @@ describe('Color-picker', () => {
`<el-color-picker v-model="color"></el-color-picker>`,
() => ({
color: '#0F0',
}),
})
)
await wrapper.find('.el-color-picker__trigger').trigger('click')
const clearBtn = document.querySelector<HTMLElement>('.el-color-dropdown__link-btn')
const clearBtn = document.querySelector<HTMLElement>(
'.el-color-dropdown__link-btn'
)
clearBtn.click()
expect(wrapper.vm.color).toEqual(null)
wrapper.unmount()
@ -124,52 +136,73 @@ describe('Color-picker', () => {
`<el-color-picker v-model="color"></el-color-picker>`,
() => ({
color: '#F00',
}),
})
)
await wrapper.find('.el-color-picker__trigger').trigger('click')
const colorPickerWrapper = wrapper.findComponent(ColorPicker)
const hueSlideWrapper = colorPickerWrapper.findComponent({ ref: 'hue' })
const hueSlideDom = hueSlideWrapper.element
const thumbDom = hueSlideWrapper.find<HTMLElement>('.el-color-hue-slider__thumb').element
const mockHueBarHeight = jest.spyOn(hueSlideDom,'getBoundingClientRect').mockReturnValue({
const thumbDom = hueSlideWrapper.find<HTMLElement>(
'.el-color-hue-slider__thumb'
).element
const mockHueBarHeight = jest
.spyOn(hueSlideDom, 'getBoundingClientRect')
.mockReturnValue({
height: 176,
width: 12,
x: 0,
y: 0,
top: 0,
} as DOMRect)
const mockThumbDom = jest.spyOn(thumbDom, 'offsetHeight', 'get').mockReturnValue(4);
(hueSlideWrapper.vm as ColorPickerVM).handleClick({ target: null, clientX: 0, clientY: 100 })
const mockThumbDom = jest
.spyOn(thumbDom, 'offsetHeight', 'get')
.mockReturnValue(4)
;(hueSlideWrapper.vm as ColorPickerVM).handleClick({
target: null,
clientX: 0,
clientY: 100,
})
const hue = colorPickerWrapper.vm.color.get('hue')
expect(hue > 0).toBeTruthy()
mockHueBarHeight.mockRestore()
mockThumbDom.mockRestore()
wrapper.unmount()
})
it('should change hue when saturation is zero', async () => {
const wrapper = _mount(
`<el-color-picker v-model="color"></el-color-picker>`,
() => ({
color: '20A0FF',
}),
})
)
await wrapper.find('.el-color-picker__trigger').trigger('click')
const colorPickerWrapper = wrapper.findComponent(ColorPicker)
const hueSlideWrapper = colorPickerWrapper.findComponent({ ref: 'hue' })
const hueSlideDom = hueSlideWrapper.element as HTMLElement
const thumbDom = hueSlideWrapper.find<HTMLElement>('.el-color-hue-slider__thumb').element
const mockHueSlideRect = jest.spyOn(hueSlideDom,'getBoundingClientRect').mockReturnValue({
const thumbDom = hueSlideWrapper.find<HTMLElement>(
'.el-color-hue-slider__thumb'
).element
const mockHueSlideRect = jest
.spyOn(hueSlideDom, 'getBoundingClientRect')
.mockReturnValue({
height: 176,
width: 12,
x: 0,
y: 0,
top: 0,
} as DOMRect)
const mockHueSlideOffsetHeight = jest.spyOn(hueSlideDom, 'offsetHeight', 'get').mockReturnValue(200)
const mockThumbDom = jest.spyOn(thumbDom, 'offsetHeight', 'get').mockReturnValue(4);
(hueSlideWrapper.vm as ColorPickerVM).handleClick({ target: null, clientX: 0, clientY: 100 })
const mockHueSlideOffsetHeight = jest
.spyOn(hueSlideDom, 'offsetHeight', 'get')
.mockReturnValue(200)
const mockThumbDom = jest
.spyOn(thumbDom, 'offsetHeight', 'get')
.mockReturnValue(4)
;(hueSlideWrapper.vm as ColorPickerVM).handleClick({
target: null,
clientX: 0,
clientY: 100,
})
await nextTick()
expect((hueSlideWrapper.vm as ColorPickerVM).thumbTop > 0).toBe(true)
@ -183,22 +216,32 @@ describe('Color-picker', () => {
`<el-color-picker v-model="color" show-alpha></el-color-picker>`,
() => ({
color: '#F00',
}),
})
)
await wrapper.find('.el-color-picker__trigger').trigger('click')
const colorPickerWrapper = wrapper.findComponent(ColorPicker)
const alphaWrapper = colorPickerWrapper.findComponent({ ref: 'alpha' })
const alphaDom = alphaWrapper.element as HTMLElement
const mockAlphaDom = jest.spyOn(alphaDom, 'getBoundingClientRect').mockReturnValue({
const mockAlphaDom = jest
.spyOn(alphaDom, 'getBoundingClientRect')
.mockReturnValue({
height: 12,
width: 280,
x: 0,
y: 0,
left: 0,
} as DOMRect)
const thumbDom = alphaWrapper.find<HTMLElement>('.el-color-alpha-slider__thumb').element
const mockThumbDom = jest.spyOn(thumbDom, 'offsetWidth', 'get').mockReturnValue(4);
(alphaWrapper.vm as ColorPickerVM).handleClick({ target: null, clientX: 50, clientY: 0 })
const thumbDom = alphaWrapper.find<HTMLElement>(
'.el-color-alpha-slider__thumb'
).element
const mockThumbDom = jest
.spyOn(thumbDom, 'offsetWidth', 'get')
.mockReturnValue(4)
;(alphaWrapper.vm as ColorPickerVM).handleClick({
target: null,
clientX: 50,
clientY: 0,
})
await nextTick()
expect(colorPickerWrapper.vm.color.get('alpha') < 100).toBeTruthy()
mockAlphaDom.mockRestore()
@ -211,13 +254,13 @@ describe('Color-picker', () => {
`<el-color-picker v-model="color" show-alpha color-format="hsv"></el-color-picker>`,
() => ({
color: 'hsv(0, 50%, 50%)',
}),
})
)
await wrapper.find('.el-color-picker__trigger').trigger('click')
const colorPickerWrapper = wrapper.findComponent(ColorPicker)
const svPanelWrapper = colorPickerWrapper.findComponent({ ref: 'svPanel' });
(svPanelWrapper.vm as ColorPickerVM).handleDrag({ clientX: 0, clientY: 0 })
const svPanelWrapper = colorPickerWrapper.findComponent({ ref: 'svPanel' })
;(svPanelWrapper.vm as ColorPickerVM).handleDrag({ clientX: 0, clientY: 0 })
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.color._saturation !== 50).toBeTruthy()
expect(wrapper.vm.color._value !== 50).toBeTruthy()
@ -240,21 +283,34 @@ describe('Color-picker', () => {
'#45aa9477',
'#892345',
],
}),
})
)
await wrapper.find('.el-color-picker__trigger').trigger('click')
const colorPickerWrapper = wrapper.findComponent(ColorPicker)
const predefineWrapper = colorPickerWrapper.findComponent({ ref: 'predefine' })
const predefineWrapper = colorPickerWrapper.findComponent({
ref: 'predefine',
})
const predefineDom = predefineWrapper.element as HTMLElement
expect(predefineDom.querySelectorAll('.el-color-predefine__color-selector').length === 9).toBeTruthy()
predefineDom.querySelector<HTMLElement>('.el-color-predefine__color-selector:nth-child(4)').click()
expect(
predefineDom.querySelectorAll('.el-color-predefine__color-selector')
.length === 9
).toBeTruthy()
predefineDom
.querySelector<HTMLElement>(
'.el-color-predefine__color-selector:nth-child(4)'
)
.click()
await nextTick()
expect(colorPickerWrapper.vm.color.get('hue')).toEqual(180)
expect(colorPickerWrapper.vm.color.get('saturation')).toEqual(65)
expect(colorPickerWrapper.vm.color.get('value')).toEqual(20)
expect(colorPickerWrapper.vm.color.get('alpha')).toEqual(50)
predefineDom.querySelector<HTMLElement>('.el-color-predefine__color-selector:nth-child(3)').click()
predefineDom
.querySelector<HTMLElement>(
'.el-color-predefine__color-selector:nth-child(3)'
)
.click()
await nextTick()
expect(colorPickerWrapper.vm.color.get('hue')).toEqual(250)
expect(colorPickerWrapper.vm.color.get('saturation')).toEqual(54)
@ -278,30 +334,56 @@ describe('Color-picker', () => {
'#45aa9477',
'#892345',
],
}),
})
)
await wrapper.find('.el-color-picker__trigger').trigger('click')
const colorPickerWrapper = wrapper.findComponent(ColorPicker)
const predefineWrapper = colorPickerWrapper.findComponent({ ref: 'predefine' })
const predefineWrapper = colorPickerWrapper.findComponent({
ref: 'predefine',
})
const predefineDom = predefineWrapper.element as HTMLElement
predefineDom.querySelector<HTMLElement>('.el-color-predefine__color-selector:nth-child(4)').click()
predefineDom
.querySelector<HTMLElement>(
'.el-color-predefine__color-selector:nth-child(4)'
)
.click()
await nextTick()
expect(predefineWrapper.find('.el-color-predefine__color-selector:nth-child(4)').classes()).toContain('selected')
expect(
predefineWrapper
.find('.el-color-predefine__color-selector:nth-child(4)')
.classes()
).toContain('selected')
const hueSlideWrapper = colorPickerWrapper.findComponent({ ref: 'hue' })
const hueSlideDom = hueSlideWrapper.element
const thumbDom = hueSlideWrapper.find<HTMLElement>('.el-color-hue-slider__thumb').element
const mockHueSlideRect = jest.spyOn(hueSlideDom,'getBoundingClientRect').mockReturnValue({
const thumbDom = hueSlideWrapper.find<HTMLElement>(
'.el-color-hue-slider__thumb'
).element
const mockHueSlideRect = jest
.spyOn(hueSlideDom, 'getBoundingClientRect')
.mockReturnValue({
height: 176,
width: 12,
x: 0,
y: 0,
top: 0,
} as DOMRect)
const mockHueSlideOffsetHeight = jest.spyOn(hueSlideDom as HTMLElement, 'offsetHeight', 'get').mockReturnValue(200)
const mockThumbDom = jest.spyOn(thumbDom, 'offsetHeight', 'get').mockReturnValue(4);
(hueSlideWrapper.vm as ColorPickerVM).handleClick({ target: null, clientX: 0, clientY: 1000 })
const mockHueSlideOffsetHeight = jest
.spyOn(hueSlideDom as HTMLElement, 'offsetHeight', 'get')
.mockReturnValue(200)
const mockThumbDom = jest
.spyOn(thumbDom, 'offsetHeight', 'get')
.mockReturnValue(4)
;(hueSlideWrapper.vm as ColorPickerVM).handleClick({
target: null,
clientX: 0,
clientY: 1000,
})
await nextTick()
expect(predefineWrapper.find('.el-color-predefine__color-selector:nth-child(4)').classes()).not.toContain('selected')
expect(
predefineWrapper
.find('.el-color-predefine__color-selector:nth-child(4)')
.classes()
).not.toContain('selected')
mockHueSlideRect.mockRestore()
mockThumbDom.mockRestore()
mockHueSlideOffsetHeight.mockRestore()

View File

@ -3,7 +3,7 @@ import { hasOwn } from '@vue/shared'
const hsv2hsl = function (hue: number, sat: number, val: number) {
return [
hue,
(sat * val / ((hue = (2 - sat) * val) < 1 ? hue : 2 - hue)) || 0,
(sat * val) / ((hue = (2 - sat) * val) < 1 ? hue : 2 - hue) || 0,
hue / 2,
]
}
@ -23,15 +23,15 @@ const bound01 = function(value: number | string, max: number | string) {
if (isOnePointZero(value)) value = '100%'
const processPercent = isPercentage(value)
value = Math.min((max as number), Math.max(0, parseFloat(value + '')))
value = Math.min(max as number, Math.max(0, parseFloat(value + '')))
// Automatically convert percentage into number
if (processPercent) {
value = parseInt((value * (max as number)) + '', 10) / 100
value = parseInt(value * (max as number) + '', 10) / 100
}
// Handle floating point rounding errors
if ((Math.abs(value - (max as number)) < 0.000001)) {
if (Math.abs(value - (max as number)) < 0.000001) {
return 1
}
@ -58,7 +58,10 @@ const HEX_INT_MAP = { A: 10, B: 11, C: 12, D: 13, E: 14, F: 15 }
const parseHexChannel = function (hex: string) {
if (hex.length === 2) {
return (HEX_INT_MAP[hex[0].toUpperCase()] || +hex[0]) * 16 + (HEX_INT_MAP[hex[1].toUpperCase()] || +hex[1])
return (
(HEX_INT_MAP[hex[0].toUpperCase()] || +hex[0]) * 16 +
(HEX_INT_MAP[hex[1].toUpperCase()] || +hex[1])
)
}
return HEX_INT_MAP[hex[1].toUpperCase()] || +hex[1]
@ -73,10 +76,11 @@ const hsl2hsv = function(hue: number, sat: number, light: number) {
// let v
light *= 2
sat *= (light <= 1) ? light : 2 - light
sat *= light <= 1 ? light : 2 - light
smin *= lmin <= 1 ? lmin : 2 - lmin
const v = (light + sat) / 2
const sv = light === 0 ? (2 * smin) / (lmin + smin) : (2 * sat) / (light + sat)
const sv =
light === 0 ? (2 * smin) / (lmin + smin) : (2 * sat) / (light + sat)
return {
h: hue,
@ -167,8 +171,7 @@ export default class Color {
public value = ''
public selected?: boolean
constructor(options?: Options) {
options = options || {} as Options
options = options || ({} as Options)
for (const option in options) {
if (hasOwn(options, option)) {
@ -179,7 +182,7 @@ export default class Color {
this.doOnChange()
}
set(prop: { [key: string]: any; } | any, value?: number) {
set(prop: { [key: string]: any } | any, value?: number) {
if (arguments.length === 1 && typeof prop === 'object') {
for (const p in prop) {
if (hasOwn(prop, p)) {
@ -221,8 +224,11 @@ export default class Color {
}
if (value.indexOf('hsl') !== -1) {
const parts = value.replace(/hsla|hsl|\(|\)/gm, '')
.split(/\s|,/g).filter(val => val !== '').map((val, index) => index > 2 ? parseFloat(val) : parseInt(val, 10))
const parts = value
.replace(/hsla|hsl|\(|\)/gm, '')
.split(/\s|,/g)
.filter((val) => val !== '')
.map((val, index) => (index > 2 ? parseFloat(val) : parseInt(val, 10)))
if (parts.length === 4) {
this._alpha = Math.floor(parseFloat(parts[3]) * 100)
@ -234,8 +240,11 @@ export default class Color {
fromHSV(h, s, v)
}
} else if (value.indexOf('hsv') !== -1) {
const parts = value.replace(/hsva|hsv|\(|\)/gm, '')
.split(/\s|,/g).filter(val => val !== '').map((val, index) => index > 2 ? parseFloat(val) : parseInt(val, 10))
const parts = value
.replace(/hsva|hsv|\(|\)/gm, '')
.split(/\s|,/g)
.filter((val) => val !== '')
.map((val, index) => (index > 2 ? parseFloat(val) : parseInt(val, 10)))
if (parts.length === 4) {
this._alpha = Math.floor(parseFloat(parts[3]) * 100)
@ -246,8 +255,11 @@ export default class Color {
fromHSV(parts[0], parts[1], parts[2])
}
} else if (value.indexOf('rgb') !== -1) {
const parts = value.replace(/rgba|rgb|\(|\)/gm, '')
.split(/\s|,/g).filter(val => val !== '').map((val, index) => index > 2 ? parseFloat(val) : parseInt(val, 10))
const parts = value
.replace(/rgba|rgb|\(|\)/gm, '')
.split(/\s|,/g)
.filter((val) => val !== '')
.map((val, index) => (index > 2 ? parseFloat(val) : parseInt(val, 10)))
if (parts.length === 4) {
this._alpha = Math.floor(parseFloat(parts[3]) * 100)
@ -260,7 +272,8 @@ export default class Color {
}
} else if (value.indexOf('#') !== -1) {
const hex = value.replace('#', '').trim()
if (!/^[0-9a-fA-F]{3}$|^[0-9a-fA-F]{6}$|^[0-9a-fA-F]{8}$/.test(hex)) return
if (!/^[0-9a-fA-F]{3}$|^[0-9a-fA-F]{6}$|^[0-9a-fA-F]{8}$/.test(hex))
return
let r, g, b
if (hex.length === 3) {
@ -274,7 +287,9 @@ export default class Color {
}
if (hex.length === 8) {
this._alpha = Math.floor(parseHexChannel(hex.substring(6)) / 255 * 100)
this._alpha = Math.floor(
(parseHexChannel(hex.substring(6)) / 255) * 100
)
} else if (hex.length === 3 || hex.length === 6) {
this._alpha = 100
}
@ -285,10 +300,12 @@ export default class Color {
}
compare(color) {
return Math.abs(color._hue - this._hue) < 2 &&
return (
Math.abs(color._hue - this._hue) < 2 &&
Math.abs(color._saturation - this._saturation) < 1 &&
Math.abs(color._value - this._value) < 1 &&
Math.abs(color._alpha - this._alpha) < 1
)
}
doOnChange() {
@ -298,11 +315,15 @@ export default class Color {
switch (format) {
case 'hsl': {
const hsl = hsv2hsl(_hue, _saturation / 100, _value / 100)
this.value = `hsla(${ _hue }, ${ Math.round(hsl[1] * 100) }%, ${ Math.round(hsl[2] * 100) }%, ${ _alpha / 100})`
this.value = `hsla(${_hue}, ${Math.round(
hsl[1] * 100
)}%, ${Math.round(hsl[2] * 100)}%, ${_alpha / 100})`
break
}
case 'hsv': {
this.value = `hsva(${ _hue }, ${ Math.round(_saturation) }%, ${ Math.round(_value) }%, ${ _alpha / 100})`
this.value = `hsva(${_hue}, ${Math.round(_saturation)}%, ${Math.round(
_value
)}%, ${_alpha / 100})`
break
}
default: {
@ -314,11 +335,15 @@ export default class Color {
switch (format) {
case 'hsl': {
const hsl = hsv2hsl(_hue, _saturation / 100, _value / 100)
this.value = `hsl(${ _hue }, ${ Math.round(hsl[1] * 100) }%, ${ Math.round(hsl[2] * 100) }%)`
this.value = `hsl(${_hue}, ${Math.round(hsl[1] * 100)}%, ${Math.round(
hsl[2] * 100
)}%)`
break
}
case 'hsv': {
this.value = `hsv(${ _hue }, ${ Math.round(_saturation) }%, ${ Math.round(_value) }%)`
this.value = `hsv(${_hue}, ${Math.round(_saturation)}%, ${Math.round(
_value
)}%)`
break
}
case 'rgb': {

View File

@ -4,32 +4,36 @@
ref="bar"
class="el-color-alpha-slider__bar"
:style="{
background
background,
}"
@click="handleClick"
>
</div>
></div>
<div
ref="thumb"
class="el-color-alpha-slider__thumb"
:style="{
left: thumbLeft + 'px',
top: thumbTop + 'px'
top: thumbTop + 'px',
}"
>
</div>
></div>
</div>
</template>
<script lang="ts">
import { defineComponent, watch, ref, onMounted, getCurrentInstance, shallowRef } from 'vue'
import {
defineComponent,
watch,
ref,
onMounted,
getCurrentInstance,
shallowRef,
} from 'vue'
import draggable from '../draggable'
import type { PropType } from 'vue'
import type { Nullable } from '@element-plus/utils/types'
import type Color from '../color'
export default defineComponent({
name: 'ElColorAlphaSlider',
props: {
@ -53,12 +57,18 @@ export default defineComponent({
const thumbTop = ref(0)
const background = ref<Nullable<string>>(null)
watch(() => props.color.get('alpha'), () => {
watch(
() => props.color.get('alpha'),
() => {
update()
})
watch(() => props.color.value, () => {
}
)
watch(
() => props.color.value,
() => {
update()
})
}
)
//methods
function getThumbLeft() {
@ -67,7 +77,9 @@ export default defineComponent({
const alpha = props.color.get('alpha')
if (!el) return 0
return Math.round(alpha * (el.offsetWidth - thumb.value.offsetWidth / 2) / 100)
return Math.round(
(alpha * (el.offsetWidth - thumb.value.offsetWidth / 2)) / 100
)
}
function getThumbTop() {
@ -76,7 +88,9 @@ export default defineComponent({
const alpha = props.color.get('alpha')
if (!el) return 0
return Math.round(alpha * (el.offsetHeight - thumb.value.offsetHeight / 2) / 100)
return Math.round(
(alpha * (el.offsetHeight - thumb.value.offsetHeight / 2)) / 100
)
}
function getBackground() {
@ -104,13 +118,27 @@ export default defineComponent({
left = Math.max(thumb.value.offsetWidth / 2, left)
left = Math.min(left, rect.width - thumb.value.offsetWidth / 2)
props.color.set('alpha', Math.round((left - thumb.value.offsetWidth / 2) / (rect.width - thumb.value.offsetWidth) * 100))
props.color.set(
'alpha',
Math.round(
((left - thumb.value.offsetWidth / 2) /
(rect.width - thumb.value.offsetWidth)) *
100
)
)
} else {
let top = event.clientY - rect.top
top = Math.max(thumb.value.offsetHeight / 2, top)
top = Math.min(top, rect.height - thumb.value.offsetHeight / 2)
props.color.set('alpha', Math.round((top - thumb.value.offsetHeight / 2) / (rect.height - thumb.value.offsetHeight) * 100))
props.color.set(
'alpha',
Math.round(
((top - thumb.value.offsetHeight / 2) /
(rect.height - thumb.value.offsetHeight)) *
100
)
)
}
}
@ -123,10 +151,10 @@ export default defineComponent({
// mounded
onMounted(() => {
const dragConfig = {
drag: event => {
drag: (event) => {
handleDrag(event)
},
end: event => {
end: (event) => {
handleDrag(event)
},
}

View File

@ -6,14 +6,21 @@
class="el-color-hue-slider__thumb"
:style="{
left: thumbLeft + 'px',
top: thumbTop + 'px'
top: thumbTop + 'px',
}"
></div>
</div>
</template>
<script lang="ts">
import { ref, computed, watch, onMounted, getCurrentInstance, defineComponent } from 'vue'
import {
ref,
computed,
watch,
onMounted,
getCurrentInstance,
defineComponent,
} from 'vue'
import draggable from '../draggable'
import type { PropType } from 'vue'
@ -43,9 +50,12 @@ export default defineComponent({
return props.color.get('hue')
})
// watch
watch(() => hueValue.value, () => {
watch(
() => hueValue.value,
() => {
update()
})
}
)
// methods
function handleClick(event: Event) {
const target = event.target
@ -64,13 +74,21 @@ export default defineComponent({
left = Math.min(left, rect.width - thumb.value.offsetWidth / 2)
left = Math.max(thumb.value.offsetWidth / 2, left)
hue = Math.round((left - thumb.value.offsetWidth / 2) / (rect.width - thumb.value.offsetWidth) * 360)
hue = Math.round(
((left - thumb.value.offsetWidth / 2) /
(rect.width - thumb.value.offsetWidth)) *
360
)
} else {
let top = event.clientY - rect.top
top = Math.min(top, rect.height - thumb.value.offsetHeight / 2)
top = Math.max(thumb.value.offsetHeight / 2, top)
hue = Math.round((top - thumb.value.offsetHeight / 2) / (rect.height - thumb.value.offsetHeight) * 360)
hue = Math.round(
((top - thumb.value.offsetHeight / 2) /
(rect.height - thumb.value.offsetHeight)) *
360
)
}
props.color.set('hue', hue)
}
@ -81,7 +99,9 @@ export default defineComponent({
const hue = props.color.get('hue')
if (!el) return 0
return Math.round(hue * (el.offsetWidth - thumb.value.offsetWidth / 2) / 360)
return Math.round(
(hue * (el.offsetWidth - thumb.value.offsetWidth / 2)) / 360
)
}
function getThumbTop() {
@ -90,7 +110,9 @@ export default defineComponent({
const hue = props.color.get('hue')
if (!el) return 0
return Math.round(hue * (el.offsetHeight - thumb.value.offsetHeight / 2) / 360)
return Math.round(
(hue * (el.offsetHeight - thumb.value.offsetHeight / 2)) / 360
)
}
function update() {
thumbLeft.value = getThumbLeft()
@ -99,10 +121,10 @@ export default defineComponent({
// mounded
onMounted(() => {
const dragConfig = {
drag: event => {
drag: (event) => {
handleDrag(event)
},
end: event => {
end: (event) => {
handleDrag(event)
},
}

View File

@ -8,8 +8,7 @@
:class="{ selected: item.selected, 'is-alpha': item._alpha < 100 }"
@click="handleSelect(index)"
>
<div :style="{backgroundColor: item.value}">
</div>
<div :style="{ backgroundColor: item.value }"></div>
</div>
</div>
</div>
@ -36,14 +35,17 @@ export default defineComponent({
const rgbaColors = ref(parseColors(props.colors, props.color))
//watch
watch(() => currentColor.value, val => {
watch(
() => currentColor.value,
(val) => {
const color = new Color()
color.fromString(val)
rgbaColors.value.forEach(item => {
rgbaColors.value.forEach((item) => {
item.selected = color.compare(item)
})
})
}
)
watchEffect(() => {
rgbaColors.value = parseColors(props.colors, props.color)
})
@ -52,7 +54,7 @@ export default defineComponent({
props.color.fromString(props.colors[index])
}
function parseColors(colors, color) {
return colors.map(value => {
return colors.map((value) => {
const c = new Color()
c.enableAlpha = true
c.format = 'rgba'

View File

@ -2,7 +2,7 @@
<div
class="el-color-svpanel"
:style="{
backgroundColor: background
backgroundColor: background,
}"
>
<div class="el-color-svpanel__white"></div>
@ -11,7 +11,7 @@
class="el-color-svpanel__cursor"
:style="{
top: cursorTop + 'px',
left: cursorLeft + 'px'
left: cursorLeft + 'px',
}"
>
<div></div>
@ -20,7 +20,14 @@
</template>
<script lang="ts">
import { defineComponent, ref, computed, watch, getCurrentInstance, onMounted } from 'vue'
import {
defineComponent,
ref,
computed,
watch,
getCurrentInstance,
onMounted,
} from 'vue'
import draggable from '../draggable'
import type { PropType } from 'vue'
@ -53,10 +60,10 @@ export default defineComponent({
const value = props.color.get('value')
const el = instance.vnode.el
let { clientWidth: width, clientHeight: height } = el
const { clientWidth: width, clientHeight: height } = el
cursorLeft.value = saturation * width / 100
cursorTop.value = (100 - value) * height / 100
cursorLeft.value = (saturation * width) / 100
cursorTop.value = ((100 - value) * height) / 100
background.value = 'hsl(' + props.color.get('hue') + ', 100%, 50%)'
}
@ -76,21 +83,24 @@ export default defineComponent({
cursorLeft.value = left
cursorTop.value = top
props.color.set({
saturation: left / rect.width * 100,
value: 100 - top / rect.height * 100,
saturation: (left / rect.width) * 100,
value: 100 - (top / rect.height) * 100,
})
}
// watch
watch(() => colorValue.value, () => {
watch(
() => colorValue.value,
() => {
update()
})
}
)
// mounted
onMounted(() => {
draggable(instance.vnode.el as HTMLElement, {
drag: event => {
drag: (event) => {
handleDrag(event)
},
end: event => {
end: (event) => {
handleDrag(event)
},
})

View File

@ -16,12 +16,7 @@
<template #default>
<div v-click-outside="hide">
<div class="el-color-dropdown__main-wrapper">
<hue-slider
ref="hue"
class="hue-slider"
:color="color"
vertical
/>
<hue-slider ref="hue" class="hue-slider" :color="color" vertical />
<sv-panel ref="svPanel" :color="color" />
</div>
<alpha-slider v-if="showAlpha" ref="alpha" :color="color" />
@ -65,21 +60,30 @@
:class="[
'el-color-picker',
colorDisabled ? 'is-disabled' : '',
colorSize ? `el-color-picker--${ colorSize }` : ''
colorSize ? `el-color-picker--${colorSize}` : '',
]"
>
<div v-if="colorDisabled" class="el-color-picker__mask"></div>
<div class="el-color-picker__trigger" @click="handleTrigger">
<span class="el-color-picker__color" :class="{ 'is-alpha': showAlpha }">
<span
class="el-color-picker__color"
:class="{ 'is-alpha': showAlpha }"
>
<span
class="el-color-picker__color-inner"
:style="{
backgroundColor: displayedColor
backgroundColor: displayedColor,
}"
></span>
<span v-if="!modelValue && !showPanelColor" class="el-color-picker__empty el-icon-close"></span>
<span
v-if="!modelValue && !showPanelColor"
class="el-color-picker__empty el-icon-close"
></span>
</span>
<span v-show="modelValue || showPanelColor" class="el-color-picker__icon el-icon-arrow-down"></span>
<span
v-show="modelValue || showPanelColor"
class="el-color-picker__icon el-icon-arrow-down"
></span>
</div>
</div>
</template>
@ -87,7 +91,17 @@
</template>
<script lang="ts">
import { computed, defineComponent, inject, nextTick, onMounted, provide, reactive, ref, watch } from 'vue'
import {
computed,
defineComponent,
inject,
nextTick,
onMounted,
provide,
reactive,
ref,
watch,
} from 'vue'
import debounce from 'lodash/debounce'
import ElButton from '@element-plus/components/button'
import { ClickOutside } from '@element-plus/directives'
@ -148,10 +162,12 @@ export default defineComponent({
const alpha = ref(null)
const popper = ref(null)
// data
const color = reactive(new Color({
const color = reactive(
new Color({
enableAlpha: props.showAlpha,
format: props.colorFormat,
}))
})
)
const showPicker = ref(false)
const showPanelColor = ref(false)
const customInput = ref('')
@ -173,24 +189,33 @@ export default defineComponent({
return !props.modelValue && !showPanelColor.value ? '' : color.value
})
// watch
watch(() => props.modelValue, newVal => {
watch(
() => props.modelValue,
(newVal) => {
if (!newVal) {
showPanelColor.value = false
} else if (newVal && newVal !== color.value) {
color.fromString(newVal)
}
})
watch(() => currentColor.value, val => {
}
)
watch(
() => currentColor.value,
(val) => {
customInput.value = val
emit('active-change', val)
// showPanelColor.value = true
})
}
)
watch(() => color.value, () => {
watch(
() => color.value,
() => {
if (!props.modelValue && !showPanelColor.value) {
showPanelColor.value = true
}
})
}
)
// methods
function displayedRgb(color, showAlpha) {
@ -269,13 +294,16 @@ export default defineComponent({
customInput.value = currentColor.value
}
})
watch(() => showPicker.value, () => {
watch(
() => showPicker.value,
() => {
nextTick(() => {
hue.value?.update()
svPanel.value?.update()
alpha.value?.update()
})
})
}
)
provide<IUseOptions>(OPTIONS_KEY, {
currentColor,

View File

@ -62,28 +62,28 @@ describe('config-provider', () => {
it('should provide locale properly', async () => {
expect(wrapper.find('.current-locale').text()).toBe(
English.el.popconfirm.confirmButtonText,
English.el.popconfirm.confirmButtonText
)
expect(wrapper.find('.opposite-locale').text()).toBe(
Chinese.el.popconfirm.confirmButtonText,
Chinese.el.popconfirm.confirmButtonText
)
})
it('should reactively update the text on page', async () => {
expect(wrapper.find('.current-locale').text()).toBe(
English.el.popconfirm.confirmButtonText,
English.el.popconfirm.confirmButtonText
)
expect(wrapper.find('.opposite-locale').text()).toBe(
Chinese.el.popconfirm.confirmButtonText,
Chinese.el.popconfirm.confirmButtonText
)
await wrapper.find('.to-zh').trigger('click')
expect(wrapper.find('.current-locale').text()).toBe(
Chinese.el.popconfirm.confirmButtonText,
Chinese.el.popconfirm.confirmButtonText
)
expect(wrapper.find('.opposite-locale').text()).toBe(
English.el.popconfirm.confirmButtonText,
English.el.popconfirm.confirmButtonText
)
})
})

View File

@ -10,10 +10,8 @@ export const ConfigProvider = defineComponent({
},
setup(_, { slots }) {
useLocale()
return () => slots.default()
},
})

Some files were not shown because too many files have changed in this diff Show More