VPagination.mjs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. import { createVNode as _createVNode, mergeProps as _mergeProps } from "vue";
  2. // Styles
  3. import "./VPagination.css";
  4. // Components
  5. import { VBtn } from "../VBtn/index.mjs"; // Composables
  6. import { useDisplay } from "../../composables/index.mjs";
  7. import { makeBorderProps } from "../../composables/border.mjs";
  8. import { makeComponentProps } from "../../composables/component.mjs";
  9. import { provideDefaults } from "../../composables/defaults.mjs";
  10. import { makeDensityProps } from "../../composables/density.mjs";
  11. import { makeElevationProps } from "../../composables/elevation.mjs";
  12. import { IconValue } from "../../composables/icons.mjs";
  13. import { useLocale, useRtl } from "../../composables/locale.mjs";
  14. import { useProxiedModel } from "../../composables/proxiedModel.mjs";
  15. import { useRefs } from "../../composables/refs.mjs";
  16. import { useResizeObserver } from "../../composables/resizeObserver.mjs";
  17. import { makeRoundedProps } from "../../composables/rounded.mjs";
  18. import { makeSizeProps } from "../../composables/size.mjs";
  19. import { makeTagProps } from "../../composables/tag.mjs";
  20. import { makeThemeProps, provideTheme } from "../../composables/theme.mjs";
  21. import { makeVariantProps } from "../../composables/variant.mjs"; // Utilities
  22. import { computed, nextTick, shallowRef, toRef } from 'vue';
  23. import { createRange, genericComponent, keyValues, propsFactory, useRender } from "../../util/index.mjs"; // Types
  24. export const makeVPaginationProps = propsFactory({
  25. activeColor: String,
  26. start: {
  27. type: [Number, String],
  28. default: 1
  29. },
  30. modelValue: {
  31. type: Number,
  32. default: props => props.start
  33. },
  34. disabled: Boolean,
  35. length: {
  36. type: [Number, String],
  37. default: 1,
  38. validator: val => val % 1 === 0
  39. },
  40. totalVisible: [Number, String],
  41. firstIcon: {
  42. type: IconValue,
  43. default: '$first'
  44. },
  45. prevIcon: {
  46. type: IconValue,
  47. default: '$prev'
  48. },
  49. nextIcon: {
  50. type: IconValue,
  51. default: '$next'
  52. },
  53. lastIcon: {
  54. type: IconValue,
  55. default: '$last'
  56. },
  57. ariaLabel: {
  58. type: String,
  59. default: '$vuetify.pagination.ariaLabel.root'
  60. },
  61. pageAriaLabel: {
  62. type: String,
  63. default: '$vuetify.pagination.ariaLabel.page'
  64. },
  65. currentPageAriaLabel: {
  66. type: String,
  67. default: '$vuetify.pagination.ariaLabel.currentPage'
  68. },
  69. firstAriaLabel: {
  70. type: String,
  71. default: '$vuetify.pagination.ariaLabel.first'
  72. },
  73. previousAriaLabel: {
  74. type: String,
  75. default: '$vuetify.pagination.ariaLabel.previous'
  76. },
  77. nextAriaLabel: {
  78. type: String,
  79. default: '$vuetify.pagination.ariaLabel.next'
  80. },
  81. lastAriaLabel: {
  82. type: String,
  83. default: '$vuetify.pagination.ariaLabel.last'
  84. },
  85. ellipsis: {
  86. type: String,
  87. default: '...'
  88. },
  89. showFirstLastPage: Boolean,
  90. ...makeBorderProps(),
  91. ...makeComponentProps(),
  92. ...makeDensityProps(),
  93. ...makeElevationProps(),
  94. ...makeRoundedProps(),
  95. ...makeSizeProps(),
  96. ...makeTagProps({
  97. tag: 'nav'
  98. }),
  99. ...makeThemeProps(),
  100. ...makeVariantProps({
  101. variant: 'text'
  102. })
  103. }, 'VPagination');
  104. export const VPagination = genericComponent()({
  105. name: 'VPagination',
  106. props: makeVPaginationProps(),
  107. emits: {
  108. 'update:modelValue': value => true,
  109. first: value => true,
  110. prev: value => true,
  111. next: value => true,
  112. last: value => true
  113. },
  114. setup(props, _ref) {
  115. let {
  116. slots,
  117. emit
  118. } = _ref;
  119. const page = useProxiedModel(props, 'modelValue');
  120. const {
  121. t,
  122. n
  123. } = useLocale();
  124. const {
  125. isRtl
  126. } = useRtl();
  127. const {
  128. themeClasses
  129. } = provideTheme(props);
  130. const {
  131. width
  132. } = useDisplay();
  133. const maxButtons = shallowRef(-1);
  134. provideDefaults(undefined, {
  135. scoped: true
  136. });
  137. const {
  138. resizeRef
  139. } = useResizeObserver(entries => {
  140. if (!entries.length) return;
  141. const {
  142. target,
  143. contentRect
  144. } = entries[0];
  145. const firstItem = target.querySelector('.v-pagination__list > *');
  146. if (!firstItem) return;
  147. const totalWidth = contentRect.width;
  148. const itemWidth = firstItem.offsetWidth + parseFloat(getComputedStyle(firstItem).marginRight) * 2;
  149. maxButtons.value = getMax(totalWidth, itemWidth);
  150. });
  151. const length = computed(() => parseInt(props.length, 10));
  152. const start = computed(() => parseInt(props.start, 10));
  153. const totalVisible = computed(() => {
  154. if (props.totalVisible != null) return parseInt(props.totalVisible, 10);else if (maxButtons.value >= 0) return maxButtons.value;
  155. return getMax(width.value, 58);
  156. });
  157. function getMax(totalWidth, itemWidth) {
  158. const minButtons = props.showFirstLastPage ? 5 : 3;
  159. return Math.max(0, Math.floor(
  160. // Round to two decimal places to avoid floating point errors
  161. +((totalWidth - itemWidth * minButtons) / itemWidth).toFixed(2)));
  162. }
  163. const range = computed(() => {
  164. if (length.value <= 0 || isNaN(length.value) || length.value > Number.MAX_SAFE_INTEGER) return [];
  165. if (totalVisible.value <= 0) return [];else if (totalVisible.value === 1) return [page.value];
  166. if (length.value <= totalVisible.value) {
  167. return createRange(length.value, start.value);
  168. }
  169. const even = totalVisible.value % 2 === 0;
  170. const middle = even ? totalVisible.value / 2 : Math.floor(totalVisible.value / 2);
  171. const left = even ? middle : middle + 1;
  172. const right = length.value - middle;
  173. if (left - page.value >= 0) {
  174. return [...createRange(Math.max(1, totalVisible.value - 1), start.value), props.ellipsis, length.value];
  175. } else if (page.value - right >= (even ? 1 : 0)) {
  176. const rangeLength = totalVisible.value - 1;
  177. const rangeStart = length.value - rangeLength + start.value;
  178. return [start.value, props.ellipsis, ...createRange(rangeLength, rangeStart)];
  179. } else {
  180. const rangeLength = Math.max(1, totalVisible.value - 3);
  181. const rangeStart = rangeLength === 1 ? page.value : page.value - Math.ceil(rangeLength / 2) + start.value;
  182. return [start.value, props.ellipsis, ...createRange(rangeLength, rangeStart), props.ellipsis, length.value];
  183. }
  184. });
  185. // TODO: 'first' | 'prev' | 'next' | 'last' does not work here?
  186. function setValue(e, value, event) {
  187. e.preventDefault();
  188. page.value = value;
  189. event && emit(event, value);
  190. }
  191. const {
  192. refs,
  193. updateRef
  194. } = useRefs();
  195. provideDefaults({
  196. VPaginationBtn: {
  197. color: toRef(props, 'color'),
  198. border: toRef(props, 'border'),
  199. density: toRef(props, 'density'),
  200. size: toRef(props, 'size'),
  201. variant: toRef(props, 'variant'),
  202. rounded: toRef(props, 'rounded'),
  203. elevation: toRef(props, 'elevation')
  204. }
  205. });
  206. const items = computed(() => {
  207. return range.value.map((item, index) => {
  208. const ref = e => updateRef(e, index);
  209. if (typeof item === 'string') {
  210. return {
  211. isActive: false,
  212. key: `ellipsis-${index}`,
  213. page: item,
  214. props: {
  215. ref,
  216. ellipsis: true,
  217. icon: true,
  218. disabled: true
  219. }
  220. };
  221. } else {
  222. const isActive = item === page.value;
  223. return {
  224. isActive,
  225. key: item,
  226. page: n(item),
  227. props: {
  228. ref,
  229. ellipsis: false,
  230. icon: true,
  231. disabled: !!props.disabled || +props.length < 2,
  232. color: isActive ? props.activeColor : props.color,
  233. 'aria-current': isActive,
  234. 'aria-label': t(isActive ? props.currentPageAriaLabel : props.pageAriaLabel, item),
  235. onClick: e => setValue(e, item)
  236. }
  237. };
  238. }
  239. });
  240. });
  241. const controls = computed(() => {
  242. const prevDisabled = !!props.disabled || page.value <= start.value;
  243. const nextDisabled = !!props.disabled || page.value >= start.value + length.value - 1;
  244. return {
  245. first: props.showFirstLastPage ? {
  246. icon: isRtl.value ? props.lastIcon : props.firstIcon,
  247. onClick: e => setValue(e, start.value, 'first'),
  248. disabled: prevDisabled,
  249. 'aria-label': t(props.firstAriaLabel),
  250. 'aria-disabled': prevDisabled
  251. } : undefined,
  252. prev: {
  253. icon: isRtl.value ? props.nextIcon : props.prevIcon,
  254. onClick: e => setValue(e, page.value - 1, 'prev'),
  255. disabled: prevDisabled,
  256. 'aria-label': t(props.previousAriaLabel),
  257. 'aria-disabled': prevDisabled
  258. },
  259. next: {
  260. icon: isRtl.value ? props.prevIcon : props.nextIcon,
  261. onClick: e => setValue(e, page.value + 1, 'next'),
  262. disabled: nextDisabled,
  263. 'aria-label': t(props.nextAriaLabel),
  264. 'aria-disabled': nextDisabled
  265. },
  266. last: props.showFirstLastPage ? {
  267. icon: isRtl.value ? props.firstIcon : props.lastIcon,
  268. onClick: e => setValue(e, start.value + length.value - 1, 'last'),
  269. disabled: nextDisabled,
  270. 'aria-label': t(props.lastAriaLabel),
  271. 'aria-disabled': nextDisabled
  272. } : undefined
  273. };
  274. });
  275. function updateFocus() {
  276. const currentIndex = page.value - start.value;
  277. refs.value[currentIndex]?.$el.focus();
  278. }
  279. function onKeydown(e) {
  280. if (e.key === keyValues.left && !props.disabled && page.value > +props.start) {
  281. page.value = page.value - 1;
  282. nextTick(updateFocus);
  283. } else if (e.key === keyValues.right && !props.disabled && page.value < start.value + length.value - 1) {
  284. page.value = page.value + 1;
  285. nextTick(updateFocus);
  286. }
  287. }
  288. useRender(() => _createVNode(props.tag, {
  289. "ref": resizeRef,
  290. "class": ['v-pagination', themeClasses.value, props.class],
  291. "style": props.style,
  292. "role": "navigation",
  293. "aria-label": t(props.ariaLabel),
  294. "onKeydown": onKeydown,
  295. "data-test": "v-pagination-root"
  296. }, {
  297. default: () => [_createVNode("ul", {
  298. "class": "v-pagination__list"
  299. }, [props.showFirstLastPage && _createVNode("li", {
  300. "key": "first",
  301. "class": "v-pagination__first",
  302. "data-test": "v-pagination-first"
  303. }, [slots.first ? slots.first(controls.value.first) : _createVNode(VBtn, _mergeProps({
  304. "_as": "VPaginationBtn"
  305. }, controls.value.first), null)]), _createVNode("li", {
  306. "key": "prev",
  307. "class": "v-pagination__prev",
  308. "data-test": "v-pagination-prev"
  309. }, [slots.prev ? slots.prev(controls.value.prev) : _createVNode(VBtn, _mergeProps({
  310. "_as": "VPaginationBtn"
  311. }, controls.value.prev), null)]), items.value.map((item, index) => _createVNode("li", {
  312. "key": item.key,
  313. "class": ['v-pagination__item', {
  314. 'v-pagination__item--is-active': item.isActive
  315. }],
  316. "data-test": "v-pagination-item"
  317. }, [slots.item ? slots.item(item) : _createVNode(VBtn, _mergeProps({
  318. "_as": "VPaginationBtn"
  319. }, item.props), {
  320. default: () => [item.page]
  321. })])), _createVNode("li", {
  322. "key": "next",
  323. "class": "v-pagination__next",
  324. "data-test": "v-pagination-next"
  325. }, [slots.next ? slots.next(controls.value.next) : _createVNode(VBtn, _mergeProps({
  326. "_as": "VPaginationBtn"
  327. }, controls.value.next), null)]), props.showFirstLastPage && _createVNode("li", {
  328. "key": "last",
  329. "class": "v-pagination__last",
  330. "data-test": "v-pagination-last"
  331. }, [slots.last ? slots.last(controls.value.last) : _createVNode(VBtn, _mergeProps({
  332. "_as": "VPaginationBtn"
  333. }, controls.value.last), null)])])]
  334. }));
  335. return {};
  336. }
  337. });
  338. //# sourceMappingURL=VPagination.mjs.map