├── .gitignore ├── lib ├── VariableSizeGrid.svelte ├── VariableSizeList.svelte ├── FixedSizeList.svelte ├── FixedSizeGrid.svelte ├── FixedSizeGridSSR.svelte ├── FixedSizeListSSR.svelte ├── VariableSizeGridSSR.svelte └── VariableSizeListSSR.svelte ├── src ├── shallowDiffers.js ├── styleString.js ├── shouldComponentUpdate.js ├── areEqual.js ├── index.js ├── timer.js ├── domHelpers.js ├── FixedSizeList.js ├── FixedSizeGrid.js ├── VariableSizeList.js ├── VariableSizeGrid.js ├── ListComponent.svelte ├── GridComponent.svelte └── index.d.ts ├── LICENSE.md ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /lib/VariableSizeGrid.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /lib/VariableSizeList.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /lib/FixedSizeList.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /lib/FixedSizeGrid.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/shallowDiffers.js: -------------------------------------------------------------------------------- 1 | // Ported from "react-window@1.8.6" 2 | // Copyright (c) 2018 Brian Vaughn 3 | 4 | 5 | // Pulled from react-compat 6 | // https://github.com/developit/preact-compat/blob/7c5de00e7c85e2ffd011bf3af02899b63f699d3a/src/index.js#L349 7 | export default function shallowDiffers(prev, next) { 8 | for (let attribute in prev) { 9 | if (!(attribute in next)) { 10 | return true; 11 | } 12 | } 13 | for (let attribute in next) { 14 | if (prev[attribute] !== next[attribute]) { 15 | return true; 16 | } 17 | } 18 | return false; 19 | } 20 | -------------------------------------------------------------------------------- /src/styleString.js: -------------------------------------------------------------------------------- 1 | export const styleString = (s) => (s.position !== undefined ? "position: " + s.position + ";" : "") + 2 | (s.left !== undefined ? "left: " + s.left + "px;" : "") + 3 | (s.right !== undefined ? "right: " + s.right + "px;" : "") + 4 | (s.top !== undefined ? "top: " + s.top + "px;" : "") + 5 | (s.height !== undefined ? 6 | "height: " + (typeof s.height === "number" ? s.height + "px" : s.height) + ";" 7 | : "") + 8 | (s.width !== undefined ? 9 | "width: " + (typeof s.width === "number" ? s.width + "px" : s.width) + ";" 10 | : ""); -------------------------------------------------------------------------------- /src/shouldComponentUpdate.js: -------------------------------------------------------------------------------- 1 | // Ported from "react-window@1.8.6" 2 | // Copyright (c) 2018 Brian Vaughn 3 | 4 | import areEqual from './areEqual'; 5 | import shallowDiffers from './shallowDiffers'; 6 | 7 | // Custom shouldComponentUpdate for class components. 8 | // It knows to compare individual style props and ignore the wrapper object. 9 | // See https://reactjs.org/docs/react-component.html#shouldcomponentupdate 10 | export default function shouldComponentUpdate( 11 | nextProps, 12 | nextState 13 | ) { 14 | return ( 15 | !areEqual(this.props, nextProps) || shallowDiffers(this.state, nextState) 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/areEqual.js: -------------------------------------------------------------------------------- 1 | // Ported from "react-window@1.8.6" 2 | // Copyright (c) 2018 Brian Vaughn 3 | import shallowDiffers from './shallowDiffers'; 4 | 5 | // Custom comparison function for React.memo(). 6 | // It knows to compare individual style props and ignore the wrapper object. 7 | // See https://reactjs.org/docs/react-api.html#reactmemo 8 | export default function areEqual( 9 | prevProps, 10 | nextProps 11 | ) { 12 | const { style: prevStyle, ...prevRest } = prevProps; 13 | const { style: nextStyle, ...nextRest } = nextProps; 14 | 15 | return ( 16 | !shallowDiffers(prevStyle, nextStyle) && !shallowDiffers(prevRest, nextRest) 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /lib/FixedSizeGridSSR.svelte: -------------------------------------------------------------------------------- 1 | 16 | {#if m} 17 | 18 | {/if} -------------------------------------------------------------------------------- /lib/FixedSizeListSSR.svelte: -------------------------------------------------------------------------------- 1 | 16 | {#if m} 17 | 18 | {/if} -------------------------------------------------------------------------------- /lib/VariableSizeGridSSR.svelte: -------------------------------------------------------------------------------- 1 | 16 | {#if m} 17 | 18 | {/if} -------------------------------------------------------------------------------- /lib/VariableSizeListSSR.svelte: -------------------------------------------------------------------------------- 1 | 16 | {#if m} 17 | 18 | {/if} -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Ported from "react-window@1.8.6" 2 | // Copyright (c) 2018 Brian Vaughn 3 | 4 | 5 | export { default as VariableSizeGrid } from '../lib/VariableSizeGrid.svelte'; 6 | export { default as VariableSizeList } from '../lib/VariableSizeList.svelte'; 7 | export { default as FixedSizeGrid } from '../lib/FixedSizeGrid.svelte'; 8 | export { default as FixedSizeList } from '../lib/FixedSizeList.svelte'; 9 | export { default as VariableSizeGridSSR } from '../lib/VariableSizeGridSSR.svelte'; 10 | export { default as VariableSizeListSSR } from '../lib/VariableSizeListSSR.svelte'; 11 | export { default as FixedSizeGridSSR } from '../lib/FixedSizeGridSSR.svelte'; 12 | export { default as FixedSizeListSSR } from '../lib/FixedSizeListSSR.svelte'; 13 | 14 | export { default as areEqual } from './areEqual'; 15 | export { default as shouldComponentUpdate } from './shouldComponentUpdate'; 16 | export {styleString} from './styleString'; 17 | -------------------------------------------------------------------------------- /src/timer.js: -------------------------------------------------------------------------------- 1 | // Ported from "react-window@1.8.6" 2 | // Copyright (c) 2018 Brian Vaughn 3 | 4 | 5 | // Animation frame based implementation of setTimeout. 6 | // Inspired by Joe Lambert, https://gist.github.com/joelambert/1002116#file-requesttimeout-js 7 | 8 | const hasNativePerformanceNow = 9 | typeof performance === 'object' && typeof performance.now === 'function'; 10 | 11 | const now = hasNativePerformanceNow 12 | ? () => performance.now() 13 | : () => Date.now(); 14 | 15 | 16 | export function cancelTimeout(timeoutID) { 17 | cancelAnimationFrame(timeoutID.id); 18 | } 19 | 20 | export function requestTimeout(callback, delay) { 21 | const start = now(); 22 | 23 | function tick() { 24 | if (now() - start >= delay) { 25 | callback.call(null); 26 | } else { 27 | timeoutID.id = requestAnimationFrame(tick); 28 | } 29 | } 30 | 31 | const timeoutID = { 32 | id: requestAnimationFrame(tick), 33 | }; 34 | 35 | return timeoutID; 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Michael Lucht 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-window", 3 | "version": "1.2.5", 4 | "description": "Svelte components for efficiently rendering large, scrollable lists and tabular data. Port of react-window to Svelte.", 5 | "main": "src/index.js", 6 | "module": "src/index.js", 7 | "svelte": "src/index.js", 8 | "types": "src/index.d.ts", 9 | "bundleDependencies": false, 10 | "contributors": [ 11 | { 12 | "name": "Michael Lucht", 13 | "email": "micha-lmxt@gradientdescent.de", 14 | "url": "https://github.com/micha-lmxt/svelte-window" 15 | } 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/micha-lmxt/svelte-window.git" 20 | }, 21 | "author": "micha-lmxt (https://gradientdescent.de)", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "svelte": "^3.32.3" 25 | }, 26 | "sideEffects": false, 27 | "type":"module", 28 | "keywords": [ 29 | "svelte", 30 | "sveltejs", 31 | "react", 32 | "reactjs", 33 | "virtual", 34 | "window", 35 | "windowed", 36 | "list", 37 | "scrolling", 38 | "infinite", 39 | "virtualized", 40 | "table", 41 | "grid", 42 | "spreadsheet" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /src/domHelpers.js: -------------------------------------------------------------------------------- 1 | // Ported from "react-window@1.8.6" 2 | // Copyright (c) 2018 Brian Vaughn 3 | let size = -1; 4 | 5 | // This utility copied from "dom-helpers" package. 6 | export function getScrollbarSize(recalculate = false) { 7 | if (size === -1 || recalculate) { 8 | const div = document.createElement('div'); 9 | const style = div.style; 10 | style.width = '50px'; 11 | style.height = '50px'; 12 | style.overflow = 'scroll'; 13 | 14 | document.body.appendChild(div); 15 | 16 | size = div.offsetWidth - div.clientWidth; 17 | 18 | document.body.removeChild(div); 19 | } 20 | 21 | return size; 22 | } 23 | 24 | let cachedRTLResult = null; 25 | 26 | // TRICKY According to the spec, scrollLeft should be negative for RTL aligned elements. 27 | // Chrome does not seem to adhere; its scrollLeft values are positive (measured relative to the left). 28 | // Safari's elastic bounce makes detecting this even more complicated wrt potential false positives. 29 | // The safest way to check this is to intentionally set a negative offset, 30 | // and then verify that the subsequent "scroll" event matches the negative offset. 31 | // If it does not match, then we can assume a non-standard RTL scroll implementation. 32 | export function getRTLOffsetType(recalculate = false) { 33 | if (cachedRTLResult === null || recalculate) { 34 | const outerDiv = document.createElement('div'); 35 | const outerStyle = outerDiv.style; 36 | outerStyle.width = '50px'; 37 | outerStyle.height = '50px'; 38 | outerStyle.overflow = 'scroll'; 39 | outerStyle.direction = 'rtl'; 40 | 41 | const innerDiv = document.createElement('div'); 42 | const innerStyle = innerDiv.style; 43 | innerStyle.width = '100px'; 44 | innerStyle.height = '100px'; 45 | 46 | outerDiv.appendChild(innerDiv); 47 | 48 | document.body.appendChild(outerDiv); 49 | 50 | if (outerDiv.scrollLeft > 0) { 51 | cachedRTLResult = 'positive-descending'; 52 | } else { 53 | outerDiv.scrollLeft = 1; 54 | if (outerDiv.scrollLeft === 0) { 55 | cachedRTLResult = 'negative'; 56 | } else { 57 | cachedRTLResult = 'positive-ascending'; 58 | } 59 | } 60 | 61 | document.body.removeChild(outerDiv); 62 | 63 | return cachedRTLResult; 64 | } 65 | 66 | return cachedRTLResult; 67 | } 68 | -------------------------------------------------------------------------------- /src/FixedSizeList.js: -------------------------------------------------------------------------------- 1 | // Ported from "react-window@1.8.6" 2 | // Copyright (c) 2018 Brian Vaughn 3 | 4 | const FixedSizeListSpecificProps = ({ 5 | getItemOffset: ({ itemSize }, index) => 6 | index * ((itemSize)), 7 | 8 | getItemSize: ({ itemSize }, index) => 9 | ((itemSize)), 10 | 11 | getEstimatedTotalSize: ({ itemCount, itemSize }) => 12 | ((itemSize)) * itemCount, 13 | 14 | getOffsetForIndexAndAlignment: ( 15 | { direction, height, itemCount, itemSize, layout, width }, 16 | index, 17 | align, 18 | scrollOffset 19 | ) => { 20 | // TODO Deprecate direction "horizontal" 21 | const isHorizontal = direction === 'horizontal' || layout === 'horizontal'; 22 | const size = (((isHorizontal ? width : height))); 23 | const lastItemOffset = Math.max( 24 | 0, 25 | itemCount * ((itemSize)) - size 26 | ); 27 | const maxOffset = Math.min( 28 | lastItemOffset, 29 | index * ((itemSize)) 30 | ); 31 | const minOffset = Math.max( 32 | 0, 33 | index * ((itemSize)) - size + ((itemSize)) 34 | ); 35 | 36 | if (align === 'smart') { 37 | if ( 38 | scrollOffset >= minOffset - size && 39 | scrollOffset <= maxOffset + size 40 | ) { 41 | align = 'auto'; 42 | } else { 43 | align = 'center'; 44 | } 45 | } 46 | 47 | switch (align) { 48 | case 'start': 49 | return maxOffset; 50 | case 'end': 51 | return minOffset; 52 | case 'center': { 53 | // "Centered" offset is usually the average of the min and max. 54 | // But near the edges of the list, this doesn't hold true. 55 | const middleOffset = Math.round( 56 | minOffset + (maxOffset - minOffset) / 2 57 | ); 58 | if (middleOffset < Math.ceil(size / 2)) { 59 | return 0; // near the beginning 60 | } else if (middleOffset > lastItemOffset + Math.floor(size / 2)) { 61 | return lastItemOffset; // near the end 62 | } else { 63 | return middleOffset; 64 | } 65 | } 66 | case 'auto': 67 | default: 68 | if (scrollOffset >= minOffset && scrollOffset <= maxOffset) { 69 | return scrollOffset; 70 | } else if (scrollOffset < minOffset) { 71 | return minOffset; 72 | } else { 73 | return maxOffset; 74 | } 75 | } 76 | }, 77 | 78 | getStartIndexForOffset: ( 79 | { itemCount, itemSize }, 80 | offset 81 | ) => 82 | Math.max( 83 | 0, 84 | Math.min(itemCount - 1, Math.floor(offset / ((itemSize)))) 85 | ), 86 | 87 | getStopIndexForStartIndex: ( 88 | { direction, height, itemCount, itemSize, layout, width }, 89 | startIndex, 90 | scrollOffset 91 | ) => { 92 | // TODO Deprecate direction "horizontal" 93 | const isHorizontal = direction === 'horizontal' || layout === 'horizontal'; 94 | const offset = startIndex * ((itemSize)); 95 | const size = (((isHorizontal ? width : height))); 96 | const numVisibleItems = Math.ceil( 97 | (size + scrollOffset - offset) / ((itemSize)) 98 | ); 99 | return Math.max( 100 | 0, 101 | Math.min( 102 | itemCount - 1, 103 | startIndex + numVisibleItems - 1 // -1 is because stop index is inclusive 104 | ) 105 | ); 106 | }, 107 | 108 | initInstanceProps(props) { 109 | // Noop 110 | }, 111 | 112 | shouldResetStyleCacheOnItemSizeChange: true, 113 | 114 | validateProps: ({ itemSize }) => { 115 | /*if (process.env.NODE_ENV !== 'production') { 116 | if (typeof itemSize !== 'number') { 117 | throw Error( 118 | 'An invalid "itemSize" prop has been specified. ' + 119 | 'Value should be a number. ' + 120 | `"${itemSize === null ? 'null' : typeof itemSize}" was specified.` 121 | ); 122 | } 123 | }*/ 124 | }, 125 | }); 126 | 127 | export default FixedSizeListSpecificProps; 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # svelte-window 2 | 3 | > Svelte components for efficiently rendering large, scrollable lists and tabular data. Port of react-window to Svelte. 4 | 5 | Svelte window works by only rendering *part* of a large data set (just enough to fill the viewport). This helps address some common performance bottlenecks: 6 | 1. It reduces the amount of work (and time) required to render the initial view and to process updates. 7 | 2. It reduces the memory footprint by avoiding over-allocation of DOM nodes. 8 | 9 | [![NPM license](https://img.shields.io/badge/license-mit-red.svg?style=for-the-badge)](LICENSE.md) 10 | 11 | ## Install 12 | 13 | ```bash 14 | npm install --save-dev svelte-window 15 | ``` 16 | 17 | ## Usage 18 | 19 | This library is a port of react-window, here are some examples: [react-window.now.sh](https://react-window.now.sh/). 20 | 21 | This is how to render a basic list: 22 | 23 | 24 | ```svelte 25 | 28 | 34 | {#each items as it (it.key)} 35 |
Row {it.index}
36 | {/each} 37 |
38 | ``` 39 | 40 | Here is another example with a fixed size grid with a scroll-to button, scrolling indicators: 41 | 42 | ```svelte 43 | 58 | 59 | 69 | {#each items as it (it.key)} 70 |
71 | {it.isScrolling ? 'Scrolling' : `Row ${it.rowIndex} - Col ${it.columnIndex}`} 72 |
73 | {/each} 74 |
75 | 76 | 77 | ``` 78 | 79 | #### Calendar example with VariableSizeGrid 80 | 81 | Here is a more complex example. Each column represents a date, workday coluns are wider than weekend columns. A sparse dataset is called. 82 | 83 | ```svelte 84 | 125 | 126 | 95} 132 | width={300} 133 | let:items> 134 | {#each items as it (it.key)} 135 |
136 | {dateCell(it.rowIndex, it.columnIndex)} 137 | {#if events[formatDate(dateCell(it.rowIndex,it.columnIndex))]} 138 | {events[formatDate(dateCell(it.rowIndex,it.columnIndex))].event} 139 | {/if} 140 |
141 | {/each} 142 |
143 | ``` 144 | 145 | 146 | ### SvelteKit 147 | 148 | This section is probably obsolete. SvelteKit is pretty stable today, so no problems should occur. 149 | ---- 150 | SvelteKit is in public beta, so a 100% compatibility cannot be guaranteed. Since version 1.2.0 `svelte-window` should work with SvelteKit when imported as a devDependency (`npm i --save-dev svelte-window`). By design, `svelte-window` is a client side library. Normal components like `FixedSizeList` need to be guarded from server-side-rendering (eg. with a `{#if mounted}...` clause). For convenience, there are SSR counterparts to all four components, which handle guarding within the library: `FixedSizeListSSR`, `FixedSizeGridSSR`, `VariableSizeListSSR`, `VariableSizeGridSSR`. In the examples above, just change eg.: 151 | 152 | ```javascript 153 | 156 | ... 157 | ``` 158 | 159 | to 160 | 161 | ```javascript 162 | 165 | ... 166 | ``` 167 | --- 168 | 169 | ## Bundle Size 170 | 171 | If you don't use all of `svelte-window`s components on a page, you can minimize the bundle size by using direct imports from the `lib` folder. Eg. you can change imports like this 172 | 173 | ```javascript 174 | import { FixedSizeListSSR as List, styleString as sty } from 'svelte-window'; 175 | ``` 176 | 177 | to 178 | 179 | ```javascript 180 | import List from 'svelte-window/lib/FixedSizeListSSR.svelte'; 181 | import {styleString as sty} from 'svelte-window/src/styleString'; 182 | ``` 183 | 184 | ## Differences to the React library 185 | 186 | 1. Grids and lists don't actively render the children. Instead, an array of item information is passed down via item props. You can use the [let:item](https://svelte.dev/tutorial/slot-props) to access it and render with the [each block](https://svelte.dev/tutorial/each-blocks). 187 | 2. Styles are passed down as objects, like in the React library, but Svelte only accepts strings for style. A helper function `styleString` is exported from `svelte-window`, to convert the style object to string 188 | 3. Variable sized variants have utilities to reset the cache, when row/cell sizes change. These are not directly added to the class, but to a member called `instance`. Eg. instead of 189 | 190 | ```javascript 191 | list.resetAfterIndex(...) 192 | ``` 193 | 194 | use 195 | 196 | ```javascript 197 | list.instance.resetAfterIndex(...) 198 | ``` 199 | 200 | Affected functions: 201 | 202 | VariableSizeList: 203 | 204 | - resetAfterIndex 205 | 206 | VariableSizeGrid: 207 | 208 | - resetAfterIndices 209 | - resetAfterColumnIndex 210 | - resetAfterRowIndex 211 | 212 | ## Related libraries 213 | 214 | * [`svelte-virtualized-auto-sizer`](https://npmjs.com/package/svelte-virtualized-auto-sizer): HOC that grows to fit all of the available space and passes the width and height values to its child. 215 | 216 | ## More information 217 | 218 | [Here is a blog post](https://gradientdescent.de/porting-react-window) about how the library was ported from React to Svelte. 219 | 220 | -------------------------------------------------------------------------------- /src/FixedSizeGrid.js: -------------------------------------------------------------------------------- 1 | // Ported from "react-window@1.8.6" 2 | // Copyright (c) 2018 Brian Vaughn 3 | 4 | const FixedSizeGridSpecificProps = { 5 | getColumnOffset: ({ columnWidth },index) => 6 | index * columnWidth, 7 | 8 | getColumnWidth: ({ columnWidth }, index) => 9 | columnWidth, 10 | 11 | getRowOffset: ({ rowHeight }, index) => 12 | index * rowHeight, 13 | 14 | getRowHeight: ({ rowHeight }, index) => 15 | rowHeight, 16 | 17 | getEstimatedTotalHeight: ({ rowCount, rowHeight }) => 18 | ((rowHeight)) * rowCount, 19 | 20 | getEstimatedTotalWidth: ({ columnCount, columnWidth }) => 21 | ((columnWidth)) * columnCount, 22 | 23 | getOffsetForColumnAndAlignment: ( 24 | { columnCount, columnWidth, width }, 25 | columnIndex, 26 | align, 27 | scrollLeft, 28 | instanceProps, 29 | scrollbarSize 30 | ) => { 31 | 32 | const lastColumnOffset = Math.max( 33 | 0, 34 | columnCount * ((columnWidth)) - width 35 | ); 36 | const maxOffset = Math.min( 37 | lastColumnOffset, 38 | columnIndex * ((columnWidth)) 39 | ); 40 | const minOffset = Math.max( 41 | 0, 42 | columnIndex * ((columnWidth)) - 43 | width + 44 | scrollbarSize + 45 | ((columnWidth)) 46 | ); 47 | 48 | if (align === 'smart') { 49 | if (scrollLeft >= minOffset - width && scrollLeft <= maxOffset + width) { 50 | align = 'auto'; 51 | } else { 52 | align = 'center'; 53 | } 54 | } 55 | 56 | switch (align) { 57 | case 'start': 58 | return maxOffset; 59 | case 'end': 60 | return minOffset; 61 | case 'center': 62 | // "Centered" offset is usually the average of the min and max. 63 | // But near the edges of the list, this doesn't hold true. 64 | const middleOffset = Math.round( 65 | minOffset + (maxOffset - minOffset) / 2 66 | ); 67 | if (middleOffset < Math.ceil(width / 2)) { 68 | return 0; // near the beginning 69 | } else if (middleOffset > lastColumnOffset + Math.floor(width / 2)) { 70 | return lastColumnOffset; // near the end 71 | } else { 72 | return middleOffset; 73 | } 74 | case 'auto': 75 | default: 76 | if (scrollLeft >= minOffset && scrollLeft <= maxOffset) { 77 | return scrollLeft; 78 | } else if (minOffset > maxOffset) { 79 | // Because we only take into account the scrollbar size when calculating minOffset 80 | // this value can be larger than maxOffset when at the end of the list 81 | return minOffset; 82 | } else if (scrollLeft < minOffset) { 83 | return minOffset; 84 | } else { 85 | return maxOffset; 86 | } 87 | } 88 | }, 89 | 90 | getOffsetForRowAndAlignment: ( 91 | { rowHeight, height, rowCount }, 92 | rowIndex, 93 | align, 94 | scrollTop, 95 | instanceProps, 96 | scrollbarSize 97 | ) => { 98 | const lastRowOffset = Math.max( 99 | 0, 100 | rowCount * ((rowHeight)) - height 101 | ); 102 | const maxOffset = Math.min( 103 | lastRowOffset, 104 | rowIndex * ((rowHeight)) 105 | ); 106 | const minOffset = Math.max( 107 | 0, 108 | rowIndex * ((rowHeight)) - 109 | height + 110 | scrollbarSize + 111 | ((rowHeight)) 112 | ); 113 | 114 | if (align === 'smart') { 115 | if (scrollTop >= minOffset - height && scrollTop <= maxOffset + height) { 116 | align = 'auto'; 117 | } else { 118 | align = 'center'; 119 | } 120 | } 121 | 122 | switch (align) { 123 | case 'start': 124 | return maxOffset; 125 | case 'end': 126 | return minOffset; 127 | case 'center': 128 | // "Centered" offset is usually the average of the min and max. 129 | // But near the edges of the list, this doesn't hold true. 130 | const middleOffset = Math.round( 131 | minOffset + (maxOffset - minOffset) / 2 132 | ); 133 | if (middleOffset < Math.ceil(height / 2)) { 134 | return 0; // near the beginning 135 | } else if (middleOffset > lastRowOffset + Math.floor(height / 2)) { 136 | return lastRowOffset; // near the end 137 | } else { 138 | return middleOffset; 139 | } 140 | case 'auto': 141 | default: 142 | if (scrollTop >= minOffset && scrollTop <= maxOffset) { 143 | return scrollTop; 144 | } else if (minOffset > maxOffset) { 145 | // Because we only take into account the scrollbar size when calculating minOffset 146 | // this value can be larger than maxOffset when at the end of the list 147 | return minOffset; 148 | } else if (scrollTop < minOffset) { 149 | return minOffset; 150 | } else { 151 | return maxOffset; 152 | } 153 | } 154 | }, 155 | 156 | getColumnStartIndexForOffset: ( 157 | { columnWidth, columnCount }, 158 | scrollLeft 159 | ) => 160 | Math.max( 161 | 0, 162 | Math.min( 163 | columnCount - 1, 164 | Math.floor(scrollLeft / ((columnWidth))) 165 | ) 166 | ), 167 | 168 | getColumnStopIndexForStartIndex: ( 169 | { columnWidth, columnCount, width }, 170 | startIndex, 171 | scrollLeft 172 | ) => { 173 | const left = startIndex * ((columnWidth)); 174 | const numVisibleColumns = Math.ceil( 175 | (width + scrollLeft - left) / ((columnWidth)) 176 | ); 177 | return Math.max( 178 | 0, 179 | Math.min( 180 | columnCount - 1, 181 | startIndex + numVisibleColumns - 1 // -1 is because stop index is inclusive 182 | ) 183 | ); 184 | }, 185 | 186 | getRowStartIndexForOffset: ( 187 | { rowHeight, rowCount }, 188 | scrollTop 189 | ) => 190 | Math.max( 191 | 0, 192 | Math.min(rowCount - 1, Math.floor(scrollTop / ((rowHeight)))) 193 | ), 194 | 195 | getRowStopIndexForStartIndex: ( 196 | { rowHeight, rowCount, height }, 197 | startIndex, 198 | scrollTop 199 | ) => { 200 | const top = startIndex * ((rowHeight)); 201 | const numVisibleRows = Math.ceil( 202 | (height + scrollTop - top) / ((rowHeight)) 203 | ); 204 | return Math.max( 205 | 0, 206 | Math.min( 207 | rowCount - 1, 208 | startIndex + numVisibleRows - 1 // -1 is because stop index is inclusive 209 | ) 210 | ); 211 | }, 212 | 213 | initInstanceProps(props) { 214 | // Noop 215 | }, 216 | 217 | shouldResetStyleCacheOnItemSizeChange: true, 218 | 219 | validateProps: ({ columnWidth, rowHeight }) => { 220 | if (process.env.NODE_ENV !== 'production') { 221 | if (typeof columnWidth !== 'number') { 222 | throw Error( 223 | 'An invalid "columnWidth" prop has been specified. ' + 224 | 'Value should be a number. ' + 225 | `"${ 226 | columnWidth === null ? 'null' : typeof columnWidth 227 | }" was specified.` 228 | ); 229 | } 230 | 231 | if (typeof rowHeight !== 'number') { 232 | throw Error( 233 | 'An invalid "rowHeight" prop has been specified. ' + 234 | 'Value should be a number. ' + 235 | `"${rowHeight === null ? 'null' : typeof rowHeight}" was specified.` 236 | ); 237 | } 238 | } 239 | }, 240 | }; 241 | 242 | export default FixedSizeGridSpecificProps; 243 | -------------------------------------------------------------------------------- /src/VariableSizeList.js: -------------------------------------------------------------------------------- 1 | // Ported from "react-window@1.8.6" 2 | // Copyright (c) 2018 Brian Vaughn 3 | 4 | const DEFAULT_ESTIMATED_ITEM_SIZE = 50; 5 | 6 | const getItemMetadata = ( 7 | props, 8 | index, 9 | instanceProps 10 | ) => { 11 | const { itemSize } = ((props)); 12 | const { itemMetadataMap, lastMeasuredIndex } = instanceProps; 13 | 14 | if (index > lastMeasuredIndex) { 15 | let offset = 0; 16 | if (lastMeasuredIndex >= 0) { 17 | const itemMetadata = itemMetadataMap[lastMeasuredIndex]; 18 | offset = itemMetadata.offset + itemMetadata.size; 19 | } 20 | 21 | for (let i = lastMeasuredIndex + 1; i <= index; i++) { 22 | let size = ((itemSize))(i); 23 | 24 | itemMetadataMap[i] = { 25 | offset, 26 | size, 27 | }; 28 | 29 | offset += size; 30 | } 31 | 32 | instanceProps.lastMeasuredIndex = index; 33 | } 34 | 35 | return itemMetadataMap[index]; 36 | }; 37 | 38 | const findNearestItem = ( 39 | props, 40 | instanceProps, 41 | offset 42 | ) => { 43 | const { itemMetadataMap, lastMeasuredIndex } = instanceProps; 44 | 45 | const lastMeasuredItemOffset = 46 | lastMeasuredIndex > 0 ? itemMetadataMap[lastMeasuredIndex].offset : 0; 47 | 48 | if (lastMeasuredItemOffset >= offset) { 49 | // If we've already measured items within this range just use a binary search as it's faster. 50 | return findNearestItemBinarySearch( 51 | props, 52 | instanceProps, 53 | lastMeasuredIndex, 54 | 0, 55 | offset 56 | ); 57 | } else { 58 | // If we haven't yet measured this high, fallback to an exponential search with an inner binary search. 59 | // The exponential search avoids pre-computing sizes for the full set of items as a binary search would. 60 | // The overall complexity for this approach is O(log n). 61 | return findNearestItemExponentialSearch( 62 | props, 63 | instanceProps, 64 | Math.max(0, lastMeasuredIndex), 65 | offset 66 | ); 67 | } 68 | }; 69 | 70 | const findNearestItemBinarySearch = ( 71 | props, 72 | instanceProps, 73 | high, 74 | low, 75 | offset 76 | ) => { 77 | while (low <= high) { 78 | const middle = low + Math.floor((high - low) / 2); 79 | const currentOffset = getItemMetadata(props, middle, instanceProps).offset; 80 | 81 | if (currentOffset === offset) { 82 | return middle; 83 | } else if (currentOffset < offset) { 84 | low = middle + 1; 85 | } else if (currentOffset > offset) { 86 | high = middle - 1; 87 | } 88 | } 89 | 90 | if (low > 0) { 91 | return low - 1; 92 | } else { 93 | return 0; 94 | } 95 | }; 96 | 97 | const findNearestItemExponentialSearch = ( 98 | props, 99 | instanceProps, 100 | index, 101 | offset 102 | ) => { 103 | const { itemCount } = props; 104 | let interval = 1; 105 | 106 | while ( 107 | index < itemCount && 108 | getItemMetadata(props, index, instanceProps).offset < offset 109 | ) { 110 | index += interval; 111 | interval *= 2; 112 | } 113 | 114 | return findNearestItemBinarySearch( 115 | props, 116 | instanceProps, 117 | Math.min(index, itemCount - 1), 118 | Math.floor(index / 2), 119 | offset 120 | ); 121 | }; 122 | 123 | const getEstimatedTotalSize = ( 124 | { itemCount }, 125 | { itemMetadataMap, estimatedItemSize, lastMeasuredIndex } 126 | ) => { 127 | let totalSizeOfMeasuredItems = 0; 128 | 129 | // Edge case check for when the number of items decreases while a scroll is in progress. 130 | // https://github.com/bvaughn/react-window/pull/138 131 | if (lastMeasuredIndex >= itemCount) { 132 | lastMeasuredIndex = itemCount - 1; 133 | } 134 | 135 | if (lastMeasuredIndex >= 0) { 136 | const itemMetadata = itemMetadataMap[lastMeasuredIndex]; 137 | totalSizeOfMeasuredItems = itemMetadata.offset + itemMetadata.size; 138 | } 139 | 140 | const numUnmeasuredItems = itemCount - lastMeasuredIndex - 1; 141 | const totalSizeOfUnmeasuredItems = numUnmeasuredItems * estimatedItemSize; 142 | 143 | return totalSizeOfMeasuredItems + totalSizeOfUnmeasuredItems; 144 | }; 145 | 146 | const VariableSizeListSpecificProps = ({ 147 | getItemOffset: ( 148 | props, 149 | index, 150 | instanceProps 151 | ) => getItemMetadata(props, index, instanceProps).offset, 152 | 153 | getItemSize: ( 154 | props, 155 | index, 156 | instanceProps 157 | ) => instanceProps.itemMetadataMap[index].size, 158 | 159 | getEstimatedTotalSize, 160 | 161 | getOffsetForIndexAndAlignment: ( 162 | props, 163 | index, 164 | align, 165 | scrollOffset, 166 | instanceProps 167 | ) => { 168 | const { direction, height, layout, width } = props; 169 | 170 | // TODO Deprecate direction "horizontal" 171 | const isHorizontal = direction === 'horizontal' || layout === 'horizontal'; 172 | const size = (((isHorizontal ? width : height))); 173 | const itemMetadata = getItemMetadata(props, index, instanceProps); 174 | 175 | // Get estimated total size after ItemMetadata is computed, 176 | // To ensure it reflects actual measurements instead of just estimates. 177 | const estimatedTotalSize = getEstimatedTotalSize(props, instanceProps); 178 | 179 | const maxOffset = Math.max( 180 | 0, 181 | Math.min(estimatedTotalSize - size, itemMetadata.offset) 182 | ); 183 | const minOffset = Math.max( 184 | 0, 185 | itemMetadata.offset - size + itemMetadata.size 186 | ); 187 | 188 | if (align === 'smart') { 189 | if ( 190 | scrollOffset >= minOffset - size && 191 | scrollOffset <= maxOffset + size 192 | ) { 193 | align = 'auto'; 194 | } else { 195 | align = 'center'; 196 | } 197 | } 198 | 199 | switch (align) { 200 | case 'start': 201 | return maxOffset; 202 | case 'end': 203 | return minOffset; 204 | case 'center': 205 | return Math.round(minOffset + (maxOffset - minOffset) / 2); 206 | case 'auto': 207 | default: 208 | if (scrollOffset >= minOffset && scrollOffset <= maxOffset) { 209 | return scrollOffset; 210 | } else if (scrollOffset < minOffset) { 211 | return minOffset; 212 | } else { 213 | return maxOffset; 214 | } 215 | } 216 | }, 217 | 218 | getStartIndexForOffset: ( 219 | props, 220 | offset, 221 | instanceProps 222 | ) => findNearestItem(props, instanceProps, offset), 223 | 224 | getStopIndexForStartIndex: ( 225 | props, 226 | startIndex, 227 | scrollOffset, 228 | instanceProps 229 | ) => { 230 | const { direction, height, itemCount, layout, width } = props; 231 | 232 | // TODO Deprecate direction "horizontal" 233 | const isHorizontal = direction === 'horizontal' || layout === 'horizontal'; 234 | const size = (((isHorizontal ? width : height))); 235 | const itemMetadata = getItemMetadata(props, startIndex, instanceProps); 236 | const maxOffset = scrollOffset + size; 237 | 238 | let offset = itemMetadata.offset + itemMetadata.size; 239 | let stopIndex = startIndex; 240 | 241 | while (stopIndex < itemCount - 1 && offset < maxOffset) { 242 | stopIndex++; 243 | offset += getItemMetadata(props, stopIndex, instanceProps).size; 244 | } 245 | 246 | return stopIndex; 247 | }, 248 | 249 | initInstanceProps(props, instance) { 250 | const { estimatedItemSize } = ((props)); 251 | 252 | const instanceProps = { 253 | itemMetadataMap: {}, 254 | estimatedItemSize: estimatedItemSize || DEFAULT_ESTIMATED_ITEM_SIZE, 255 | lastMeasuredIndex: -1, 256 | }; 257 | 258 | instance.resetAfterIndex = ( 259 | index, 260 | shouldForceUpdate = true 261 | ) => { 262 | instanceProps.lastMeasuredIndex = Math.min( 263 | instanceProps.lastMeasuredIndex, 264 | index - 1 265 | ); 266 | 267 | // We could potentially optimize further by only evicting styles after this index, 268 | // But since styles are only cached while scrolling is in progress- 269 | // It seems an unnecessary optimization. 270 | // It's unlikely that resetAfterIndex() will be called while a user is scrolling. 271 | instance._getItemStyleCache(-1); 272 | 273 | 274 | //if (shouldForceUpdate) { 275 | // Not sure if this does anything, though 276 | //instance.$set({}); 277 | //} 278 | }; 279 | 280 | return instanceProps; 281 | }, 282 | 283 | shouldResetStyleCacheOnItemSizeChange: false, 284 | 285 | validateProps: ({ itemSize }) => { 286 | /*if (process.env.NODE_ENV !== 'production') { 287 | if (typeof itemSize !== 'function') { 288 | throw Error( 289 | 'An invalid "itemSize" prop has been specified. ' + 290 | 'Value should be a function. ' + 291 | `"${itemSize === null ? 'null' : typeof itemSize}" was specified.` 292 | ); 293 | } 294 | }*/ 295 | }, 296 | }); 297 | 298 | export default VariableSizeListSpecificProps; 299 | -------------------------------------------------------------------------------- /src/VariableSizeGrid.js: -------------------------------------------------------------------------------- 1 | // Ported from "react-window@1.8.6" 2 | // Copyright (c) 2018 Brian Vaughn 3 | 4 | const DEFAULT_ESTIMATED_ITEM_SIZE = 50; 5 | 6 | const getEstimatedTotalHeight = ( 7 | { rowCount }, 8 | { rowMetadataMap, estimatedRowHeight, lastMeasuredRowIndex } 9 | ) => { 10 | let totalSizeOfMeasuredRows = 0; 11 | 12 | // Edge case check for when the number of items decreases while a scroll is in progress. 13 | // https://github.com/bvaughn/react-window/pull/138 14 | if (lastMeasuredRowIndex >= rowCount) { 15 | lastMeasuredRowIndex = rowCount - 1; 16 | } 17 | 18 | if (lastMeasuredRowIndex >= 0) { 19 | const itemMetadata = rowMetadataMap[lastMeasuredRowIndex]; 20 | totalSizeOfMeasuredRows = itemMetadata.offset + itemMetadata.size; 21 | } 22 | 23 | const numUnmeasuredItems = rowCount - lastMeasuredRowIndex - 1; 24 | const totalSizeOfUnmeasuredItems = numUnmeasuredItems * estimatedRowHeight; 25 | 26 | return totalSizeOfMeasuredRows + totalSizeOfUnmeasuredItems; 27 | }; 28 | 29 | const getEstimatedTotalWidth = ( 30 | { columnCount }, 31 | { 32 | columnMetadataMap, 33 | estimatedColumnWidth, 34 | lastMeasuredColumnIndex, 35 | } 36 | ) => { 37 | let totalSizeOfMeasuredRows = 0; 38 | 39 | // Edge case check for when the number of items decreases while a scroll is in progress. 40 | // https://github.com/bvaughn/react-window/pull/138 41 | if (lastMeasuredColumnIndex >= columnCount) { 42 | lastMeasuredColumnIndex = columnCount - 1; 43 | } 44 | 45 | if (lastMeasuredColumnIndex >= 0) { 46 | const itemMetadata = columnMetadataMap[lastMeasuredColumnIndex]; 47 | totalSizeOfMeasuredRows = itemMetadata.offset + itemMetadata.size; 48 | } 49 | 50 | const numUnmeasuredItems = columnCount - lastMeasuredColumnIndex - 1; 51 | const totalSizeOfUnmeasuredItems = numUnmeasuredItems * estimatedColumnWidth; 52 | 53 | return totalSizeOfMeasuredRows + totalSizeOfUnmeasuredItems; 54 | }; 55 | 56 | const getItemMetadata = ( 57 | itemType, 58 | props, 59 | index, 60 | instanceProps 61 | ) => { 62 | let itemMetadataMap, itemSize, lastMeasuredIndex; 63 | if (itemType === 'column') { 64 | itemMetadataMap = instanceProps.columnMetadataMap; 65 | itemSize = ((props.columnWidth)); 66 | lastMeasuredIndex = instanceProps.lastMeasuredColumnIndex; 67 | } else { 68 | itemMetadataMap = instanceProps.rowMetadataMap; 69 | itemSize = ((props.rowHeight)); 70 | lastMeasuredIndex = instanceProps.lastMeasuredRowIndex; 71 | } 72 | 73 | if (index > lastMeasuredIndex) { 74 | let offset = 0; 75 | if (lastMeasuredIndex >= 0) { 76 | const itemMetadata = itemMetadataMap[lastMeasuredIndex]; 77 | offset = itemMetadata.offset + itemMetadata.size; 78 | } 79 | 80 | for (let i = lastMeasuredIndex + 1; i <= index; i++) { 81 | let size = itemSize(i); 82 | 83 | itemMetadataMap[i] = { 84 | offset, 85 | size, 86 | }; 87 | 88 | offset += size; 89 | } 90 | 91 | if (itemType === 'column') { 92 | instanceProps.lastMeasuredColumnIndex = index; 93 | } else { 94 | instanceProps.lastMeasuredRowIndex = index; 95 | } 96 | } 97 | 98 | return itemMetadataMap[index]; 99 | }; 100 | 101 | const findNearestItem = ( 102 | itemType, 103 | props, 104 | instanceProps, 105 | offset 106 | ) => { 107 | let itemMetadataMap, lastMeasuredIndex; 108 | if (itemType === 'column') { 109 | itemMetadataMap = instanceProps.columnMetadataMap; 110 | lastMeasuredIndex = instanceProps.lastMeasuredColumnIndex; 111 | } else { 112 | itemMetadataMap = instanceProps.rowMetadataMap; 113 | lastMeasuredIndex = instanceProps.lastMeasuredRowIndex; 114 | } 115 | 116 | const lastMeasuredItemOffset = 117 | lastMeasuredIndex > 0 ? itemMetadataMap[lastMeasuredIndex].offset : 0; 118 | 119 | if (lastMeasuredItemOffset >= offset) { 120 | // If we've already measured items within this range just use a binary search as it's faster. 121 | return findNearestItemBinarySearch( 122 | itemType, 123 | props, 124 | instanceProps, 125 | lastMeasuredIndex, 126 | 0, 127 | offset 128 | ); 129 | } else { 130 | // If we haven't yet measured this high, fallback to an exponential search with an inner binary search. 131 | // The exponential search avoids pre-computing sizes for the full set of items as a binary search would. 132 | // The overall complexity for this approach is O(log n). 133 | return findNearestItemExponentialSearch( 134 | itemType, 135 | props, 136 | instanceProps, 137 | Math.max(0, lastMeasuredIndex), 138 | offset 139 | ); 140 | } 141 | }; 142 | 143 | const findNearestItemBinarySearch = ( 144 | itemType, 145 | props, 146 | instanceProps, 147 | high, 148 | low, 149 | offset 150 | ) => { 151 | while (low <= high) { 152 | const middle = low + Math.floor((high - low) / 2); 153 | const currentOffset = getItemMetadata( 154 | itemType, 155 | props, 156 | middle, 157 | instanceProps 158 | ).offset; 159 | 160 | if (currentOffset === offset) { 161 | return middle; 162 | } else if (currentOffset < offset) { 163 | low = middle + 1; 164 | } else if (currentOffset > offset) { 165 | high = middle - 1; 166 | } 167 | } 168 | 169 | if (low > 0) { 170 | return low - 1; 171 | } else { 172 | return 0; 173 | } 174 | }; 175 | 176 | const findNearestItemExponentialSearch = ( 177 | itemType, 178 | props, 179 | instanceProps, 180 | index, 181 | offset 182 | ) => { 183 | const itemCount = itemType === 'column' ? props.columnCount : props.rowCount; 184 | let interval = 1; 185 | 186 | while ( 187 | index < itemCount && 188 | getItemMetadata(itemType, props, index, instanceProps).offset < offset 189 | ) { 190 | index += interval; 191 | interval *= 2; 192 | } 193 | 194 | return findNearestItemBinarySearch( 195 | itemType, 196 | props, 197 | instanceProps, 198 | Math.min(index, itemCount - 1), 199 | Math.floor(index / 2), 200 | offset 201 | ); 202 | }; 203 | 204 | const getOffsetForIndexAndAlignment = ( 205 | itemType, 206 | props, 207 | index, 208 | align, 209 | scrollOffset, 210 | instanceProps, 211 | scrollbarSize 212 | ) => { 213 | const size = itemType === 'column' ? props.width : props.height; 214 | const itemMetadata = getItemMetadata(itemType, props, index, instanceProps); 215 | 216 | // Get estimated total size after ItemMetadata is computed, 217 | // To ensure it reflects actual measurements instead of just estimates. 218 | const estimatedTotalSize = 219 | itemType === 'column' 220 | ? getEstimatedTotalWidth(props, instanceProps) 221 | : getEstimatedTotalHeight(props, instanceProps); 222 | 223 | const maxOffset = Math.max( 224 | 0, 225 | Math.min(estimatedTotalSize - size, itemMetadata.offset) 226 | ); 227 | const minOffset = Math.max( 228 | 0, 229 | itemMetadata.offset - size + scrollbarSize + itemMetadata.size 230 | ); 231 | 232 | if (align === 'smart') { 233 | if (scrollOffset >= minOffset - size && scrollOffset <= maxOffset + size) { 234 | align = 'auto'; 235 | } else { 236 | align = 'center'; 237 | } 238 | } 239 | 240 | switch (align) { 241 | case 'start': 242 | return maxOffset; 243 | case 'end': 244 | return minOffset; 245 | case 'center': 246 | return Math.round(minOffset + (maxOffset - minOffset) / 2); 247 | case 'auto': 248 | default: 249 | if (scrollOffset >= minOffset && scrollOffset <= maxOffset) { 250 | return scrollOffset; 251 | } else if (minOffset > maxOffset) { 252 | // Because we only take into account the scrollbar size when calculating minOffset 253 | // this value can be larger than maxOffset when at the end of the list 254 | return minOffset; 255 | } else if (scrollOffset < minOffset) { 256 | return minOffset; 257 | } else { 258 | return maxOffset; 259 | } 260 | } 261 | }; 262 | 263 | const VariableSizeGridSpecificProps = ({ 264 | getColumnOffset: ( 265 | props, 266 | index, 267 | instanceProps 268 | ) => getItemMetadata('column', props, index, instanceProps).offset, 269 | 270 | getColumnStartIndexForOffset: ( 271 | props, 272 | scrollLeft, 273 | instanceProps 274 | ) => findNearestItem('column', props, instanceProps, scrollLeft), 275 | 276 | getColumnStopIndexForStartIndex: ( 277 | props, 278 | startIndex, 279 | scrollLeft, 280 | instanceProps 281 | ) => { 282 | const { columnCount, width } = props; 283 | 284 | const itemMetadata = getItemMetadata( 285 | 'column', 286 | props, 287 | startIndex, 288 | instanceProps 289 | ); 290 | const maxOffset = scrollLeft + width; 291 | 292 | let offset = itemMetadata.offset + itemMetadata.size; 293 | let stopIndex = startIndex; 294 | 295 | while (stopIndex < columnCount - 1 && offset < maxOffset) { 296 | stopIndex++; 297 | offset += getItemMetadata('column', props, stopIndex, instanceProps).size; 298 | } 299 | 300 | return stopIndex; 301 | }, 302 | 303 | getColumnWidth: ( 304 | props, 305 | index, 306 | instanceProps 307 | ) => instanceProps.columnMetadataMap[index].size, 308 | 309 | getEstimatedTotalHeight, 310 | getEstimatedTotalWidth, 311 | 312 | getOffsetForColumnAndAlignment: ( 313 | props, 314 | index, 315 | align, 316 | scrollOffset, 317 | instanceProps, 318 | scrollbarSize 319 | ) => 320 | getOffsetForIndexAndAlignment( 321 | 'column', 322 | props, 323 | index, 324 | align, 325 | scrollOffset, 326 | instanceProps, 327 | scrollbarSize 328 | ), 329 | 330 | getOffsetForRowAndAlignment: ( 331 | props, 332 | index, 333 | align, 334 | scrollOffset, 335 | instanceProps, 336 | scrollbarSize 337 | ) => 338 | getOffsetForIndexAndAlignment( 339 | 'row', 340 | props, 341 | index, 342 | align, 343 | scrollOffset, 344 | instanceProps, 345 | scrollbarSize 346 | ), 347 | 348 | getRowOffset: ( 349 | props, 350 | index, 351 | instanceProps 352 | ) => getItemMetadata('row', props, index, instanceProps).offset, 353 | 354 | getRowHeight: ( 355 | props, 356 | index, 357 | instanceProps 358 | ) => instanceProps.rowMetadataMap[index].size, 359 | 360 | getRowStartIndexForOffset: ( 361 | props, 362 | scrollTop, 363 | instanceProps 364 | ) => findNearestItem('row', props, instanceProps, scrollTop), 365 | 366 | getRowStopIndexForStartIndex: ( 367 | props, 368 | startIndex, 369 | scrollTop, 370 | instanceProps 371 | ) => { 372 | const { rowCount, height } = props; 373 | 374 | const itemMetadata = getItemMetadata( 375 | 'row', 376 | props, 377 | startIndex, 378 | instanceProps 379 | ); 380 | const maxOffset = scrollTop + height; 381 | 382 | let offset = itemMetadata.offset + itemMetadata.size; 383 | let stopIndex = startIndex; 384 | 385 | while (stopIndex < rowCount - 1 && offset < maxOffset) { 386 | stopIndex++; 387 | offset += getItemMetadata('row', props, stopIndex, instanceProps).size; 388 | } 389 | 390 | return stopIndex; 391 | }, 392 | 393 | initInstanceProps(props, instance) { 394 | const { 395 | estimatedColumnWidth, 396 | estimatedRowHeight, 397 | } = ((props)); 398 | 399 | const instanceProps = { 400 | columnMetadataMap: {}, 401 | estimatedColumnWidth: estimatedColumnWidth || DEFAULT_ESTIMATED_ITEM_SIZE, 402 | estimatedRowHeight: estimatedRowHeight || DEFAULT_ESTIMATED_ITEM_SIZE, 403 | lastMeasuredColumnIndex: -1, 404 | lastMeasuredRowIndex: -1, 405 | rowMetadataMap: {}, 406 | }; 407 | 408 | instance.resetAfterColumnIndex = ( 409 | columnIndex, 410 | shouldForceUpdate = true 411 | ) => { 412 | instance.resetAfterIndices({ columnIndex, shouldForceUpdate }); 413 | }; 414 | 415 | instance.resetAfterRowIndex = ( 416 | rowIndex, 417 | shouldForceUpdate = true 418 | ) => { 419 | instance.resetAfterIndices({ rowIndex, shouldForceUpdate }); 420 | }; 421 | 422 | instance.resetAfterIndices = ({ 423 | columnIndex, 424 | rowIndex, 425 | shouldForceUpdate = true, 426 | }) => { 427 | if (typeof columnIndex === 'number') { 428 | instanceProps.lastMeasuredColumnIndex = Math.min( 429 | instanceProps.lastMeasuredColumnIndex, 430 | columnIndex - 1 431 | ); 432 | } 433 | if (typeof rowIndex === 'number') { 434 | instanceProps.lastMeasuredRowIndex = Math.min( 435 | instanceProps.lastMeasuredRowIndex, 436 | rowIndex - 1 437 | ); 438 | } 439 | 440 | // We could potentially optimize further by only evicting styles after this index, 441 | // But since styles are only cached while scrolling is in progress- 442 | // It seems an unnecessary optimization. 443 | // It's unlikely that resetAfterIndex() will be called while a user is scrolling. 444 | instance._getItemStyleCache(-1); 445 | /* 446 | if (shouldForceUpdate) { 447 | // not sure if this does anything, though 448 | instance.$set({}); 449 | } 450 | */ 451 | }; 452 | 453 | return instanceProps; 454 | }, 455 | 456 | shouldResetStyleCacheOnItemSizeChange: false, 457 | 458 | validateProps: ({ columnWidth, rowHeight }) => { 459 | /* 460 | if (process.env.NODE_ENV !== 'production') { 461 | if (typeof columnWidth !== 'function') { 462 | throw Error( 463 | 'An invalid "columnWidth" prop has been specified. ' + 464 | 'Value should be a function. ' + 465 | `"${ 466 | columnWidth === null ? 'null' : typeof columnWidth 467 | }" was specified.` 468 | ); 469 | } else if (typeof rowHeight !== 'function') { 470 | throw Error( 471 | 'An invalid "rowHeight" prop has been specified. ' + 472 | 'Value should be a function. ' + 473 | `"${rowHeight === null ? 'null' : typeof rowHeight}" was specified.` 474 | ); 475 | } 476 | }*/ 477 | }, 478 | }); 479 | 480 | export default VariableSizeGridSpecificProps; 481 | -------------------------------------------------------------------------------- /src/ListComponent.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 463 | 464 | 472 | 473 |
478 |
481 | 482 |
483 |
484 | -------------------------------------------------------------------------------- /src/GridComponent.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 634 | 635 | 643 | 644 |
650 |
653 | 654 |
655 |
656 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for svelte-window 2 | // Definitions by: Michael Lucht 3 | // 4 | // original: 5 | // Type definitions for react-window 1.8 6 | // Project: https://github.com/bvaughn/react-window/, http://react-window.now.sh 7 | // Definitions by: Martynas Kadiša 8 | // Alex Guerra 9 | // John Gozde 10 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 11 | // TypeScript Version: 2.8 12 | 13 | import { SvelteComponentTyped } from 'svelte'; 14 | 15 | export type CSSDirection = "ltr" | "rtl"; 16 | export type Direction = "vertical" | "horizontal"; // TODO: deprecate in favour of Layout 17 | export type Layout = "vertical" | "horizontal"; 18 | export type ScrollDirection = "forward" | "backward"; 19 | export type Align = "auto" | "smart" | "center" | "end" | "start"; 20 | 21 | export interface ListChildComponentProps { 22 | index: number; 23 | style: StyleObject; 24 | data: any; 25 | isScrolling?: boolean; 26 | key: string | number; 27 | 28 | } 29 | 30 | export interface GridChildComponentProps { 31 | columnIndex: number; 32 | rowIndex: number; 33 | style: StyleObject; 34 | data: any; 35 | isScrolling?: boolean; 36 | key: string | number; 37 | } 38 | 39 | 40 | export interface CommonProps { 41 | /** 42 | * Optional CSS class to attach to outermost
element. 43 | */ 44 | className?: string; 45 | /** 46 | * Tag name passed to document.createElement to create the inner container element. This is an advanced property; in most cases, the default ("div") should be used. 47 | */ 48 | innerElementType?: string; 49 | /** 50 | * Ref to attach to the inner container element. This is an advanced property. 51 | */ 52 | innerRef?: HTMLDivElement; 53 | /** 54 | * Tag name passed to document.createElement to create the inner container element. This is an advanced property; in most cases, the default ("div") should be used. 55 | * 56 | * @deprecated since 1.4.0 57 | */ 58 | innerTagName?: string; 59 | /** 60 | * Contextual data to be passed to the item renderer as a data prop. This is a light-weight alternative to React's built-in context API. 61 | * 62 | * Item data is useful for item renderers that are class components. 63 | */ 64 | itemData?: any; 65 | /** 66 | * Tag name passed to document.createElement to create the outer container element. This is an advanced property; in most cases, the default ("div") should be used. 67 | */ 68 | outerElementType?: string; 69 | /** 70 | * Ref to attach to the outer container element. This is an advanced property. 71 | */ 72 | outerRef?: HTMLDivElement; 73 | /** 74 | * Tag name passed to document.createElement to create the outer container element. This is an advanced property; in most cases, the default ("div") should be used. 75 | * 76 | * @deprecated since 1.4.0 77 | */ 78 | outerTagName?: string; 79 | /** 80 | * Optional inline style to attach to outermost
element. 81 | */ 82 | style?: string; 83 | /** 84 | * Adds an additional isScrolling parameter to the children render function. This parameter can be used to show a placeholder row or column while the list is being scrolled. 85 | * 86 | * Note that using this parameter will result in an additional render call after scrolling has stopped (when isScrolling changes from true to false). 87 | */ 88 | useIsScrolling?: boolean; 89 | } 90 | 91 | export type ListItemKeySelector = (index: number, data: any) => string | number; 92 | 93 | export interface ListOnItemsRenderedProps { 94 | overscanStartIndex: number; 95 | overscanStopIndex: number; 96 | visibleStartIndex: number; 97 | visibleStopIndex: number; 98 | } 99 | 100 | export interface ListOnScrollProps { 101 | scrollDirection: ScrollDirection; 102 | scrollOffset: number; 103 | scrollUpdateWasRequested: boolean; 104 | } 105 | 106 | export interface ListProps extends CommonProps { 107 | 108 | /** 109 | * Height of the list. 110 | * 111 | * For vertical lists, this must be a number. It affects the number of rows that will be rendered (and displayed) at any given time. 112 | * 113 | * For horizontal lists, this can be a number or a string (e.g. "50%"). 114 | */ 115 | height: number | string; 116 | /** 117 | * Total number of items in the list. Note that only a few items will be rendered and displayed at a time. 118 | */ 119 | itemCount: number; 120 | /** 121 | * Width of the list. 122 | * 123 | * For horizontal lists, this must be a number. It affects the number of columns that will be rendered (and displayed) at any given time. 124 | * 125 | * For vertical lists, this can be a number or a string (e.g. "50%"). 126 | */ 127 | width: number | string; 128 | /** 129 | * Determines the direction of text and horizontal scrolling. 130 | * 131 | * This property also automatically sets the CSS direction style for the list component. 132 | * 133 | * Specifying "horizontal" or "vertical" for this value is deprecated. Use "layout" prop instead. 134 | * 135 | * @default "ltr" 136 | */ 137 | direction?: CSSDirection | Direction; 138 | /** 139 | * Layout/orientation of the list. 140 | * 141 | * Acceptable values are: 142 | * - "vertical" (default) - Up/down scrolling. 143 | * - "horizontal" - Left/right scrolling. 144 | * 145 | * Note that lists may scroll in both directions (depending on CSS) but content will only be windowed in the layout direction specified. 146 | */ 147 | layout?: Layout; 148 | /** 149 | * Scroll offset for initial render. 150 | * 151 | * For vertical lists, this affects scrollTop. For horizontal lists, this affects scrollLeft. 152 | */ 153 | initialScrollOffset?: number; 154 | /** 155 | * By default, lists will use an item's index as its key. This is okay if: 156 | * 157 | * - Your collections of items is never sorted or modified 158 | * - Your item renderer is not stateful and does not extend PureComponent 159 | * 160 | * If your list does not satisfy the above constraints, use the itemKey property to specify your own keys for items 161 | */ 162 | itemKey?: ListItemKeySelector; 163 | /** 164 | * The number of items (rows or columns) to render outside of the visible area. This property can be important for two reasons: 165 | * 166 | * - Overscanning by one row or column allows the tab key to focus on the next (not yet visible) item. 167 | * - Overscanning slightly can reduce or prevent a flash of empty space when a user first starts scrolling. 168 | * 169 | * Note that overscanning too much can negatively impact performance. By default, List overscans by one item. 170 | */ 171 | overscanCount?: number; 172 | /** 173 | * Called when the items rendered by the list change. 174 | */ 175 | onItemsRendered?: (props: ListOnItemsRenderedProps) => any; 176 | /** 177 | * Called when the list scroll positions changes, as a result of user scrolling or scroll-to method calls. 178 | */ 179 | onScroll?: (props: ListOnScrollProps) => any; 180 | } 181 | 182 | export type GridItemKeySelector = (params: { 183 | columnIndex: number; 184 | rowIndex: number; 185 | data: any; 186 | }) => string | number; 187 | 188 | export interface GridOnItemsRenderedProps { 189 | overscanColumnStartIndex: number; 190 | overscanColumnStopIndex: number; 191 | overscanRowStartIndex: number; 192 | overscanRowStopIndex: number; 193 | visibleColumnStartIndex: number; 194 | visibleColumnStopIndex: number; 195 | visibleRowStartIndex: number; 196 | visibleRowStopIndex: number; 197 | } 198 | 199 | export interface GridOnScrollProps { 200 | horizontalScrollDirection: ScrollDirection; 201 | scrollLeft: number; 202 | scrollTop: number; 203 | scrollUpdateWasRequested: boolean; 204 | verticalScrollDirection: ScrollDirection; 205 | } 206 | 207 | export interface GridProps extends CommonProps { 208 | /** 209 | * Number of columns in the grid. Note that only a few columns will be rendered and displayed at a time. 210 | */ 211 | columnCount: number; 212 | /** 213 | * Determines the direction of text and horizontal scrolling. 214 | * 215 | * This property also automatically sets the CSS direction style for the grid component. 216 | * 217 | * @default "ltr" 218 | */ 219 | direction?: CSSDirection; 220 | /** 221 | * Height of the grid. This affects the number of rows that will be rendered (and displayed) at any given time. 222 | */ 223 | height: number; 224 | /** 225 | * Horizontal scroll offset for initial render. 226 | */ 227 | initialScrollLeft?: number; 228 | /** 229 | * Vertical scroll offset for initial render. 230 | */ 231 | initialScrollTop?: number; 232 | /** 233 | * By default, grids will use an item's indices as its key. This is okay if: 234 | * 235 | * - Your collections of items is never sorted or modified 236 | * - Your item renderer is not stateful and does not extend PureComponent 237 | * 238 | * If your grid does not satisfy the above constraints, use the itemKey property to specify your own keys for items. 239 | */ 240 | itemKey?: GridItemKeySelector; 241 | /** 242 | * Called when the items rendered by the grid change. 243 | */ 244 | onItemsRendered?: (props: GridOnItemsRenderedProps) => any; 245 | /** 246 | * Called when the grid scroll positions changes, as a result of user scrolling or scroll-to method calls. 247 | */ 248 | onScroll?: (props: GridOnScrollProps) => any; 249 | /** 250 | * @deprecated since version 1.8.2, please use overscanColumnCount 251 | */ 252 | overscanColumnsCount?: number; 253 | /** 254 | * The number of columns to render outside of the visible area. This property can be important for two reasons: 255 | * 256 | * - Overscanning by one row or column allows the tab key to focus on the next (not yet visible) item. 257 | * - Overscanning slightly can reduce or prevent a flash of empty space when a user first starts scrolling. 258 | * 259 | * Note that overscanning too much can negatively impact performance. By default, grid overscans by one item. 260 | */ 261 | overscanColumnCount?: number; 262 | /** 263 | * @deprecated since version 1.8.2, please use overscanRowCount 264 | */ 265 | overscanRowsCount?: number; 266 | /** 267 | * The number of rows to render outside of the visible area. This property can be important for two reasons: 268 | * 269 | * - Overscanning by one row or column allows the tab key to focus on the next (not yet visible) item. 270 | * - Overscanning slightly can reduce or prevent a flash of empty space when a user first starts scrolling. 271 | * 272 | * Note that overscanning too much can negatively impact performance. By default, grid overscans by one item. 273 | */ 274 | overscanRowCount?: number; 275 | /** 276 | * The number of items (rows or columns) to render outside of the visible area. This property can be important for two reasons: 277 | * 278 | * - Overscanning by one row or column allows the tab key to focus on the next (not yet visible) item. 279 | * - Overscanning slightly can reduce or prevent a flash of empty space when a user first starts scrolling. 280 | * 281 | * Note that overscanning too much can negatively impact performance. By default, grid overscans by one item. 282 | * 283 | * @deprecated since version 1.4.0 284 | */ 285 | overscanCount?: number; 286 | /** 287 | * Number of rows in the grid. Note that only a few rows will be rendered and displayed at a time. 288 | */ 289 | rowCount: number; 290 | /** 291 | * Width of the grid. This affects the number of columns that will be rendered (and displayed) at any given time. 292 | */ 293 | width: number; 294 | } 295 | 296 | export interface FixedSizeListProps extends ListProps { 297 | /** 298 | * Size of a item in the direction being windowed. For vertical lists, this is the row height. For horizontal lists, this is the column width. 299 | */ 300 | itemSize: number; 301 | } 302 | 303 | export interface VariableSizeListProps extends ListProps { 304 | /** 305 | * Estimated size of a item in the direction being windowed. For vertical lists, this is the row height. For horizontal lists, this is the column width. 306 | * 307 | * This value is used to calculated the estimated total size of a list before its items have all been measured. The total size impacts user scrolling behavior. 308 | * It is updated whenever new items are measured. 309 | */ 310 | estimatedItemSize?: number; 311 | /** 312 | * Returns the size of a item in the direction being windowed. For vertical lists, this is the row height. For horizontal lists, this is the column width. 313 | */ 314 | itemSize: (index: number) => number; 315 | } 316 | 317 | export interface FixedSizeGridProps extends GridProps { 318 | /** 319 | * Width of an individual column within the grid. 320 | */ 321 | columnWidth: number; 322 | /** 323 | * Height of an individual row within the grid. 324 | */ 325 | rowHeight: number; 326 | } 327 | 328 | export interface VariableSizeGridProps extends GridProps { 329 | /** 330 | * Returns the width of the specified column. 331 | */ 332 | columnWidth: (index: number) => number; 333 | /** 334 | * Average (or estimated) column width for unrendered columns. 335 | * 336 | * This value is used to calculated the estimated total width of a Grid before its columns have all been measured. 337 | * The estimated width impacts user scrolling behavior. It is updated whenever new columns are measured. 338 | */ 339 | estimatedColumnWidth?: number; 340 | /** 341 | * Average (or estimated) row height for unrendered rows. 342 | * 343 | * This value is used to calculated the estimated total height of a Grid before its rows have all been measured. 344 | * The estimated height impacts user scrolling behavior. It is updated whenever new rows are measured. 345 | */ 346 | estimatedRowHeight?: number; 347 | /** 348 | * Returns the height of the specified row. 349 | */ 350 | rowHeight: (index: number) => number; 351 | } 352 | 353 | /** 354 | * Fixed Size List 355 | * 356 | * usage: 357 | * 358 | * {#each items as item (item.key)} 359 | * {item.index} 360 | * {/each} 361 | * 362 | */ 363 | export class FixedSizeList extends SvelteComponentTyped { 366 | /** 367 | * Scroll to the specified offset (scrollTop or scrollLeft, depending on the direction prop). 368 | */ 369 | scrollTo(scrollOffset: number): void; 370 | /** 371 | * Scroll to the specified item. 372 | * 373 | * By default, the List will scroll as little as possible to ensure the item is visible. 374 | * You can control the alignment of the item though by specifying a second alignment parameter. Acceptable values are: 375 | * 376 | * - auto (default) - Scroll as little as possible to ensure the item is visible. (If the item is already visible, it won't scroll at all.) 377 | * - smart 378 | * - If the item is already visible, don't scroll at all. 379 | * - If it is less than one viewport away, scroll as little as possible so that it becomes visible. 380 | * - If it is more than one viewport away, scroll so that it is centered within the list. 381 | * - center - Center align the item within the list. 382 | * - end - Align the item to the end of the list (the bottom for vertical lists or the right for horizontal lists). 383 | * - start - Align the item to the beginning of the list (the top for vertical lists or the left for horizontal lists). 384 | */ 385 | scrollToItem(index: number, align?: Align): void; 386 | } 387 | 388 | /** 389 | * Fixed Size List for use in Server-Side-Rendered environment 390 | * 391 | * usage: 392 | * 393 | * {#each items as item (item.key)} 394 | * {item.index} 395 | * {/each} 396 | * 397 | */ 398 | export type FixedSizeListSSR = FixedSizeList; 399 | 400 | /** 401 | * Variable Size List 402 | * 403 | * usage: 404 | * 405 | * {#each items as item (item.key)} 406 | * {item.index} 407 | * {/each} 408 | * * 409 | */ 410 | export class VariableSizeList extends SvelteComponentTyped { 413 | /** 414 | * Scroll to the specified offset (scrollTop or scrollLeft, depending on the direction prop). 415 | */ 416 | scrollTo(scrollOffset: number): void; 417 | /** 418 | * Scroll to the specified item. 419 | * 420 | * By default, the List will scroll as little as possible to ensure the item is visible. 421 | * You can control the alignment of the item though by specifying a second alignment parameter. Acceptable values are: 422 | * 423 | * - auto (default) - Scroll as little as possible to ensure the item is visible. (If the item is already visible, it won't scroll at all.) 424 | * - smart 425 | * - If the item is already visible, don't scroll at all. 426 | * - If it is less than one viewport away, scroll as little as possible so that it becomes visible. 427 | * - If it is more than one viewport away, scroll so that it is centered within the list. 428 | * - center - Center align the item within the list. 429 | * - end - Align the item to the end of the list (the bottom for vertical lists or the right for horizontal lists). 430 | * - start - Align the item to the beginning of the list (the top for vertical lists or the left for horizontal lists). 431 | */ 432 | scrollToItem(index: number, align?: Align): void; 433 | /** 434 | * VariableSizeList caches offsets and measurements for each index for performance purposes. 435 | * This method clears that cached data for all items after (and including) the specified index. 436 | * It should be called whenever a item's size changes. (Note that this is not a typical occurrence.) 437 | * 438 | * By default the list will automatically re-render after the index is reset. 439 | * If you would like to delay this re-render until e.g. a state update has completed in the parent component, 440 | * specify a value of false for the second, optional parameter. 441 | */ 442 | instance: { 443 | resetAfterIndex: (index: number, shouldForceUpdate?: boolean) => void; 444 | } 445 | } 446 | /** 447 | * Variable Size List for use in Server-Side-Rendered environment 448 | * 449 | * usage: 450 | * 451 | * {#each items as item (item.key)} 452 | * {item.index} 453 | * {/each} 454 | * * 455 | */ 456 | export type VariableSizeListSSR = VariableSizeList 457 | 458 | /** 459 | * Fixed Size Grid 460 | * 461 | * usage: 462 | * 463 | * {#each items as item (item.key)} 464 | * {item.rowIndex} - {item.columnIndex} 465 | * {/each} 466 | * * 467 | */ 468 | export class FixedSizeGrid extends SvelteComponentTyped { 471 | /** 472 | * Scroll to the specified offsets. 473 | */ 474 | scrollTo(params: { scrollLeft: number; scrollTop: number }): void; 475 | /** 476 | * Scroll to the specified item. 477 | * 478 | * By default, the Grid will scroll as little as possible to ensure the item is visible. 479 | * You can control the alignment of the item though by specifying an `align` property. Acceptable values are: 480 | * 481 | * - auto (default) - Scroll as little as possible to ensure the item is visible. (If the item is already visible, it won't scroll at all.) 482 | * - smart 483 | * - If the item is already visible, don't scroll at all. 484 | * - If it is less than one viewport away, scroll as little as possible so that it becomes visible. 485 | * - If it is more than one viewport away, scroll so that it is centered within the grid. 486 | * - center - Center align the item within the grid. 487 | * - end - Align the item to the bottom, right hand side of the grid. 488 | * - start - Align the item to the top, left hand of the grid. 489 | * 490 | * If either `columnIndex` or `rowIndex` are omitted, `scrollLeft` or `scrollTop` will be unchanged (respectively). 491 | */ 492 | scrollToItem(params: { 493 | align?: Align; 494 | columnIndex?: number; 495 | rowIndex?: number; 496 | }): void; 497 | } 498 | 499 | /** 500 | * Fixed Size Grid for use in Server-Side-Rendered environment 501 | * 502 | * usage: 503 | * 504 | * {#each items as item (item.key)} 505 | * {item.rowIndex} - {item.columnIndex} 506 | * {/each} 507 | * * 508 | */ 509 | export type FixedSizeGridSSR = FixedSizeGrid 510 | 511 | 512 | /** 513 | * Variable Size Grid 514 | * 515 | * usage: 516 | * 517 | * {#each items as item (item.key)} 518 | * {item.rowIndex} - {item.columnIndex} 519 | * {/each} 520 | * * 521 | */ 522 | export class VariableSizeGrid extends SvelteComponentTyped { 525 | /** 526 | * Scroll to the specified offsets. 527 | */ 528 | scrollTo(params: { scrollLeft: number; scrollTop: number }): void; 529 | /** 530 | * Scroll to the specified item. 531 | * 532 | * By default, the Grid will scroll as little as possible to ensure the item is visible. 533 | * You can control the alignment of the item though by specifying an `align` property. Acceptable values are: 534 | * 535 | * - auto (default) - Scroll as little as possible to ensure the item is visible. (If the item is already visible, it won't scroll at all.) 536 | * - smart 537 | * - If the item is already visible, don't scroll at all. 538 | * - If it is less than one viewport away, scroll as little as possible so that it becomes visible. 539 | * - If it is more than one viewport away, scroll so that it is centered within the grid. 540 | * - center - Center align the item within the grid. 541 | * - end - Align the item to the bottom, right hand side of the grid. 542 | * - start - Align the item to the top, left hand of the grid. 543 | * 544 | * If either `columnIndex` or `rowIndex` are omitted, `scrollLeft` or `scrollTop` will be unchanged (respectively). 545 | */ 546 | scrollToItem(params: { 547 | align?: Align; 548 | columnIndex?: number; 549 | rowIndex?: number; 550 | }): void; 551 | 552 | instance: { 553 | /** 554 | * VariableSizeGrid caches offsets and measurements for each column index for performance purposes. 555 | * This method clears that cached data for all columns after (and including) the specified index. 556 | * It should be called whenever a column's width changes. (Note that this is not a typical occurrence.) 557 | * 558 | * By default the grid will automatically re-render after the index is reset. 559 | * If you would like to delay this re-render until e.g. a state update has completed in the parent component, 560 | * specify a value of false for the second, optional parameter. 561 | */ 562 | resetAfterColumnIndex: (index: number, shouldForceUpdate?: boolean) => void; 563 | /** 564 | * VariableSizeGrid caches offsets and measurements for each item for performance purposes. 565 | * This method clears that cached data for all items after (and including) the specified indices. 566 | * It should be called whenever an items size changes. (Note that this is not a typical occurrence.) 567 | * 568 | * By default the grid will automatically re-render after the index is reset. 569 | * If you would like to delay this re-render until e.g. a state update has completed in the parent component, 570 | * specify a value of false for the optional shouldForceUpdate parameter. 571 | */ 572 | resetAfterIndices: (params: { 573 | columnIndex: number; 574 | rowIndex: number; 575 | shouldForceUpdate?: boolean; 576 | }) => void; 577 | /** 578 | * VariableSizeGrid caches offsets and measurements for each row index for performance purposes. 579 | * This method clears that cached data for all rows after (and including) the specified index. 580 | * It should be called whenever a row's height changes. (Note that this is not a typical occurrence.) 581 | * 582 | * By default the grid will automatically re-render after the index is reset. 583 | * If you would like to delay this re-render until e.g. a state update has completed in the parent component, 584 | * specify a value of false for the second, optional parameter. 585 | */ 586 | resetAfterRowIndex: (index: number, shouldForceUpdate?: boolean) => void; 587 | } 588 | } 589 | 590 | /** 591 | * Variable Size Grid for use in Server-Side-Rendered environment 592 | * 593 | * usage: 594 | * 595 | * {#each items as item (item.key)} 596 | * {item.rowIndex} - {item.columnIndex} 597 | * {/each} 598 | * * 599 | */ 600 | export type VariableSizeGridSSR = VariableSizeGrid 601 | 602 | /** 603 | * Custom comparison function for React.memo(). 604 | * It knows to compare individual style props and ignore the wrapper object. 605 | * 606 | * @see https://reactjs.org/docs/react-api.html#reactmemo 607 | */ 608 | export function areEqual( 609 | prevProps: Readonly, 610 | nextProps: Readonly 611 | ): boolean; 612 | 613 | /** 614 | * Custom shouldComponentUpdate for class components. 615 | * It knows to compare individual style props and ignore the wrapper object. 616 | * 617 | * @see https://reactjs.org/docs/react-component.html#shouldcomponentupdate 618 | */ 619 | export function shouldComponentUpdate

( 620 | this: { props: P; state: S }, 621 | nextProps: Readonly

, 622 | nextState: Readonly 623 | ): boolean; 624 | 625 | export interface StyleObject { 626 | position?: string, 627 | width?: number|string, 628 | height?: number|string, 629 | top?: number, 630 | left?: number, 631 | right?: number 632 | } 633 | export function styleString(styleObject:StyleObject) : string; --------------------------------------------------------------------------------