diff --git a/src/vue/ListItem.tsx b/src/vue/ListItem.tsx index 07988a96..435ccc44 100644 --- a/src/vue/ListItem.tsx +++ b/src/vue/ListItem.tsx @@ -16,6 +16,7 @@ import { StateVersion, VirtualStore, } from "../core"; +import { ItemProps } from "./utils"; /** * @internal @@ -33,6 +34,7 @@ export const ListItem = /*#__PURE__*/ defineComponent({ _isHorizontal: { type: Boolean }, _isSSR: { type: Boolean }, _as: { type: String as PropType, required: true }, + _itemProps: Object as PropType>, }, setup(props) { const elementRef = ref(); @@ -65,6 +67,8 @@ export const ListItem = /*#__PURE__*/ defineComponent({ } = props; const isHide = hide.value; + const { style: styleProp, ...rest } = props._itemProps ?? {}; + const style: StyleValue = { position: isHide && isSSR ? undefined : "absolute", [isHorizontal ? "height" : "width"]: "100%", @@ -72,13 +76,14 @@ export const ListItem = /*#__PURE__*/ defineComponent({ [isHorizontal ? (isRTLDocument() ? "right" : "left") : "top"]: offset.value + "px", visibility: !isHide || isSSR ? "visible" : "hidden", + ...styleProp, }; if (isHorizontal) { style.display = "flex"; } return ( - + {children} ); diff --git a/src/vue/VList.tsx b/src/vue/VList.tsx index a24f6284..b658a676 100644 --- a/src/vue/VList.tsx +++ b/src/vue/VList.tsx @@ -7,8 +7,10 @@ import { ComponentObjectPropsOptions, ref, VNode, + PropType, } from "vue"; import { Virtualizer, VirtualizerHandle } from "./Virtualizer"; +import { ItemProps } from "./utils"; interface VListHandle extends VirtualizerHandle {} @@ -41,6 +43,16 @@ const props = { * A prop for SSR. If set, the specified amount of items will be mounted in the initial rendering regardless of the container size until hydrated. */ ssrCount: Number, + /** + * A function that provides properties/attributes for item element + * + * **This prop will be merged into `item` prop in the future** + */ + itemProps: Function as PropType, + /** + * List of indexes that should be always mounted, even when off screen. + */ + keepMounted: Array as PropType, } satisfies ComponentObjectPropsOptions; export const VList = /*#__PURE__*/ defineComponent({ @@ -93,9 +105,11 @@ export const VList = /*#__PURE__*/ defineComponent({ data={props.data} overscan={props.overscan} itemSize={props.itemSize} + itemProps={props.itemProps} shift={props.shift} ssrCount={props.ssrCount} horizontal={horizontal} + keepMounted={props.keepMounted} onScroll={onScroll} onScrollEnd={onScrollEnd} > diff --git a/src/vue/Virtualizer.tsx b/src/vue/Virtualizer.tsx index 24819a37..4648251f 100644 --- a/src/vue/Virtualizer.tsx +++ b/src/vue/Virtualizer.tsx @@ -27,9 +27,10 @@ import { ItemsRange, ScrollToIndexOpts, microtask, + sort, } from "../core"; import { ListItem } from "./ListItem"; -import { getKey, isSameRange } from "./utils"; +import { getKey, isSameRange, ItemProps } from "./utils"; export interface VirtualizerHandle { /** @@ -127,6 +128,16 @@ const props = { * @defaultValue "div" */ item: { type: String as PropType, default: "div" }, + /** + * A function that provides properties/attributes for item element + * + * **This prop will be merged into `item` prop in the future** + */ + itemProps: Function as PropType, + /** + * List of indexes that should be always mounted, even when off screen. + */ + keepMounted: Array as PropType, } satisfies ComponentObjectPropsOptions; export const Virtualizer = /*#__PURE__*/ defineComponent({ @@ -248,9 +259,10 @@ export const Virtualizer = /*#__PURE__*/ defineComponent({ const total = totalSize.value; const items: VNode[] = []; - for (let i = startIndex, j = endIndex; i <= j; i++) { + + function getListItem(i: number) { const e = slots.default({ item: props.data![i]!, index: i })[0]!; - items.push( + return ( ); } + for (let i = startIndex, j = endIndex; i <= j; i++) { + items.push(getListItem(i)); + } + + if (props.keepMounted) { + const startItems: VNode[] = []; + const endItems: VNode[] = []; + sort(props.keepMounted).forEach((index) => { + if (index < startIndex) { + startItems.push(getListItem(index)); + } + if (index > endIndex) { + endItems.push(getListItem(index)); + } + }); + + items.unshift(...startItems); + items.push(...endItems); + } return ( => { export const isSameRange = (prev: ItemsRange, next: ItemsRange): boolean => { return prev[0] === next[0] && prev[1] === next[1]; }; + +export type ItemProps = (payload: { + item: any; + index: number; +}) => { [key: string]: any; style?: CSSProperties; class?: string } | undefined; diff --git a/stories/vue/StickyGroup.vue b/stories/vue/StickyGroup.vue new file mode 100644 index 00000000..928cdb9b --- /dev/null +++ b/stories/vue/StickyGroup.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/stories/vue/VList.stories.ts b/stories/vue/VList.stories.ts index 8d0e7e4d..6ba16044 100644 --- a/stories/vue/VList.stories.ts +++ b/stories/vue/VList.stories.ts @@ -3,6 +3,7 @@ import { VList } from "../../src/vue"; import DefaultComponent from "./Default.vue"; import HorizontalComponent from "./Horizontal.vue"; import ControllsComponent from "./Controlls.vue"; +import StickyGroupComponent from './StickyGroup.vue'; export default { component: VList, @@ -28,3 +29,10 @@ export const Controlls: StoryObj = { template: "", }), }; + +export const StickyGroup: StoryObj = { + render: () => ({ + components: { Component: StickyGroupComponent }, + template: "", + }), +} \ No newline at end of file