@bagelink/vue 1.2.89 → 1.2.93
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Pagination.vue.d.ts +35 -0
- package/dist/components/Pagination.vue.d.ts.map +1 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/index.cjs +617 -437
- package/dist/index.mjs +617 -437
- package/dist/style.css +21 -0
- package/package.json +1 -1
- package/src/components/Pagination.vue +252 -0
- package/src/components/index.ts +1 -0
package/dist/style.css
CHANGED
|
@@ -4368,6 +4368,27 @@ body:has(.bg-dark.is-active) {
|
|
|
4368
4368
|
}
|
|
4369
4369
|
}
|
|
4370
4370
|
|
|
4371
|
+
.indicator[data-v-0443aea2] {
|
|
4372
|
+
position: absolute;
|
|
4373
|
+
height: 30px;
|
|
4374
|
+
background-color: var(--bgl-primary);
|
|
4375
|
+
transition: all 0.3s ease;
|
|
4376
|
+
z-index: -1;
|
|
4377
|
+
}
|
|
4378
|
+
.selected[data-v-0443aea2] {
|
|
4379
|
+
color: white !important;
|
|
4380
|
+
}
|
|
4381
|
+
.pagination-info[data-v-0443aea2] {
|
|
4382
|
+
min-width: 60px;
|
|
4383
|
+
text-align: center;
|
|
4384
|
+
font-size: 14px;
|
|
4385
|
+
}
|
|
4386
|
+
.pagination-ellipsis[data-v-0443aea2] {
|
|
4387
|
+
display: flex;
|
|
4388
|
+
align-items: center;
|
|
4389
|
+
padding: 0 4px;
|
|
4390
|
+
}
|
|
4391
|
+
|
|
4371
4392
|
.bgl_pill-btn[data-v-764b6b8b]{
|
|
4372
4393
|
color: var(--pill-btn-color);
|
|
4373
4394
|
background: var(--pill-btn-bg);
|
package/package.json
CHANGED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Btn } from '@bagelink/vue'
|
|
3
|
+
import { watch, nextTick } from 'vue'
|
|
4
|
+
|
|
5
|
+
interface Range {
|
|
6
|
+
start: number
|
|
7
|
+
end: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface PaginationProps {
|
|
11
|
+
totalItems: number
|
|
12
|
+
perPage?: number
|
|
13
|
+
totalPages?: number
|
|
14
|
+
variant?: 'default' | 'simple'
|
|
15
|
+
rtl?: boolean
|
|
16
|
+
maxVisiblePages?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const props = withDefaults(defineProps<PaginationProps>(), {
|
|
20
|
+
totalItems: 0,
|
|
21
|
+
perPage: 25,
|
|
22
|
+
totalPages: undefined,
|
|
23
|
+
variant: 'default',
|
|
24
|
+
rtl: false,
|
|
25
|
+
maxVisiblePages: 3
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const page = defineModel<number>('page', { default: 1 })
|
|
29
|
+
const range = defineModel<Range>('range')
|
|
30
|
+
|
|
31
|
+
const paginationContainer = $ref<HTMLElement>()
|
|
32
|
+
let indicatorPosition = $ref(0)
|
|
33
|
+
let indicatorWidth = $ref(0)
|
|
34
|
+
|
|
35
|
+
// Calculate totalPages from totalItems and perPage if not provided directly
|
|
36
|
+
const computedTotalPages = $computed(() => {
|
|
37
|
+
if (props.totalPages !== undefined) return props.totalPages
|
|
38
|
+
const { perPage } = props
|
|
39
|
+
return Math.max(1, Math.ceil(props.totalItems / perPage))
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
watch(
|
|
43
|
+
[() => page.value, () => props.perPage, () => props.totalItems],
|
|
44
|
+
() => {
|
|
45
|
+
if (range.value) {
|
|
46
|
+
const { perPage } = props
|
|
47
|
+
// Calculate zero-based indices
|
|
48
|
+
const start = (page.value - 1) * perPage
|
|
49
|
+
const end = Math.min(start + perPage - 1, props.totalItems - 1)
|
|
50
|
+
range.value = { start, end }
|
|
51
|
+
}
|
|
52
|
+
// Update indicator position when page changes
|
|
53
|
+
nextTick(() => { updateIndicatorPosition() })
|
|
54
|
+
},
|
|
55
|
+
{ immediate: true }
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
// Also watch for RTL changes
|
|
59
|
+
watch(() => props.rtl, updateIndicatorPosition)
|
|
60
|
+
|
|
61
|
+
// Calculate which page numbers to show
|
|
62
|
+
const visiblePages = $computed(() => {
|
|
63
|
+
const { maxVisiblePages } = props
|
|
64
|
+
|
|
65
|
+
// If we have few enough pages, show all of them
|
|
66
|
+
if (computedTotalPages <= maxVisiblePages * 2) {
|
|
67
|
+
return Array.from({ length: computedTotalPages }, (_, i) => i + 1)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Always include current page and adjacent pages for navigation
|
|
71
|
+
const mustInclude = new Set<number>()
|
|
72
|
+
|
|
73
|
+
// Current page is always included
|
|
74
|
+
mustInclude.add(page.value)
|
|
75
|
+
|
|
76
|
+
// Ensure adjacent pages are included for navigation
|
|
77
|
+
if (page.value > 1) mustInclude.add(page.value - 1)
|
|
78
|
+
if (page.value < computedTotalPages) mustInclude.add(page.value + 1)
|
|
79
|
+
|
|
80
|
+
// Always include first and last pages
|
|
81
|
+
mustInclude.add(1)
|
|
82
|
+
mustInclude.add(computedTotalPages)
|
|
83
|
+
|
|
84
|
+
// Start with explicitly required pages
|
|
85
|
+
const pageArray = Array.from(mustInclude).sort((a, b) => a - b)
|
|
86
|
+
|
|
87
|
+
// Now fill remaining slots if there's room within maxVisiblePages
|
|
88
|
+
if (pageArray.length < maxVisiblePages + 2) {
|
|
89
|
+
// Try to add pages between existing ones
|
|
90
|
+
for (let i = 0; i < pageArray.length - 1; i++) {
|
|
91
|
+
const current = pageArray[i]
|
|
92
|
+
const next = pageArray[i + 1]
|
|
93
|
+
|
|
94
|
+
if (next - current > 1) {
|
|
95
|
+
// There's a gap - fill in additional pages
|
|
96
|
+
const pagesToAdd = Math.min(next - current - 1, maxVisiblePages + 2 - pageArray.length)
|
|
97
|
+
|
|
98
|
+
const newPages = Array.from(
|
|
99
|
+
{ length: pagesToAdd },
|
|
100
|
+
(_, idx) => current + idx + 1
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
pageArray.splice(i + 1, 0, ...newPages)
|
|
104
|
+
i += newPages.length // Skip past inserted items
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return pageArray
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
function updateIndicatorPosition() {
|
|
113
|
+
if (!paginationContainer) return
|
|
114
|
+
|
|
115
|
+
const selectedButton = paginationContainer.querySelector('.selected')
|
|
116
|
+
if (!selectedButton) return
|
|
117
|
+
|
|
118
|
+
const containerRect = paginationContainer.getBoundingClientRect()
|
|
119
|
+
const buttonRect = selectedButton.getBoundingClientRect()
|
|
120
|
+
|
|
121
|
+
// Get position and dimensions
|
|
122
|
+
indicatorWidth = buttonRect.width
|
|
123
|
+
|
|
124
|
+
if (props.rtl) {
|
|
125
|
+
// RTL positioning - align to right edge
|
|
126
|
+
const rightOffset = containerRect.right - buttonRect.right
|
|
127
|
+
indicatorPosition = rightOffset
|
|
128
|
+
} else {
|
|
129
|
+
// LTR positioning - align to left edge
|
|
130
|
+
const leftOffset = buttonRect.left - containerRect.left
|
|
131
|
+
indicatorPosition = leftOffset
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function handleClick(p: number) {
|
|
136
|
+
if (p < 1 || p > computedTotalPages) return
|
|
137
|
+
page.value = p
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function next() {
|
|
141
|
+
handleClick(page.value + 1)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function prev() {
|
|
145
|
+
handleClick(page.value - 1)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Convert zero-based index to one-based for display
|
|
149
|
+
function displayIndex(index: number | undefined): string {
|
|
150
|
+
return index === undefined ? '-' : (index + 1).toString()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface PageItem {
|
|
154
|
+
type: 'page' | 'ellipsis'
|
|
155
|
+
key: string | number
|
|
156
|
+
number?: number
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Enhanced rendering - creates UI with ellipses in the right positions
|
|
160
|
+
const renderPageButtons = $computed(() => {
|
|
161
|
+
const items: PageItem[] = []
|
|
162
|
+
|
|
163
|
+
for (let i = 0; i < visiblePages.length; i++) {
|
|
164
|
+
const pageNum = visiblePages[i]
|
|
165
|
+
|
|
166
|
+
// Insert ellipsis before this page if needed
|
|
167
|
+
if (i > 0) {
|
|
168
|
+
const prevPage = visiblePages[i - 1]
|
|
169
|
+
if (pageNum - prevPage > 1) {
|
|
170
|
+
items.push({
|
|
171
|
+
type: 'ellipsis',
|
|
172
|
+
key: `ellipsis-${i}`
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Add the page button
|
|
178
|
+
items.push({
|
|
179
|
+
type: 'page',
|
|
180
|
+
number: pageNum,
|
|
181
|
+
key: pageNum
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return items
|
|
186
|
+
})
|
|
187
|
+
</script>
|
|
188
|
+
|
|
189
|
+
<template>
|
|
190
|
+
<div v-if="computedTotalPages > 1" ref="paginationContainer" class="relative flex gap-1 justify-content">
|
|
191
|
+
<!-- Default pagination with page numbers -->
|
|
192
|
+
<template v-if="variant !== 'simple'">
|
|
193
|
+
<div
|
|
194
|
+
class="indicator radius-1"
|
|
195
|
+
:style="{
|
|
196
|
+
[rtl ? 'right' : 'left']: `${indicatorPosition}px`,
|
|
197
|
+
width: `${indicatorWidth}px`,
|
|
198
|
+
}"
|
|
199
|
+
/>
|
|
200
|
+
<!-- Render the page buttons and ellipses in order -->
|
|
201
|
+
<template v-for="item in renderPageButtons" :key="item.key">
|
|
202
|
+
<!-- Page button -->
|
|
203
|
+
<Btn
|
|
204
|
+
v-if="item.type === 'page'" flat thin
|
|
205
|
+
:class="{ selected: item.number === page }"
|
|
206
|
+
:value="item.number ? item.number.toString() : ''"
|
|
207
|
+
@click="item.number ? handleClick(item.number) : null"
|
|
208
|
+
/>
|
|
209
|
+
|
|
210
|
+
<!-- Ellipsis -->
|
|
211
|
+
<div v-else-if="item.type === 'ellipsis'" class="pagination-ellipsis">
|
|
212
|
+
...
|
|
213
|
+
</div>
|
|
214
|
+
</template>
|
|
215
|
+
</template>
|
|
216
|
+
|
|
217
|
+
<!-- Simple pagination with prev/next buttons -->
|
|
218
|
+
<template v-else>
|
|
219
|
+
<Btn flat thin :disabled="page <= 1" icon="chevron_left" @click="prev" />
|
|
220
|
+
<span class="pagination-info">
|
|
221
|
+
{{ displayIndex(range?.start) }}-{{ displayIndex(range?.end) }} / {{ props.totalItems }}
|
|
222
|
+
</span>
|
|
223
|
+
<Btn flat thin :disabled="page >= computedTotalPages" icon="chevron_right" @click="next" />
|
|
224
|
+
</template>
|
|
225
|
+
</div>
|
|
226
|
+
</template>
|
|
227
|
+
|
|
228
|
+
<style scoped>
|
|
229
|
+
.indicator {
|
|
230
|
+
position: absolute;
|
|
231
|
+
height: 30px;
|
|
232
|
+
background-color: var(--bgl-primary);
|
|
233
|
+
transition: all 0.3s ease;
|
|
234
|
+
z-index: -1;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.selected {
|
|
238
|
+
color: white !important;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.pagination-info {
|
|
242
|
+
min-width: 60px;
|
|
243
|
+
text-align: center;
|
|
244
|
+
font-size: 14px;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.pagination-ellipsis {
|
|
248
|
+
display: flex;
|
|
249
|
+
align-items: center;
|
|
250
|
+
padding: 0 4px;
|
|
251
|
+
}
|
|
252
|
+
</style>
|
package/src/components/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ export { default as ModalConfirm } from './ModalConfirm.vue'
|
|
|
32
32
|
export { default as ModalForm } from './ModalForm.vue'
|
|
33
33
|
export { default as NavBar } from './NavBar.vue'
|
|
34
34
|
export { default as PageTitle } from './PageTitle.vue'
|
|
35
|
+
export { default as Pagination } from './Pagination.vue'
|
|
35
36
|
export { default as Pill } from './Pill.vue'
|
|
36
37
|
export { default as RouterWrapper } from './RouterWrapper.vue'
|
|
37
38
|
export { default as Slider } from './Slider.vue'
|