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

Commit

Permalink
feat: add prop respectScrollToOnResize
Browse files Browse the repository at this point in the history
  • Loading branch information
rocwang committed Sep 27, 2022
1 parent 7db1339 commit 6d9b9b6
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 56 deletions.
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,17 @@ npm install vue-virtual-scroll-grid

## Available Props

| Name | Description | Type | Validation |
|----------------------------|-----------------------------------------------------------------------------------|----------------------------------------------------------------|------------------------------------------------------|
| `tag` | The HTML tag used as container element. Default value is `div` | `string` | Any valid HTML tag |
| `probeTag` | The HTML tag used as probe element. Default value is `div` | `string` | Any valid HTML tag |
| `length` | The number of items in the list | `number` | Required, an integer greater than or equal to 0 |
| `pageProvider` | The callback that returns a page of items as a promise. `pageNumber` start with 0 | `(pageNumber: number, pageSize: number) => Promise<unknown[]>` | Required |
| `pageProviderDebounceTime` | Debounce window in milliseconds on the calls to `pageProvider` | `number` | Optional, an integer greater than or equal to 0 |
| `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 |
| `scrollTo` | Scroll to a specific item by index | `number` | Optional, an integer from 0 to the `length` prop - 1 |
| `scrollBehavior` | The behavior of `scrollTo`. Default value is `smooth` | `smooth` &#124; `auto` | Optional, a string to be `smooth` or `auto` |
| Name | Description | Type | Validation |
| -------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------- |
| `length` | The number of items in the list | `number` | Required, an integer greater than or equal to 0 |
| `pageProvider` | The callback that returns a page of items as a promise. `pageNumber` start with 0 | `(pageNumber: number, pageSize: number) => Promise<unknown[]>` | Required |
| `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 |
| `pageProviderDebounceTime` | Debounce window in milliseconds on the calls to `pageProvider` | `number` | Optional, an integer greater than or equal to 0, defaults to `0` |
| `probeTag` | The HTML tag used as probe element. Default value is `div` | `string` | Optional, any valid HTML tag, defaults to `div` |
| `respectScrollToOnResize` | Snap to the position set by `scrollTo` when the grid container is resized | `boolean` | Optional, defaults to `false` |
| `scrollBehavior` | The behavior of `scrollTo`. Default value is `smooth` | `smooth` &#124; `auto` | Optional, a string to be `smooth` or `auto`, defaults to `smooth` |
| `scrollTo` | Scroll to a specific item by index | `number` | Optional, an integer from 0 to the `length` prop - 1, defaults to 0 |
| `tag` | The HTML tag used as container element. Default value is `div` | `string` | Optional, any valid HTML tag, defaults to `div` |

Example:

