mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
refactor(components): refactor pagination (#3526)
* refactor(components): refactor pagination * fix: tests * fix: emits * refactor: improve props
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { nextTick, ref, h } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Pagination from '../src/index'
|
||||
import Pagination from '../src/pagination'
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
|
||||
const assertElementsExistence = (
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
import Pagination from './src'
|
||||
import { withInstall } from '@element-plus/utils/with-install'
|
||||
|
||||
import type { App } from 'vue'
|
||||
import type { SFCWithInstall } from '@element-plus/utils/types'
|
||||
import Pagination from './src/pagination'
|
||||
|
||||
const _Pagination = Pagination as SFCWithInstall<typeof Pagination>
|
||||
|
||||
_Pagination.install = (app: App) => {
|
||||
app.component(_Pagination.name, _Pagination)
|
||||
}
|
||||
|
||||
export default _Pagination
|
||||
export const ElPagination = _Pagination
|
||||
export const ElPagination = withInstall(Pagination)
|
||||
export default ElPagination
|
||||
|
||||
export * from './src/pagination'
|
||||
|
||||
@@ -20,37 +20,37 @@
|
||||
import { computed, defineComponent, ref } from 'vue'
|
||||
import { useLocaleInject } from '@element-plus/hooks'
|
||||
import ElInput from '@element-plus/components/input'
|
||||
import { usePagination } from './usePagination'
|
||||
|
||||
import type { Nullable } from '@element-plus/utils/types'
|
||||
import { usePagination } from '../usePagination'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElPaginationJumper',
|
||||
components: {
|
||||
ElInput,
|
||||
},
|
||||
|
||||
setup() {
|
||||
const { t } = useLocaleInject()
|
||||
const { pagination, pageCount, disabled, currentPage } = usePagination()
|
||||
const userInput = ref<Nullable<number>>(null)
|
||||
const innerValue = computed(() => userInput.value ?? currentPage.value)
|
||||
const { pageCount, disabled, currentPage, changeEvent } = usePagination()
|
||||
const userInput = ref<number>()
|
||||
const innerValue = computed(() => userInput.value ?? currentPage?.value)
|
||||
|
||||
function handleInput(val: number | string) {
|
||||
userInput.value = Number(val)
|
||||
userInput.value = +val
|
||||
}
|
||||
|
||||
function handleChange(val: number | string) {
|
||||
pagination?.changeEvent(Number(val))
|
||||
userInput.value = null
|
||||
changeEvent?.(+val)
|
||||
userInput.value = undefined
|
||||
}
|
||||
|
||||
return {
|
||||
t,
|
||||
userInput,
|
||||
pageCount,
|
||||
disabled,
|
||||
innerValue,
|
||||
|
||||
t,
|
||||
handleInput,
|
||||
handleChange,
|
||||
innerValue,
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -4,7 +4,7 @@
|
||||
class="btn-next"
|
||||
:disabled="internalDisabled"
|
||||
:aria-disabled="internalDisabled"
|
||||
@click.self.prevent
|
||||
@click="$emit('click', $event)"
|
||||
>
|
||||
<span v-if="nextText">{{ nextText }}</span>
|
||||
<i v-else class="el-icon el-icon-arrow-right"></i>
|
||||
@@ -13,23 +13,29 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue'
|
||||
export default defineComponent({
|
||||
name: 'Next',
|
||||
props: {
|
||||
disabled: Boolean,
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
pageCount: {
|
||||
type: Number,
|
||||
default: 50,
|
||||
},
|
||||
nextText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
const paginationNextProps = {
|
||||
disabled: Boolean,
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
pageCount: {
|
||||
type: Number,
|
||||
default: 50,
|
||||
},
|
||||
nextText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
} as const
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElPaginationNext',
|
||||
|
||||
props: paginationNextProps,
|
||||
emits: ['click'],
|
||||
|
||||
setup(props) {
|
||||
const internalDisabled = computed(
|
||||
() =>
|
||||
@@ -47,28 +47,34 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, watchEffect } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElPager',
|
||||
props: {
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
pageCount: {
|
||||
type: Number,
|
||||
},
|
||||
pagerCount: {
|
||||
type: Number,
|
||||
default: 7,
|
||||
},
|
||||
disabled: Boolean,
|
||||
const paginationPagerProps = {
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
pageCount: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
pagerCount: {
|
||||
type: Number,
|
||||
default: 7,
|
||||
},
|
||||
disabled: Boolean,
|
||||
} as const
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElPaginationPager',
|
||||
|
||||
props: paginationPagerProps,
|
||||
emits: ['change'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const showPrevMore = ref(false)
|
||||
const showNextMore = ref(false)
|
||||
const quicknextIconClass = ref('el-icon-more')
|
||||
const quickprevIconClass = ref('el-icon-more')
|
||||
|
||||
const pagers = computed(() => {
|
||||
const pagerCount = props.pagerCount
|
||||
const halfPagerCount = (pagerCount - 1) / 2
|
||||
@@ -85,7 +91,7 @@ export default defineComponent({
|
||||
showNextMore = true
|
||||
}
|
||||
}
|
||||
const array = []
|
||||
const array: number[] = []
|
||||
if (showPrevMore && !showNextMore) {
|
||||
const startPage = pageCount - (pagerCount - 2)
|
||||
for (let i = startPage; i < pageCount; i++) {
|
||||
@@ -190,6 +196,7 @@ export default defineComponent({
|
||||
quicknextIconClass,
|
||||
quickprevIconClass,
|
||||
pagers,
|
||||
|
||||
onMouseenter,
|
||||
onPagerClick,
|
||||
onEnter,
|
||||
@@ -4,7 +4,7 @@
|
||||
class="btn-prev"
|
||||
:disabled="internalDisabled"
|
||||
:aria-disabled="internalDisabled"
|
||||
@click.self.prevent
|
||||
@click="$emit('click', $event)"
|
||||
>
|
||||
<span v-if="prevText">{{ prevText }}</span>
|
||||
<i v-else class="el-icon el-icon-arrow-left"></i>
|
||||
@@ -14,19 +14,24 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Prev',
|
||||
props: {
|
||||
disabled: Boolean,
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
prevText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
const paginationPrevProps = {
|
||||
disabled: Boolean,
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
prevText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
} as const
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElPaginationPrev',
|
||||
|
||||
props: paginationPrevProps,
|
||||
emits: ['click'],
|
||||
|
||||
setup(props) {
|
||||
const internalDisabled = computed(
|
||||
() => props.disabled || props.currentPage <= 1
|
||||
@@ -20,39 +20,44 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, watch, computed, ref } from 'vue'
|
||||
import isEqual from 'lodash/isEqual'
|
||||
import ElSelect from '@element-plus/components/select'
|
||||
import { ElSelect, ElOption } from '@element-plus/components/select'
|
||||
import { useLocaleInject } from '@element-plus/hooks'
|
||||
import { usePagination } from './usePagination'
|
||||
import { buildProp, mutable } from '@element-plus/utils/props'
|
||||
import { usePagination } from '../usePagination'
|
||||
|
||||
import type { PropType } from 'vue'
|
||||
import type { Nullable } from '@element-plus/utils/types'
|
||||
|
||||
const { Option: ElOption } = ElSelect
|
||||
const defaultPageSizes = mutable([10, 20, 30, 40, 50, 100] as const)
|
||||
const paginationSizesProps = {
|
||||
pageSize: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
pageSizes: buildProp<number[], false, typeof defaultPageSizes>({
|
||||
type: Array,
|
||||
default: () => defaultPageSizes,
|
||||
} as const),
|
||||
popperClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
disabled: Boolean,
|
||||
} as const
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Sizes',
|
||||
name: 'ElPaginationSizes',
|
||||
|
||||
components: {
|
||||
ElSelect,
|
||||
ElOption,
|
||||
},
|
||||
props: {
|
||||
pageSize: Number,
|
||||
pageSizes: {
|
||||
type: Array as PropType<Array<number>>,
|
||||
default: () => {
|
||||
return [10, 20, 30, 40, 50, 100]
|
||||
},
|
||||
},
|
||||
popperClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
disabled: Boolean,
|
||||
},
|
||||
|
||||
props: paginationSizesProps,
|
||||
emits: ['page-size-change'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const { t } = useLocaleInject()
|
||||
const { pagination } = usePagination()
|
||||
const pagination = usePagination()
|
||||
const innerPageSize = ref<Nullable<number>>(props.pageSize)
|
||||
|
||||
watch(
|
||||
@@ -81,14 +86,15 @@ export default defineComponent({
|
||||
function handleChange(val: number) {
|
||||
if (val !== innerPageSize.value) {
|
||||
innerPageSize.value = val
|
||||
pagination?.handleSizeChange(Number(val))
|
||||
pagination.handleSizeChange?.(Number(val))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
t,
|
||||
innerPagesizes,
|
||||
innerPageSize,
|
||||
|
||||
t,
|
||||
handleChange,
|
||||
}
|
||||
},
|
||||
@@ -12,14 +12,21 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import { useLocaleInject } from '@element-plus/hooks'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Total',
|
||||
props: {
|
||||
total: {
|
||||
type: Number,
|
||||
default: 1000,
|
||||
},
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
|
||||
const paginationTotalProps = {
|
||||
total: {
|
||||
type: Number,
|
||||
default: 1000,
|
||||
},
|
||||
} as const
|
||||
export type PaginationTotalProps = ExtractPropTypes<typeof paginationTotalProps>
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElPaginationTotal',
|
||||
|
||||
props: paginationTotalProps,
|
||||
|
||||
setup() {
|
||||
const { t } = useLocaleInject()
|
||||
return {
|
||||
@@ -1,346 +0,0 @@
|
||||
import {
|
||||
h,
|
||||
ref,
|
||||
provide,
|
||||
computed,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { debugWarn } from '@element-plus/utils/error'
|
||||
import { useLocaleInject } from '@element-plus/hooks'
|
||||
|
||||
import Prev from './prev.vue'
|
||||
import Next from './next.vue'
|
||||
import Sizes from './sizes.vue'
|
||||
import Jumper from './jumper.vue'
|
||||
import Total from './total.vue'
|
||||
import Pager from './pager.vue'
|
||||
|
||||
import type { IPagination } from './pagination'
|
||||
|
||||
/**
|
||||
* It it user's responsibility to guarantee that the value of props.total... is number
|
||||
* (same as pageSize, defaultPageSize, currentPage, defaultCurrentPage, pageCount)
|
||||
* Otherwise we can reasonable infer that the corresponding field is absent
|
||||
*/
|
||||
const isAbsent = (v: unknown) => typeof v !== 'number'
|
||||
|
||||
const componentName = 'ElPagination'
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
|
||||
components: {
|
||||
Prev,
|
||||
Next,
|
||||
Sizes,
|
||||
Jumper,
|
||||
Total,
|
||||
Pager,
|
||||
},
|
||||
props: {
|
||||
total: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
pageSize: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
defaultPageSize: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
currentPage: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
defaultCurrentPage: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
pageCount: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
pagerCount: {
|
||||
type: Number,
|
||||
validator: (value: number) => {
|
||||
return (
|
||||
(value | 0) === value && value > 4 && value < 22 && value % 2 === 1
|
||||
)
|
||||
},
|
||||
default: 7,
|
||||
},
|
||||
|
||||
layout: {
|
||||
type: String,
|
||||
default: 'prev, pager, next, jumper, ->, total',
|
||||
},
|
||||
|
||||
pageSizes: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [10, 20, 30, 40, 50, 100]
|
||||
},
|
||||
},
|
||||
|
||||
popperClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
prevText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
nextText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
small: Boolean,
|
||||
|
||||
background: Boolean,
|
||||
|
||||
disabled: Boolean,
|
||||
|
||||
hideOnSinglePage: Boolean,
|
||||
},
|
||||
|
||||
emits: [
|
||||
'update:current-page',
|
||||
'update:page-size',
|
||||
// events below are depracated
|
||||
// v-model:current-page and v-model:page-size are better choices
|
||||
'size-change',
|
||||
'current-change',
|
||||
'prev-click',
|
||||
'next-click',
|
||||
],
|
||||
setup(props, { emit, slots }) {
|
||||
const { t } = useLocaleInject()
|
||||
const vnodeProps = getCurrentInstance().vnode.props || {}
|
||||
// we can find @xxx="xxx" props on `vnodeProps` to check if user bind corresponding events
|
||||
const hasCurrentPageListener =
|
||||
'onUpdate:currentPage' in vnodeProps ||
|
||||
'onUpdate:current-page' in vnodeProps ||
|
||||
'onCurrentChange' in vnodeProps
|
||||
const hasPageSizeListener =
|
||||
'onUpdate:pageSize' in vnodeProps ||
|
||||
'onUpdate:page-size' in vnodeProps ||
|
||||
'onSizeChange' in vnodeProps
|
||||
const assertValidUsage = computed(() => {
|
||||
// Users have to set either one, otherwise count of pages cannot be determined
|
||||
if (isAbsent(props.total) && isAbsent(props.pageCount)) return false
|
||||
// <el-pagination ...otherProps :current-page="xxx" /> without corresponding listener is forbidden now
|
||||
// Users have to use two way binding of `currentPage`
|
||||
// If users just want to provide a default value, `defaultCurrentPage` is here for you
|
||||
if (!isAbsent(props.currentPage) && !hasCurrentPageListener) return false
|
||||
// When you want to change sizes, things get more complex, detailed below
|
||||
// Basically the most important value we need is page count
|
||||
// either directly from props.pageCount
|
||||
// or calculated from props.total
|
||||
// we will take props.pageCount precedence over props.total
|
||||
if (props.layout.includes('sizes')) {
|
||||
if (!isAbsent(props.pageCount)) {
|
||||
// if props.pageCount is assign by user, then user have to watch pageSize change
|
||||
// and recalculate pageCount
|
||||
if (!hasPageSizeListener) return false
|
||||
} else if (!isAbsent(props.total)) {
|
||||
// Otherwise, we will see if user have props.pageSize defined
|
||||
// If so, meaning user want to have pageSize controlled himself/herself from component
|
||||
// Thus page size listener is required
|
||||
// users are account for page size change
|
||||
if (!isAbsent(props.pageSize)) {
|
||||
if (!hasPageSizeListener) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
// (else block just for explaination)
|
||||
// else page size is controlled by el-pagination internally
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const innerPageSize = ref(
|
||||
isAbsent(props.defaultPageSize) ? 10 : props.defaultPageSize
|
||||
)
|
||||
const innerCurrentPage = ref(
|
||||
isAbsent(props.defaultCurrentPage) ? 1 : props.defaultCurrentPage
|
||||
)
|
||||
|
||||
const pageSizeBridge = computed({
|
||||
get() {
|
||||
return isAbsent(props.pageSize) ? innerPageSize.value : props.pageSize
|
||||
},
|
||||
set(v: number) {
|
||||
if (isAbsent(props.pageSize)) {
|
||||
innerPageSize.value = v
|
||||
}
|
||||
if (hasPageSizeListener) {
|
||||
emit('update:page-size', v)
|
||||
emit('size-change', v)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const pageCountBridge = computed<number>(() => {
|
||||
let pageCount = 0
|
||||
if (!isAbsent(props.pageCount)) {
|
||||
pageCount = props.pageCount
|
||||
} else if (!isAbsent(props.total)) {
|
||||
pageCount = Math.max(1, Math.ceil(props.total / pageSizeBridge.value))
|
||||
}
|
||||
return pageCount
|
||||
})
|
||||
|
||||
const currentPageBridge = computed<number>({
|
||||
get() {
|
||||
return isAbsent(props.currentPage)
|
||||
? innerCurrentPage.value
|
||||
: props.currentPage
|
||||
},
|
||||
set(v) {
|
||||
let newCurrentPage = v
|
||||
if (v < 1) {
|
||||
newCurrentPage = 1
|
||||
} else if (v > pageCountBridge.value) {
|
||||
newCurrentPage = pageCountBridge.value
|
||||
}
|
||||
if (isAbsent(props.currentPage)) {
|
||||
innerCurrentPage.value = newCurrentPage
|
||||
}
|
||||
if (hasCurrentPageListener) {
|
||||
emit('update:current-page', newCurrentPage)
|
||||
emit('current-change', newCurrentPage)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
watch(pageCountBridge, (val) => {
|
||||
if (currentPageBridge.value > val) currentPageBridge.value = val
|
||||
})
|
||||
|
||||
function handleCurrentChange(val: number) {
|
||||
currentPageBridge.value = val
|
||||
}
|
||||
|
||||
function handleSizeChange(val: number) {
|
||||
pageSizeBridge.value = val
|
||||
const newPageCount = pageCountBridge.value
|
||||
if (currentPageBridge.value > newPageCount) {
|
||||
currentPageBridge.value = newPageCount
|
||||
}
|
||||
}
|
||||
|
||||
function prev() {
|
||||
if (props.disabled) return
|
||||
currentPageBridge.value -= 1
|
||||
emit('prev-click', currentPageBridge.value)
|
||||
}
|
||||
|
||||
function next() {
|
||||
if (props.disabled) return
|
||||
currentPageBridge.value += 1
|
||||
emit('next-click', currentPageBridge.value)
|
||||
}
|
||||
|
||||
provide<IPagination>('pagination', {
|
||||
pageCount: pageCountBridge,
|
||||
disabled: computed(() => props.disabled),
|
||||
currentPage: currentPageBridge,
|
||||
changeEvent: handleCurrentChange,
|
||||
handleSizeChange,
|
||||
})
|
||||
|
||||
return () => {
|
||||
if (!assertValidUsage.value) {
|
||||
debugWarn(componentName, t('el.pagination.deprecationWarning'))
|
||||
return null
|
||||
}
|
||||
if (!props.layout) return null
|
||||
if (props.hideOnSinglePage && pageCountBridge.value <= 1) return null
|
||||
const rootChildren = []
|
||||
const rightWrapperChildren = []
|
||||
const rightWrapperRoot = h(
|
||||
'div',
|
||||
{ class: 'el-pagination__rightwrapper' },
|
||||
rightWrapperChildren
|
||||
)
|
||||
const TEMPLATE_MAP = {
|
||||
prev: h(Prev, {
|
||||
disabled: props.disabled,
|
||||
currentPage: currentPageBridge.value,
|
||||
prevText: props.prevText,
|
||||
onClick: prev,
|
||||
}),
|
||||
jumper: h(Jumper as any),
|
||||
pager: h(Pager as any, {
|
||||
currentPage: currentPageBridge.value,
|
||||
pageCount: pageCountBridge.value,
|
||||
pagerCount: props.pagerCount,
|
||||
onChange: handleCurrentChange,
|
||||
disabled: props.disabled,
|
||||
}),
|
||||
next: h(Next as any, {
|
||||
disabled: props.disabled,
|
||||
currentPage: currentPageBridge.value,
|
||||
pageCount: pageCountBridge.value,
|
||||
nextText: props.nextText,
|
||||
onClick: next,
|
||||
}),
|
||||
sizes: h(Sizes as any, {
|
||||
pageSize: pageSizeBridge.value,
|
||||
pageSizes: props.pageSizes,
|
||||
popperClass: props.popperClass,
|
||||
disabled: props.disabled,
|
||||
}),
|
||||
slot: slots?.default?.() ?? null,
|
||||
total: h(Total, { total: isAbsent(props.total) ? 0 : props.total }),
|
||||
}
|
||||
|
||||
const components = props.layout
|
||||
.split(',')
|
||||
.map((item: string) => item.trim())
|
||||
|
||||
let haveRightWrapper = false
|
||||
|
||||
components.forEach((c: keyof typeof TEMPLATE_MAP | '->') => {
|
||||
if (c === '->') {
|
||||
haveRightWrapper = true
|
||||
return
|
||||
}
|
||||
if (!haveRightWrapper) {
|
||||
rootChildren.push(TEMPLATE_MAP[c])
|
||||
} else {
|
||||
rightWrapperChildren.push(TEMPLATE_MAP[c])
|
||||
}
|
||||
})
|
||||
|
||||
if (haveRightWrapper && rightWrapperChildren.length > 0) {
|
||||
rootChildren.unshift(rightWrapperRoot)
|
||||
}
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
role: 'pagination',
|
||||
'aria-label': 'pagination',
|
||||
class: [
|
||||
'el-pagination',
|
||||
{
|
||||
'is-background': props.background,
|
||||
'el-pagination--small': props.small,
|
||||
},
|
||||
],
|
||||
},
|
||||
rootChildren
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,35 +1,335 @@
|
||||
import type { ComputedRef } from 'vue'
|
||||
import type { AnyFunction } from '@element-plus/utils/types'
|
||||
import {
|
||||
h,
|
||||
ref,
|
||||
provide,
|
||||
computed,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { useLocaleInject } from '@element-plus/hooks'
|
||||
import { debugWarn } from '@element-plus/utils/error'
|
||||
import { buildProp, mutable } from '@element-plus/utils/props'
|
||||
import { elPaginationKey } from '@element-plus/tokens'
|
||||
|
||||
export interface IPagination {
|
||||
currentPage?: ComputedRef<number>
|
||||
pageCount?: ComputedRef<number>
|
||||
disabled?: ComputedRef<boolean>
|
||||
changeEvent?: AnyFunction<any>
|
||||
handleSizeChange?: AnyFunction<any>
|
||||
}
|
||||
import Prev from './components/prev.vue'
|
||||
import Next from './components/next.vue'
|
||||
import Sizes from './components/sizes.vue'
|
||||
import Jumper from './components/jumper.vue'
|
||||
import Total from './components/total.vue'
|
||||
import Pager from './components/pager.vue'
|
||||
|
||||
export interface IPaginationProps {
|
||||
pageSize: number
|
||||
small: boolean
|
||||
total: number
|
||||
pageCount: number
|
||||
pagerCount: number
|
||||
currentPage: number
|
||||
layout: Record<string, string | undefined>
|
||||
pageSizes: Array<number>
|
||||
popperClass: string
|
||||
prevText: string
|
||||
nextText: string
|
||||
background: boolean
|
||||
disabled: boolean
|
||||
hideOnSinglePage: boolean
|
||||
}
|
||||
import type { VNode, ExtractPropTypes } from 'vue'
|
||||
|
||||
export interface IPaginationSetups {
|
||||
currentPage: number
|
||||
pageCount: number
|
||||
pagerCount: number
|
||||
disabled: boolean
|
||||
pageSizes: Array<number>
|
||||
/**
|
||||
* It it user's responsibility to guarantee that the value of props.total... is number
|
||||
* (same as pageSize, defaultPageSize, currentPage, defaultCurrentPage, pageCount)
|
||||
* Otherwise we can reasonable infer that the corresponding field is absent
|
||||
*/
|
||||
const isAbsent = (v: unknown): v is undefined => typeof v !== 'number'
|
||||
|
||||
type LayoutKey =
|
||||
| 'prev'
|
||||
| 'pager'
|
||||
| 'next'
|
||||
| 'jumper'
|
||||
| '->'
|
||||
| 'total'
|
||||
| 'sizes'
|
||||
| 'slot'
|
||||
|
||||
const defaultPageSizes = mutable([10, 20, 30, 40, 50, 100] as const)
|
||||
|
||||
export const paginationProps = {
|
||||
total: Number,
|
||||
pageSize: Number,
|
||||
defaultPageSize: Number,
|
||||
currentPage: Number,
|
||||
defaultCurrentPage: Number,
|
||||
pageCount: Number,
|
||||
pagerCount: {
|
||||
type: Number,
|
||||
validator: (value: unknown) => {
|
||||
return (
|
||||
typeof value === 'number' &&
|
||||
(value | 0) === value &&
|
||||
value > 4 &&
|
||||
value < 22 &&
|
||||
value % 2 === 1
|
||||
)
|
||||
},
|
||||
default: 7,
|
||||
},
|
||||
layout: {
|
||||
type: String,
|
||||
default: (
|
||||
['prev', 'pager', 'next', 'jumper', '->', 'total'] as LayoutKey[]
|
||||
).join(', '),
|
||||
},
|
||||
pageSizes: buildProp<number[], false, typeof defaultPageSizes>({
|
||||
type: Array,
|
||||
default: () => defaultPageSizes,
|
||||
}),
|
||||
popperClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
prevText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
nextText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
small: Boolean,
|
||||
background: Boolean,
|
||||
disabled: Boolean,
|
||||
hideOnSinglePage: Boolean,
|
||||
} as const
|
||||
export type PaginationProps = ExtractPropTypes<typeof paginationProps>
|
||||
|
||||
export const paginationEmits = {
|
||||
'update:current-page': (val: number) => typeof val === 'number',
|
||||
'update:page-size': (val: number) => typeof val === 'number',
|
||||
'size-change': (val: number) => typeof val === 'number',
|
||||
'current-change': (val: number) => typeof val === 'number',
|
||||
'prev-click': (val: number) => typeof val === 'number',
|
||||
'next-click': (val: number) => typeof val === 'number',
|
||||
}
|
||||
export type PaginationEmits = typeof paginationEmits
|
||||
|
||||
const componentName = 'ElPagination'
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
|
||||
props: paginationProps,
|
||||
emits: paginationEmits,
|
||||
|
||||
setup(props, { emit, slots }) {
|
||||
const { t } = useLocaleInject()
|
||||
const vnodeProps = getCurrentInstance()!.vnode.props || {}
|
||||
// we can find @xxx="xxx" props on `vnodeProps` to check if user bind corresponding events
|
||||
const hasCurrentPageListener =
|
||||
'onUpdate:currentPage' in vnodeProps ||
|
||||
'onUpdate:current-page' in vnodeProps ||
|
||||
'onCurrentChange' in vnodeProps
|
||||
const hasPageSizeListener =
|
||||
'onUpdate:pageSize' in vnodeProps ||
|
||||
'onUpdate:page-size' in vnodeProps ||
|
||||
'onSizeChange' in vnodeProps
|
||||
const assertValidUsage = computed(() => {
|
||||
// Users have to set either one, otherwise count of pages cannot be determined
|
||||
if (isAbsent(props.total) && isAbsent(props.pageCount)) return false
|
||||
// <el-pagination ...otherProps :current-page="xxx" /> without corresponding listener is forbidden now
|
||||
// Users have to use two way binding of `currentPage`
|
||||
// If users just want to provide a default value, `defaultCurrentPage` is here for you
|
||||
if (!isAbsent(props.currentPage) && !hasCurrentPageListener) return false
|
||||
// When you want to change sizes, things get more complex, detailed below
|
||||
// Basically the most important value we need is page count
|
||||
// either directly from props.pageCount
|
||||
// or calculated from props.total
|
||||
// we will take props.pageCount precedence over props.total
|
||||
if (props.layout.includes('sizes')) {
|
||||
if (!isAbsent(props.pageCount)) {
|
||||
// if props.pageCount is assign by user, then user have to watch pageSize change
|
||||
// and recalculate pageCount
|
||||
if (!hasPageSizeListener) return false
|
||||
} else if (!isAbsent(props.total)) {
|
||||
// Otherwise, we will see if user have props.pageSize defined
|
||||
// If so, meaning user want to have pageSize controlled himself/herself from component
|
||||
// Thus page size listener is required
|
||||
// users are account for page size change
|
||||
if (!isAbsent(props.pageSize)) {
|
||||
if (!hasPageSizeListener) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
// (else block just for explaination)
|
||||
// else page size is controlled by el-pagination internally
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const innerPageSize = ref(
|
||||
isAbsent(props.defaultPageSize) ? 10 : props.defaultPageSize
|
||||
)
|
||||
const innerCurrentPage = ref(
|
||||
isAbsent(props.defaultCurrentPage) ? 1 : props.defaultCurrentPage
|
||||
)
|
||||
|
||||
const pageSizeBridge = computed({
|
||||
get() {
|
||||
return isAbsent(props.pageSize) ? innerPageSize.value : props.pageSize
|
||||
},
|
||||
set(v: number) {
|
||||
if (isAbsent(props.pageSize)) {
|
||||
innerPageSize.value = v
|
||||
}
|
||||
if (hasPageSizeListener) {
|
||||
emit('update:page-size', v)
|
||||
emit('size-change', v)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const pageCountBridge = computed<number>(() => {
|
||||
let pageCount = 0
|
||||
if (!isAbsent(props.pageCount)) {
|
||||
pageCount = props.pageCount
|
||||
} else if (!isAbsent(props.total)) {
|
||||
pageCount = Math.max(1, Math.ceil(props.total / pageSizeBridge.value))
|
||||
}
|
||||
return pageCount
|
||||
})
|
||||
|
||||
const currentPageBridge = computed<number>({
|
||||
get() {
|
||||
return isAbsent(props.currentPage)
|
||||
? innerCurrentPage.value
|
||||
: props.currentPage
|
||||
},
|
||||
set(v) {
|
||||
let newCurrentPage = v
|
||||
if (v < 1) {
|
||||
newCurrentPage = 1
|
||||
} else if (v > pageCountBridge.value) {
|
||||
newCurrentPage = pageCountBridge.value
|
||||
}
|
||||
if (isAbsent(props.currentPage)) {
|
||||
innerCurrentPage.value = newCurrentPage
|
||||
}
|
||||
if (hasCurrentPageListener) {
|
||||
emit('update:current-page', newCurrentPage)
|
||||
emit('current-change', newCurrentPage)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
watch(pageCountBridge, (val) => {
|
||||
if (currentPageBridge.value > val) currentPageBridge.value = val
|
||||
})
|
||||
|
||||
function handleCurrentChange(val: number) {
|
||||
currentPageBridge.value = val
|
||||
}
|
||||
|
||||
function handleSizeChange(val: number) {
|
||||
pageSizeBridge.value = val
|
||||
const newPageCount = pageCountBridge.value
|
||||
if (currentPageBridge.value > newPageCount) {
|
||||
currentPageBridge.value = newPageCount
|
||||
}
|
||||
}
|
||||
|
||||
function prev() {
|
||||
if (props.disabled) return
|
||||
currentPageBridge.value -= 1
|
||||
emit('prev-click', currentPageBridge.value)
|
||||
}
|
||||
|
||||
function next() {
|
||||
if (props.disabled) return
|
||||
currentPageBridge.value += 1
|
||||
emit('next-click', currentPageBridge.value)
|
||||
}
|
||||
|
||||
provide(elPaginationKey, {
|
||||
pageCount: pageCountBridge,
|
||||
disabled: computed(() => props.disabled),
|
||||
currentPage: currentPageBridge,
|
||||
changeEvent: handleCurrentChange,
|
||||
handleSizeChange,
|
||||
})
|
||||
|
||||
return () => {
|
||||
if (!assertValidUsage.value) {
|
||||
debugWarn(componentName, t('el.pagination.deprecationWarning'))
|
||||
return null
|
||||
}
|
||||
if (!props.layout) return null
|
||||
if (props.hideOnSinglePage && pageCountBridge.value <= 1) return null
|
||||
const rootChildren: Array<VNode | VNode[] | null> = []
|
||||
const rightWrapperChildren: Array<VNode | VNode[] | null> = []
|
||||
const rightWrapperRoot = h(
|
||||
'div',
|
||||
{ class: 'el-pagination__rightwrapper' },
|
||||
rightWrapperChildren
|
||||
)
|
||||
const TEMPLATE_MAP: Record<
|
||||
Exclude<LayoutKey, '->'>,
|
||||
VNode | VNode[] | null
|
||||
> = {
|
||||
prev: h(Prev, {
|
||||
disabled: props.disabled,
|
||||
currentPage: currentPageBridge.value,
|
||||
prevText: props.prevText,
|
||||
onClick: prev,
|
||||
}),
|
||||
jumper: h(Jumper),
|
||||
pager: h(Pager, {
|
||||
currentPage: currentPageBridge.value,
|
||||
pageCount: pageCountBridge.value,
|
||||
pagerCount: props.pagerCount,
|
||||
onChange: handleCurrentChange,
|
||||
disabled: props.disabled,
|
||||
}),
|
||||
next: h(Next, {
|
||||
disabled: props.disabled,
|
||||
currentPage: currentPageBridge.value,
|
||||
pageCount: pageCountBridge.value,
|
||||
nextText: props.nextText,
|
||||
onClick: next,
|
||||
}),
|
||||
sizes: h(Sizes, {
|
||||
pageSize: pageSizeBridge.value,
|
||||
pageSizes: props.pageSizes,
|
||||
popperClass: props.popperClass,
|
||||
disabled: props.disabled,
|
||||
}),
|
||||
slot: slots?.default?.() ?? null,
|
||||
total: h(Total, { total: isAbsent(props.total) ? 0 : props.total }),
|
||||
}
|
||||
|
||||
const components = props.layout
|
||||
.split(',')
|
||||
.map((item: string) => item.trim()) as LayoutKey[]
|
||||
|
||||
let haveRightWrapper = false
|
||||
|
||||
components.forEach((c) => {
|
||||
if (c === '->') {
|
||||
haveRightWrapper = true
|
||||
return
|
||||
}
|
||||
if (!haveRightWrapper) {
|
||||
rootChildren.push(TEMPLATE_MAP[c])
|
||||
} else {
|
||||
rightWrapperChildren.push(TEMPLATE_MAP[c])
|
||||
}
|
||||
})
|
||||
|
||||
if (haveRightWrapper && rightWrapperChildren.length > 0) {
|
||||
rootChildren.unshift(rightWrapperRoot)
|
||||
}
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
role: 'pagination',
|
||||
'aria-label': 'pagination',
|
||||
class: [
|
||||
'el-pagination',
|
||||
{
|
||||
'is-background': props.background,
|
||||
'el-pagination--small': props.small,
|
||||
},
|
||||
],
|
||||
},
|
||||
rootChildren
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
import { inject } from 'vue'
|
||||
import type { IPagination } from './pagination'
|
||||
import { elPaginationKey } from '@element-plus/tokens'
|
||||
|
||||
export const usePagination = () => {
|
||||
const pagination = inject<IPagination>('pagination', {})
|
||||
|
||||
return {
|
||||
pagination,
|
||||
pageCount: pagination.pageCount,
|
||||
disabled: pagination.disabled,
|
||||
currentPage: pagination.currentPage,
|
||||
}
|
||||
}
|
||||
export const usePagination = () => inject(elPaginationKey, {})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './form'
|
||||
export * from './button'
|
||||
export * from './breadcrumb'
|
||||
export * from './pagination'
|
||||
|
||||
12
packages/tokens/pagination.ts
Normal file
12
packages/tokens/pagination.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { InjectionKey, ComputedRef } from 'vue'
|
||||
|
||||
export interface ElPaginationContext {
|
||||
currentPage?: ComputedRef<number>
|
||||
pageCount?: ComputedRef<number>
|
||||
disabled?: ComputedRef<boolean>
|
||||
changeEvent?: (val: number) => void
|
||||
handleSizeChange?: (val: number) => void
|
||||
}
|
||||
|
||||
export const elPaginationKey: InjectionKey<ElPaginationContext> =
|
||||
Symbol('elPaginationKey')
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Mutable } from './types'
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
/**
|
||||
@@ -69,5 +70,7 @@ export function buildProp<
|
||||
}
|
||||
|
||||
export const keyOf = <T>(arr: T) => Object.keys(arr) as Array<keyof T>
|
||||
export const mutable = <T extends readonly any[]>(val: T) =>
|
||||
val as Mutable<typeof val>
|
||||
|
||||
export const componentSize = ['large', 'medium', 'small', 'mini'] as const
|
||||
|
||||
@@ -49,3 +49,5 @@ export type TimeoutHandle = ReturnType<typeof global.setTimeout>
|
||||
export type ComponentSize = 'large' | 'medium' | 'small' | 'mini'
|
||||
|
||||
export type StyleValue = string | CSSProperties | Array<StyleValue>
|
||||
|
||||
export type Mutable<T> = { -readonly [P in keyof T]: T[P] }
|
||||
|
||||
Reference in New Issue
Block a user