Skip to content
This repository was archived by the owner on Nov 5, 2023. It is now read-only.

Commit a9f5505

Browse files
authored
feature: support scrolling to a specific item by index (#65)
resolve #53
1 parent 64d346b commit a9f5505

File tree

6 files changed

+90
-19
lines changed

6 files changed

+90
-19
lines changed

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,20 @@ npm install vue-virtual-scroll-grid
2929

3030
## Available Props
3131

32-
| Name | Description | Type | Validation |
33-
|----------------|---------------------------------------------------------------------------|----------------------------------------------------------------|-------------------------------------------------|
34-
| `length` | The number of items in the list | `number` | Required, an integer greater than or equal to 0 |
35-
| `pageProvider` | The callback that returns a page of items as a promise | `(pageNumber: number, pageSize: number) => Promise<unknown[]>` | Required |
36-
| `pageSize` | The number of items in a page from the item provider (e.g. a backend API) | `number` | Required, an integer greater than or equal to 1 |
32+
| Name | Description | Type | Validation |
33+
|----------------|---------------------------------------------------------------------------|----------------------------------------------------------------|---------------------------------------------------------------------------------|
34+
| `length` | The number of items in the list | `number` | Required, an integer greater than or equal to 0 |
35+
| `pageProvider` | The callback that returns a page of items as a promise | `(pageNumber: number, pageSize: number) => Promise<unknown[]>` | Required |
36+
| `pageSize` | The number of items in a page from the item provider (e.g. a backend API) | `number` | Required, an integer greater than or equal to 1 |
37+
| `scrollTo` | Scroll to a specific item by index | `number` | Optional, an integer from 0 to the `length` prop - 1 |
3738

3839
Example:
3940

4041
```vue
4142
<Grid :length="1000"
4243
:pageProvider="async (pageNumber, pageSize) => Array(pageSize).fill('x')"
4344
:pageSize="40"
45+
:scrollTo="10"
4446
>
4547
<!-- ...slots -->
4648
</Grid>

src/Grid.vue

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,15 @@
4040
</template>
4141

4242
<script lang="ts">
43-
import { defineComponent, PropType, ref } from "vue";
43+
import { defineComponent, onUpdated, PropType, ref } from "vue";
4444
import {
4545
fromProp,
4646
fromResizeObserver,
4747
fromWindowScroll,
4848
useObservable,
4949
} from "./utilites";
5050
import { PageProvider, pipeline } from "./pipeline";
51+
import { once } from "ramda";
5152
5253
export default defineComponent({
5354
name: "Grid",
@@ -69,6 +70,12 @@ export default defineComponent({
6970
required: true,
7071
validator: (value: number) => Number.isInteger(value) && value >= 1,
7172
},
73+
// Scroll to a specific item by index, must be less than the length prop
74+
scrollTo: {
75+
type: Number as PropType<number>,
76+
required: false,
77+
validator: (value: number) => Number.isInteger(value) && value >= 0,
78+
},
7279
},
7380
setup(props) {
7481
// template refs
@@ -79,6 +86,7 @@ export default defineComponent({
7986
const {
8087
buffer$, // the items in the current scanning window
8188
contentHeight$, // the height of the whole list
89+
windowScrollTo$, // the value sent to window.scrollTo()
8290
} = pipeline({
8391
// streams of prop
8492
length$: fromProp(props, "length"),
@@ -90,8 +98,17 @@ export default defineComponent({
9098
rootResize$: fromResizeObserver(rootRef, "target"),
9199
// a stream of root elements when scrolling
92100
scroll$: fromWindowScroll(rootRef),
101+
scrollTo$: fromProp(props, "scrollTo"),
93102
});
94103
104+
onUpdated(
105+
once(() => {
106+
windowScrollTo$.subscribe((next) => {
107+
window.scrollTo({ top: next, behavior: "smooth" });
108+
});
109+
})
110+
);
111+
95112
return {
96113
rootRef,
97114
probeRef,

src/demo/App.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
:length="length"
66
:pageSize="pageSize"
77
:pageProvider="pageProvider"
8+
:scrollTo="scrollTo"
89
:class="$style.grid"
910
>
1011
<template v-slot:probe>
@@ -42,7 +43,7 @@ import Grid from "../Grid.vue";
4243
import Header from "./Header.vue";
4344
import Control from "./Control.vue";
4445
import ProductItem from "./ProductItem.vue";
45-
import { length, pageSize, pageProvider } from "./store";
46+
import { length, pageSize, pageProvider, scrollTo } from "./store";
4647
4748
export default defineComponent({
4849
name: "App",
@@ -51,6 +52,7 @@ export default defineComponent({
5152
length,
5253
pageSize,
5354
pageProvider,
55+
scrollTo,
5456
}),
5557
});
5658
</script>

src/demo/Control.vue

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<div :class="$style.root">
33
<div :class="$style.length">
4-
<label for="length" :class="$style.rangeLabel">
4+
<label for="length" :class="$style.label">
55
Item Count: {{ length }}
66
</label>
77
<input
@@ -16,7 +16,7 @@
1616
</div>
1717

1818
<div :class="$style.pageSize">
19-
<label for="pageSize" :class="$style.rangeLabel">
19+
<label for="pageSize" :class="$style.label">
2020
Items Per Page: {{ pageSize }}
2121
</label>
2222
<input
@@ -67,17 +67,29 @@
6767
</label>
6868
</div>
6969
</div>
70+
71+
<div :class="$style.scrollTo">
72+
<label for="pageSize" :class="$style.label"> Scroll To: </label>
73+
<input
74+
type="number"
75+
id="scrollTo"
76+
min="0"
77+
max="1000"
78+
v-model.number="scrollTo"
79+
:class="$style.number"
80+
/>
81+
</div>
7082
</div>
7183
</template>
7284

7385
<script lang="ts">
7486
import { defineComponent } from "vue";
75-
import { collection, length, pageSize } from "./store";
87+
import { collection, length, pageSize, scrollTo } from "./store";
7688
7789
export default defineComponent({
7890
name: "Control",
7991
setup: () => {
80-
return { length, pageSize, collection };
92+
return { length, pageSize, collection, scrollTo };
8193
},
8294
});
8395
</script>
@@ -96,6 +108,7 @@ export default defineComponent({
96108
grid-template:
97109
"length pageProvider" auto
98110
"pageSize pageProvider" auto
111+
"scrollTo pageProvider" auto
99112
/ 2fr 1fr;
100113
place-items: center stretch;
101114
grid-gap: 1.5rem;
@@ -117,6 +130,10 @@ export default defineComponent({
117130
justify-content: flex-start;
118131
}
119132
133+
.scrollTo {
134+
grid-area: scrollTo;
135+
}
136+
120137
.radioList {
121138
flex: 1 1 auto;
122139
display: flex;
@@ -140,7 +157,16 @@ export default defineComponent({
140157
width: 100%;
141158
}
142159
143-
.rangeLabel {
160+
.number {
161+
background-color: var(--color-rice);
162+
width: 100%;
163+
border: 1px solid var(--color-black);
164+
padding: 5px;
165+
font-size: 1.4rem;
166+
color: var(--color-black);
167+
}
168+
169+
.label {
144170
margin-bottom: 0.5rem;
145171
font-weight: 700;
146172
}
@@ -153,11 +179,8 @@ export default defineComponent({
153179
@media (min-width: 760px) {
154180
.root {
155181
grid-template:
156-
"length pageSize pageProvider" auto
157-
/ 1fr 1fr 1fr;
158-
}
159-
160-
.pageProvider {
182+
"length pageSize pageProvider scrollTo" auto
183+
/ 2fr 2fr 2fr 1fr;
161184
}
162185
163186
.category {

src/demo/store.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { curry, prop } from "ramda";
44

55
export const length = ref<number>(1000);
66
export const pageSize = ref<number>(40);
7+
export const scrollTo = ref<number | undefined>(undefined);
78

89
export type Collection = "" | "all-mens" | "womens-view-all";
910
export const collection = ref<Collection>("");

src/pipeline.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
1-
import { combineLatest, merge, Observable, range } from "rxjs";
1+
import { combineLatest, merge, Observable, of, range } from "rxjs";
22
import {
33
distinct,
44
distinctUntilChanged,
5+
filter,
56
map,
67
mergeMap,
78
scan,
89
shareReplay,
910
switchMap,
11+
take,
1012
withLatestFrom,
1113
} from "rxjs/operators";
1214
import {
1315
__,
1416
addIndex,
1517
apply,
18+
complement,
1619
concat,
1720
difference,
1821
equals,
1922
identity,
2023
ifElse,
2124
insertAll,
25+
isNil,
2226
map as ramdaMap,
2327
pipe,
2428
remove,
@@ -221,11 +225,13 @@ interface PipelineInput {
221225
itemRect$: Observable<DOMRectReadOnly>;
222226
rootResize$: Observable<Element>;
223227
scroll$: Observable<Element>;
228+
scrollTo$: Observable<number | undefined>;
224229
}
225230

226231
interface PipelineOutput {
227232
buffer$: Observable<InternalItem[]>;
228233
contentHeight$: Observable<number>;
234+
windowScrollTo$: Observable<number>;
229235
}
230236

231237
export function pipeline({
@@ -235,6 +241,7 @@ export function pipeline({
235241
itemRect$,
236242
rootResize$,
237243
scroll$,
244+
scrollTo$,
238245
}: PipelineInput): PipelineOutput {
239246
// region: measurements of the visual grid
240247
const heightAboveWindow$: Observable<number> = merge(
@@ -253,6 +260,25 @@ export function pipeline({
253260
);
254261
// endregion
255262

263+
// region: scroll to a given item by index
264+
const windowScrollTo$ = scrollTo$.pipe(
265+
filter(complement(isNil)),
266+
switchMap((scrollTo) =>
267+
combineLatest([of(scrollTo), resizeMeasurement$, rootResize$]).pipe(
268+
take(1)
269+
)
270+
),
271+
map(
272+
([scrollTo, { columns, itemHeightWithGap }, rootEl]) =>
273+
// The offset within the grid
274+
Math.floor(scrollTo / columns) * itemHeightWithGap +
275+
// The offset of grid root to the document
276+
(rootEl.getBoundingClientRect().top +
277+
document.documentElement.scrollTop)
278+
)
279+
);
280+
// endregion
281+
256282
// region: rendering buffer
257283
const bufferMeta$: Observable<BufferMeta> = combineLatest(
258284
[heightAboveWindow$, resizeMeasurement$],
@@ -285,5 +311,5 @@ export function pipeline({
285311
).pipe(scan(accumulateBuffer, []));
286312
// endregion
287313

288-
return { buffer$, contentHeight$ };
314+
return { buffer$, contentHeight$, windowScrollTo$ };
289315
}

0 commit comments

Comments
 (0)