Expand All @@ -50,7 +51,7 @@ Example:
:pageSize="40"
:scrollTo="10"
>
<!-- ...slots -->
<!-- ...slots -->
</Grid>
```

Expand Down
11 changes: 9 additions & 2 deletions src/Grid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ export default defineComponent({
required: false,
validator: (value: number) => Number.isInteger(value) && value >= 0,
},
// Snap to `scrollTo` when the grid container is resized
respectScrollToOnResize: {
type: Boolean as PropType<boolean>,
required: false,
default: true,
},
scrollBehavior: {
type: String as PropType<"smooth" | "auto">,
required: false,
Expand Down Expand Up @@ -129,13 +135,14 @@ export default defineComponent({
rootResize$: fromResizeObserver(rootRef, "target"),
// a stream of root elements when scrolling
scroll$: fromWindowScroll(rootRef),
respectScrollToOnResize$: fromProp(props, "respectScrollToOnResize"),
scrollTo$: fromProp(props, "scrollTo"),
});
onUpdated(
once(() => {
scrollAction$.subscribe(({ target, offset }: ScrollAction) => {
target.scrollTo({ ...offset, behavior: props.scrollBehavior });
scrollAction$.subscribe(({ target, top, left }: ScrollAction) => {
target.scrollTo({ top, left, behavior: props.scrollBehavior });
});
})
);
Expand Down
3 changes: 3 additions & 0 deletions src/demo/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
:pageProviderDebounceTime="0"
:scrollTo="scrollTo"
:scrollBehavior="scrollBehavior"
:respectScrollToOnResize="respectScrollToOnResize"
:class="[$style.grid, $style[scrollMode]]"
>
<template v-slot:probe>
Expand Down Expand Up @@ -56,6 +57,7 @@ import {
scrollMode,
scrollTo,
scrollBehavior,
respectScrollToOnResize,
} from "./store";
export default defineComponent({
Expand All @@ -68,6 +70,7 @@ export default defineComponent({
scrollMode,
scrollTo,
scrollBehavior,
respectScrollToOnResize,
}),
});
</script>
Expand Down
49 changes: 36 additions & 13 deletions src/demo/Control.vue
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,18 @@
/>
</div>

<div :class="$style.respectScrollToOnResize">
<label for="respectScrollToOnResize" :class="$style.radioLabel">
<input
type="checkbox"
id="respectScrollToOnResize"
v-model="respectScrollToOnResize"
:class="$style.radio"
/>
Snap to "Scroll To" on resizing
</label>
</div>

<div :class="$style.scrollBehavior">
<p :class="$style.category">Scroll Behavior:</p>

Expand Down Expand Up @@ -147,6 +159,7 @@ import {
scrollMode,
scrollTo,
scrollBehavior,
respectScrollToOnResize,
} from "./store";
export default defineComponent({
Expand All @@ -159,6 +172,7 @@ export default defineComponent({
scrollMode,
scrollTo,
scrollBehavior,
respectScrollToOnResize,
};
},
});
Expand All @@ -176,34 +190,34 @@ export default defineComponent({
display: grid;
grid-template:
"length scrollMode pageProvider" auto
"pageSize scrollMode pageProvider" auto
"scrollTo scrollMode pageProvider" auto
"scrollBehavior scrollMode pageProvider" auto
/ 2fr 1fr 1fr;
place-items: center stretch;
grid-gap: 1.5rem;
"length pageSize" auto
"pageProvider scrollTo" auto
"scrollMode scrollBehavior" auto
"respectScrollTo respectScrollTo" auto
/ 1fr 1fr;
place-items: start;
grid-gap: 1rem;
}
.length {
grid-area: length;
justify-self: stretch;
}
.pageSize {
grid-area: pageSize;
justify-self: stretch;
}
.pageProvider {
grid-area: pageProvider;
place-self: stretch;
display: flex;
flex-flow: column nowrap;
justify-content: flex-start;
}
.scrollMode {
grid-area: scrollMode;
place-self: stretch;
display: flex;
flex-flow: column nowrap;
justify-content: flex-start;
Expand All @@ -215,16 +229,19 @@ export default defineComponent({
.scrollBehavior {
grid-area: scrollBehavior;
place-self: stretch;
display: flex;
flex-flow: column nowrap;
justify-content: flex-start;
}
.respectScrollToOnResize {
grid-area: respectScrollTo;
}
.radioList {
flex: 1 1 auto;
display: flex;
flex-flow: column nowrap;
flex-flow: row nowrap;
justify-content: space-between;
}
Expand All @@ -238,6 +255,7 @@ export default defineComponent({
.radioLabel {
display: inline;
margin-right: 1rem;
}
.range {
Expand Down Expand Up @@ -266,8 +284,9 @@ export default defineComponent({
@media (min-width: 760px) {
.root {
grid-template:
"length pageSize pageProvider scrollMode scrollTo scrollBehavior" auto
/ 2fr 2fr 2fr 2fr 1fr 1fr;
"length pageSize pageProvider scrollMode scrollTo respectScrollTo scrollBehavior" auto
/ 3fr 3fr 3fr 3fr 1fr 3fr 2fr;
grid-gap: 1.5rem;
}
.category {
Expand All @@ -282,5 +301,9 @@ export default defineComponent({
.radioLabel {
margin-right: 2rem;
}
.respectScrollToOnResize {
place-self: center;
}
}
</style>
1 change: 1 addition & 0 deletions src/demo/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { curry, prop } from "ramda";
export const length = ref<number>(1000);
export const pageSize = ref<number>(40);
export const scrollTo = ref<number | undefined>(undefined);
export const respectScrollToOnResize = ref<boolean>(false);

export type ScrollBehavior = "smooth" | "auto";
export const scrollBehavior = ref<ScrollBehavior>("smooth");
Expand Down
58 changes: 28 additions & 30 deletions src/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ import {
filter,
map,
merge,
mergeAll,
mergeMap,
Observable,
of,
range,
scan,
shareReplay,
switchMap,
take,
withLatestFrom,
} from "rxjs";
import {
__,
Expand Down Expand Up @@ -320,17 +318,14 @@ interface PipelineInput {
itemRect$: Observable<DOMRectReadOnly>;
rootResize$: Observable<Element>;
scroll$: Observable<Element>;
respectScrollToOnResize$: Observable<boolean>;
scrollTo$: Observable<number | undefined>;
}

interface ScrollOffset {
left?: number;
top?: number;
}

export type ScrollAction = {
target: Element;
offset: ScrollOffset;
top: number;
left: number;
};

interface PipelineOutput {
Expand All @@ -347,6 +342,7 @@ export function pipeline({
itemRect$,
rootResize$,
scroll$,
respectScrollToOnResize$,
scrollTo$,
}: PipelineInput): PipelineOutput {
// region: measurements of the visual grid
Expand All @@ -370,12 +366,24 @@ export function pipeline({
const scrollToNotNil$: Observable<number> = scrollTo$.pipe(
filter(complement(isNil))
);
const scrollAction$: Observable<ScrollAction> = combineLatest([
scrollToNotNil$,
resizeMeasurement$,
rootResize$,
]).pipe(
mergeMap<[number, ResizeMeasurement, Element], ScrollAction[]>(
const scrollAction$: Observable<ScrollAction> = respectScrollToOnResize$.pipe(
switchMap((respectScrollToOnResize) =>
respectScrollToOnResize
? // Emit when any input stream emits
combineLatest<[number, ResizeMeasurement, Element]>([
scrollToNotNil$,
resizeMeasurement$,
rootResize$,
])
: // Emit only when the source stream emmits
scrollToNotNil$.pipe(
withLatestFrom<number, [ResizeMeasurement, Element]>(
resizeMeasurement$,
rootResize$
)
)
),
map<[number, ResizeMeasurement, Element], ScrollAction>(
([scrollTo, resizeMeasurement, rootEl]) => {
const { vertical: verticalScrollEl, horizontal: horizontalScrollEl } =
getScrollParents(rootEl);
Expand Down Expand Up @@ -409,21 +417,11 @@ export function pipeline({

const { x, y } = getItemOffsetByIndex(scrollTo, resizeMeasurement);

const scrollLeft =
x + leftToGridContainer + gridPaddingLeft + gridBoarderLeft;
const scrollTop =
y + topToGridContainer + gridPaddingTop + gridBoarderTop;

return [
{
target: verticalScrollEl,
offset: { top: scrollTop },
},
{
target: horizontalScrollEl,
offset: { left: scrollLeft },
},
];
return {
target: verticalScrollEl,
top: y + topToGridContainer + gridPaddingTop + gridBoarderTop,
left: x + leftToGridContainer + gridPaddingLeft + gridBoarderLeft,
};
}
)
);
Expand Down

0 comments on commit 6d9b9b6

Please sign in to comment.