diff --git a/README.md b/README.md index aff1e2a..860648e 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,8 @@ Also works pretty well with [`svelte-infinite-loading`](https://github.com/Skayo | :---------------- | :------------------------------------------------ | :-------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | width | `number \| string`\* | ✓ | Width of List. This property will determine the number of rendered items when scrollDirection is `'horizontal'`. | | height | `number \| string`\* | ✓ | Height of List. This property will determine the number of rendered items when scrollDirection is `'vertical'`. | -| itemCount | `number` | ✓ | The number of items you want to render | +| items | `any[]` | | The items you want to render | +| itemCount | `number` | | The number of items you want to render | | itemSize | `number \| number[] \| (index: number) => number` | ✓ | Either a fixed height/width (depending on the scrollDirection), an array containing the heights of all the items in your list, or a function that returns the height of an item given its index: `(index: number): number` | | scrollDirection | `string` | | Whether the list should scroll vertically or horizontally. One of `'vertical'` (default) or `'horizontal'`. | | scrollOffset | `number` | | Can be used to control the scroll offset; Also useful for setting an initial scroll offset | diff --git a/package.json b/package.json index dc52054..942d1b3 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,8 @@ "name": "svelte-tiny-virtual-list", "version": "2.0.5", "description": "A tiny but mighty list virtualization component for svelte, with zero dependencies 💪", - "svelte": "src/index.js", - "main": "dist/svelte-tiny-virtual-list.js", - "module": "dist/svelte-tiny-virtual-list.mjs", - "types": "types/index.d.ts", + "svelte": "src/VirtualList.svelte", + "main": "src/VirtualList.svelte", "scripts": { "build": "rollup -c", "lint": "eslint src/** test/**", @@ -29,8 +27,7 @@ }, "files": [ "src", - "dist", - "types" + "dist" ], "keywords": [ "svelte", diff --git a/src/SizeAndPositionManager.js b/src/SizeAndPositionManager.js index fc70b5b..ee9f6c6 100644 --- a/src/SizeAndPositionManager.js +++ b/src/SizeAndPositionManager.js @@ -17,6 +17,8 @@ import { ALIGNMENT } from './constants'; * @type {object} * @property {number} size * @property {number} offset + * @property {number} expandSize + * @property {number} expandOffset */ /** @@ -29,7 +31,10 @@ import { ALIGNMENT } from './constants'; * @type {object} * @property {number} itemCount * @property {ItemSize} itemSize + * @property {Array} expandItems + * @property {ItemSize} expandItemSize * @property {number} estimatedItemSize + * @property {number} estimatedExpandItemSize */ export default class SizeAndPositionManager { @@ -37,7 +42,13 @@ export default class SizeAndPositionManager { /** * @param {Options} options */ - constructor({ itemSize, itemCount, estimatedItemSize }) { + constructor({ itemCount, itemSize, expandItems, expandItemSize, estimatedItemSize, estimatedExpandItemSize }) { + /** + * @private + * @type {number} + */ + this.itemCount = itemCount; + /** * @private * @type {ItemSize} @@ -46,9 +57,15 @@ export default class SizeAndPositionManager { /** * @private - * @type {number} + * @type {Array} */ - this.itemCount = itemCount; + this.expandItems = expandItems; + + /** + * @private + * @type {ItemSize} + */ + this.expandItemSize = expandItemSize; /** * @private @@ -56,6 +73,12 @@ export default class SizeAndPositionManager { */ this.estimatedItemSize = estimatedItemSize; + /** + * @private + * @type {number} + */ + this.estimatedExpandItemSize = estimatedExpandItemSize; + /** * Cache of size and position data for items, mapped by item index. * @@ -84,17 +107,29 @@ export default class SizeAndPositionManager { /** * @param {Options} options */ - updateConfig({ itemSize, itemCount, estimatedItemSize }) { + updateConfig({ itemCount, itemSize, expandItems, expandItemSize, estimatedItemSize, estimatedExpandItemSize }) { if (itemCount != null) { this.itemCount = itemCount; } + if (itemSize != null) { + this.itemSize = itemSize; + } + + if (expandItems != null) { + this.expandItems = expandItems; + } + + if (expandItemSize != null) { + this.expandItemSize = expandItemSize; + } + if (estimatedItemSize != null) { this.estimatedItemSize = estimatedItemSize; } - if (itemSize != null) { - this.itemSize = itemSize; + if (estimatedExpandItemSize != null) { + this.estimatedExpandItemSize = estimatedExpandItemSize; } this.checkForMismatchItemSizeAndItemCount(); @@ -127,6 +162,21 @@ export default class SizeAndPositionManager { return Array.isArray(itemSize) ? itemSize[index] : itemSize; } + /** + * @param {number} index + */ + getExpandSize(index) { + if (!this.expandItems[index]) return 0; + + const { expandItemSize } = this; + + if (typeof expandItemSize === 'function') { + return expandItemSize(index); + } + + return Array.isArray(expandItemSize) ? expandItemSize[index] : expandItemSize; + } + /** * Compute the totalSize and itemSizeAndPositionData at the start, * only when itemSize is a number or an array. @@ -135,12 +185,16 @@ export default class SizeAndPositionManager { let totalSize = 0; for (let i = 0; i < this.itemCount; i++) { const size = this.getSize(i); + const expandSize = this.getExpandSize(i); const offset = totalSize; - totalSize += size; + const expandOffset = totalSize + size; + totalSize += size + expandSize; this.itemSizeAndPositionData[i] = { - offset, size, + offset, + expandSize, + expandOffset, }; } @@ -180,17 +234,23 @@ export default class SizeAndPositionManager { const lastMeasuredSizeAndPosition = this.getSizeAndPositionOfLastMeasuredItem(); let offset = lastMeasuredSizeAndPosition.offset + lastMeasuredSizeAndPosition.size; + let expandOffset = lastMeasuredSizeAndPosition.expandOffset + lastMeasuredSizeAndPosition.expandSize; for (let i = this.lastMeasuredIndex + 1; i <= index; i++) { const size = this.getSize(i); - + const expandSize = this.getExpandSize(i); if (size == null || isNaN(size)) { throw Error(`Invalid size returned for index ${i} of value ${size}`); } + if (expandSize == null || isNaN(expandSize)) { + throw Error(`Invalid expandSize returned for index ${i} of value ${expandSize}`); + } this.itemSizeAndPositionData[i] = { offset, size, + expandOffset, + expandSize, }; offset += size; @@ -205,7 +265,7 @@ export default class SizeAndPositionManager { getSizeAndPositionOfLastMeasuredItem() { return this.lastMeasuredIndex >= 0 ? this.itemSizeAndPositionData[this.lastMeasuredIndex] - : { offset: 0, size: 0 }; + : { offset: 0, size: 0, expandOffset: 0, expandSize: 0 }; } /** @@ -216,7 +276,6 @@ export default class SizeAndPositionManager { getTotalSize() { // Return the pre computed totalSize when itemSize is number or array. if (this.totalSize) return this.totalSize; - /** * When itemSize is a function, * This value will be completedly estimated initially. @@ -227,7 +286,7 @@ export default class SizeAndPositionManager { return ( lastMeasuredSizeAndPosition.offset + lastMeasuredSizeAndPosition.size + - (this.itemCount - this.lastMeasuredIndex - 1) * this.estimatedItemSize + (this.itemCount - this.lastMeasuredIndex - 1) * (this.estimatedItemSize + this.estimatedExpandItemSize) ); } @@ -240,7 +299,7 @@ export default class SizeAndPositionManager { * @param {number | undefined} targetIndex * @return {number} Offset to use to ensure the specified item is visible */ - getUpdatedOffsetForIndex({ align = ALIGNMENT.START, containerSize, currentOffset, targetIndex }) { + getUpdatedOffsetForIndex(align = ALIGNMENT.START, containerSize, currentOffset, targetIndex) { if (containerSize <= 0) { return 0; } @@ -276,30 +335,30 @@ export default class SizeAndPositionManager { * @param {number} overscanCount * @return {{stop: number|undefined, start: number|undefined}} */ - getVisibleRange({ containerSize = 0, offset, overscanCount }) { + getVisibleRange(containerSize = 0, offset, overscanCount) { const totalSize = this.getTotalSize(); if (totalSize === 0) { return {}; } - - const maxOffset = offset + containerSize; + + const maxOffset = Math.max(0, offset || 0) + containerSize; let start = this.findNearestItem(offset); - + if (start === undefined) { throw Error(`Invalid offset ${offset} specified`); } const datum = this.getSizeAndPositionForIndex(start); - offset = datum.offset + datum.size; + offset = datum.offset + datum.size + datum.expandSize; let stop = start; while (offset < maxOffset && stop < this.itemCount - 1) { stop++; - offset += this.getSizeAndPositionForIndex(stop).size; + offset += this.getSizeAndPositionForIndex(stop).size + this.getSizeAndPositionForIndex(stop).expandSize; } - + if (overscanCount) { start = Math.max(0, start - overscanCount); stop = Math.min(stop + overscanCount, this.itemCount - 1); diff --git a/src/VirtualList.svelte b/src/VirtualList.svelte index 3c5cbe0..089234b 100644 --- a/src/VirtualList.svelte +++ b/src/VirtualList.svelte @@ -1,29 +1,4 @@ - - -
-
- {#each items as item (getKey ? getKey(item.index) : item.index)} - - {/each} -
+ {#if mode === WRAPPER_MODE.DIV} +
+ {#each visibleItems as item (getKey ? getKey(item.index) : item.index)} +
+ +
+ {#if expandItems[item.index]} +
+ +
+ {/if} + {/each} +
+ {:else} + + {#each visibleItems as item (getKey ? getKey(item.index) : item.index)} + + + + {#if expandItems[item.index]} + + + + {/if} + {/each} +
+ {/if}
+ + :global(.virtual-list-container) { + position: relative !important; + overflow: auto !important; + } + \ No newline at end of file diff --git a/src/constants.js b/src/constants.js index 041f75a..d37053d 100644 --- a/src/constants.js +++ b/src/constants.js @@ -24,3 +24,8 @@ export const SCROLL_PROP_LEGACY = { [DIRECTION.VERTICAL]: 'scrollTop', [DIRECTION.HORIZONTAL]: 'scrollLeft', }; + +export const WRAPPER_MODE = { + DIV: 'div', + TABLE: 'table', +}; \ No newline at end of file diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 4b0f77a..0000000 --- a/src/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as default } from './VirtualList.svelte'; \ No newline at end of file diff --git a/types/index.d.ts b/types/index.d.ts deleted file mode 100644 index 2874056..0000000 --- a/types/index.d.ts +++ /dev/null @@ -1,186 +0,0 @@ -/// -import { SvelteComponentTyped } from 'svelte'; - -export type Alignment = "auto" | "start" | "center" | "end"; -export type ScrollBehaviour = "auto" | "smooth" | "instant"; - -export type Direction = "horizontal" | "vertical"; - -export type ItemSizeGetter = (index: number) => number; -export type ItemSize = number | number[] | ItemSizeGetter; - -/** - * VirtualList props - */ -export interface VirtualListProps { - /** - * Width of List. This property will determine the number of rendered items when scrollDirection is `'horizontal'`. - * - * @default '100%' - */ - width?: number | string; - - /** - * Height of List. This property will determine the number of rendered items when scrollDirection is `'vertical'`. - */ - height: number | string; - - /** - * The number of items you want to render - */ - itemCount: number; - - /** - * Either a fixed height/width (depending on the scrollDirection), - * an array containing the heights of all the items in your list, - * or a function that returns the height of an item given its index: `(index: number): number` - */ - itemSize: ItemSize; - - /** - * Whether the list should scroll vertically or horizontally. One of `'vertical'` (default) or `'horizontal'`. - * - * @default 'vertical' - */ - scrollDirection?: Direction; - - /** - * Can be used to control the scroll offset; Also useful for setting an initial scroll offset - */ - scrollOffset?: number; - - /** - * Item index to scroll to (by forcefully scrolling if necessary) - */ - scrollToIndex?: number; - - /** - * Used in combination with `scrollToIndex`, this prop controls the alignment of the scrolled to item. - * One of: `'start'`, `'center'`, `'end'` or `'auto'`. - * Use `'start'` to always align items to the top of the container and `'end'` to align them bottom. - * Use `'center'` to align them in the middle of the container. - * `'auto'` scrolls the least amount possible to ensure that the specified `scrollToIndex` item is fully visible. - */ - scrollToAlignment?: Alignment; - - /** - * Used in combination with `scrollToIndex`, this prop controls the behaviour of the scrolling. - * One of: `'auto'`, `'smooth'` or `'instant'` (default). - */ - scrollToBehaviour?: ScrollBehaviour; - - /** - * An array of indexes (eg. `[0, 10, 25, 30]`) to make certain items in the list sticky (`position: sticky`) - */ - stickyIndices?: number[]; - - /** - * Number of extra buffer items to render above/below the visible items. - * Tweaking this can help reduce scroll flickering on certain browsers/devices. - * - * @default 3 - */ - overscanCount?: number; - - /** - * Used to estimate the total size of the list before all of its items have actually been measured. - * The estimated total height is progressively adjusted as items are rendered. - */ - estimatedItemSize?: number; - - /** - * Function that returns the key of an item in the list, which is used to uniquely identify an item. - * This is useful for dynamic data coming from a database or similar. - * By default, it's using the item's index. - * - * @param index - The index of the item. - * @return - Anything that uniquely identifies the item. - */ - getKey?: (index: number) => any; -} - - -/** - * VirtualList slots - */ -export interface VirtualListSlots { - /** - * Slot for each item - */ - item: { - /** - * Item index - */ - index: number, - - /** - * Item style, must be applied to the slot (look above for example) - */ - style: string - }; - - /** - * Slot for the elements that should appear at the top of the list - */ - header: {}; - - /** - * Slot for the elements that should appear at the bottom of the list (e.g. `VirtualList` component from `svelte-infinite-loading`) - */ - footer: {}; -} - - -export interface ItemsUpdatedDetail { - /** - * Index of the first visible item - */ - start: number; - - /** - * Index of the last visible item - */ - end: number; -} - -export interface ItemsUpdatedEvent extends CustomEvent { -} - - -export interface AfterScrollDetail { - /** - * The original scroll event - */ - event: Event; - - /** - * Either the value of `wrapper.scrollTop` or `wrapper.scrollLeft` - */ - offset: number; -} - -export interface AfterScrollEvent extends CustomEvent { -} - - -/** - * VirtualList events - */ -export interface VirtualListEvents { - /** - * Fired when the visible items are updated - */ - itemsUpdated: ItemsUpdatedEvent; - - /** - * Fired after handling the scroll event - */ - afterScroll: AfterScrollEvent; -} - - -/** - * VirtualList component - */ -export default class VirtualList extends SvelteComponentTyped { -}