diff --git a/.babelrc b/.babelrc index 8480d8968..343a8bc32 100644 --- a/.babelrc +++ b/.babelrc @@ -3,7 +3,8 @@ "plugins": [ "syntax-dynamic-import", "@ant-design-vue/babel-plugin-jsx", - "@babel/plugin-proposal-optional-chaining" + "@babel/plugin-proposal-optional-chaining", + "@babel/plugin-proposal-nullish-coalescing-operator" ], "env": { "utils": { diff --git a/README.md b/README.md index 99c7daa75..8eec2596f 100644 --- a/README.md +++ b/README.md @@ -4,21 +4,25 @@ 2. 组件加上白名单,比如加一个 'input',执行 `npm run dev`。 3. 访问文档(也可访问 http://localhost:8086/#/ ),在对应的组件页面调试报错即可,完成的记得标记。 4. 提交代码前请先拉取代码,commit 时信息格式为 key: content,如 `refactor: refactor alert`,注意表达简洁易懂。 + ## Install + ```js npm install element3 -S ``` + ## Quick Start + ```js -import "element3/lib/theme-chalk/index.css"; -import { createApp } from "vue"; -import Element3 from "element3"; -import App from "./App.vue"; +import 'element3/lib/theme-chalk/index.css' +import { createApp } from 'vue' +import Element3 from 'element3' +import App from './App.vue' -const app = createApp(App); -app.use(Element3); -app.mount("#app"); +const app = createApp(App) +app.use(Element3) +app.mount('#app') // or import { @@ -28,11 +32,16 @@ import { Vue.use(ElButton) ``` + > 注意暂时不要在生产坏境使用 ## export components + 目前已经导出可以使用的组件列表 + - ElButton +- ElSwitch +- ElProgress ## Join Discussion Group @@ -44,7 +53,6 @@ Scan the QR code using [Dingtalk App](https://www.dingtalk.com/) to join in disc [See Contributing Guide.](https://juejin.im/post/6864462363039531022) -

diff --git a/build/bin/build-entry.js b/build/bin/build-entry.js index 997ef8fb7..f333d560f 100644 --- a/build/bin/build-entry.js +++ b/build/bin/build-entry.js @@ -11,7 +11,7 @@ var INSTALL_COMPONENT_TEMPLATE = ' {{name}}' var MAIN_TEMPLATE = `/* Automatically generated by './build/bin/build-entry.js' */ {{include}} import locale from 'element-ui/src/locale'; - import CollapseTransition from 'element-ui/src/transitions/collapse-transition'; +import CollapseTransition from 'element-ui/src/transitions/collapse-transition'; const components = [ {{install}}, @@ -26,15 +26,15 @@ const install = function(app, opts = {}) { app.component(component.name, component); }); - // app.use(InfiniteScroll); - // app.use(Loading.directive); + app.use(InfiniteScroll); + app.use(Loading.directive); app.config.globalProperties.$ELEMENT = { size: opts.size || '', zIndex: opts.zIndex || 2000 }; - // app.config.globalProperties.$loading = Loading.service; + app.config.globalProperties.$loading = Loading.service; // app.config.globalProperties.$msgbox = MessageBox; // app.config.globalProperties.$alert = MessageBox.alert; // app.config.globalProperties.$confirm = MessageBox.confirm; @@ -56,7 +56,6 @@ export default { i18n: locale.i18n, install, CollapseTransition, - // Loading, {{list}} }; ` @@ -72,6 +71,8 @@ var listTemplate = [] ComponentNames.forEach((name) => { if ( [ + 'infinite-scroll', + 'loading', 'image', 'card', 'alert', @@ -111,12 +112,19 @@ ComponentNames.forEach((name) => { 'rate', 'divider', 'progress', + 'form', + 'form-item' + 'message', + 'pagination', 'notification', 'page-header', 'message', + 'timeline', + 'timeline-item', 'input-number', 'step', 'steps', + 'popconfirm', 'drawer', 'transfer' ].indexOf(name) > -1 @@ -148,7 +156,7 @@ ComponentNames.forEach((name) => { ) } - if (componentName !== 'Loading') listTemplate.push(` ${componentName}`) + listTemplate.push(` ${componentName}`) } else { } }) diff --git a/examples/docs/en-US/infiniteScroll.md b/examples/docs/en-US/infiniteScroll.md index 1e8657c0b..e23f1d1dc 100644 --- a/examples/docs/en-US/infiniteScroll.md +++ b/examples/docs/en-US/infiniteScroll.md @@ -7,7 +7,12 @@ Add `v-infinite-scroll` to the list to automatically execute loading method when :::demo ```html @@ -38,7 +43,8 @@ Add `v-infinite-scroll` to the list to automatically execute loading method when

Loading...

diff --git a/examples/docs/en-US/popconfirm.md b/examples/docs/en-US/popconfirm.md index 199627e22..023ddbf7f 100644 --- a/examples/docs/en-US/popconfirm.md +++ b/examples/docs/en-US/popconfirm.md @@ -9,11 +9,13 @@ Popconfirm is similar to Popover. So for some duplicated attributes, please refe :::demo Only `title` attribute is avaliable in Popconfirm, `content` will be ignored. ```html ```` ::: @@ -23,15 +25,17 @@ You can customise Popconfirm like: :::demo ```html ``` ::: diff --git a/examples/docs/es/infiniteScroll.md b/examples/docs/es/infiniteScroll.md index 8b121c8fc..677d8c881 100644 --- a/examples/docs/es/infiniteScroll.md +++ b/examples/docs/es/infiniteScroll.md @@ -8,7 +8,12 @@ Añada `v-infinite-scroll` a la lista para ejecutar automáticamente el método ```html @@ -39,7 +44,9 @@ Añada `v-infinite-scroll` a la lista para ejecutar automáticamente el método

Loading...

diff --git a/examples/docs/es/popconfirm.md b/examples/docs/es/popconfirm.md index 199627e22..023ddbf7f 100644 --- a/examples/docs/es/popconfirm.md +++ b/examples/docs/es/popconfirm.md @@ -9,11 +9,13 @@ Popconfirm is similar to Popover. So for some duplicated attributes, please refe :::demo Only `title` attribute is avaliable in Popconfirm, `content` will be ignored. ```html ```` ::: @@ -23,15 +25,17 @@ You can customise Popconfirm like: :::demo ```html ``` ::: diff --git a/examples/docs/fr-FR/infiniteScroll.md b/examples/docs/fr-FR/infiniteScroll.md index 022be00be..878250c6d 100644 --- a/examples/docs/fr-FR/infiniteScroll.md +++ b/examples/docs/fr-FR/infiniteScroll.md @@ -7,7 +7,12 @@ Ajoutez `v-infinite-scroll` à la liste pour exécuter automatiquement la métho :::demo ```html @@ -38,7 +43,8 @@ Ajoutez `v-infinite-scroll` à la liste pour exécuter automatiquement la métho

Loading...

diff --git a/examples/docs/fr-FR/popconfirm.md b/examples/docs/fr-FR/popconfirm.md index 199627e22..023ddbf7f 100644 --- a/examples/docs/fr-FR/popconfirm.md +++ b/examples/docs/fr-FR/popconfirm.md @@ -9,11 +9,13 @@ Popconfirm is similar to Popover. So for some duplicated attributes, please refe :::demo Only `title` attribute is avaliable in Popconfirm, `content` will be ignored. ```html ```` ::: @@ -23,15 +25,17 @@ You can customise Popconfirm like: :::demo ```html ``` ::: diff --git a/examples/docs/zh-CN/form.md b/examples/docs/zh-CN/form.md index 6bb86ee38..5db9b31e8 100644 --- a/examples/docs/zh-CN/form.md +++ b/examples/docs/zh-CN/form.md @@ -8,25 +8,10 @@ :::demo 在 Form 组件中,每一个表单域由一个 Form-Item 组件构成,表单域中可以放置各种类型的表单控件,包括 Input、Select、Checkbox、Radio、Switch、DatePicker、TimePicker ```html - + - - - - - - - - - - - - - - - - @@ -70,7 +55,7 @@ }, methods: { onSubmit() { - console.log('submit!'); + console.log('submit!', this.form); } } } @@ -78,12 +63,6 @@ ``` ::: -:::tip -W3C 标准中有如下[规定](https://www.w3.org/MarkUp/html-spec/html-spec_8.html#SEC8.2): -> When there is only one single-line text input field in a form, the user agent should accept Enter in that field as a request to submit the form. - -即:当一个 form 元素中只有一个输入框时,在该输入框中按下回车应提交该表单。如果希望阻止这一默认行为,可以在 `` 标签上添加 `@submit.native.prevent`。 -::: ### 行内表单 @@ -95,11 +74,8 @@ W3C 标准中有如下[规定](https://www.w3.org/MarkUp/html-spec/html-spec_8.h - - - - - + + 查询 @@ -111,7 +87,7 @@ W3C 标准中有如下[规定](https://www.w3.org/MarkUp/html-spec/html-spec_8.h return { formInline: { user: '', - region: '' + delivery: false } } }, @@ -175,25 +151,6 @@ W3C 标准中有如下[规定](https://www.w3.org/MarkUp/html-spec/html-spec_8.h - - - - - - - - - - - - - - - - - - - - @@ -224,7 +181,7 @@ W3C 标准中有如下[规定](https://www.w3.org/MarkUp/html-spec/html-spec_8.h data() { return { ruleForm: { - name: '', + name: 'aaa', region: '', date1: '', date2: '', @@ -238,15 +195,6 @@ W3C 标准中有如下[规定](https://www.w3.org/MarkUp/html-spec/html-spec_8.h { required: true, message: '请输入活动名称', trigger: 'blur' }, { min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' } ], - region: [ - { required: true, message: '请选择活动区域', trigger: 'change' } - ], - date1: [ - { type: 'date', required: true, message: '请选择日期', trigger: 'change' } - ], - date2: [ - { type: 'date', required: true, message: '请选择时间', trigger: 'change' } - ], type: [ { type: 'array', required: true, message: '请至少选择一个活动性质', trigger: 'change' } ], @@ -308,7 +256,7 @@ W3C 标准中有如下[规定](https://www.w3.org/MarkUp/html-spec/html-spec_8.h return callback(new Error('年龄不能为空')); } setTimeout(() => { - if (!Number.isInteger(value)) { + if (!Number.isInteger(parseInt(value))) { callback(new Error('请输入数字值')); } else { if (value < 18) { @@ -381,205 +329,6 @@ W3C 标准中有如下[规定](https://www.w3.org/MarkUp/html-spec/html-spec_8.h 自定义校验 callback 必须被调用。 更多高级用法可参考 [async-validator](https://github.com/yiminghe/async-validator)。 ::: -### 动态增减表单项 - -:::demo 除了在 Form 组件上一次性传递所有的验证规则外还可以在单个的表单域上传递属性的验证规则 -```html - - - - - - 删除 - - - 提交 - 新增域名 - 重置 - - - -``` -::: - -### 数字类型验证 - -:::demo 数字类型的验证需要在 `v-model` 处加上 `.number` 的修饰符,这是 `Vue` 自身提供的用于将绑定值转化为 `number` 类型的修饰符。 -```html - - - - - - 提交 - 重置 - - - -``` -::: - -:::tip -嵌套在 `el-form-item` 中的 `el-form-item` 标签宽度默认为零,不会继承 `el-form` 的 `label-width`。如果需要可以为其单独设置 `label-width` 属性。 -::: - -### 表单内组件尺寸控制 - -通过设置 Form 上的 `size` 属性可以使该表单内所有可调节大小的组件继承该尺寸。Form-Item 也具有该属性。 - -:::demo 如果希望某个表单项或某个表单组件的尺寸不同于 Form 上的`size`属性,直接为这个表单项或表单组件设置自己的`size`即可。 -```html - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 立即创建 - 取消 - - - - -``` -::: - ### Form Attributes | 参数 | 说明 | 类型 | 可选值 | 默认值 | diff --git a/examples/docs/zh-CN/infiniteScroll.md b/examples/docs/zh-CN/infiniteScroll.md index 6d76ba2bc..d4f896dec 100644 --- a/examples/docs/zh-CN/infiniteScroll.md +++ b/examples/docs/zh-CN/infiniteScroll.md @@ -7,7 +7,12 @@ :::demo ```html @@ -38,7 +43,8 @@
    + :infinite-scroll-delay="0" + :infinite-scroll-disabled="disabled">
  • {{ i }}

加载中...

diff --git a/examples/docs/zh-CN/loading.md b/examples/docs/zh-CN/loading.md index 9bd6d7ee3..6ac31c95b 100644 --- a/examples/docs/zh-CN/loading.md +++ b/examples/docs/zh-CN/loading.md @@ -2,6 +2,7 @@ 加载数据时显示动效。 + ### 服务 + Loading 还可以以服务的方式调用。引入 Loading 服务: + ```javascript -import { Loading } from 'element-ui'; +import { Loading } from 'element-ui' ``` + 在需要调用时: + ```javascript -Loading.service(options); +Loading.service(options) ``` + 其中 `options` 参数为 Loading 的配置项,具体见下表。`LoadingService` 会返回一个 Loading 实例,可通过调用该实例的 `close` 方法来关闭它: + ```javascript -let loadingInstance = Loading.service(options); -this.$nextTick(() => { // 以服务的方式调用的 Loading 需要异步关闭 - loadingInstance.close(); -}); +let loadingInstance = Loading.service(options) +this.$nextTick(() => { + // 以服务的方式调用的 Loading 需要异步关闭 + loadingInstance.close() +}) ``` + 需要注意的是,以服务的方式调用的全屏 Loading 是单例的:若在前一个全屏 Loading 关闭前再次调用全屏 Loading,并不会创建一个新的 Loading 实例,而是返回现有全屏 Loading 的实例: + ```javascript -let loadingInstance1 = Loading.service({ fullscreen: true }); -let loadingInstance2 = Loading.service({ fullscreen: true }); -console.log(loadingInstance1 === loadingInstance2); // true +let loadingInstance1 = Loading.service({ fullscreen: true }) +let loadingInstance2 = Loading.service({ fullscreen: true }) +console.log(loadingInstance1 === loadingInstance2) // true ``` + 此时调用它们中任意一个的 `close` 方法都能关闭这个全屏 Loading。 -如果完整引入了 Element,那么 Vue.prototype 上会有一个全局方法 `$loading`,它的调用方式为:`this.$loading(options)`,同样会返回一个 Loading 实例。 +如果完整引入了 Element,那么会通过 `app.config.globalProperties.$loading = service` 的方式挂载 `LoadingService`。在当前组件中首先通过 `getCurrentInstance` 获取当前组件实例对象,然后通过该实例对象即可以调用了。 + +:::demo 首先通过 `getCurrentInstance` 获取组件实例对象,然后通过实例对象的 `$loading` 方法调用。 + +```html + + +``` + +::: ### Options -| 参数 | 说明 | 类型 | 可选值 | 默认值 | -|---------- |-------------- |---------- |-------------------------------- |-------- | -| target | Loading 需要覆盖的 DOM 节点。可传入一个 DOM 对象或字符串;若传入字符串,则会将其作为参数传入 `document.querySelector`以获取到对应 DOM 节点 | object/string | — | document.body | -| body | 同 `v-loading` 指令中的 `body` 修饰符 | boolean | — | false | -| fullscreen | 同 `v-loading` 指令中的 `fullscreen` 修饰符 | boolean | — | true | -| lock | 同 `v-loading` 指令中的 `lock` 修饰符 | boolean | — | false | -| text | 显示在加载图标下方的加载文案 | string | — | — | -| spinner | 自定义加载图标类名 | string | — | — | -| background | 遮罩背景色 | string | — | — | -| customClass | Loading 的自定义类名 | string | — | — | \ No newline at end of file + +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | ------ | ------------- | +| target | Loading 需要覆盖的 DOM 节点。可传入一个 DOM 对象或字符串;若传入字符串,则会将其作为参数传入 `document.querySelector`以获取到对应 DOM 节点 | object/string | — | document.body | +| body | 同 `v-loading` 指令中的 `body` 修饰符 | boolean | — | false | +| fullscreen | 同 `v-loading` 指令中的 `fullscreen` 修饰符 | boolean | — | true | +| lock | 同 `v-loading` 指令中的 `lock` 修饰符 | boolean | — | false | +| text | 显示在加载图标下方的加载文案 | string | — | — | +| spinner | 自定义加载图标类名 | string | — | — | +| background | 遮罩背景色 | string | — | — | +| customClass | Loading 的自定义类名 | string | — | — | diff --git a/examples/docs/zh-CN/pagination.md b/examples/docs/zh-CN/pagination.md index f9a2b4163..30af1edcd 100644 --- a/examples/docs/zh-CN/pagination.md +++ b/examples/docs/zh-CN/pagination.md @@ -173,11 +173,11 @@ |--------------------|----------------------------------------------------------|-------------------|-------------|--------| | small | 是否使用小型分页样式 | boolean | — | false | | background | 是否为分页按钮添加背景色 | boolean | — | false | -| page-size | 每页显示条目个数,支持 .sync 修饰符 | number | — | 10 | +| page-size | 每页显示条目个数,支持具名v-model v-model:pageSize | number | — | 10 | | total | 总条目数 | number | — | — | | page-count | 总页数,total 和 page-count 设置任意一个就可以达到显示页码的功能;如果要支持 page-sizes 的更改,则需要使用 total 属性 | Number | — | — | | pager-count | 页码按钮的数量,当总页数超过该值时会折叠 | number | 大于等于 5 且小于等于 21 的奇数 | 7 | -| current-page | 当前页数,支持 .sync 修饰符 | number | — | 1 | +| current-page | 当前页数,支持具名v-model v-model:currentPage | number | — | 1 | | layout | 组件布局,子组件名用逗号分隔| String | `sizes`, `prev`, `pager`, `next`, `jumper`, `->`, `total`, `slot` | 'prev, pager, next, jumper, ->, total' | | page-sizes | 每页显示个数选择器的选项设置 | number[] | — | [10, 20, 30, 40, 50, 100] | | popper-class | 每页显示个数选择器的下拉框类名 | string | — | — | diff --git a/examples/docs/zh-CN/popconfirm.md b/examples/docs/zh-CN/popconfirm.md index 852a1faf5..69567134e 100644 --- a/examples/docs/zh-CN/popconfirm.md +++ b/examples/docs/zh-CN/popconfirm.md @@ -8,11 +8,13 @@ Popconfirm 的属性与 Popover 很类似,因此对于重复属性,请参考 :::demo 在 Popconfirm 中,只有 `title` 属性可用,`content` 属性不会被展示。 ```html ```` ::: @@ -23,15 +25,17 @@ Popconfirm 的属性与 Popover 很类似,因此对于重复属性,请参考 :::demo ```html ``` ::: diff --git a/examples/versions.json b/examples/versions.json index 482375e0d..ac4db7a43 100644 --- a/examples/versions.json +++ b/examples/versions.json @@ -1 +1 @@ -{"1.4.13":"1.4","2.0.11":"2.0","2.1.0":"2.1","2.2.2":"2.2","2.3.9":"2.3","2.4.11":"2.4","2.5.4":"2.5","2.6.3":"2.6","2.7.2":"2.7","2.8.2":"2.8","2.9.2":"2.9","2.10.1":"2.10","2.11.1":"2.11","2.12.0":"2.12","0.0.3":"2.13"} \ No newline at end of file +{"1.4.13":"1.4","2.0.11":"2.0","2.1.0":"2.1","2.2.2":"2.2","2.3.9":"2.3","2.4.11":"2.4","2.5.4":"2.5","2.6.3":"2.6","2.7.2":"2.7","2.8.2":"2.8","2.9.2":"2.9","2.10.1":"2.10","2.11.1":"2.11","2.12.0":"2.12","0.0.4":"2.13"} \ No newline at end of file diff --git a/migrate.md b/migrate.md index aa2a3501a..c3f9cb33c 100644 --- a/migrate.md +++ b/migrate.md @@ -70,7 +70,7 @@ | Dialog 对话框 | ❌ | | | Tooltip 文字提示 | ❌ | | | Popover 弹出框 | ❌ | | -| Popconfirm 气泡确认框 | ❌ | | +| Popconfirm 气泡确认框 | ✅ | | | Card 卡片 | ❌ | | | Carousel 走马灯 | ✅ | | | Collapse 折叠面板 | ❌ | | diff --git a/package.json b/package.json index cdc9841bb..b68626fe7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "element3", - "version": "0.0.3", + "version": "0.0.5", "description": "A Component Library for Vue.js.", "main": "dist/element3-ui.esm.js", "module": "dist/element3-ui.esm.js", @@ -87,6 +87,7 @@ "devDependencies": { "@ant-design-vue/babel-plugin-jsx": "^1.0.0-beta.4", "@babel/core": "^7.10.5", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", "@babel/plugin-proposal-optional-chaining": "^7.11.0", "@babel/preset-env": "^7.10.4", "@rollup/plugin-commonjs": "^15.0.0", diff --git a/packages/checkbox-group/CheckboxGroup.vue b/packages/checkbox-group/CheckboxGroup.vue index 1291b2413..491a65b95 100644 --- a/packages/checkbox-group/CheckboxGroup.vue +++ b/packages/checkbox-group/CheckboxGroup.vue @@ -39,9 +39,7 @@ export default { }) const checkboxGroupDisabled = computed(() => { - return ( - props.disabled || elFormItem.props.disabled || elForm.props.disabled - ) + return props.disabled || elFormItem.disabled || elForm.disabled }) watch(props.modelValue, (v) => { diff --git a/packages/color-picker/src/components/picker-dropdown.vue b/packages/color-picker/src/components/picker-dropdown.vue index d254d3cd6..160cab1c1 100644 --- a/packages/color-picker/src/components/picker-dropdown.vue +++ b/packages/color-picker/src/components/picker-dropdown.vue @@ -6,7 +6,7 @@ ref="hue" :color="color" vertical - style="float: right;" + style="float: right" > diff --git a/packages/form-item/FormItem.vue b/packages/form-item/FormItem.vue new file mode 100644 index 000000000..eb6cbcc60 --- /dev/null +++ b/packages/form-item/FormItem.vue @@ -0,0 +1,480 @@ + + diff --git a/packages/form-item/LabelWrap.vue b/packages/form-item/LabelWrap.vue new file mode 100644 index 000000000..f657b85e3 --- /dev/null +++ b/packages/form-item/LabelWrap.vue @@ -0,0 +1,103 @@ + diff --git a/packages/form-item/__tests__/FormItem.spec.js b/packages/form-item/__tests__/FormItem.spec.js new file mode 100644 index 000000000..fb152d531 --- /dev/null +++ b/packages/form-item/__tests__/FormItem.spec.js @@ -0,0 +1,264 @@ +import FormItem from '../FormItem.vue' +import { mount } from '@vue/test-utils' +import { nextTick, h, reactive } from 'vue' + +describe('FormItem', () => { + test('label', () => { + const wrapper = mount(FormItem, { + props: { + label: '用户名' + }, + global: { + provide: { + elForm: { + rules: {}, + labelSuffix: '' + } + } + } + }) + + const label = wrapper.find('.el-form-item__label') + expect(label.exists()).toBe(true) + expect(label.text()).toBe('用户名') + }) + + test('label slot', () => { + const wrapper = mount(FormItem, { + slots: { + label() { + return '用户名:' + } + }, + global: { + provide: { + elForm: { + rules: {} + } + } + } + }) + + const label = wrapper.find('.el-form-item__label') + expect(label.exists()).toBe(true) + expect(label.text()).toBe('用户名:') + }) + + test('fixed label width', () => { + const wrapper = mount(FormItem, { + props: { + label: '用户名', + labelWidth: '80px' + }, + global: { + provide: { + elForm: { + rules: {} + } + } + } + }) + + const label = wrapper.find('.el-form-item__label') + expect(label.attributes().style).toContain('width: 80px') + }) + + test('auto label width', () => { + const wrapper = mount(FormItem, { + props: { + label: '用户名', + labelWidth: 'auto' + }, + global: { + provide: { + elForm: { + rules: {} + } + } + } + }) + + const label = wrapper.find('.el-form-item__label') + expect(label.attributes().style).toContain('width: auto') + }) + + test('should contain a is-required class when set property required', () => { + const wrapper = mount(FormItem, { + props: { + label: '用户名', + required: true + }, + global: { + provide: { + elForm: { + rules: {} + } + } + } + }) + + const item = wrapper.find('.el-form-item') + expect(item.classes()).toContain('is-required') + }) + + test('should contain a is-error class when validation is invalid', async () => { + const wrapper = mount(FormItem, { + props: { + label: '用户名', + prop: 'username', + rules: [{ required: true, message: '请输入用户名' }] + }, + global: { + provide: { + elForm: { + rules: { + username: [{ required: true, message: '用户名为必填项' }] + }, + model: { + username: '' + }, + emit() {} + } + } + } + }) + + const item = wrapper.find('.el-form-item') + expect(item.classes()).toContain('is-required') + + wrapper.vm.validate() + await nextTick() + + expect(item.classes()).toContain('is-error') + + const error = wrapper.find('.el-form-item__error') + setTimeout(() => { + expect(error.exists()).toBe(true) + }, 300) + }) + + test('should show a error message when set property error', async () => { + const wrapper = mount(FormItem, { + props: { + label: '用户名', + error: 'some error' + }, + global: { + provide: { + elForm: {} + } + } + }) + + const item = wrapper.find('.el-form-item') + const error = wrapper.find('.el-form-item__error') + expect(item.classes()).toContain('is-error') + expect(wrapper.vm.validateState).toBe('error') + setTimeout(() => { + expect(error.exists()).toBe(true) + }, 300) + }) + + test('small size', async () => { + const wrapper = mount(FormItem, { + props: { + label: '用户名' + }, + global: { + provide: { + elForm: {} + } + } + }) + + const item = wrapper.find('.el-form-item') + expect(item.classes()).not.toContain('el-form-item--small') + await wrapper.setProps({ size: 'small' }) + expect(item.classes()).toContain('el-form-item--small') + }) + + it('scoped slot error', () => { + const wrapper = mount(FormItem, { + props: { + label: '用户名', + error: '请输入用户名' + }, + slots: { + error: ({ error }) => h('div', { class: 'error-message' }, error) + }, + global: { + provide: { + elForm: {} + } + } + }) + + const error = wrapper.find('.error-message') + setTimeout(() => { + expect(error.exists()).toBe(true) + }, 300) + }) + + it('clear validate result', () => { + const wrapper = mount(FormItem, { + props: { + label: '用户名', + error: '请输入用户名' + }, + global: { + provide: { + elForm: {} + } + } + }) + + // 错误class和errorMessage一开始有 + const item = wrapper.find('.el-form-item') + const error = wrapper.find('.el-form-item__error') + expect(item.classes()).toContain('is-error') + + // 清除校验内容之后,希望清除掉error class和errorMessage + wrapper.vm.clearValidate() + + setTimeout(() => { + expect(item.classes()).not.toContain('is-error') + expect(error.exists()).toBe(false) + }, 300) + }) + + it('reset field', async () => { + const model = reactive({ + username: 'young cunzhang' + }) + const wrapper = mount(FormItem, { + props: { + label: '用户名', + prop: 'username', + rules: [{ required: true, message: '请输入用户名' }] + }, + global: { + provide: { + elForm: { model, emit() {} } + } + } + }) + + // 一开始username有值,验证一下initialValue + const vm = wrapper.vm + // 把初始值置空,然后执行校验 + model.username = '' + vm.validate() + await nextTick() + + // 此时会添加error class + const item = wrapper.find('.el-form-item') + expect(item.classes()).toContain('is-error') + + // 重置字段 + vm.resetField() + await nextTick() + + // 此时error class应该移除 + expect(item.classes()).not.toContain('is-error') + }) +}) diff --git a/packages/form-item/index.js b/packages/form-item/index.js index 17182119a..e803dff61 100644 --- a/packages/form-item/index.js +++ b/packages/form-item/index.js @@ -1,4 +1,4 @@ -import ElFormItem from '../form/src/form-item' +import ElFormItem from './FormItem.vue' /* istanbul ignore next */ ElFormItem.install = function (app) { diff --git a/packages/form/Form.vue b/packages/form/Form.vue new file mode 100644 index 000000000..5094bffed --- /dev/null +++ b/packages/form/Form.vue @@ -0,0 +1,243 @@ + + diff --git a/packages/form/__tests__/Form.spec.js b/packages/form/__tests__/Form.spec.js new file mode 100644 index 000000000..d946a00f2 --- /dev/null +++ b/packages/form/__tests__/Form.spec.js @@ -0,0 +1,390 @@ +import Form from '../Form.vue' +import FormItem from '../../form-item/FormItem.vue' + +import { mount } from '@vue/test-utils' +import { nextTick } from 'vue' + +const components = { + ElForm: Form, + ElFormItem: FormItem +} + +describe('Form', () => { + it('label', () => { + const wrapper = mount({ + template: ` + + + + `, + components + }) + + expect(wrapper.find('.el-form-item__label').text()).toBe('活动名称') + }) + + it('label position', () => { + const wrapperTop = mount(Form, { + props: { + labelPosition: 'top' + } + }) + + const wrapperLeft = mount(Form, { + props: { + labelPosition: 'left' + } + }) + + expect(wrapperTop.find('.el-form').classes()).toContain( + 'el-form--label-top' + ) + expect(wrapperLeft.find('.el-form').classes()).toContain( + 'el-form--label-left' + ) + }) + + it('inline form', () => { + const wrapper = mount(Form, { + props: { + inline: true + } + }) + + expect(wrapper.find('.el-form').classes()).toContain('el-form--inline') + }) + + it('label with', () => { + const wrapperForm = mount({ + template: ` + + + + `, + components + }) + + const wrapperFormItem = mount({ + template: ` + + + + `, + components + }) + + const wrapperWithPositionTop = mount({ + template: ` + + + + `, + components + }) + + expect(wrapperForm.find('.el-form-item__label').element.style.width).toBe( + '80px' + ) + expect( + wrapperFormItem.find('.el-form-item__label').element.style.width + ).toBe('80px') + expect( + wrapperWithPositionTop.find('.el-form-item__label').element.style.width + ).toBe('') + }) + + // todo autoWith 使用了dom的API getComputedStyle, 在单元测试中暂时还没办法获取到真正的宽度 + // it('auto label width', async () => { + // const wrapper = mount({ + // template: ` + // + // + // + // + // + // + // + // + // `, + // data() { + // return { + // display: true + // } + // }, + // components: { + // ElForm: Form, + // ElFormItem: FormItem + // } + // }) + + // const contents = wrapper.findAll('.el-form-item__content') + + // const marginLeft = contents[0].element.style.marginLeft + // const marginLeft1 = contents[1].element.style.marginLeft + // wrapper.vm.display = false + // await wrapper.vm.$nextTick() + + // const newMarginLeft = contents[0].element.style.marginLeft + // expect(marginLeft === marginLeft1).toBe(true) + // expect(newMarginLeft < marginLeft).toBe(true) + // }) + + // it('label size', () => { + // const wrapperForm = mount({ + // template: ` + // + + // + // `, + // components + // }) + + // const wrapperFormItem = mount({ + // template: ` + // + + // + // `, + // components + // }) + + // expect(wrapperForm.find('.el-form-item').classes()).toContain('el-form-item--mini') + // expect(wrapperFormItem.find('.el-form-item').classes()).toContain('el-form-item--mini') + // }) + + it('show message', (done) => { + const wrapper = mount({ + template: ` + + + + `, + components, + data() { + return { + form: { + name: '' + } + } + } + }) + + wrapper.vm.$refs.form.validate((valid) => { + expect(valid).toBe(false) + wrapper.vm.$refs.form.$nextTick((_) => { + expect(wrapper.find('.el-form-item__error').exists()).toBe(false) + done() + }) + }) + }) + + it('reset field', async () => { + const wrapper = mount({ + template: ` + + + + + + + `, + components, + data() { + return { + form: { + name: '', + address: '' + }, + rules: { + name: [ + { required: true, message: '请输入活动名称', trigger: 'blur' } + ], + address: [ + { required: true, message: '请选择活动区域', trigger: 'change' } + ] + } + } + }, + methods: { + setValue() { + this.form.name = 'jack' + this.form.address = 'aaaa' + } + } + }) + wrapper.vm.setValue() + wrapper.vm.$refs.form.resetFields() + await wrapper.vm.$refs.form.$nextTick() + + expect(wrapper.vm.form.name).toEqual('') + expect(wrapper.vm.form.address).toEqual('') + }) + + it('clear validate', (done) => { + const wrapper = mount({ + template: ` + + + + + + `, + components, + data() { + return { + form: { + name: '' + }, + rules: { + name: [ + { required: true, message: '请输入活动名称', trigger: 'blur' } + ] + } + } + } + }) + + const form = wrapper.vm.$refs.form + form.validate((valid) => console.log(valid)) + form.$nextTick(() => { + expect(wrapper.find('.el-form-item__error').text()).toBe('请输入活动名称') + form.clearValidate(['name']) + setTimeout(() => { + expect(wrapper.find('.el-form-item__error').exists()).toBe(false) + done() + }, 100) + }) + }) + + it('form item nest', (done) => { + const wrapper = mount({ + template: ` + + + + + + + + + `, + components, + data() { + return { + form: { + name: '', + address: '' + }, + rules: { + name: [ + { required: true, message: '请输入活动名称', trigger: 'blur' } + ], + address: [ + { required: true, message: '请选择活动区域', trigger: 'change' } + ] + } + } + } + }) + wrapper.findComponent(Form).vm.validate((valid) => { + expect(valid).toBe(false) + done() + }) + }) + + it('validate event', (done) => { + const wrapper = mount({ + template: ` + + + + `, + components, + data() { + return { + form: { + name: '' + }, + valid: { + name: null + }, + error: { + name: null + }, + rules: { + name: [ + { required: true, message: '请输入活动名称', trigger: 'change' } + ] + } + } + }, + methods: { + onValidate(prop, valid, msg) { + this.valid[prop] = valid + this.error[prop] = msg + }, + setValue(prop, value) { + this.form[prop] = value + } + } + }) + + wrapper.vm.setValue('name', 'name') + wrapper.findComponent(FormItem).vm.$emit('el.form.change') + + setTimeout(() => { + expect(wrapper.vm.valid.name).toEqual(true) + expect(wrapper.vm.error.name).toEqual(null) + + wrapper.vm.setValue('name', '') + wrapper.findComponent(FormItem).vm.$emit('el.form.change') + setTimeout(() => { + expect(wrapper.vm.valid.name).toEqual(false) + expect(wrapper.vm.error.name).toEqual('请输入活动名称') + done() + }, 100) + }, 100) + }) + + it('modify rules', async () => { + const rules = { + name: [{ min: 5, message: '用户名至少5位' }] + } + const wrapper = mount({ + template: ` + + + + + `, + components, + data() { + return { + form: { + name: 'tom' + }, + rules + } + } + }) + + const form = wrapper.findComponent(Form).vm + form.validate((valid) => console.log(valid)) + await nextTick() + // 开始校验有错误 + expect(wrapper.find('.el-form-item__error').text()).toBe('用户名至少5位') + // 修改规则后重新校验,错误信息消失 + rules.name[0].min = 3 + setTimeout(() => { + expect(wrapper.find('.el-form-item__error').exists()).toBe(false) + }, 500) + }) +}) diff --git a/packages/form/index.js b/packages/form/index.js index ddc9c9199..0fd2e7c7c 100644 --- a/packages/form/index.js +++ b/packages/form/index.js @@ -1,4 +1,4 @@ -import ElForm from './src/form' +import ElForm from './Form.vue' /* istanbul ignore next */ ElForm.install = function (app) { diff --git a/packages/form/src/form-item.vue b/packages/form/src/form-item.vue deleted file mode 100644 index 0df55b18f..000000000 --- a/packages/form/src/form-item.vue +++ /dev/null @@ -1,347 +0,0 @@ - - diff --git a/packages/form/src/form.vue b/packages/form/src/form.vue deleted file mode 100644 index 385d9795f..000000000 --- a/packages/form/src/form.vue +++ /dev/null @@ -1,195 +0,0 @@ - - diff --git a/packages/form/src/label-wrap.vue b/packages/form/src/label-wrap.vue deleted file mode 100644 index cba3921c6..000000000 --- a/packages/form/src/label-wrap.vue +++ /dev/null @@ -1,85 +0,0 @@ - diff --git a/packages/infinite-scroll/src/main.js b/packages/infinite-scroll/src/main.js index 29d18549e..c0be5c98e 100644 --- a/packages/infinite-scroll/src/main.js +++ b/packages/infinite-scroll/src/main.js @@ -2,7 +2,6 @@ import throttle from 'throttle-debounce/debounce' import { isHtmlElement, isFunction, - isUndefined, isDefined } from 'element-ui/src/utils/types' import { getScrollContainer } from 'element-ui/src/utils/dom' @@ -58,13 +57,17 @@ const attributes = { } } -const getScrollOptions = (el, vm) => { +const getScrollOptions = (el) => { if (!isHtmlElement(el)) return {} return entries(attributes).reduce((map, [key, option]) => { const { type, default: defaultValue } = option - let value = el.getAttribute(`infinite-scroll-${key}`) - value = isUndefined(vm[value]) ? value : vm[value] + const attributeName = `infinite-scroll-${key}` + + let value = el.hasAttribute(attributeName) + ? el.getAttribute(attributeName) + : defaultValue + switch (type) { case Number: value = Number(value) @@ -88,8 +91,8 @@ const getScrollOptions = (el, vm) => { const getElementTop = (el) => el.getBoundingClientRect().top const handleScroll = function (cb) { - const { el, vm, container, observer } = this[scope] - const { distance, disabled } = getScrollOptions(el, vm) + const { el, container, observer } = this[scope] + const { distance, disabled } = getScrollOptions(el) if (disabled) return @@ -113,7 +116,7 @@ const handleScroll = function (cb) { } if (shouldTrigger && isFunction(cb)) { - cb.call(vm) + cb.call() } else if (observer) { observer.disconnect() this[scope].observer = null @@ -122,16 +125,16 @@ const handleScroll = function (cb) { export default { name: 'InfiniteScroll', - inserted(el, binding, vnode) { + + mounted(el, binding, vnode) { const cb = binding.value - const vm = vnode.context // only include vertical scroll const container = getScrollContainer(el, true) - const { delay, immediate } = getScrollOptions(el, vm) + const { delay, immediate } = getScrollOptions(el) const onScroll = throttle(delay, handleScroll.bind(el, cb)) - el[scope] = { el, vm, container, onScroll } + el[scope] = { el, container, onScroll } if (container) { container.addEventListener('scroll', onScroll) @@ -143,7 +146,8 @@ export default { } } }, - unbind(el) { + + unmounted(el) { const { container, onScroll } = el[scope] if (container) { container.removeEventListener('scroll', onScroll) diff --git a/packages/input/Input.vue b/packages/input/Input.vue index 7fc57acaf..482d14985 100644 --- a/packages/input/Input.vue +++ b/packages/input/Input.vue @@ -1,6 +1,7 @@ + + diff --git a/packages/loading/__tests__/Loading.spec.js b/packages/loading/__tests__/Loading.spec.js new file mode 100644 index 000000000..7912924a7 --- /dev/null +++ b/packages/loading/__tests__/Loading.spec.js @@ -0,0 +1,293 @@ +// import { getStyle } from '../../../src/utils/dom' +// import { createVue, destroyVM } from '../util' +// import Vue from 'vue' +// import LoadingRaw from 'packages/loading' +// const Loading = LoadingRaw.service + +// import { mount } from '@vue/test-utils' +import Loading from '../index' + +const LoadingService = Loading.service + +describe('Loading', () => { + let loadingInstance, loadingInstance2 + afterEach((done) => { + if (loadingInstance) { + loadingInstance.close() + loadingInstance.ctx.$el && + loadingInstance.ctx.$el.parentNode && + loadingInstance.ctx.$el.parentNode.removeChild(loadingInstance.ctx.$el) + } + if (loadingInstance2) { + loadingInstance2.close() + loadingInstance2.ctx.$el && + loadingInstance2.ctx.$el.parentNode && + loadingInstance2.ctx.$el.parentNode.removeChild( + loadingInstance2.ctx.$el + ) + } + loadingInstance = loadingInstance2 = null + setTimeout(() => { + done() + }, 100) + }) + + // describe('as a directive', () => { + // it('create', done => { + // vm = createVue({ + // template: ` + //
+ // `, + // + // data() { + // return { + // loading: true + // } + // } + // }) + // Vue.nextTick(() => { + // const mask = vm.$el.querySelector('.el-loading-mask') + // expect(mask).to.exist + // vm.loading = false + // setTimeout(() => { + // expect(mask.style.display).to.equal('none') + // done() + // }, 100) + // }) + // }) + // + // it('unbind', done => { + // const vm1 = createVue({ + // template: ` + //
+ // `, + // + // data() { + // return { + // show: true, + // loading: true + // } + // } + // }) + // const vm2 = createVue({ + // template: ` + //
+ // `, + // + // data() { + // return { + // show: true, + // loading: true + // } + // } + // }) + // Vue.nextTick(() => { + // vm1.loading = false + // vm2.loading = false + // Vue.nextTick(() => { + // vm1.show = false + // vm2.show = false + // Vue.nextTick(() => { + // expect(document.querySelector('.el-loading-mask')).to.not.exist + // done() + // }) + // }) + // }) + // }) + // + // it('body', done => { + // vm = createVue({ + // template: ` + //
+ // `, + // + // data() { + // return { + // loading: true + // } + // } + // }, true) + // Vue.nextTick(() => { + // const mask = document.querySelector('.el-loading-mask') + // expect(mask.parentNode === document.body).to.true + // vm.loading = false + // document.body.removeChild(mask) + // document.body.removeChild(vm.$el) + // done() + // }) + // }) + // + // it('fullscreen', done => { + // vm = createVue({ + // template: ` + //
+ // `, + // + // data() { + // return { + // loading: true + // } + // } + // }, true) + // Vue.nextTick(() => { + // const mask = document.querySelector('.el-loading-mask') + // expect(mask.parentNode === document.body).to.true + // expect(mask.classList.contains('is-fullscreen')).to.true + // vm.loading = false + // document.body.removeChild(mask) + // document.body.removeChild(vm.$el) + // done() + // }) + // }) + // + // it('lock', done => { + // vm = createVue({ + // template: ` + //
+ // `, + // + // data() { + // return { + // loading: true + // } + // } + // }, true) + // Vue.nextTick(() => { + // expect(getStyle(document.body, 'overflow')).to.equal('hidden') + // vm.loading = false + // document.body.removeChild(document.querySelector('.el-loading-mask')) + // document.body.removeChild(vm.$el) + // done() + // }) + // }) + // + // it('text', done => { + // vm = createVue({ + // template: ` + //
+ // `, + // + // data() { + // return { + // loading: true + // } + // } + // }, true) + // Vue.nextTick(() => { + // const mask = document.querySelector('.el-loading-mask') + // const text = mask.querySelector('.el-loading-text') + // expect(text).to.exist + // expect(text.textContent).to.equal('拼命加载中') + // done() + // }) + // }) + // + // it('customClass', done => { + // vm = createVue({ + // template: ` + //
+ // `, + // + // data() { + // return { + // loading: true + // } + // } + // }, true) + // Vue.nextTick(() => { + // const mask = document.querySelector('.el-loading-mask') + // expect(mask.classList.contains('loading-custom-class')).to.true + // done() + // }) + // }) + // }) + + describe('as a service', () => { + it('create', () => { + loadingInstance = LoadingService() + expect(document.querySelector('.el-loading-mask')).toBeTruthy() + loadingInstance.close() + }) + + it('close', () => { + loadingInstance = LoadingService() + loadingInstance.close() + expect(loadingInstance.ctx.visible).toBe(false) + }) + + it('target', () => { + const container = document.createElement('div') + container.classList.add('loading-container') + document.body.appendChild(container) + loadingInstance = Loading.service({ target: container }) + const mask = document.querySelector('.el-loading-mask') + expect(mask).toBeTruthy() + expect(mask.parentNode).toEqual(container) + }) + + it('body', () => { + const container = document.createElement('div') + container.classList.add('loading-container') + document.body.appendChild(container) + loadingInstance = Loading.service({ target: container, body: true }) + const mask = document.querySelector('.el-loading-mask') + expect(mask).toBeTruthy() + expect(mask.parentNode).toEqual(document.body) + loadingInstance.close() + }) + + it('fullscreen', () => { + loadingInstance = LoadingService({ + fullscreen: true + }) + const mask = document.querySelector('.el-loading-mask') + expect(mask.parentNode).toEqual(document.body) + expect(mask.classList.contains('is-fullscreen')).toBe(true) + }) + + it('fullscreen singleton', (done) => { + loadingInstance = LoadingService({ fullScreen: true }) + setTimeout(() => { + loadingInstance2 = LoadingService({ fullScreen: true }) + + setTimeout(() => { + let masks = document.querySelectorAll('.el-loading-mask') + expect(masks.length).toEqual(1) + loadingInstance2.close() + + setTimeout(() => { + masks = document.querySelectorAll('.el-loading-mask') + expect(masks.length).toEqual(0) + done() + }, 350) + }, 50) + }, 50) + }) + + it('lock', () => { + loadingInstance = LoadingService({ + lock: true + }) + expect( + document.body.classList.contains('el-loading-parent--hidden') + ).toBe(true) + }) + + it('text', () => { + loadingInstance = LoadingService({ + text: 'Loading...' + }) + const text = document.querySelector('.el-loading-text') + expect(text).toBeTruthy() + expect(text.textContent).toEqual('Loading...') + }) + + it('customClass', () => { + loadingInstance = LoadingService({ + customClass: 'el-loading-custom-class' + }) + const customClass = document.querySelector('.el-loading-custom-class') + expect(customClass).toBeTruthy() + }) + }) +}) diff --git a/packages/loading/src/directive.js b/packages/loading/directive.js similarity index 100% rename from packages/loading/src/directive.js rename to packages/loading/directive.js diff --git a/packages/loading/index.js b/packages/loading/index.js index 8aef49abf..120d9291d 100644 --- a/packages/loading/index.js +++ b/packages/loading/index.js @@ -1,11 +1,11 @@ -import directive from './src/directive' -import service from './src/index' +// import directive from './src/directive' +import service from './service' export default { install(app) { - app.use(directive) + // app.use(directive) app.config.globalProperties.$loading = service }, - directive, + // directive, service } diff --git a/packages/loading/src/index.js b/packages/loading/service.js similarity index 58% rename from packages/loading/src/index.js rename to packages/loading/service.js index 593dfc139..5ae7f825d 100644 --- a/packages/loading/src/index.js +++ b/packages/loading/service.js @@ -1,73 +1,20 @@ -import { nextTick } from 'vue' -import loadingVue from './loading.vue' +import loadingVue from './Loading.vue' +import { createComponent, unmountComponent } from 'element-ui/src/use/component' import { addClass, removeClass, getStyle } from 'element-ui/src/utils/dom' import { PopupManager } from 'element-ui/src/utils/popup' -import afterLeave from 'element-ui/src/utils/after-leave' import merge from 'element-ui/src/utils/merge' -const LoadingConstructor = { extends: loadingVue } - const defaults = { - text: null, - fullscreen: true, + target: null, body: false, + fullscreen: true, lock: false, + text: null, + spinner: null, + background: null, customClass: '' } - let fullscreenLoading - -LoadingConstructor.prototype.originalPosition = '' -LoadingConstructor.prototype.originalOverflow = '' - -LoadingConstructor.prototype.close = function () { - if (this.fullscreen) { - fullscreenLoading = undefined - } - afterLeave( - this, - (_) => { - const target = this.fullscreen || this.body ? document.body : this.target - removeClass(target, 'el-loading-parent--relative') - removeClass(target, 'el-loading-parent--hidden') - if (this.$el && this.$el.parentNode) { - this.$el.parentNode.removeChild(this.$el) - } - this.$destroy() - }, - 300 - ) - this.visible = false -} - -const addStyle = (options, parent, instance) => { - const maskStyle = {} - if (options.fullscreen) { - instance.originalPosition = getStyle(document.body, 'position') - instance.originalOverflow = getStyle(document.body, 'overflow') - maskStyle.zIndex = PopupManager.nextZIndex() - } else if (options.body) { - instance.originalPosition = getStyle(document.body, 'position') - ;['top', 'left'].forEach((property) => { - const scroll = property === 'top' ? 'scrollTop' : 'scrollLeft' - maskStyle[property] = - options.target.getBoundingClientRect()[property] + - document.body[scroll] + - document.documentElement[scroll] + - 'px' - }) - ;['height', 'width'].forEach((property) => { - maskStyle[property] = - options.target.getBoundingClientRect()[property] + 'px' - }) - } else { - instance.originalPosition = getStyle(parent, 'position') - } - Object.keys(maskStyle).forEach((property) => { - instance.$el.style[property] = maskStyle[property] - }) -} - const Loading = (options = {}) => { // if (Vue.prototype.$isServer) return options = merge({}, defaults, options) @@ -85,29 +32,71 @@ const Loading = (options = {}) => { } const parent = options.body ? document.body : options.target - const instance = new LoadingConstructor({ - el: document.createElement('div'), - data: options + const instance = createComponent(loadingVue, { + ...options, + visible: true, + onAfterLeave() { + if (options.fullscreen) { + fullscreenLoading = undefined + } + const target = + options.fullscreen || options.body ? document.body : options.target + removeClass(target, 'el-loading-parent--relative') + removeClass(target, 'el-loading-parent--hidden') + unmountComponent(instance) + } }) - - addStyle(options, parent, instance) + addStyle(options, parent, instance.ctx) if ( instance.originalPosition !== 'absolute' && instance.originalPosition !== 'fixed' ) { addClass(parent, 'el-loading-parent--relative') } + if (options.fullscreen && options.lock) { addClass(parent, 'el-loading-parent--hidden') } - parent.appendChild(instance.$el) - nextTick(() => { - instance.visible = true - }) + + parent.appendChild(instance.ctx.$el) + if (options.fullscreen) { fullscreenLoading = instance } + + instance.close = close + return instance } +const close = function () { + this.ctx.close() +} + +const addStyle = (options, parent, ctx) => { + const maskStyle = {} + if (options.fullscreen) { + ctx.originalPosition = getStyle(document.body, 'position') + ctx.originalOverflow = getStyle(document.body, 'overflow') + maskStyle.zIndex = PopupManager.nextZIndex() + } else if (options.body) { + ctx.originalPosition = getStyle(document.body, 'position') + ;['top', 'left'].forEach((property) => { + const scroll = property === 'top' ? 'scrollTop' : 'scrollLeft' + maskStyle[property] = + options.target.getBoundingClientRect()[property] + // document.body[scroll] + + document.documentElement[scroll] + + 'px' + }) + ;['height', 'width'].forEach((property) => { + maskStyle[property] = + options.target.getBoundingClientRect()[property] + 'px' + }) + } else { + ctx.originalPosition = getStyle(parent, 'position') + } + Object.keys(maskStyle).forEach((property) => { + ctx.$el.style[property] = maskStyle[property] + }) +} export default Loading diff --git a/packages/loading/src/loading.vue b/packages/loading/src/loading.vue deleted file mode 100644 index d92f91f9f..000000000 --- a/packages/loading/src/loading.vue +++ /dev/null @@ -1,42 +0,0 @@ - - - diff --git a/packages/notification/__tests__/Notification.spec.js b/packages/notification/__tests__/Notification.spec.js new file mode 100644 index 000000000..22a80823f --- /dev/null +++ b/packages/notification/__tests__/Notification.spec.js @@ -0,0 +1,103 @@ +import { mount } from '@vue/test-utils' +import Notification from '../Notification.vue' + +describe('Notification', () => { + afterEach(() => { + const el = document.querySelector('.el-notification') + if (!el) return + if (el.parentNode) { + el.parentNode.removeChild(el) + } + if (el.__vue__) { + el.__vue__.$destroy() + } + }) + + test('automatically close', async (done) => { + const notification = await mount(Notification, { + props: { + message: '玻璃蜡烛', + duration: 500 + } + }) + expect(notification.find('.el-notification').exists()).toBe(true) + setTimeout(() => { + // 此处2.x的用例测试是否存在,看实现close效果为display:none + // expect(notification.find('.el-notification').exists()).toBe(false) + expect(notification.find('.el-notification').element.style.display).toBe('none') + done() + }, 1000) + }) + + test('manually close', async (done) => { + const notification = await mount(Notification, { + props: { + message: '苍白母马' + } + }) + notification.find('.el-notification__closeBtn').trigger('click') + setTimeout(() => { + expect(notification.find('.el-notification').element.style.display).toBe('none') + done() + }, 500) + }) + + test('create', () => { + const notification = mount(Notification, { + props: { + title: '狮子', + message: '狮鹫', + duration: 0 + } + }) + const group = notification.find('.el-notification__group') + const title = group.find('.el-notification__title') + const message = group.find('.el-notification__content p') + expect(notification.find('.el-notification').exists()).toBe(true) + expect(title.text()).toBe('狮子') + expect(message.text()).toBe('狮鹫') + }) + + test('html string as message', () => { + const notification = mount(Notification, { + props: { + title: '狮子', + message: '狮鹫', + dangerouslyUseHTMLString: true, + duration: 0 + } + }) + const message = notification.find('.el-notification__content strong') + expect(message.text()).toBe('狮鹫') + }) + + // 怀疑2.x版本以下用例的功能未实现 + // create by vnode + // alias by vnode + // invoke with type + + test('reset timer', async (done) => { + const notification = await mount(Notification, { + props: { + message: '芳香总管', + duration: 1000 + } + }) + notification.find('.el-notification').trigger('mouseenter') + setTimeout(() => { + notification.find('.el-notification').trigger('mouseleave') + expect(notification.find('.el-notification').exists()).toBe(true) + done() + }, 700) + }) + + test('no close button', async () => { + const notification = await mount(Notification, { + props: { + message: 'Hello', + showClose: false + } + }) + expect(notification.find('.el-notification__closeBtn').exists()).toBe(false) + }) +}) diff --git a/packages/pagination/Pagination.js b/packages/pagination/Pagination.js new file mode 100644 index 000000000..f2f550b64 --- /dev/null +++ b/packages/pagination/Pagination.js @@ -0,0 +1,293 @@ +import { ref, computed, watch, h, toRefs, Fragment } from 'vue' +import Pager from './components/Pager' +import Prev from './components/Prev.vue' +import Next from './components/Next' +import Jumper from './components/Jumper' +import Total from './components/Total' + +const useLayout = (layout) => { + const template = [] + const rightWrapper = [] + const components = layout.value.split(',').map((item) => item.trim()) + const findIndex = components.findIndex((item) => item === '->') + for (let i = 0; i < components.length; i++) { + if (findIndex >= 0) { + if (i === findIndex) continue + if (i < findIndex) { + template.push(components[i]) + } else { + rightWrapper.push(components[i]) + } + } else { + template.push(components[i]) + } + } + + return { + template, + rightWrapper + } +} + +const getValidCurrentPage = (value, pageCount) => { + value = parseInt(value, 10) + + const havePageCount = typeof pageCount === 'number' + + let resetValue + if (!havePageCount) { + if (isNaN(value) || value < 1) resetValue = 1 + } else { + if (value < 1) { + resetValue = 1 + } else if (value > pageCount) { + resetValue = pageCount + } + } + + if ((resetValue === undefined && isNaN(value)) || resetValue === 0) { + resetValue = 1 + } + + return resetValue ?? value +} + +const useInternalCurrentPage = ({ currentPage, emit, emitted }) => { + const innerCurrentPage = ref(null) + return computed({ + get() { + return innerCurrentPage.value ?? currentPage?.value ?? 1 + }, + set(v) { + emit('update:currentPage', v) + emit('currentChange', v) + if (currentPage) { + watch(currentPage, () => { + emitted.value = true + }) + } + if (emitted.value) innerCurrentPage.value = null + else innerCurrentPage.value = v + emitted.value = false + } + }) +} + +const userInternalPageSize = ({ pageSize, emit, emitted }) => { + const innerPageSize = ref(null) + return computed({ + get() { + return innerPageSize.value ?? pageSize?.value + }, + set(v) { + emit('update:pageSize', v) + emit('sizeChange', v) + innerPageSize.value = v + if (pageSize) { + watch(pageSize, () => { + emitted.value = true + }) + } + if (emitted.value) innerPageSize.value = null + emitted.value = false + } + }) +} + +const useInternalPageCount = ({ pageCount, total, pageSize }) => { + const internalPageCount = computed(() => { + if (!total && !pageCount) return 5 + if (typeof total?.value === 'number') { + return Math.max(1, Math.ceil(total.value / pageSize.value)) + } else if (typeof pageCount?.value === 'number') { + return Math.max(1, pageCount.value) + } + }) + + return { + internalPageCount + } +} + +export default { + name: 'ElPagination', + + props: { + pageSize: { + type: Number, + default: 10 + }, + + small: Boolean, + + total: Number, + + pageCount: Number, + + pagerCount: { + type: Number, + validator(value) { + return ( + (value || 0) === value && value > 4 && value < 22 && value % 2 === 1 + ) + }, + default: 7 + }, + + currentPage: Number, + + layout: { + default: 'prev, pager, next, jumper, ->, total' + }, + + pageSizes: { + type: Array, + default() { + return [10, 20, 30, 40, 50, 100] + } + }, + + popperClass: String, + + prevText: String, + + nextText: String, + + background: Boolean, + + disabled: Boolean, + + hideOnSinglePage: Boolean + }, + + emits: [ + 'update:currentPage', + 'update:pageSize', + 'currentChange', + 'sizeChange', + 'prevClick', + 'nextClick' + ], + + setup(props, { emit, slots }) { + const currentPageEmitted = ref(false) + const pageSizeEmitted = ref(false) + const { + layout, + small, + background, + currentPage, + total, + pageCount, + pageSize, + disabled, + hideOnSinglePage + } = toRefs(props) + + if (!layout.value) return null + + const internalCurrentPage = useInternalCurrentPage({ + currentPage, + emit, + emitted: currentPageEmitted + }) + const internalPageSize = userInternalPageSize({ + pageSize, + emit, + emitted: pageSizeEmitted + }) + const { internalPageCount } = useInternalPageCount({ + pageCount, + total, + pageSize: internalPageSize + }) + + if (hideOnSinglePage && internalPageCount.value === 1) return null + + const { template, rightWrapper } = useLayout(layout) + + return () => { + const templateComponent = { + prev: h(Prev, { + currentPage: internalCurrentPage.value, + disabled: disabled.value, + prevText: props.prevText, + prev() { + if (disabled.value) return + internalCurrentPage.value = getValidCurrentPage( + internalCurrentPage.value - 1, + internalPageCount.value + ) + emit('prevClick', internalCurrentPage.value) + } + }), + jumper: h(Jumper, { + currentPage: internalCurrentPage.value, + pageCount: internalPageCount.value, + handleChange(val) { + internalCurrentPage.value = getValidCurrentPage( + val, + internalPageCount.value + ) + }, + disabled: disabled.value + }), + pager: h(Pager, { + currentPage: internalCurrentPage.value, + 'onUpdate:currentPage'(val) { + internalCurrentPage.value = val + }, + pageCount: internalPageCount.value, + disabled: disabled.value + }), + next: h(Next, { + currentPage: internalCurrentPage.value, + pageCount: internalPageCount.value, + disabled: disabled.value, + nextText: props.nextText, + next() { + if (disabled.value) return + internalCurrentPage.value = getValidCurrentPage( + internalCurrentPage.value + 1, + internalPageCount.value + ) + emit('nextClick', internalCurrentPage.value) + } + }), + // sizes: , + slot: h(Fragment, slots.default?.() ?? ''), + total: h(Total, { + total: total?.value + }) + } + + return ( +
+ {rightWrapper.length ? ( +
+ {rightWrapper.map((item) => { + const Comp = templateComponent[item] + return + })} +
+ ) : ( + '' + )} + + {template.map((item) => { + const Comp = templateComponent[item] + return + })} +
+ ) + } + } +} diff --git a/packages/pagination/__tests__/Pagination.spec.js b/packages/pagination/__tests__/Pagination.spec.js new file mode 100644 index 000000000..f782dbedb --- /dev/null +++ b/packages/pagination/__tests__/Pagination.spec.js @@ -0,0 +1,394 @@ +import { mount } from '@vue/test-utils' +import { ref, nextTick, h } from 'vue' +import Pager from '../components/Pager' +import Prev from '../components/Prev' +import Next from '../components/Next' +import Total from '../components/Total' +import Pagination from '../Pagination' +import Jumper from '../components/Jumper' +import Input from 'element-ui/packages/input' + +describe('components', () => { + describe('Pager', () => { + it('normal render', () => { + const wrapper = mount(Pager) + expect(wrapper.find('ul').exists()).toBeTruthy() + expect(wrapper.findAll('li.number').length).toBe(5) + }) + it('show next more', async () => { + const wrapper = mount(Pager, { + props: { + pageCount: 100 + } + }) + + expect(wrapper.find('.btn-quickprev').exists()).toBeFalsy() + expect(wrapper.find('.btn-quicknext').exists()).toBeTruthy() + await wrapper.find('.btn-quicknext').trigger('click') + expect(wrapper.emitted()['update:currentPage'][0]).toEqual([6]) + }) + it('show prev more', async () => { + const wrapper = mount(Pager, { + props: { + currentPage: 97, + pageCount: 100 + } + }) + + await nextTick() + expect(wrapper.find('.btn-quickprev').exists()).toBeTruthy() + expect(wrapper.find('.btn-quicknext').exists()).toBeFalsy() + await wrapper.find('.btn-quickprev').trigger('click') + expect(wrapper.emitted()['update:currentPage'][0]).toEqual([92]) + }) + it('show both more', async () => { + const wrapper = mount(Pager, { + props: { + currentPage: 50, + pageCount: 100 + } + }) + + await nextTick() + expect(wrapper.find('.btn-quickprev').exists()).toBeTruthy() + expect(wrapper.find('.btn-quicknext').exists()).toBeTruthy() + }) + it('click event', async () => { + const currentPage = ref(1) + const wrapper = mount(Pager, { + props: { + currentPage, + pageCount: 100, + 'onUpdate:currentPage'(val) { + currentPage.value = val + } + } + }) + + await nextTick() + expect(currentPage.value).toBe(1) + expect(wrapper.find('.btn-quickprev').exists()).toBeFalsy() + expect(wrapper.find('.btn-quicknext').exists()).toBeTruthy() + await wrapper.findAll('.number')[6].trigger('click') + expect(wrapper.find('.btn-quickprev').exists()).toBeTruthy() + expect(wrapper.find('.btn-quicknext').exists()).toBeFalsy() + expect(currentPage.value).toBe(100) + }) + }) + + describe('Prev', () => { + it('normal render', () => { + const wrapper = mount(Prev) + + expect(wrapper.find('.btn-prev').exists()).toBeTruthy() + }) + it('prevText', () => { + const wrapper = mount(Prev, { + props: { + prevText: '1' + } + }) + + expect(wrapper.text()).toBe('1') + }) + it('click event', async () => { + const prev = jest.fn() + const wrapper = mount(Prev, { + props: { + currentPage: 10, + prev + } + }) + + await wrapper.trigger('click') + expect(prev).toBeCalled() + }) + it('disabled', async () => { + // disabled has two situations: + // first: disabled set true + const prev = jest.fn() + const wrapper = mount(Prev, { + props: { + disabled: true, + currentPage: 10, + prev + } + }) + // second: currentPage equal 1 + const wrapper2 = mount(Prev, { + props: { + prev + } + }) + + await wrapper.trigger('click') + expect(prev).not.toBeCalled() + await wrapper2.trigger('click') + expect(prev).not.toBeCalled() + }) + }) + + describe('Next', () => { + it('normal render', () => { + const wrapper = mount(Next) + + expect(wrapper.find('.btn-next').exists()).toBeTruthy() + }) + it('nextText', () => { + const wrapper = mount(Next, { + props: { + nextText: '1' + } + }) + + expect(wrapper.text()).toBe('1') + }) + it('click event', async () => { + const next = jest.fn() + const wrapper = mount(Next, { + props: { + currentPage: 10, + next + } + }) + + await wrapper.trigger('click') + expect(next).toBeCalled() + }) + it('disabled', async () => { + // disabled has three situations: + // first: disabled set true + const next = jest.fn() + const wrapper = mount(Next, { + props: { + disabled: true, + currentPage: 10, + next + } + }) + // second: currentPage equal pageCount + const wrapper2 = mount(Next, { + props: { + currentPage: 3, + pageCount: 3, + next + } + }) + // third: pageCount equal 0 (why not <= 1? because second situation include this) + const wrapper3 = mount(Next, { + props: { + pageCount: 0, + next + } + }) + + await wrapper.trigger('click') + expect(next).not.toBeCalled() + await wrapper2.trigger('click') + expect(next).not.toBeCalled() + await wrapper3.trigger('click') + expect(next).not.toBeCalled() + }) + }) + + describe('Jumper', () => { + it('handleChange', async () => { + const currentPage = ref(1) + const wrapper = mount(Jumper, { + props: { + currentPage, + handleChange(val) { + currentPage.value = Number(val) + } + }, + global: { + components: { + Input + } + } + }) + + await wrapper.find('input').setValue(3) + await wrapper.trigger('keydown', 13) + expect(currentPage.value).toBe(3) + }) + }) + + describe('Total', () => { + it('total prop is number', () => { + const wrapper = mount(Total, { + props: { + total: 5 + } + }) + + expect(wrapper.find('.el-pagination__total').exists()).toBeTruthy() + }) + it('total prop is not number', () => { + const wrapper = mount(Total, { + props: { + total: undefined + } + }) + + expect(wrapper.find('.el-pagination__total').exists()).toBeFalsy() + }) + }) + + describe('Size', () => { + it.todo('normal render') + it.todo('popperClass') + }) +}) + +describe('Pagination', () => { + describe('props', () => { + it('layout', () => { + const wrapper = mount(Pagination, { + props: { + layout: 'prev, slot, next' + }, + slots: { + default: h('div', { class: 'slot' }, 'slot') + } + }) + + expect(wrapper.find('.btn-prev').exists()).toBeTruthy() + expect(wrapper.find('.slot').exists()).toBeTruthy() + expect(wrapper.find('.btn-next').exists()).toBeTruthy() + }) + it('no currentPage', async () => { + const wrapper = mount(Pagination) + + expect(wrapper.find('.active').text()).toBe('1') + await wrapper.findAll('.number')[1].trigger('click') + expect(wrapper.find('.active').text()).toBe('2') + }) + it('total', () => { + const wrapper = mount(Pagination, { + props: { + total: 30 + } + }) + + expect(wrapper.findAll('.number').length).toBe(3) + expect(wrapper.find('.el-pagination__total').text()).toContain('30') + }) + it('pageSize', () => { + const wrapper = mount(Pagination, { + props: { + total: 10, + pageSize: 2 + } + }) + + expect(wrapper.findAll('.number').length).toBe(5) + }) + it('currentChange', async () => { + const currentPage = ref(1) + const wrapper = mount(Pagination, { + props: { + currentPage, + onCurrentChange(val) { + currentPage.value = val + } + } + }) + + await wrapper.findAll('.number')[3].trigger('click') + expect(currentPage.value).toBe(4) + }) + it('hide-on-single-page', () => { + const wrapper = mount(Pagination, { + props: { + hideOnSinglePage: true, + pageCount: 1 + } + }) + const wrapper2 = mount(Pagination, { + props: { + hideOnSinglePage: true, + pageCount: 5 + } + }) + + expect(wrapper.find('.el-pagination').exists()).toBeFalsy() + expect(wrapper2.find('.el-pagination').exists()).toBeTruthy() + }) + it.todo('sizeChange') + it.todo('pageSizes') + }) + + describe('ability', () => { + it('change current-page by Jumper', async () => { + const currentPage = ref(1) + const wrapper = mount(Pagination, { + props: { + layout: 'pager, jumper', + currentPage, + 'onUpdate:currentPage'(val) { + currentPage.value = val + } + } + }) + await wrapper.find('input').setValue(3) + await wrapper.trigger('keydown', 13) + expect(currentPage.value).toBe(3) + await wrapper.find('input').setValue(10) + await wrapper.trigger('keydown', 13) + expect(currentPage.value).toBe(5) + }) + it('change current-page by Prev', async () => { + const onPrevClick = jest.fn() + const wrapper = mount(Pagination, { + props: { + layout: 'prev, pager', + currentPage: 2, + onPrevClick + } + }) + + await wrapper.findComponent(Prev).trigger('click') + expect(onPrevClick).toBeCalled() + }) + it('cannot change current-page by Prev', async () => { + const onPrevClick = jest.fn() + const wrapper = mount(Pagination, { + props: { + layout: 'prev, pager', + currentPage: 1, + onPrevClick + } + }) + + await wrapper.findComponent(Prev).trigger('click') + expect(onPrevClick).not.toBeCalled() + }) + it('change current-page by Next', async () => { + const onNextClick = jest.fn() + const wrapper = mount(Pagination, { + props: { + layout: 'pager, next', + currentPage: 4, + onNextClick + } + }) + + await wrapper.findComponent(Next).trigger('click') + expect(onNextClick).toBeCalled() + }) + it('cannot change current-page by Next', async () => { + const onNextClick = jest.fn() + const wrapper = mount(Pagination, { + props: { + layout: 'pager, next', + currentPage: 5, + onNextClick + } + }) + + await wrapper.findComponent(Next).trigger('click') + expect(onNextClick).not.toBeCalled() + }) + }) +}) diff --git a/packages/pagination/components/Jumper.vue b/packages/pagination/components/Jumper.vue new file mode 100644 index 000000000..87f2a643b --- /dev/null +++ b/packages/pagination/components/Jumper.vue @@ -0,0 +1,70 @@ + + + diff --git a/packages/pagination/components/Next.vue b/packages/pagination/components/Next.vue new file mode 100644 index 000000000..f28b67066 --- /dev/null +++ b/packages/pagination/components/Next.vue @@ -0,0 +1,50 @@ + + + diff --git a/packages/pagination/components/Pager.vue b/packages/pagination/components/Pager.vue new file mode 100644 index 000000000..c5adda05f --- /dev/null +++ b/packages/pagination/components/Pager.vue @@ -0,0 +1,205 @@ + + + diff --git a/packages/pagination/components/Prev.vue b/packages/pagination/components/Prev.vue new file mode 100644 index 000000000..1a3cb2ee6 --- /dev/null +++ b/packages/pagination/components/Prev.vue @@ -0,0 +1,40 @@ + + + diff --git a/packages/pagination/components/Total.js b/packages/pagination/components/Total.js new file mode 100644 index 000000000..05fd0add0 --- /dev/null +++ b/packages/pagination/components/Total.js @@ -0,0 +1,13 @@ +import { useLocale } from 'element-ui/src/use/locale' +import { h } from 'vue' + +const Total = (props) => { + return typeof props.total === 'number' + ? h( + 'span', + { class: 'el-pagination__total' }, + useLocale()('el.pagination.total', { total: props.total }) + ) + : '' +} +export default Total diff --git a/packages/pagination/index.js b/packages/pagination/index.js index 7f71d90f3..01451bc8c 100644 --- a/packages/pagination/index.js +++ b/packages/pagination/index.js @@ -1,4 +1,4 @@ -import Pagination from './src/pagination' +import Pagination from './Pagination' /* istanbul ignore next */ Pagination.install = function (app) { diff --git a/packages/pagination/src/pager.vue b/packages/pagination/src/pager.vue deleted file mode 100644 index 7b9e16bfe..000000000 --- a/packages/pagination/src/pager.vue +++ /dev/null @@ -1,174 +0,0 @@ - - - diff --git a/packages/pagination/src/pagination.js b/packages/pagination/src/pagination.js deleted file mode 100644 index 55e94901a..000000000 --- a/packages/pagination/src/pagination.js +++ /dev/null @@ -1,428 +0,0 @@ -import Pager from './pager.vue' -import ElSelect from 'element-ui/packages/select' -import ElOption from 'element-ui/packages/option' -import ElInput from 'element-ui/packages/input' -import Locale from 'element-ui/src/mixins/locale' -import { valueEquals } from 'element-ui/src/utils/util' - -export default { - name: 'ElPagination', - - props: { - pageSize: { - type: Number, - default: 10 - }, - - small: Boolean, - - total: Number, - - pageCount: Number, - - pagerCount: { - type: Number, - validator(value) { - return ( - (value | 0) === value && value > 4 && value < 22 && value % 2 === 1 - ) - }, - default: 7 - }, - - currentPage: { - type: Number, - default: 1 - }, - - layout: { - default: 'prev, pager, next, jumper, ->, total' - }, - - pageSizes: { - type: Array, - default() { - return [10, 20, 30, 40, 50, 100] - } - }, - - popperClass: String, - - prevText: String, - - nextText: String, - - background: Boolean, - - disabled: Boolean, - - hideOnSinglePage: Boolean - }, - - data() { - return { - internalCurrentPage: 1, - internalPageSize: 0, - lastEmittedPage: -1, - userChangePageSize: false - } - }, - - render(h) { - const layout = this.layout - if (!layout) return null - if ( - this.hideOnSinglePage && - (!this.internalPageCount || this.internalPageCount === 1) - ) - return null - - const template = ( -
- ) - const TEMPLATE_MAP = { - prev: , - jumper: , - pager: ( - - ), - next: , - sizes: , - slot: {this.$slots.default ? this.$slots.default : ''}, - total: - } - const components = layout.split(',').map((item) => item.trim()) - const rightWrapper =
- let haveRightWrapper = false - - template.children = template.children || [] - rightWrapper.children = rightWrapper.children || [] - components.forEach((compo) => { - if (compo === '->') { - haveRightWrapper = true - return - } - - if (!haveRightWrapper) { - template.children.push(TEMPLATE_MAP[compo]) - } else { - rightWrapper.children.push(TEMPLATE_MAP[compo]) - } - }) - - if (haveRightWrapper) { - template.children.unshift(rightWrapper) - } - - return template - }, - - components: { - Prev: { - render(h) { - return ( - - ) - } - }, - - Next: { - render(h) { - return ( - - ) - } - }, - - Sizes: { - mixins: [Locale], - - props: { - pageSizes: Array - }, - - watch: { - pageSizes: { - immediate: true, - handler(newVal, oldVal) { - if (valueEquals(newVal, oldVal)) return - if (Array.isArray(newVal)) { - this.$parent.internalPageSize = - newVal.indexOf(this.$parent.pageSize) > -1 - ? this.$parent.pageSize - : this.pageSizes[0] - } - } - } - }, - - render(h) { - return ( - - - {this.pageSizes.map((item) => ( - - ))} - - - ) - }, - - components: { - ElSelect, - ElOption - }, - - methods: { - handleChange(val) { - if (val !== this.$parent.internalPageSize) { - this.$parent.internalPageSize = val = parseInt(val, 10) - this.$parent.userChangePageSize = true - this.$parent.$emit('update:pageSize', val) - this.$parent.$emit('size-change', val) - } - } - } - }, - - Jumper: { - mixins: [Locale], - - components: { ElInput }, - - data() { - return { - userInput: null - } - }, - - watch: { - '$parent.internalCurrentPage'() { - this.userInput = null - } - }, - - methods: { - handleKeyup({ keyCode, target }) { - // Chrome, Safari, Firefox triggers change event on Enter - // Hack for IE: https://github.com/ElemeFE/element/issues/11710 - // Drop this method when we no longer supports IE - if (keyCode === 13) { - this.handleChange(target.value) - } - }, - handleInput(value) { - this.userInput = value - }, - handleChange(value) { - this.$parent.internalCurrentPage = this.$parent.getValidCurrentPage( - value - ) - this.$parent.emitChange() - this.userInput = null - } - }, - - render(h) { - return ( - - {this.t('el.pagination.goto')} - - {this.t('el.pagination.pageClassifier')} - - ) - } - }, - - Total: { - mixins: [Locale], - - render(h) { - return typeof this.$parent.total === 'number' ? ( - - {this.t('el.pagination.total', { total: this.$parent.total })} - - ) : ( - '' - ) - } - }, - - Pager - }, - - methods: { - handleCurrentChange(val) { - this.internalCurrentPage = this.getValidCurrentPage(val) - this.userChangePageSize = true - this.emitChange() - }, - - prev() { - if (this.disabled) return - const newVal = this.internalCurrentPage - 1 - this.internalCurrentPage = this.getValidCurrentPage(newVal) - this.$emit('prev-click', this.internalCurrentPage) - this.emitChange() - }, - - next() { - if (this.disabled) return - const newVal = this.internalCurrentPage + 1 - this.internalCurrentPage = this.getValidCurrentPage(newVal) - this.$emit('next-click', this.internalCurrentPage) - this.emitChange() - }, - - getValidCurrentPage(value) { - value = parseInt(value, 10) - - const havePageCount = typeof this.internalPageCount === 'number' - - let resetValue - if (!havePageCount) { - if (isNaN(value) || value < 1) resetValue = 1 - } else { - if (value < 1) { - resetValue = 1 - } else if (value > this.internalPageCount) { - resetValue = this.internalPageCount - } - } - - if (resetValue === undefined && isNaN(value)) { - resetValue = 1 - } else if (resetValue === 0) { - resetValue = 1 - } - - return resetValue === undefined ? value : resetValue - }, - - emitChange() { - this.$nextTick(() => { - if ( - this.internalCurrentPage !== this.lastEmittedPage || - this.userChangePageSize - ) { - this.$emit('current-change', this.internalCurrentPage) - this.lastEmittedPage = this.internalCurrentPage - this.userChangePageSize = false - } - }) - } - }, - - computed: { - internalPageCount() { - if (typeof this.total === 'number') { - return Math.max(1, Math.ceil(this.total / this.internalPageSize)) - } else if (typeof this.pageCount === 'number') { - return Math.max(1, this.pageCount) - } - return null - } - }, - - watch: { - currentPage: { - immediate: true, - handler(val) { - this.internalCurrentPage = this.getValidCurrentPage(val) - } - }, - - pageSize: { - immediate: true, - handler(val) { - this.internalPageSize = isNaN(val) ? 10 : val - } - }, - - internalCurrentPage: { - immediate: true, - handler(newVal) { - this.$emit('update:currentPage', newVal) - this.lastEmittedPage = -1 - } - }, - - internalPageCount(newVal) { - /* istanbul ignore if */ - const oldPage = this.internalCurrentPage - if (newVal > 0 && oldPage === 0) { - this.internalCurrentPage = 1 - } else if (oldPage > newVal) { - this.internalCurrentPage = newVal === 0 ? 1 : newVal - this.userChangePageSize && this.emitChange() - } - this.userChangePageSize = false - } - } -} diff --git a/packages/popconfirm/src/main.vue b/packages/popconfirm/Popconfirm.vue similarity index 79% rename from packages/popconfirm/src/main.vue rename to packages/popconfirm/Popconfirm.vue index 8708b0bf3..748e237c4 100644 --- a/packages/popconfirm/src/main.vue +++ b/packages/popconfirm/Popconfirm.vue @@ -19,11 +19,14 @@ - +