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,16 +6,14 @@
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.
- Antes de someter un informe sobre algún problema, sírvase de revisar sí ya hubo un informe.
- Antes de someter un informe sobre algún problema, sírvase de revisar sí ya hubo un informe.
- 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í.
@ -28,9 +26,8 @@ Estamos orgullosos de que usted esta interesado en contribuir al proyecto `Eleme
- “Rebase” antes de crear un “pull request (PR)” para mantener la historia de “commits” limpia.
- Asegúrese que sus PRs se refrieran a la rama `dev` y no a la rama `master`.
- Asegúrese que sus PRs se refrieran a la rama `dev` y no a la rama `master`.
- 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,14 +2,14 @@
### 1.1.0-beta.8
*2021-08-31*
_2021-08-31_
#### Breaking changes:
- Please refer to: [Breaking changes made in 1.1.0-beta.1](https://github.com/element-plus/element-plus/discussions/3020)
#### Features
- Components form add scrollToField method (#3110 by @sxzz)
- Components select-v2 support filter-method & remote-search (#3092 by @msidolphin)
- Components button-group add size (#3098 by @sxzz)
@ -18,7 +18,7 @@
- Menu add ellipsis in horizontal mode (#3083 by @kooriookami)
#### Bug fixes
- Theme-chalk add the missing margin-left to transfer
- Remove useless and duplicate declaration (#3082 by @BeADre)
- Components image no emit switch event (close #3132) (#3134 by @imswk)
@ -36,25 +36,25 @@
- [image-viewer] two-finger zoom in and out (#3114 by @Alanscut)
#### Refactors
- Enhance type definition (#3062 by @sxzz)
### 1.1.0-beta.7
*2021-08-26*
_2021-08-26_
#### Bug fixes
- Components style path error for radio button (#3079 by @JeremyWuuuuu)
- Input autosize type problem (#3008 by @imswk) (#3012)
- Build marking side effects for webpack importing styles
### 1.1.0-beta.6
*2021-08-26*
_2021-08-26_
#### Bug fixes
- Components [table] avoid table border style conflict (#3064 by @adaex)
- Eslint ignore bundle file (#3061 by @sxzz)
- Form el-from/src/token.d.ts file miss (#2979 by @imswk) (#2988)
@ -63,14 +63,14 @@
### 1.1.0-beta.5
*2021-08-25*
_2021-08-25_
#### Features
- Select-v2 support allow-create feature (#3017 by @msidolphin)
#### Bug fixes
- Missing folders and umd locale builds and i18n docs for umd builds (#3053 by @JeremyWuuuuu)
- Rateuse constant (#3011 by @Notryag)
- Stop pinning vue peer dependency version (#3051 by @sodatea)
@ -81,47 +81,46 @@
### 1.1.0-beta.4
*2021-08-25*
_2021-08-25_
#### Bug fixes
- Message-box fix type (#3038 by @sxzz)
- [label-wrap] Fix misspellings (#3030 by @wangbincyzj)
- Components [descriptions] avoid conflict with el-table (#3005 by @adaex)
- Build exposing installer function and version for umd build (#3041 by @JeremyWuuuuu)
#### Refactors
- Rename el-submenu to el-sub-menu (#3037 by @sxzz)
### 1.1.0-beta.3
*2021-08-24*
_2021-08-24_
#### Bug fixes
- Build fix rollup full bundile issue (#3034 by @JeremyWuuuuu)
### 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
- Date-picker custom date-editor width (#2836 by @YunYouJun)
- Card add var background-color & prepare for dark mode (#2912 by @YunYouJun)
- Link remove href prototype when empty (#2969 by @adaex)
- Add ElSkeleton template scoped parameter `key`. (#2944 by @callmesoul)
- Add ElSkeleton template scoped parameter `key`. (#2944 by @callmesoul)
#### Bug fixes
@ -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
@ -484,14 +486,14 @@
- Fix checkbox label auto convert to bool when label is empty (#2287)
- Fix update peerDependencies vue version (#2352)
- Fix virtual-list template string to render function (#2388)
- Fix code space (#2376)
- Fix code space (#2376)
- Fix cascader arrow position (#2356)
- Fix carousel not working when using v-show (#2361)
- Revert fix scrollbar update when slot changed (#2322)
### 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,25 +747,26 @@
### 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
- Fix input prefix-icon-incorrect-height (#1766)
- Fix select fix filter method is not called when input first letter (#1711)
- Fix select fix filter method is not called when input first letter (#1711)
- Fix picker add focus (#1475)
- Fix time-select input value not changed with v-model bindings (#1725)
- Fix tooltip component tabindex prop (#1621)
@ -762,12 +774,12 @@
- Fix utils isIE logic (#1757)
- Refactor card remove needless div tag (#1732)
- Fix input add keydown event listener for textarea (#1723)
- Fix time-picker update oldValue when visible change (#1635)
- Fix time-picker update oldValue when visible change (#1635)
- Fix drawer close button outline issue when focusing (#1727)
### 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,11 +851,11 @@
### 1.0.2-beta.33
*2021-03-03*
_2021-03-03_
#### Bug fixes
- Fix cascader-panel value can be falsy value (#1533)
- Fix cascader-panel value can be falsy value (#1533)
- Fix scss error (#1542)
- Fix popper error cause by vue version update (#1556)
- Fix col will always be hidden when responsive span is zero (#1532)
@ -851,7 +863,7 @@
- Fix layout gutter bug (#1537)
- Fix select options watch flush post (#1513)
- Fix select noMatchText error show (#1523)
- Fix avatar need reset hasLoadError to false, if src changed (#1515)
- Fix avatar need reset hasLoadError to false, if src changed (#1515)
- Fix input remove invalid attrs after render (#1489)
- Fix select emit blur event (#1504)
- Fix table bug of invalid tooltipEffect prop (#1470)
@ -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,22 +14,22 @@ 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
advances
* 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
professional setting
- 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
address, without explicit permission
- 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

@ -7,35 +7,39 @@ import { buildOutput } from './paths'
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',
],
moduleId: name,
}, cb)
const transform = function (filename, name, cb) {
require('@babel/core').transformFile(
resolve(localePath, filename),
{
plugins: ['@babel/plugin-transform-modules-umd'],
moduleId: name,
},
cb
)
}
fileList
.filter(function(file) {
.filter(function (file) {
return /\.ts$/.test(file)
})
.forEach(function(file) {
.forEach(function (file) {
const name = basename(file, '.ts')
transform(file, name, function(err, result) {
transform(file, name, function (err, result) {
if (err) {
console.error(err)
} 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,29 +42,33 @@ 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
}
; (async () => {
// run type diagnoses first
yellow('Start building types for individual components')
await genDefs(compRoot)
green('Typing generated successfully')
;(async () => {
// run type diagnoses first
yellow('Start building types for individual components')
await genDefs(compRoot)
green('Typing generated successfully')
yellow('Start building individual components')
await buildComponents()
green('Components built successfully')
yellow('Start building individual components')
await buildComponents()
green('Components built successfully')
yellow('Start building entry file')
await buildEntry()
green('Entry built successfully')
})().then(() => {
yellow('Start building entry file')
await buildEntry()
green('Entry built successfully')
})()
.then(() => {
console.log('Individual component build finished')
process.exit(0)
}).catch((e) => {
})
.catch((e) => {
console.error(e.message)
process.exit(1)
})
@ -72,55 +76,53 @@ const pathsRewriter = id => {
async function buildComponents() {
const componentPaths = await getComponents()
const builds = componentPaths.map(async ({
path: p,
name: componentName,
}) => {
const entry = path.resolve(p, './index.ts')
if (!fs.existsSync(entry)) return
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))
}
const esm = {
format: 'es',
file: `${outputDir}/es/components/${componentName}/index.js`,
plugins: [
filesize({
reporter,
})
],
paths: pathsRewriter,
}
const external = (id) => {
return (
id.startsWith(VUE_REGEX) ||
id.startsWith(VUE_MONO) ||
id.startsWith(EP_PREFIX) ||
externals.some((i) => id.startsWith(i))
)
}
const esm = {
format: 'es',
file: `${outputDir}/es/components/${componentName}/index.js`,
plugins: [
filesize({
reporter,
}),
],
paths: pathsRewriter,
}
const cjs = {
format: 'cjs',
file: `${outputDir}/lib/components/${componentName}/index.js`,
exports: 'named',
plugins: [
filesize({
reporter,
})
],
paths: pathsRewriter,
const cjs = {
format: 'cjs',
file: `${outputDir}/lib/components/${componentName}/index.js`,
exports: 'named',
plugins: [
filesize({
reporter,
}),
],
paths: pathsRewriter,
}
const rollupConfig = {
input: entry,
plugins,
external,
}
const bundle = await rollup.rollup(rollupConfig)
await bundle.write(esm as any)
await bundle.write(cjs as any)
}
const rollupConfig = {
input: entry,
plugins,
external,
}
const bundle = await rollup.rollup(rollupConfig)
await bundle.write(esm as any)
await bundle.write(cjs as any)
})
)
try {
await Promise.all(
builds
)
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 => {
return f.name !== 'package.json' && f.name !== 'README.md'
}).map(f => path.resolve(epRoot, f.name))
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))
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

@ -1,13 +1,11 @@
import through2 from 'through2'
const rewriter = (rewriteTo = '../..') => {
return through2.obj(function(file, _, cb) {
return through2.obj(function (file, _, cb) {
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

@ -13,7 +13,7 @@ const tsProject = ts.createProject('tsconfig.json', {
})
const rewriter = () => {
return through2.obj(function(file, _, cb) {
return through2.obj(function (file, _, cb) {
const compIdentifier = new RegExp('@element-plus/components', 'g')
const compReplacer = '../../../components'
const themeIdentifier = new RegExp('@element-plus/theme-chalk', 'g')
@ -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,34 +17,37 @@ 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: [{
format: 'es',
file: getOutFile(name, 'es'),
paths(id) {
if (/^@element-plus/.test(id)) {
if (noElPrefixFile.test(id)) return id.replace('@element-plus', '..')
return id.replace('@element-plus/', '../el-')
}
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) {
if (/^@element-plus/.test(id)) {
if (noElPrefixFile.test(id)) return id.replace('@element-plus', '..')
return id.replace('@element-plus/', '../el-')
}
},
},
}, {
format: 'cjs',
file: getOutFile(name, 'lib'),
exports: 'named',
paths(id) {
if (/^@element-plus/.test(id)) {
if (noElPrefixFile.test(id)) return id.replace('@element-plus', '..')
return id.replace('@element-plus/', '../el-')
}
{
format: 'cjs',
file: getOutFile(name, 'lib'),
exports: 'named',
paths(id) {
if (/^@element-plus/.test(id)) {
if (noElPrefixFile.test(id)) return id.replace('@element-plus', '..')
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,18 +24,17 @@ export default async function reporter(opt, outputOptions, info) {
[`${info.bundleSize}`],
// ),
...(info.minSize
?
// info.minSizeBefore
// ? [
// `${title('Minified Size: ')} ${value(info.minSize)} (was ${value(
// info.minSizeBefore,
// )}${info.lastVersion
// ? ` in version ${info.lastVersion}`
// : ' in last build'
// })`,
// ]
// :
[`${info.minSize}`]
? // info.minSizeBefore
// ? [
// `${title('Minified Size: ')} ${value(info.minSize)} (was ${value(
// info.minSizeBefore,
// )}${info.lastVersion
// ? ` in version ${info.lastVersion}`
// : ' in last build'
// })`,
// ]
// :
[`${info.minSize}`]
: []),
// ...(info.gzipSize
// ? info.gzipSizeBefore
@ -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,21 +8,22 @@ 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')))
; (async () => {
console.log(
chalk.cyan(
['NOTICE:', `$TAG_VERSION: ${tagVersion}`, `$GIT_HEAD: ${gitHead}`].join(
'\n'
)
)
)
;(async () => {
console.log(chalk.yellow(`Updating package.json for element-plus`))
const pkgJson = path.resolve(epRoot, './package.json')

View File

@ -23,15 +23,17 @@ const plugins = [
const entry = path.resolve(__dirname, '../packages/element-plus/index.ts')
if (!isFullMode) {
externals.push({
'@popperjs/core': '@popperjs/core',
'async-validator': 'async-validator',
'mitt': 'mitt',
'normalize-wheel': 'normalize-wheel',
'resize-observer-polyfill': 'resize-observer-polyfill',
},
/^dayjs.*/,
/^lodash.*/)
externals.push(
{
'@popperjs/core': '@popperjs/core',
'async-validator': 'async-validator',
mitt: 'mitt',
'normalize-wheel': 'normalize-wheel',
'resize-observer-polyfill': 'resize-observer-polyfill',
},
/^dayjs.*/,
/^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,10 +6,9 @@ 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 () => {
;(async () => {
const { filePaths, workerId, outDir } = workerData
let messagePort
parentPort.once('message', async ({ port }) => {
@ -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,36 +31,38 @@ const TSCONFIG_PATH = path.resolve(process.cwd(), 'tsconfig.dts.json')
const sourceFiles = []
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)
const { script, scriptSetup } = sfc.descriptor
if (script || scriptSetup) {
let content = ''
let isTS = false
if (script && script.content) {
content += script.content
if (script.lang === 'ts') isTS = true
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)
const { script, scriptSetup } = sfc.descriptor
if (script || scriptSetup) {
let content = ''
let isTS = false
if (script && script.content) {
content += script.content
if (script.lang === 'ts') isTS = true
}
if (scriptSetup) {
const compiled = vueCompiler.compileScript(sfc.descriptor, {
id: 'xxx',
})
content += compiled.content
if (scriptSetup.lang === 'ts') isTS = true
}
const sourceFile = project.createSourceFile(
path.relative(process.cwd(), file) + (isTS ? '.ts' : '.js'),
content
)
sourceFiles.push(sourceFile)
}
if (scriptSetup) {
const compiled = vueCompiler.compileScript(sfc.descriptor, {
id: 'xxx',
})
content += compiled.content
if (scriptSetup.lang === 'ts') isTS = true
}
const sourceFile = project.createSourceFile(
path.relative(process.cwd(), file) + (isTS ? '.ts' : '.js'),
content,
)
} else if (file.endsWith('.ts')) {
const sourceFile = project.addSourceFileAtPath(file)
sourceFiles.push(sourceFile)
}
} else if (file.endsWith('.ts')) {
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({
components: {
'el-affix': Affix,
},
template,
}, { attachTo: document.body })
const _mount = (template: string) =>
mount(
{
components: {
'el-affix': Affix,
},
template,
},
{ 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,18 +36,22 @@ describe('Affix.vue', () => {
<el-affix>${AXIOM}</el-affix>
`)
expect(wrapper.text()).toEqual(AXIOM)
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({
height: 200,
width: 1000,
top: 0,
bottom: 200,
} as DOMRect)
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({
height: 200,
width: 1000,
top: 0,
bottom: 200,
} as DOMRect)
expect(wrapper.find('.el-affix--fixed').exists()).toBe(false)
await makeScroll(document.documentElement, 'scrollTop', 200)
expect(wrapper.find('.el-affix--fixed').exists()).toBe(true)
@ -50,21 +63,27 @@ describe('Affix.vue', () => {
const wrapper = _mount(`
<el-affix :offset="30">${AXIOM}</el-affix>
`)
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({
height: 200,
width: 1000,
top: 0,
bottom: 200,
} as DOMRect)
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({
height: 200,
width: 1000,
top: 0,
bottom: 200,
} 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,21 +93,27 @@ describe('Affix.vue', () => {
<el-affix position="bottom" :offset="20">${AXIOM}</el-affix>
`)
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({
height: 200,
width: 1000,
top: 0,
bottom: 200,
} as DOMRect)
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({
height: 200,
width: 1000,
top: 0,
bottom: 200,
} 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,18 +126,22 @@ describe('Affix.vue', () => {
<div style="height: 1000px"></div>
`)
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({
height: 200,
width: 1000,
top: -100,
bottom: 100,
} as DOMRect)
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({
height: 200,
width: 1000,
top: -100,
bottom: 100,
} as DOMRect)
await makeScroll(document.documentElement, 'scrollTop', 100)
expect(wrapper.find('.el-affix--fixed').exists()).toBe(true)
mockAffixRect.mockReturnValue({
@ -137,21 +166,27 @@ 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({
height: 40,
width: 1000,
top: -100,
bottom: -80,
} as DOMRect)
const mockDocumentRect = jest.spyOn(document.documentElement, 'getBoundingClientRect').mockReturnValue({
height: 200,
width: 1000,
top: 0,
bottom: 200,
} as DOMRect)
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({
height: 200,
width: 1000,
top: 0,
bottom: 200,
} 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

@ -1,14 +1,25 @@
<template>
<div ref="root" class="el-affix" :style="rootStyle">
<div :class="{'el-affix--fixed': state.fixed}" :style="affixStyle">
<div :class="{ 'el-affix--fixed': state.fixed }" :style="affixStyle">
<slot></slot>
</div>
</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, () => {
emit('change', 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'],
@ -76,13 +88,19 @@ export default defineComponent({
const visible = ref(true)
// 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 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' : ''
)
// methods
const close = evt => {
const close = (evt) => {
visible.value = false
ctx.emit('close', evt)
}

View File

@ -6,28 +6,35 @@ jest.unmock('lodash/debounce')
import Autocomplete from '../src/index.vue'
const _mount = (payload = {}) => mount({
components: {
'el-autocomplete': Autocomplete,
},
data() {
return {
state: '',
list: [
{ value: 'Java', tag: 'java' },
{ value: 'Go', tag: 'go' },
{ value: 'JavaScript', tag: 'javascript' },
{ value: 'Python', tag: 'python' },
],
payload,
}
},
methods: {
querySearch(queryString, cb) {
cb(queryString ? this.list.filter(i => i.value.indexOf(queryString.toLowerCase()) === 0) : this.list)
const _mount = (payload = {}) =>
mount({
components: {
'el-autocomplete': Autocomplete,
},
},
template: `
data() {
return {
state: '',
list: [
{ value: 'Java', tag: 'java' },
{ value: 'Go', tag: 'go' },
{ value: 'JavaScript', tag: 'javascript' },
{ value: 'Python', tag: 'python' },
],
payload,
}
},
methods: {
querySearch(queryString, cb) {
cb(
queryString
? this.list.filter(
(i) => i.value.indexOf(queryString.toLowerCase()) === 0
)
: this.list
)
},
},
template: `
<el-autocomplete
ref="autocomplete"
v-model="state"
@ -35,7 +42,7 @@ const _mount = (payload = {}) => mount({
v-bind="payload"
/>
`,
})
})
describe('Autocomplete.vue', () => {
beforeEach(() => {
@ -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>
@ -56,7 +56,7 @@ export default defineComponent({
const src = toRef(props, 'src')
// need reset hasLoadError to false if src changed
watch(src,()=>{
watch(src, () => {
hasLoadError.value = false
})
@ -77,23 +77,31 @@ export default defineComponent({
const sizeStyle = computed(() => {
const { size } = props
return typeof size === 'number' ? {
height: `${size}px`,
width: `${size}px`,
lineHeight: `${size}px`,
} : {}
return typeof size === 'number'
? {
height: `${size}px`,
width: `${size}px`,
lineHeight: `${size}px`,
}
: {}
})
const fitStyle = computed(() => ({
objectFit: props.fit,
}) as CSSProperties)
const fitStyle = computed(
() =>
({
objectFit: props.fit,
} 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({
components: {
'el-backtop': Backtop,
},
template,
}, { attachTo: document.body })
const _mount = (template: string) =>
mount(
{
components: {
'el-backtop': Backtop,
},
template,
},
{ 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({
components: {
'el-breadcrumb': Breadcrumb,
'el-breadcrumb-item': BreadcrumbItem,
},
template,
}, {
global: {
provide: {
breadcrumb: {},
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', () => {
@ -127,22 +125,31 @@ describe('Button Group', () => {
it('button group reactive size', async () => {
const size = ref('small')
const wrapper = mount({
setup(){
return () => h(ButtonGroup, { size: size.value }, () => [
h(Button, { type: 'primary' }, () => 'Prev'),
h(Button, { type: 'primary' }, () => 'Next'),
h(Button, { type: 'primary', size :'mini' }, () => 'Mini'),
])
setup() {
return () =>
h(ButtonGroup, { size: size.value }, () => [
h(Button, { type: 'primary' }, () => 'Prev'),
h(Button, { type: 'primary' }, () => 'Next'),
h(Button, { type: 'primary', size: 'mini' }, () => 'Mini'),
])
},
})
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

@ -19,10 +19,13 @@ export default defineComponent({
validator: isValidComponentSize,
},
},
setup(props){
provide(elButtonGroupKey, reactive({
size: toRef(props,'size'),
}))
setup(props) {
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,34 +2,44 @@ import { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
import Calendar from '../src/index.vue'
const _mount = (template: string, data?, otherObj?) => mount({
components: {
'el-calendar': Calendar,
},
template,
data,
...otherObj,
})
const _mount = (template: string, data?, otherObj?) =>
mount({
components: {
'el-calendar': Calendar,
},
template,
data,
...otherObj,
})
describe('Calendar.vue', () => {
it('create', async() => {
const wrapper = _mount(`
it('create', async () => {
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,68 +63,92 @@ 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() => {
it('range tow monthes', async () => {
const wrapper = _mount(`
<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()
})
// https://github.com/element-plus/element-plus/issues/3155
it('range tow monthes when the start date will be calculated to last month', async() => {
it('range tow monthes when the start date will be calculated to last month', async () => {
const wrapper = _mount(`
<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) => ({
text: start.date() + index,
type: 'current',
}))
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,42 +133,58 @@ 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')
const lastMonth = lastDay.get('month')
// Current mouth
if(firstMonth === lastMonth){
if (firstMonth === lastMonth) {
return [[firstDay, lastDay]]
}
// Two adjacent months
else if(firstMonth + 1 === lastMonth ){
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 ){
else if (firstMonth + 2 === lastMonth) {
const firstMonthLastDay = firstDay.endOf('month')
const secondMonthFisrtDay = firstDay.add(1,'month').startOf('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

@ -10,7 +10,7 @@
<button
v-show="
(arrow === 'always' || data.hover) &&
(props.loop || data.activeIndex > 0)
(props.loop || data.activeIndex > 0)
"
type="button"
class="el-carousel__arrow el-carousel__arrow--left"
@ -25,7 +25,7 @@
<button
v-show="
(arrow === 'always' || data.hover) &&
(props.loop || data.activeIndex < items.length - 1)
(props.loop || data.activeIndex < items.length - 1)
"
type="button"
class="el-carousel__arrow el-carousel__arrow--right"
@ -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,10 +248,10 @@ 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()
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,22 +97,22 @@ const RADIO = '.el-radio__input'
let id = 0
const _mount: typeof mount = options => mount({
components: {
CascaderPanel,
},
...options,
})
const _mount: typeof mount = (options) =>
mount({
components: {
CascaderPanel,
},
...options,
})
const lazyLoad = (node, resolve) => {
const { level } = node
setTimeout(() => {
const nodes = Array.from({ length: level + 1 })
.map(() => ({
value: ++id,
label: `option${id}`,
leaf: level >= 1,
}))
const nodes = Array.from({ length: level + 1 }).map(() => ({
value: ++id,
label: `option${id}`,
leaf: level >= 1,
}))
resolve(nodes)
}, 1000)
}
@ -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,12 +589,11 @@ describe('CascaderPanel.vue', () => {
lazyLoad(node, resolve) {
const { level } = node
setTimeout(() => {
const nodes = Array.from({ length: level + 1 })
.map(() => ({
value: { id: ++id },
label: `option${id}`,
leaf: level >= 1,
}))
const nodes = Array.from({ length: level + 1 }).map(() => ({
value: { id: ++id },
label: `option${id}`,
leaf: level >= 1,
}))
resolve(nodes)
}, 1000)
},
@ -619,18 +621,17 @@ describe('CascaderPanel.vue', () => {
props: {
multiple: true,
lazy: true,
lazyLoad (node, resolve) {
lazyLoad(node, resolve) {
const { level } = node
setTimeout(() => {
const nodes = Array.from({ length: level + 1 })
.map(() => {
++id
return {
value: id,
label: `option${id}`,
leaf: id === 3,
}
})
const nodes = Array.from({ length: level + 1 }).map(() => {
++id
return {
value: id,
label: `option${id}`,
leaf: id === 3,
}
})
resolve(nodes)
}, 1000)
},
@ -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 = () => {
@ -148,14 +148,18 @@ export default defineComponent({
newMenus.push(node.children)
}
if(expandingNode.value?.uid !== newExpandingNode?.uid) {
if (expandingNode.value?.uid !== newExpandingNode?.uid) {
expandingNode.value = node
menus.value = newMenus
!silent && emit('expand-change', node?.pathValues || [])
}
}
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,36 +308,41 @@ export default defineComponent({
}
}
provide(CASCADER_PANEL_INJECTION_KEY, reactive({
config,
expandingNode,
checkedNodes,
isHoverMenu,
renderLabelFn,
lazyLoad,
expandNode,
handleCheckChange,
}))
watch(
[config, () => props.options],
initStore,
{ deep: true, immediate: true },
provide(
CASCADER_PANEL_INJECTION_KEY,
reactive({
config,
expandingNode,
checkedNodes,
isHoverMenu,
renderLabelFn,
lazyLoad,
expandNode,
handleCheckChange,
})
)
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'
@ -60,7 +49,7 @@ export default defineComponent({
props: {
nodes: {
type: Array as PropType< CascaderNode[]>,
type: Array as PropType<CascaderNode[]>,
required: true,
},
index: {
@ -69,7 +58,7 @@ export default defineComponent({
},
},
setup (props) {
setup(props) {
const instance = getCurrentInstance()
const { t } = useLocaleInject()
const id = generateId()
@ -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
@ -74,11 +79,11 @@ class Node {
indeterminate = false
loading = false
constructor (
constructor(
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,35 +94,41 @@ 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 {
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 {
get isLeaf(): boolean {
const { data, config, childrenData, loaded } = this
const { lazy, leaf } = config
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
}
get valueByOption () {
get valueByOption() {
return this.config.emitPath ? this.pathValues : this.value
}
appendChild (childData: CascaderOption) {
appendChild(childData: CascaderOption) {
const { childrenData, children } = this
const node = new Node(childData, this.config, this)
@ -132,15 +143,15 @@ class Node {
return node
}
calcText (allLevels: boolean, separator: string) {
calcText(allLevels: boolean, separator: string) {
const text = allLevels ? this.pathLabels.join(separator) : this.label
this.text = text
return text
}
broadcast (event: string, ...args: unknown[]) {
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)
@ -149,7 +160,7 @@ class Node {
})
}
emit (event: string, ...args: unknown[]) {
emit(event: string, ...args: unknown[]) {
const { parent } = this
const handlerName = `onChild${capitalize(event)}`
if (parent) {
@ -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'
@ -85,7 +89,7 @@ export default defineComponent({
emits: ['expand'],
setup (props, { emit }) {
setup(props, { emit }) {
const panel = inject(CASCADER_PANEL_INJECTION_KEY)
const isHoverMenu = computed(() => panel.isHoverMenu)
@ -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) {
@ -27,18 +26,20 @@ export default class Store {
readonly allNodes: Node[]
readonly leafNodes: Node[]
constructor (data: CascaderOption[], readonly config: CascaderConfig) {
const nodes = (data || []).map(nodeData => new Node(nodeData, this.config))
constructor(data: CascaderOption[], readonly config: CascaderConfig) {
const nodes = (data || []).map(
(nodeData) => new Node(nodeData, this.config)
)
this.nodes = nodes
this.allNodes = flatNodes(nodes, false)
this.leafNodes = flatNodes(nodes, true)
}
getNodes () {
getNodes() {
return this.nodes
}
getFlattedNodes (leafOnly: boolean) {
getFlattedNodes(leafOnly: boolean) {
return leafOnly ? this.leafNodes : this.allNodes
}
@ -54,26 +55,30 @@ 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
}
getSameNode (node: Node): Nullable<Node> {
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({
components: {
Cascader,
},
...options,
}, {
attachTo: 'body',
})
const _mount: typeof mount = (options) =>
mount(
{
components: {
Cascader,
},
...options,
},
{
attachTo: 'body',
}
)
afterEach(() => {
document.body.innerHTML = ''
@ -79,7 +83,7 @@ describe('Cascader.vue', () => {
@expand-change="handleExpandChange"
/>
`,
data () {
data() {
return {
value: [],
options: OPTIONS,
@ -194,11 +198,14 @@ describe('Cascader.vue', () => {
:props="props"
/>
`,
data () {
data() {
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()
@ -237,7 +247,7 @@ describe('Cascader.vue', () => {
filterable
/>
`,
data () {
data() {
return {
options: OPTIONS,
value: [],
@ -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')
@ -272,7 +284,7 @@ describe('Cascader.vue', () => {
filterable
/>
`,
data () {
data() {
return {
options: OPTIONS,
props: { multiple: true },

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,15 +330,17 @@ 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)
: ''
})
const checkedValue = computed<CascaderValue>({
get () {
get() {
return props.modelValue
},
set (val) {
set(val) {
emit(UPDATE_MODEL_EVENT, val)
emit(CHANGE_EVENT, val)
elFormItem.formItemMitt?.emit('el.form.change', [val])
@ -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()
}
@ -484,7 +526,7 @@ export default defineComponent({
const handleSuggestionClick = (node: CascaderNode) => {
const { checked } = node
if (multiple.value ) {
if (multiple.value) {
panel.value.handleCheckChange(node, !checked, false)
} else {
!checked && panel.value.handleCheckChange(node, true, false)
@ -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,25 +15,27 @@ describe('CheckTag.vue', () => {
expect(wrapper.classes()).toContain('el-check-tag')
})
test('functionality', async () => {
const wrapper = mount({
template: `<el-check-tag @change="checked = !checked" :checked="checked">
const wrapper = mount(
{
template: `<el-check-tag @change="checked = !checked" :checked="checked">
${AXIOM}
</el-check-tag>`,
components: {
'el-check-tag': CheckTag,
components: {
'el-check-tag': CheckTag,
},
data() {
return {
checked: false,
}
},
},
data() {
return {
checked: false,
}
},
}, {
slots: {
default: AXIOM,
},
})
{
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,20 +4,28 @@ 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>({
components: {
'el-checkbox': Checkbox,
'el-checkbox-group': CheckboxGroup,
'el-checkbox-button': CheckboxButton,
},
template,
data,
...otherObj,
})
const _mount = <D>(
template: string,
data: () => D,
otherObj?: Record<string, unknown>
) =>
mount<D>({
components: {
'el-checkbox': Checkbox,
'el-checkbox-group': CheckboxGroup,
'el-checkbox-button': CheckboxButton,
},
template,
data,
...otherObj,
})
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,13 +163,13 @@ describe('Checkbox', () => {
() => ({
checked: false,
checklist: [],
}),
})
) as any
expect(wrapper.vm.checked).toBe(true)
expect(wrapper.vm.checklist).toEqual(['a'])
})
test('label', async() => {
test('label', async () => {
const wrapper = _mount(
`
<div>
@ -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 => {
elFormItem.formItemMitt?.emit('el.form.change', [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 => {
elFormItem.formItemMitt?.emit('el.form.change', [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

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

View File

@ -64,22 +64,24 @@ 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]}`)
else if(size > 0) ret.push(`el-col-${prop}-${props[prop]}`)
if (prop === 'span') ret.push(`el-col-${props[prop]}`)
else if (size > 0) ret.push(`el-col-${prop}-${props[prop]}`)
}
})
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,14 +94,15 @@ const ElCol = defineComponent({
return ret
})
return () => h(
props.tag,
{
class: ['el-col', classList.value],
style: style.value,
},
slots.default?.(),
)
return () =>
h(
props.tag,
{
class: ['el-col', classList.value],
style: style.value,
},
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

@ -1,7 +1,7 @@
<template>
<div
class="el-collapse-item"
:class="{'is-active': isActive, 'is-disabled': disabled }"
:class="{ 'is-active': isActive, 'is-disabled': disabled }"
>
<div
role="tab"
@ -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"
@ -26,7 +26,7 @@
<slot name="title">{{ title }}</slot>
<i
class="el-collapse-item__arrow el-icon-arrow-right"
:class="{'is-active': isActive}"
:class="{ 'is-active': isActive }"
>
</i>
</div>
@ -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'
@ -88,7 +88,7 @@ export default defineComponent({
const handleFocus = () => {
setTimeout(() => {
if(!isClick.value) {
if (!isClick.value) {
focusing.value = true
} else {
isClick.value = false
@ -97,7 +97,7 @@ export default defineComponent({
}
const handleHeaderClick = () => {
if(props.disabled) return
if (props.disabled) return
collapseMitt?.emit('item-click', props.name)
focusing.value = false
isClick.value = true

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'
@ -26,7 +20,7 @@ export default defineComponent({
accordion: Boolean,
modelValue: {
type: [Array, String, Number] as PropType<
string | number | Array<string | number>
string | number | Array<string | number>
>,
default: () => [],
},
@ -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({
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 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 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({
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 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,
})
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({
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 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,
})
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({
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 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,
})
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

@ -1,37 +1,37 @@
import { hasOwn } from '@vue/shared'
const hsv2hsl = function(hue: number, sat: number, val: number) {
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,
]
}
// Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
// <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
const isOnePointZero = function(n: unknown) {
const isOnePointZero = function (n: unknown) {
return typeof n === 'string' && n.indexOf('.') !== -1 && parseFloat(n) === 1
}
const isPercentage = function(n: unknown) {
const isPercentage = function (n: unknown) {
return typeof n === 'string' && n.indexOf('%') !== -1
}
// Take input from [0, n] and return it as [0, 1]
const bound01 = function(value: number | string, max: number | string) {
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
}
@ -41,8 +41,8 @@ const bound01 = function(value: number | string, max: number | string) {
const INT_HEX_MAP = { 10: 'A', 11: 'B', 12: 'C', 13: 'D', 14: 'E', 15: 'F' }
const toHex = function({ r, g, b }) {
const hexOne = function(value: number) {
const toHex = function ({ r, g, b }) {
const hexOne = function (value: number) {
value = Math.min(Math.round(value), 255)
const high = Math.floor(value / 16)
const low = value % 16
@ -56,15 +56,18 @@ const toHex = function({ r, g, b }) {
const HEX_INT_MAP = { A: 10, B: 11, C: 12, D: 13, E: 14, F: 15 }
const parseHexChannel = function(hex: string) {
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]
}
const hsl2hsv = function(hue: number, sat: number, light: number) {
const hsl2hsv = function (hue: number, sat: number, light: number) {
sat = sat / 100
light = light / 100
let smin = sat
@ -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,
@ -89,7 +93,7 @@ const hsl2hsv = function(hue: number, sat: number, light: number) {
// Converts an RGB color value to HSV
// *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
// *Returns:* { h, s, v } in [0,1]
const rgb2hsv = function(r, g, b) {
const rgb2hsv = function (r, g, b) {
r = bound01(r, 255)
g = bound01(g, 255)
b = bound01(b, 255)
@ -129,7 +133,7 @@ const rgb2hsv = function(r, g, b) {
// Converts an HSV color value to RGB.
// *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
// *Returns:* { r, g, b } in the set [0, 255]
const hsv2rgb = function(h, s, v) {
const hsv2rgb = function (h, s, v) {
h = bound01(h, 360) * 6
s = bound01(s, 100)
v = bound01(v, 100)
@ -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,30 +315,38 @@ 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: {
const { r, g, b } = hsv2rgb(_hue, _saturation, _value)
this.value = `rgba(${r}, ${g}, ${b}, ${ _alpha / 100 })`
this.value = `rgba(${r}, ${g}, ${b}, ${_alpha / 100})`
}
}
} else {
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':{
case 'rgb': {
const { r, g, b } = hsv2rgb(_hue, _saturation, _value)
this.value = `rgb(${r}, ${g}, ${b})`
break

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'), () => {
update()
})
watch(() => props.color.value, () => {
update()
})
watch(
() => props.color.get('alpha'),
() => {
update()
}
)
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, () => {
update()
})
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

@ -5,11 +5,10 @@
v-for="(item, index) in rgbaColors"
:key="colors[index]"
class="el-color-predefine__color-selector"
:class="{selected: item.selected, 'is-alpha': item._alpha < 100}"
: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 => {
const color = new Color()
color.fromString(val)
watch(
() => currentColor.value,
(val) => {
const color = new Color()
color.fromString(val)
rgbaColors.value.forEach(item => {
item.selected = color.compare(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, () => {
update()
})
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({
enableAlpha: props.showAlpha,
format: props.colorFormat,
}))
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 => {
if (!newVal) {
showPanelColor.value = false
} else if (newVal && newVal !== color.value) {
color.fromString(newVal)
watch(
() => props.modelValue,
(newVal) => {
if (!newVal) {
showPanelColor.value = false
} else if (newVal && newVal !== color.value) {
color.fromString(newVal)
}
}
})
watch(() => currentColor.value, val => {
customInput.value = val
emit('active-change', val)
// showPanelColor.value = true
})
)
watch(
() => currentColor.value,
(val) => {
customInput.value = val
emit('active-change', val)
// showPanelColor.value = true
}
)
watch(() => color.value, () => {
if (!props.modelValue && !showPanelColor.value) {
showPanelColor.value = true
watch(
() => color.value,
() => {
if (!props.modelValue && !showPanelColor.value) {
showPanelColor.value = true
}
}
})
)
// methods
function displayedRgb(color, showAlpha) {
@ -200,8 +225,8 @@ export default defineComponent({
const { r, g, b } = color.toRgb()
return showAlpha
? `rgba(${ r }, ${ g }, ${ b }, ${ color.get('alpha') / 100 })`
: `rgb(${ r }, ${ g }, ${ b })`
? `rgba(${r}, ${g}, ${b}, ${color.get('alpha') / 100})`
: `rgb(${r}, ${g}, ${b})`
}
function setShowPicker(value) {
@ -269,13 +294,16 @@ export default defineComponent({
customInput.value = currentColor.value
}
})
watch(() => showPicker.value, () => {
nextTick(() => {
hue.value?.update()
svPanel.value?.update()
alpha.value?.update()
})
})
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