@omnitend/dashboard-for-laravel 0.31.0 → 0.32.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnitend/dashboard-for-laravel",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "Vue 3 dashboard components for Laravel with Bootstrap Vue Next",
5
5
  "type": "module",
6
6
  "main": "./dist/dashboard-for-laravel.umd.cjs",
@@ -19,16 +19,40 @@
19
19
  <template>
20
20
  <div class="dx-table-pagination mt-3">
21
21
  <!-- Top row: Pagination and Per-page selector -->
22
- <div class="d-flex justify-content-between align-items-center mb-2">
23
- <!-- Pagination controls (only when multiple pages) -->
24
- <DPagination
22
+ <div class="dx-pager-row d-flex justify-content-between align-items-center gap-2 mb-2">
23
+ <!-- Windowed pager (#155): «Previous / leading / … / window / … /
24
+ trailing / Next». Client-computed from current + last page, so it
25
+ works in every DXTable mode. Only shown when >1 page. -->
26
+ <nav
25
27
  v-if="showPagination && pagination.total > pagination.per_page"
26
- :model-value="pagination.current_page"
27
- :total-rows="pagination.total"
28
- :per-page="pagination.per_page"
29
- size="sm"
30
- @update:model-value="emit('page-change', $event as number)"
31
- />
28
+ class="dx-pager d-flex flex-wrap align-items-center gap-1"
29
+ aria-label="Pagination"
30
+ >
31
+ <DButton
32
+ variant="outline-secondary"
33
+ size="sm"
34
+ :disabled="!canPrev"
35
+ @click="goToPage(currentPage - 1)"
36
+ >&laquo; Previous</DButton>
37
+
38
+ <template v-for="(item, index) in pageItems" :key="item.type === 'page' ? `p${item.page}` : `e${index}`">
39
+ <DButton
40
+ v-if="item.type === 'page'"
41
+ size="sm"
42
+ :variant="item.active ? 'primary' : 'outline-secondary'"
43
+ :aria-current="item.active ? 'page' : undefined"
44
+ @click="goToPage(item.page)"
45
+ >{{ item.page }}</DButton>
46
+ <span v-else class="dx-pager-ellipsis" aria-hidden="true">&hellip;</span>
47
+ </template>
48
+
49
+ <DButton
50
+ variant="outline-secondary"
51
+ size="sm"
52
+ :disabled="!canNext"
53
+ @click="goToPage(currentPage + 1)"
54
+ >Next &raquo;</DButton>
55
+ </nav>
32
56
  <div v-else></div>
33
57
 
34
58
  <!-- Per-page selector -->
@@ -67,7 +91,8 @@
67
91
  </template>
68
92
 
69
93
  <script setup lang="ts">
70
- import DPagination from "../base/DPagination.vue";
94
+ import { computed } from "vue";
95
+ import DButton from "../base/DButton.vue";
71
96
  import DFormSelect from "../base/DFormSelect.vue";
72
97
  import type { PaginationData } from "./DXTable.vue";
73
98
 
@@ -92,7 +117,7 @@ interface Props {
92
117
  hasActiveFilters: boolean;
93
118
  }
94
119
 
95
- defineProps<Props>();
120
+ const props = defineProps<Props>();
96
121
 
97
122
  const emit = defineEmits<{
98
123
  /** The user picked a page. Payload is the new 1-based page number. */
@@ -104,26 +129,95 @@ const emit = defineEmits<{
104
129
  */
105
130
  'per-page-change': [value: number | string | null | (number | string)[]];
106
131
  }>();
132
+
133
+ // Windowed pager (#155). Always show the first/last BOUNDARY pages and a window
134
+ // of AROUND pages each side of the current page; fill gaps with an ellipsis,
135
+ // except a single-page gap which shows the page itself (an ellipsis hiding one
136
+ // page is silly). Client-computed from current + last page so it works in every
137
+ // DXTable mode (provider / client-side / inertia) — not just where a server
138
+ // `links` array exists. Reproduces custard's `1 2 … 8 9 10 [11] 12 13 14 … 44 45`.
139
+ const BOUNDARY = 2;
140
+ const AROUND = 3;
141
+
142
+ type PageItem = { type: "page"; page: number; active: boolean } | { type: "ellipsis" };
143
+
144
+ const currentPage = computed(() => props.pagination.current_page);
145
+ const lastPage = computed(() =>
146
+ Math.max(1, Math.ceil(props.pagination.total / props.pagination.per_page)),
147
+ );
148
+
149
+ const pageItems = computed<PageItem[]>(() => {
150
+ const last = lastPage.value;
151
+ const current = currentPage.value;
152
+
153
+ const shown = new Set<number>();
154
+ // Leading + trailing boundary pages (1,2 … last-1,last).
155
+ for (let page = 1; page <= Math.min(BOUNDARY, last); page += 1) shown.add(page);
156
+ for (let page = Math.max(1, last - BOUNDARY + 1); page <= last; page += 1) shown.add(page);
157
+ // A window around the current page. When it clamps at an edge, push the other
158
+ // edge out by the shortfall so the row stays a full width near the start/end
159
+ // (a fuller run of numbers, like the house pager) instead of going sparse.
160
+ let windowLow = current - AROUND;
161
+ let windowHigh = current + AROUND;
162
+ const shortfall = 2 * AROUND + 1 - (Math.min(last, windowHigh) - Math.max(1, windowLow) + 1);
163
+ if (shortfall > 0 && windowLow < 1) windowHigh += shortfall;
164
+ else if (shortfall > 0 && windowHigh > last) windowLow -= shortfall;
165
+ for (let page = Math.max(1, windowLow); page <= Math.min(last, windowHigh); page += 1) {
166
+ shown.add(page);
167
+ }
168
+
169
+ const sorted = [...shown].sort((a, b) => a - b);
170
+ const items: PageItem[] = [];
171
+ let previous = 0;
172
+ for (const page of sorted) {
173
+ if (page - previous === 2) {
174
+ // Exactly one page missing — show it rather than an ellipsis.
175
+ items.push({ type: "page", page: previous + 1, active: previous + 1 === current });
176
+ } else if (page - previous > 2) {
177
+ items.push({ type: "ellipsis" });
178
+ }
179
+ items.push({ type: "page", page, active: page === current });
180
+ previous = page;
181
+ }
182
+ return items;
183
+ });
184
+
185
+ const canPrev = computed(() => currentPage.value > 1);
186
+ const canNext = computed(() => currentPage.value < lastPage.value);
187
+
188
+ const goToPage = (page: number) => {
189
+ if (page < 1 || page > lastPage.value || page === currentPage.value) return;
190
+ emit("page-change", page);
191
+ };
107
192
  </script>
108
193
 
109
194
  <style scoped>
110
- /* Pagination button sizing to match form controls. Anchored on this component's
111
- own `.dx-table-pagination` root so the scope-id has a plain-element host — a
112
- `:deep()` rule that targeted the pager from DXTable would not reach it once
113
- the pager lives here (the scope-id doesn't cross the component boundary). */
114
- .dx-table-pagination :deep(.pagination) {
115
- margin-bottom: 0;
195
+ /* Windowed pager (#155). The buttons are DButtons; give the numbered ones a
196
+ consistent min-width so they read as an even row of keys (like the house
197
+ custard pager), and let the whole row wrap on narrow widths rather than
198
+ overflow. `:deep(.btn)` is anchored on the plain `.dx-pager` host, so the
199
+ scope-id has a deterministic element to attach to. */
200
+ .dx-pager :deep(.btn) {
201
+ min-width: 2.5rem;
202
+ }
203
+
204
+ /* Previous / Next carry text, so they shouldn't be forced square. */
205
+ .dx-pager :deep(.btn):first-child,
206
+ .dx-pager :deep(.btn):last-child {
207
+ min-width: auto;
116
208
  }
117
209
 
118
- .dx-table-pagination :deep(.pagination-sm .page-link) {
119
- min-width: 2.25rem;
120
- height: auto;
210
+ .dx-pager-ellipsis {
211
+ display: inline-flex;
212
+ align-items: flex-end;
213
+ padding: 0 0.15rem;
214
+ color: var(--bs-secondary-color);
215
+ user-select: none;
121
216
  }
122
217
 
123
- /* Make disabled pagination buttons more subtle */
124
- .dx-table-pagination :deep(.pagination .page-item.disabled .page-link) {
125
- background-color: transparent;
126
- border-color: transparent;
127
- opacity: 0.3;
218
+ /* On narrow screens the row can get long; allow it to scroll horizontally as a
219
+ fallback so it never blows out the table width. */
220
+ .dx-pager-row {
221
+ flex-wrap: wrap;
128
222
  }
129
223
  </style>