340 lines
12 KiB
JavaScript
340 lines
12 KiB
JavaScript
import { createVNode as _createVNode, mergeProps as _mergeProps } from "vue";
|
|
// Styles
|
|
import "./VPagination.css";
|
|
|
|
// Components
|
|
import { VBtn } from "../VBtn/index.mjs"; // Composables
|
|
import { useDisplay } from "../../composables/index.mjs";
|
|
import { makeBorderProps } from "../../composables/border.mjs";
|
|
import { makeComponentProps } from "../../composables/component.mjs";
|
|
import { provideDefaults } from "../../composables/defaults.mjs";
|
|
import { makeDensityProps } from "../../composables/density.mjs";
|
|
import { makeElevationProps } from "../../composables/elevation.mjs";
|
|
import { IconValue } from "../../composables/icons.mjs";
|
|
import { useLocale, useRtl } from "../../composables/locale.mjs";
|
|
import { useProxiedModel } from "../../composables/proxiedModel.mjs";
|
|
import { useRefs } from "../../composables/refs.mjs";
|
|
import { useResizeObserver } from "../../composables/resizeObserver.mjs";
|
|
import { makeRoundedProps } from "../../composables/rounded.mjs";
|
|
import { makeSizeProps } from "../../composables/size.mjs";
|
|
import { makeTagProps } from "../../composables/tag.mjs";
|
|
import { makeThemeProps, provideTheme } from "../../composables/theme.mjs";
|
|
import { makeVariantProps } from "../../composables/variant.mjs"; // Utilities
|
|
import { computed, nextTick, shallowRef, toRef } from 'vue';
|
|
import { createRange, genericComponent, keyValues, propsFactory, useRender } from "../../util/index.mjs"; // Types
|
|
export const makeVPaginationProps = propsFactory({
|
|
activeColor: String,
|
|
start: {
|
|
type: [Number, String],
|
|
default: 1
|
|
},
|
|
modelValue: {
|
|
type: Number,
|
|
default: props => props.start
|
|
},
|
|
disabled: Boolean,
|
|
length: {
|
|
type: [Number, String],
|
|
default: 1,
|
|
validator: val => val % 1 === 0
|
|
},
|
|
totalVisible: [Number, String],
|
|
firstIcon: {
|
|
type: IconValue,
|
|
default: '$first'
|
|
},
|
|
prevIcon: {
|
|
type: IconValue,
|
|
default: '$prev'
|
|
},
|
|
nextIcon: {
|
|
type: IconValue,
|
|
default: '$next'
|
|
},
|
|
lastIcon: {
|
|
type: IconValue,
|
|
default: '$last'
|
|
},
|
|
ariaLabel: {
|
|
type: String,
|
|
default: '$vuetify.pagination.ariaLabel.root'
|
|
},
|
|
pageAriaLabel: {
|
|
type: String,
|
|
default: '$vuetify.pagination.ariaLabel.page'
|
|
},
|
|
currentPageAriaLabel: {
|
|
type: String,
|
|
default: '$vuetify.pagination.ariaLabel.currentPage'
|
|
},
|
|
firstAriaLabel: {
|
|
type: String,
|
|
default: '$vuetify.pagination.ariaLabel.first'
|
|
},
|
|
previousAriaLabel: {
|
|
type: String,
|
|
default: '$vuetify.pagination.ariaLabel.previous'
|
|
},
|
|
nextAriaLabel: {
|
|
type: String,
|
|
default: '$vuetify.pagination.ariaLabel.next'
|
|
},
|
|
lastAriaLabel: {
|
|
type: String,
|
|
default: '$vuetify.pagination.ariaLabel.last'
|
|
},
|
|
ellipsis: {
|
|
type: String,
|
|
default: '...'
|
|
},
|
|
showFirstLastPage: Boolean,
|
|
...makeBorderProps(),
|
|
...makeComponentProps(),
|
|
...makeDensityProps(),
|
|
...makeElevationProps(),
|
|
...makeRoundedProps(),
|
|
...makeSizeProps(),
|
|
...makeTagProps({
|
|
tag: 'nav'
|
|
}),
|
|
...makeThemeProps(),
|
|
...makeVariantProps({
|
|
variant: 'text'
|
|
})
|
|
}, 'VPagination');
|
|
export const VPagination = genericComponent()({
|
|
name: 'VPagination',
|
|
props: makeVPaginationProps(),
|
|
emits: {
|
|
'update:modelValue': value => true,
|
|
first: value => true,
|
|
prev: value => true,
|
|
next: value => true,
|
|
last: value => true
|
|
},
|
|
setup(props, _ref) {
|
|
let {
|
|
slots,
|
|
emit
|
|
} = _ref;
|
|
const page = useProxiedModel(props, 'modelValue');
|
|
const {
|
|
t,
|
|
n
|
|
} = useLocale();
|
|
const {
|
|
isRtl
|
|
} = useRtl();
|
|
const {
|
|
themeClasses
|
|
} = provideTheme(props);
|
|
const {
|
|
width
|
|
} = useDisplay();
|
|
const maxButtons = shallowRef(-1);
|
|
provideDefaults(undefined, {
|
|
scoped: true
|
|
});
|
|
const {
|
|
resizeRef
|
|
} = useResizeObserver(entries => {
|
|
if (!entries.length) return;
|
|
const {
|
|
target,
|
|
contentRect
|
|
} = entries[0];
|
|
const firstItem = target.querySelector('.v-pagination__list > *');
|
|
if (!firstItem) return;
|
|
const totalWidth = contentRect.width;
|
|
const itemWidth = firstItem.offsetWidth + parseFloat(getComputedStyle(firstItem).marginRight) * 2;
|
|
maxButtons.value = getMax(totalWidth, itemWidth);
|
|
});
|
|
const length = computed(() => parseInt(props.length, 10));
|
|
const start = computed(() => parseInt(props.start, 10));
|
|
const totalVisible = computed(() => {
|
|
if (props.totalVisible != null) return parseInt(props.totalVisible, 10);else if (maxButtons.value >= 0) return maxButtons.value;
|
|
return getMax(width.value, 58);
|
|
});
|
|
function getMax(totalWidth, itemWidth) {
|
|
const minButtons = props.showFirstLastPage ? 5 : 3;
|
|
return Math.max(0, Math.floor(
|
|
// Round to two decimal places to avoid floating point errors
|
|
+((totalWidth - itemWidth * minButtons) / itemWidth).toFixed(2)));
|
|
}
|
|
const range = computed(() => {
|
|
if (length.value <= 0 || isNaN(length.value) || length.value > Number.MAX_SAFE_INTEGER) return [];
|
|
if (totalVisible.value <= 0) return [];else if (totalVisible.value === 1) return [page.value];
|
|
if (length.value <= totalVisible.value) {
|
|
return createRange(length.value, start.value);
|
|
}
|
|
const even = totalVisible.value % 2 === 0;
|
|
const middle = even ? totalVisible.value / 2 : Math.floor(totalVisible.value / 2);
|
|
const left = even ? middle : middle + 1;
|
|
const right = length.value - middle;
|
|
if (left - page.value >= 0) {
|
|
return [...createRange(Math.max(1, totalVisible.value - 1), start.value), props.ellipsis, length.value];
|
|
} else if (page.value - right >= (even ? 1 : 0)) {
|
|
const rangeLength = totalVisible.value - 1;
|
|
const rangeStart = length.value - rangeLength + start.value;
|
|
return [start.value, props.ellipsis, ...createRange(rangeLength, rangeStart)];
|
|
} else {
|
|
const rangeLength = Math.max(1, totalVisible.value - 3);
|
|
const rangeStart = rangeLength === 1 ? page.value : page.value - Math.ceil(rangeLength / 2) + start.value;
|
|
return [start.value, props.ellipsis, ...createRange(rangeLength, rangeStart), props.ellipsis, length.value];
|
|
}
|
|
});
|
|
|
|
// TODO: 'first' | 'prev' | 'next' | 'last' does not work here?
|
|
function setValue(e, value, event) {
|
|
e.preventDefault();
|
|
page.value = value;
|
|
event && emit(event, value);
|
|
}
|
|
const {
|
|
refs,
|
|
updateRef
|
|
} = useRefs();
|
|
provideDefaults({
|
|
VPaginationBtn: {
|
|
color: toRef(props, 'color'),
|
|
border: toRef(props, 'border'),
|
|
density: toRef(props, 'density'),
|
|
size: toRef(props, 'size'),
|
|
variant: toRef(props, 'variant'),
|
|
rounded: toRef(props, 'rounded'),
|
|
elevation: toRef(props, 'elevation')
|
|
}
|
|
});
|
|
const items = computed(() => {
|
|
return range.value.map((item, index) => {
|
|
const ref = e => updateRef(e, index);
|
|
if (typeof item === 'string') {
|
|
return {
|
|
isActive: false,
|
|
key: `ellipsis-${index}`,
|
|
page: item,
|
|
props: {
|
|
ref,
|
|
ellipsis: true,
|
|
icon: true,
|
|
disabled: true
|
|
}
|
|
};
|
|
} else {
|
|
const isActive = item === page.value;
|
|
return {
|
|
isActive,
|
|
key: item,
|
|
page: n(item),
|
|
props: {
|
|
ref,
|
|
ellipsis: false,
|
|
icon: true,
|
|
disabled: !!props.disabled || +props.length < 2,
|
|
color: isActive ? props.activeColor : props.color,
|
|
'aria-current': isActive,
|
|
'aria-label': t(isActive ? props.currentPageAriaLabel : props.pageAriaLabel, item),
|
|
onClick: e => setValue(e, item)
|
|
}
|
|
};
|
|
}
|
|
});
|
|
});
|
|
const controls = computed(() => {
|
|
const prevDisabled = !!props.disabled || page.value <= start.value;
|
|
const nextDisabled = !!props.disabled || page.value >= start.value + length.value - 1;
|
|
return {
|
|
first: props.showFirstLastPage ? {
|
|
icon: isRtl.value ? props.lastIcon : props.firstIcon,
|
|
onClick: e => setValue(e, start.value, 'first'),
|
|
disabled: prevDisabled,
|
|
'aria-label': t(props.firstAriaLabel),
|
|
'aria-disabled': prevDisabled
|
|
} : undefined,
|
|
prev: {
|
|
icon: isRtl.value ? props.nextIcon : props.prevIcon,
|
|
onClick: e => setValue(e, page.value - 1, 'prev'),
|
|
disabled: prevDisabled,
|
|
'aria-label': t(props.previousAriaLabel),
|
|
'aria-disabled': prevDisabled
|
|
},
|
|
next: {
|
|
icon: isRtl.value ? props.prevIcon : props.nextIcon,
|
|
onClick: e => setValue(e, page.value + 1, 'next'),
|
|
disabled: nextDisabled,
|
|
'aria-label': t(props.nextAriaLabel),
|
|
'aria-disabled': nextDisabled
|
|
},
|
|
last: props.showFirstLastPage ? {
|
|
icon: isRtl.value ? props.firstIcon : props.lastIcon,
|
|
onClick: e => setValue(e, start.value + length.value - 1, 'last'),
|
|
disabled: nextDisabled,
|
|
'aria-label': t(props.lastAriaLabel),
|
|
'aria-disabled': nextDisabled
|
|
} : undefined
|
|
};
|
|
});
|
|
function updateFocus() {
|
|
const currentIndex = page.value - start.value;
|
|
refs.value[currentIndex]?.$el.focus();
|
|
}
|
|
function onKeydown(e) {
|
|
if (e.key === keyValues.left && !props.disabled && page.value > +props.start) {
|
|
page.value = page.value - 1;
|
|
nextTick(updateFocus);
|
|
} else if (e.key === keyValues.right && !props.disabled && page.value < start.value + length.value - 1) {
|
|
page.value = page.value + 1;
|
|
nextTick(updateFocus);
|
|
}
|
|
}
|
|
useRender(() => _createVNode(props.tag, {
|
|
"ref": resizeRef,
|
|
"class": ['v-pagination', themeClasses.value, props.class],
|
|
"style": props.style,
|
|
"role": "navigation",
|
|
"aria-label": t(props.ariaLabel),
|
|
"onKeydown": onKeydown,
|
|
"data-test": "v-pagination-root"
|
|
}, {
|
|
default: () => [_createVNode("ul", {
|
|
"class": "v-pagination__list"
|
|
}, [props.showFirstLastPage && _createVNode("li", {
|
|
"key": "first",
|
|
"class": "v-pagination__first",
|
|
"data-test": "v-pagination-first"
|
|
}, [slots.first ? slots.first(controls.value.first) : _createVNode(VBtn, _mergeProps({
|
|
"_as": "VPaginationBtn"
|
|
}, controls.value.first), null)]), _createVNode("li", {
|
|
"key": "prev",
|
|
"class": "v-pagination__prev",
|
|
"data-test": "v-pagination-prev"
|
|
}, [slots.prev ? slots.prev(controls.value.prev) : _createVNode(VBtn, _mergeProps({
|
|
"_as": "VPaginationBtn"
|
|
}, controls.value.prev), null)]), items.value.map((item, index) => _createVNode("li", {
|
|
"key": item.key,
|
|
"class": ['v-pagination__item', {
|
|
'v-pagination__item--is-active': item.isActive
|
|
}],
|
|
"data-test": "v-pagination-item"
|
|
}, [slots.item ? slots.item(item) : _createVNode(VBtn, _mergeProps({
|
|
"_as": "VPaginationBtn"
|
|
}, item.props), {
|
|
default: () => [item.page]
|
|
})])), _createVNode("li", {
|
|
"key": "next",
|
|
"class": "v-pagination__next",
|
|
"data-test": "v-pagination-next"
|
|
}, [slots.next ? slots.next(controls.value.next) : _createVNode(VBtn, _mergeProps({
|
|
"_as": "VPaginationBtn"
|
|
}, controls.value.next), null)]), props.showFirstLastPage && _createVNode("li", {
|
|
"key": "last",
|
|
"class": "v-pagination__last",
|
|
"data-test": "v-pagination-last"
|
|
}, [slots.last ? slots.last(controls.value.last) : _createVNode(VBtn, _mergeProps({
|
|
"_as": "VPaginationBtn"
|
|
}, controls.value.last), null)])])]
|
|
}));
|
|
return {};
|
|
}
|
|
});
|
|
//# sourceMappingURL=VPagination.mjs.map
|