From 472c3964758cc8f4699e533cc36c04a26e8a4bbc Mon Sep 17 00:00:00 2001 From: Daminski Date: Wed, 17 Jan 2024 23:51:58 +0100 Subject: [PATCH 01/29] migrate to svelte 5 & tailwind --- .eslintignore | 13 + .eslintrc.cjs | 15 + .eslintrc.json | 22 - .github/workflows/main.yml | 65 --- .gitignore | 13 +- .npmrc | 1 + .prettierignore | 4 + .prettierrc | 8 + README.md | 181 ++---- assets/ListLogo.sketch | Bin 9222 -> 0 bytes assets/ListLogo.svg | 11 - babel.config.json | 3 - jest.config.js | 8 - jsconfig.json | 12 + package.json | 109 ++-- postcss.config.js | 6 + rollup.config.js | 35 -- src/VirtualList.svelte | 351 ------------ src/app.html | 12 + src/index.js | 1 - src/lib/components/VirtualList.svelte | 335 +++++++++++ src/lib/index.js | 1 + src/lib/styles/app.css | 3 + src/lib/utils/ListProps.js | 133 +++++ src/lib/utils/ListState.js | 18 + src/{ => lib/utils}/SizeAndPositionManager.js | 0 src/{ => lib/utils}/constants.js | 0 src/routes/+layout.svelte | 9 + src/routes/+page.svelte | 115 ++++ static/favicon.png | Bin 0 -> 1571 bytes svelte.config.js | 12 + tailwind.config.js | 9 + test/SizeAndPositionManager.spec.js | 537 ------------------ test/VirtualList.spec.js | 17 - types/index.d.ts | 186 ------ vite.config.js | 8 + 36 files changed, 827 insertions(+), 1426 deletions(-) create mode 100644 .eslintignore create mode 100644 .eslintrc.cjs delete mode 100644 .eslintrc.json delete mode 100644 .github/workflows/main.yml create mode 100644 .npmrc create mode 100644 .prettierignore create mode 100644 .prettierrc delete mode 100644 assets/ListLogo.sketch delete mode 100644 assets/ListLogo.svg delete mode 100644 babel.config.json delete mode 100644 jest.config.js create mode 100644 jsconfig.json create mode 100644 postcss.config.js delete mode 100644 rollup.config.js delete mode 100644 src/VirtualList.svelte create mode 100644 src/app.html delete mode 100644 src/index.js create mode 100644 src/lib/components/VirtualList.svelte create mode 100644 src/lib/index.js create mode 100644 src/lib/styles/app.css create mode 100644 src/lib/utils/ListProps.js create mode 100644 src/lib/utils/ListState.js rename src/{ => lib/utils}/SizeAndPositionManager.js (100%) rename src/{ => lib/utils}/constants.js (100%) create mode 100644 src/routes/+layout.svelte create mode 100644 src/routes/+page.svelte create mode 100644 static/favicon.png create mode 100644 svelte.config.js create mode 100644 tailwind.config.js delete mode 100644 test/SizeAndPositionManager.spec.js delete mode 100644 test/VirtualList.spec.js delete mode 100644 types/index.d.ts create mode 100644 vite.config.js diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..3897265 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,13 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..50b97a5 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,15 @@ +/** @type { import("eslint").Linter.Config } */ +module.exports = { + root: true, + extends: ['eslint:recommended', 'plugin:svelte/recommended', 'prettier'], + parserOptions: { + sourceType: 'module', + ecmaVersion: 2020, + extraFileExtensions: ['.svelte'] + }, + env: { + browser: true, + es2017: true, + node: true + } +}; diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 04024fc..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "eslint:recommended", - "rules": { - "no-mixed-spaces-and-tabs": ["error", "smart-tabs"] - }, - "parserOptions": { - "ecmaVersion": 9, - "sourceType": "module" - }, - "plugins": ["svelte3"], - "overrides": [ - { - "files": ["*.svelte"], - "processor": "svelte3/svelte3" - } - ], - "env": { - "es6": true, - "browser": true, - "jest": true - } -} \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index d27deb6..0000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Test & Publish -on: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - test: - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [10.x, 12.x] - steps: - - name: Checkout 🛎️ - uses: actions/checkout@v2.3.3 - with: - persist-credentials: false - - - name: Setup 🔧 - uses: actions/setup-node@v2.1.2 - with: - node-version: ${{ matrix.node-version }} - - - name: Install ♻ - run: npm install - - - name: Lint 👓 - run: npm run lint - - - name: Test ✅ - run: npm run test - - publish: - needs: test - runs-on: ubuntu-latest - if: github.event_name == 'push' - steps: - - name: Checkout 🛎️ - uses: actions/checkout@v2.3.3 - with: - persist-credentials: false - - - name: Check Version Changes ↗ - uses: EndBug/version-check@v1.6.0 - id: check - - - name: Setup 🔧 - if: steps.check.outputs.changed == 'true' - uses: actions/setup-node@v2.1.2 - with: - node-version: 12 - registry-url: https://registry.npmjs.org - - - name: Install ♻ - if: steps.check.outputs.changed == 'true' - run: npm install - - - name: Publish 🚀 - if: steps.check.outputs.changed == 'true' - run: npm publish - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 6d2c13b..d3085f4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,12 @@ .DS_Store node_modules +/build +/dist +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* package-lock.json -test/public/bundle.js -.idea -dist -pnpm-lock.yaml \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..cc41cea --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..9573023 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/README.md b/README.md index aff1e2a..a704f67 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,9 @@ -

ListLogo

svelte-tiny-virtual-list

-

A tiny but mighty list virtualization library, with zero dependencies 💪

-

- NPM VERSION - NPM DOWNLOADS - DEPENDENCIES -

+

A tiny but mighty list virtualization library for Svelte 5 & Tailwind 💪

AboutFeatures • - Installation • + RequirementsUsageExamplesLicense @@ -19,6 +13,12 @@ Instead of rendering all your data in a huge list, the virtual list component just renders the items that are visible, keeping your page nice and light. This is heavily inspired by [react-tiny-virtual-list](https://github.com/clauderic/react-tiny-virtual-list) and uses most of its code and functionality! +This repo was forked from [skayo/svelte-tiny-virtual-list](https://github.com/skayo/svelte-tiny-virtual-list) + +## Requirements + +- **Svelte 5** +- Tailwind ### Features @@ -27,41 +27,6 @@ This is heavily inspired by [react-tiny-virtual-list](https://github.com/clauder - **Scroll to index** or **set the initial scroll offset** - **Supports fixed** or **variable** heights/widths - **Vertical** or **Horizontal** lists -- [`svelte-infinite-loading`](https://github.com/Skayo/svelte-infinite-loading) compatibility - -## Installation - -> If you're using this component in a Sapper application, make sure to install the package to `devDependencies`! -> [More Details](https://github.com/sveltejs/sapper-template#using-external-components) - -With npm: - -```shell -$ npm install svelte-tiny-virtual-list -``` - -With yarn: - -```shell -$ yarn add svelte-tiny-virtual-list -``` - -With [pnpm](https://pnpm.js.org/) (recommended): - -```shell -$ npm i -g pnpm -$ pnpm install svelte-tiny-virtual-list -``` - -From CDN (via [unpkg](https://unpkg.com/)): - -```html - - - - - -``` ## Usage @@ -69,55 +34,20 @@ From CDN (via [unpkg](https://unpkg.com/)): -

- Letter: {data[index]}, Row: #{index} -
- -``` - -Also works pretty well with [`svelte-infinite-loading`](https://github.com/Skayo/svelte-infinite-loading): - -```svelte - - - -
- Letter: {data[index]}, Row: #{index} -
- -
- -
+ itemSize={50} +> + {#snippet row({ index, style })} +
+ {data[index]}, Row: #{index} +
+ {/snippet}
``` @@ -138,26 +68,28 @@ Also works pretty well with [`svelte-infinite-loading`](https://github.com/Skayo | overscanCount | `number` | | Number of extra buffer items to render above/below the visible items. Tweaking this can help reduce scroll flickering on certain browsers/devices. | | estimatedItemSize | `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. | | getKey | `(index: number) => any` | | 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. | +| onAfterScroll | `({ index: number, style: string }) => any` | | Function that is called after handling the scroll event | +| onItemsUpdated | `({ start: number, end: number }) => any` | | Function that is called after when the visible items are updated | _\* `height` must be a number when `scrollDirection` is `'vertical'`. Similarly, `width` must be a number if `scrollDirection` is `'horizontal'`_ -### Slots +### Children -- `item` - Slot for each item +- `row` - Snippet for each item - Props: - `index: number` - Item index - `style: string` - Item style, must be applied to the slot (look above for example) -- `header` - Slot for the elements that should appear at the top of the list -- `footer` - Slot for the elements that should appear at the bottom of the list (e.g. `InfiniteLoading` component from `svelte-infinite-loading`) +- `header` - Snippet for the elements that should appear at the top of the list +- `footer` - Snippet for the elements that should appear at the bottom of the list -### Events +### Event handlers -- `afterScroll` - Fired after handling the scroll event - - `detail` Props: +- `onAfterScroll` - Called after handling the scroll event + - Props: - `event: ScrollEvent` - The original scroll event - `offset: number` - Either the value of `wrapper.scrollTop` or `wrapper.scrollLeft` -- `itemsUpdated` - Fired when the visible items are updated - - `detail` Props: +- `onItemsUpdated` - Called when the visible items are updated + - Props: - `start: number` - Index of the first visible item - `end: number` - Index of the last visible item @@ -172,29 +104,31 @@ However, if you're passing a function to `itemSize`, that type of comparison is ```svelte - + -
- Letter: {data[index]}, Row: #{index} -
+ bind:this={virtualList} + width="100%" + height={600} + itemCount={data.length} + itemSize={50} +> + {#snippet row({ index, style })} +
+ {data[index]}, Row: #{index} +
+ {/snippet}
``` @@ -206,35 +140,26 @@ You can style the elements of the virtual list like this: -
+
-
- Letter: {data[index]}, Row: #{index} -
+ itemSize={50} + > + {#snippet row({ index, style })} +
+ {data[index]}, Row: #{index} +
+ {/snippet}
- - ``` -## Examples / Demo +## Examples / Demo (OUTDATED) - **Basic setup** - [Elements of equal height](https://svelte.dev/repl/e3811b44f311461dbbc7c2df830cde68) @@ -247,4 +172,4 @@ You can style the elements of the virtual list like this: ## License -[MIT License](https://github.com/Skayo/svelte-tiny-virtual-list/blob/master/LICENSE) +[MIT License](https://github.com/daminski/svelte-tiny-virtual-list-tailwind/blob/master/LICENSE) diff --git a/assets/ListLogo.sketch b/assets/ListLogo.sketch deleted file mode 100644 index f02cbb0ff9d985fc0f38c75b1115f4b6668e1b42..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9222 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 diff --git a/assets/ListLogo.svg b/assets/ListLogo.svg deleted file mode 100644 index 0d1f162..0000000 --- a/assets/ListLogo.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - \ No newline at end of file diff --git a/babel.config.json b/babel.config.json deleted file mode 100644 index b4d43ab..0000000 --- a/babel.config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@babel/preset-env"] -} \ No newline at end of file diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 9b76e4f..0000000 --- a/jest.config.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - transform: { - '^.+\\.[t|j]sx?$': 'babel-jest', - '^.+\\.svelte$': 'svelte-jester', - }, - moduleFileExtensions: ['js', 'svelte'], - setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'], -}; \ No newline at end of file diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..1394d6c --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +} diff --git a/package.json b/package.json index dc52054..599dfeb 100644 --- a/package.json +++ b/package.json @@ -1,60 +1,53 @@ { - "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", - "scripts": { - "build": "rollup -c", - "lint": "eslint src/** test/**", - "test": "jest test", - "test:watch": "npm run test -- --watch", - "prepublishOnly": "npm run build" - }, - "devDependencies": { - "@babel/core": "^7.12.10", - "@babel/preset-env": "^7.12.10", - "@rollup/plugin-node-resolve": "^11.0.0", - "@testing-library/jest-dom": "^5.11.6", - "@testing-library/svelte": "^3.0.0", - "eslint": "^7.15.0", - "eslint-plugin-svelte3": "^2.7.3", - "jest": "^26.6.3", - "rollup": "^2.34.2", - "rollup-plugin-svelte": "^7.0.0", - "svelte": "^3.31.0", - "svelte-jester": "^1.3.0" - }, - "files": [ - "src", - "dist", - "types" - ], - "keywords": [ - "svelte", - "virtual", - "list", - "scroll", - "infinite", - "loading", - "component", - "plugin", - "svelte-components" - ], - "author": { - "name": "Skayo", - "email": "contact@skayo.dev", - "url": "https://skayo.dev" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/Skayo/svelte-tiny-virtual-list.git" - }, - "bugs": { - "url": "https://github.com/Skayo/svelte-tiny-virtual-list/issues" - }, - "homepage": "https://github.com/Skayo/svelte-tiny-virtual-list" + "name": "sveltekit-tiny-virtual-list-tailwind", + "version": "0.0.1", + "scripts": { + "dev": "vite dev", + "build": "vite build && npm run package", + "preview": "vite preview", + "package": "svelte-kit sync && svelte-package && publint", + "prepublishOnly": "npm run package", + "lint": "prettier --check . && eslint .", + "format": "prettier --write ." + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "svelte": "./dist/index.js" + } + }, + "files": [ + "dist", + "!dist/**/*.test.*", + "!dist/**/*.spec.*" + ], + "peerDependencies": { + "svelte": "^4.0.0" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^3.1.0", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/package": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@types/eslint": "8.56.0", + "autoprefixer": "^10.4.16", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-svelte": "^2.36.0-next.4", + "postcss": "^8.4.33", + "prettier": "^3.1.1", + "prettier-plugin-svelte": "^3.1.2", + "publint": "^0.1.9", + "svelte": "^5.0.0-next.1", + "tailwindcss": "^3.4.1", + "tslib": "^2.4.1", + "typescript": "^5.3.2", + "vite": "^5.0.11" + }, + "svelte": "./dist/index.js", + "types": "./dist/index.d.ts", + "type": "module", + "dependencies": { + "@sveltejs/adapter-static": "^3.0.1" + } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index 28b5996..0000000 --- a/rollup.config.js +++ /dev/null @@ -1,35 +0,0 @@ -import resolve from '@rollup/plugin-node-resolve'; -import svelte from 'rollup-plugin-svelte'; -import pkg from './package.json'; - -const name = pkg.name - .replace(/^(@\S+\/)?(svelte-)?(\S+)/, '$3') - .replace(/^\w/, m => m.toUpperCase()) - .replace(/-\w/g, m => m[1].toUpperCase()); - -export default { - input: 'src/index.js', - output: [ - { file: pkg.module, format: 'es' }, - { file: pkg.main, format: 'umd', name: 'VirtualList' }, - ], - plugins: [ - svelte({ emitCss: false }), - resolve() - ], - - /* tests - { - input: 'test/src/index.js', - output: { - file: 'test/public/bundle.js', - format: 'iife', - }, - plugins: [ - resolve(), - commonjs(), - svelte() - ], - }, - */ -}; \ No newline at end of file diff --git a/src/VirtualList.svelte b/src/VirtualList.svelte deleted file mode 100644 index 3c5cbe0..0000000 --- a/src/VirtualList.svelte +++ /dev/null @@ -1,351 +0,0 @@ - - - - -
- - -
- {#each items as item (getKey ? getKey(item.index) : item.index)} - - {/each} -
- - -
- - diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..f22aeaa --- /dev/null +++ b/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + 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/src/lib/components/VirtualList.svelte b/src/lib/components/VirtualList.svelte new file mode 100644 index 0000000..1cb013e --- /dev/null +++ b/src/lib/components/VirtualList.svelte @@ -0,0 +1,335 @@ + + +
+ {#if header} + {@render header()} + {/if} + +
+ {#each items as item (getKey ? getKey(item.index) : item.index)} + {@render row({ index: item.index, style: item.style })} + {/each} +
+ + {#if footer} + {@render footer()} + {/if} +
diff --git a/src/lib/index.js b/src/lib/index.js new file mode 100644 index 0000000..de58bb4 --- /dev/null +++ b/src/lib/index.js @@ -0,0 +1 @@ +export { default as default } from './components/VirtualList.svelte'; diff --git a/src/lib/styles/app.css b/src/lib/styles/app.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/src/lib/styles/app.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/src/lib/utils/ListProps.js b/src/lib/utils/ListProps.js new file mode 100644 index 0000000..981c1b0 --- /dev/null +++ b/src/lib/utils/ListProps.js @@ -0,0 +1,133 @@ +/** + * Store for previous list props + */ + +export class ListProps { + + /** + * Constructor + * @param {number|null|undefined} scrollToIndex + * @param {string|null|undefined} scrollToAlignment + * @param {number|null|undefined} scrollOffset + * @param {number} itemCount + * @param {number} itemSize + * @param {number} estimatedItemSize + * @param {number} height + * @param {string} width + * @param {number[]|null|undefined} stickyIndices + */ + constructor (scrollToIndex = null, scrollToAlignment = null, scrollOffset = null, itemCount = 0, itemSize = 0, estimatedItemSize = 50, height = 600, width = "100%", stickyIndices = null) { + this.scrollToIndex = scrollToIndex; + this.scrollToAlignment = scrollToAlignment; + this.scrollOffset = scrollOffset; + this.itemCount = itemCount; + this.itemSize = itemSize; + this.estimatedItemSize = estimatedItemSize; + this.height = height; + this.width = width; + this.stickyIndices = stickyIndices; + }; + + /** + * Check if props have changed and update current object + * @param {number|null|undefined} scrollOffset + * @param {number|null|undefined} scrollToIndex + * @param {string|null|undefined} scrollToAlignment + * @param {number} itemCount + * @param {number} itemSize + * @param {number} estimatedItemSize + * @param {number} height + * @param {string} width + * @param {number[]|null|undefined} stickyIndices + * @returns {Object} + */ + havePropsChanged (scrollOffset, scrollToIndex, scrollToAlignment, itemCount, itemSize, estimatedItemSize, height, width, stickyIndices) { + return { + scrollOffsetHasChanged: this._hasScrollOffsetChanged(scrollOffset), + scrollPropsHaveChanged: this._haveScrollPropsChanged(scrollToIndex, scrollToAlignment), + itemPropsHaveChanged: this._haveItemPropsChanged(itemCount, itemSize, estimatedItemSize), + listPropsHaveChanged: this._haveListPropsChanged(height, width, stickyIndices) + }; + }; + + /** + * Check if scrollOffset has changed and update + * @param {number|null|undefined} scrollOffset + * @returns {boolean} + */ + _hasScrollOffsetChanged (scrollOffset) { + let _tmp = false + if(this.scrollOffset !== scrollOffset) { + _tmp = true; + this.scrollOffset = scrollOffset; + } + return _tmp; + }; + + /** + * Check if scroll props have changed and update + * @param {number|null|undefined} scrollToIndex + * @param {string} scrollToAlignment + * @returns + */ + _haveScrollPropsChanged (scrollToIndex, scrollToAlignment) { + let _tmp = false + if(this.scrollToIndex !== scrollToIndex) { + _tmp = true; + this.scrollToIndex = scrollToIndex; + } + if(this.scrollToAlignment !== scrollToAlignment) { + _tmp = true; + this.scrollToAlignment = scrollToAlignment; + } + return _tmp; + }; + + /** + * Check if item props have changed and update + * @param {number} itemCount + * @param {number} itemSize + * @param {number} estimatedItemSize + * @returns {boolean} + */ + _haveItemPropsChanged (itemCount, itemSize, estimatedItemSize) { + let _tmp = false; + if(this.itemCount !== itemCount) { + _tmp = true; + this.itemCount = itemCount; + } + if(this.itemSize !== itemSize) { + _tmp = true; + this.itemSize = itemSize; + } + if(this.estimatedItemSize !== estimatedItemSize) { + _tmp = true; + this.estimatedItemSize = estimatedItemSize; + } + return _tmp; + }; + + /** + * Check if list props have changed and update + * @param {number} height + * @param {string} width + * @param {number[]|null|undefined} stickyIndices + * @returns {boolean} + */ + _haveListPropsChanged (height, width, stickyIndices) { + let _tmp = false; + if(this.height !== height) { + _tmp = true; + this.height = height; + } + if(this.width !== width) { + _tmp = true; + this.width = width; + } + if(this.stickyIndices !== stickyIndices) { + _tmp = true; + this.stickyIndices = stickyIndices; + } + return _tmp; + }; +}; \ No newline at end of file diff --git a/src/lib/utils/ListState.js b/src/lib/utils/ListState.js new file mode 100644 index 0000000..d918de7 --- /dev/null +++ b/src/lib/utils/ListState.js @@ -0,0 +1,18 @@ +import { SCROLL_CHANGE_REASON } from "./constants.js"; + +/** + * Store for list state + */ + +export class ListState { + + /** + * Constructor + * @param {Number|null|undefined} offset + * @param {Number} scrollChangeReason + */ + constructor (offset = 0, scrollChangeReason = SCROLL_CHANGE_REASON.REQUESTED) { + this.offset = offset; + this.scrollChangeReason = scrollChangeReason; + }; +}; \ No newline at end of file diff --git a/src/SizeAndPositionManager.js b/src/lib/utils/SizeAndPositionManager.js similarity index 100% rename from src/SizeAndPositionManager.js rename to src/lib/utils/SizeAndPositionManager.js diff --git a/src/constants.js b/src/lib/utils/constants.js similarity index 100% rename from src/constants.js rename to src/lib/utils/constants.js diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..9b4d8c1 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,9 @@ + + + + Sveltekit-tiny-virtual-list-tailwind + + + \ No newline at end of file diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..5083a65 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,115 @@ + + +
+

Sveltekit-tiny-virtual-list-tailwind

+ +
+
+ Settings +
+ + + + +
+ +
+
+ +
+
+
+
+ +
+ + {#snippet header()} +
Header
+ {/snippet} + {#snippet row({ index, style })} +
+ {fakeData[index].content}, Row: #{index} +
+ {/snippet} + {#snippet footer()} +
Footer
+ {/snippet} +
+
+
diff --git a/static/favicon.png b/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..825b9e65af7c104cfb07089bb28659393b4f2097 GIT binary patch literal 1571 zcmV+;2Hg3HP)Px)-AP12RCwC$UE6KzI1p6{F2N z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z zgj#$x=!~7LGqHW?IO8+*oE1MyDp!G=L0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;| zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb zghxQU22N}F51}^yfDSt786oMTc!W&V;d?76)9KXX1 z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFNgpIod~R{>@#@5x9zJKEHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2` zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL z@iUO?SBBG-0cQuo+an4TsLy-g-x;8P4UVwk|D8{W@U1Zi z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA zI8U?CWdY@}cRq6{5~y+)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6 zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6 zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5 zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH Array.from({ length: N }, (_, k) => k + 1); - -describe('SizeAndPositionManager', () => { - - /** - * @param {number} itemCount - * @param {number} estimatedItemSize - * @return {{sizeAndPositionManager: SizeAndPositionManager, itemSizeGetterCalls: number[]}} - */ - function getItemSizeAndPositionManager(itemCount = 100, estimatedItemSize = 15) { - /** @type {number[]} */ - let itemSizeGetterCalls = []; - - const sizeAndPositionManager = new SizeAndPositionManager({ - itemCount, - itemSize: (index) => { - itemSizeGetterCalls.push(index); - return ITEM_SIZE; - }, - estimatedItemSize, - }); - - return { - sizeAndPositionManager, - itemSizeGetterCalls, - }; - } - - /** - * @param {number} itemCount - * @param {number} itemSize - * @return {{sizeAndPositionManager: SizeAndPositionManager, totalSize: number, itemSize: number}} - */ - function getItemSizeAndPositionManagerNumber(itemCount = 100, itemSize = 50) { - const sizeAndPositionManager = new SizeAndPositionManager({ - itemCount, - itemSize, - }); - - return { - sizeAndPositionManager, - itemSize, - totalSize: itemSize * itemCount, - }; - } - - /** - * @param {number} itemCount - * @return {{sizeAndPositionManager: SizeAndPositionManager, totalSize: number, itemSize: number[]}} - */ - function getItemSizeAndPositionManagerArray(itemCount = 100) { - const itemSize = range(itemCount).map(() => { - return Math.max(Math.round(Math.random() * 100), 32); - }); - - const sizeAndPositionManager = new SizeAndPositionManager({ - itemCount, - itemSize, - }); - - return { - sizeAndPositionManager, - itemSize, - totalSize: itemSize.reduce((acc, curr) => { - return acc + curr; - }, 0), - }; - } - - describe('findNearestItem', () => { - it('should error if given NaN', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManager(); - expect(() => sizeAndPositionManager.findNearestItem(NaN)).toThrow(); - }); - - it('should handle offets outisde of bounds (to account for elastic scrolling)', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManager(); - expect(sizeAndPositionManager.findNearestItem(-100)).toEqual(0); - expect(sizeAndPositionManager.findNearestItem(1234567890)).toEqual(99); - }); - - it('should find the first item', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManager(); - expect(sizeAndPositionManager.findNearestItem(0)).toEqual(0); - expect(sizeAndPositionManager.findNearestItem(9)).toEqual(0); - }); - - it('should find the last item', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManager(); - expect(sizeAndPositionManager.findNearestItem(990)).toEqual(99); - expect(sizeAndPositionManager.findNearestItem(991)).toEqual(99); - }); - - it('should find the a item that exactly matches a specified offset in the middle', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManager(); - expect(sizeAndPositionManager.findNearestItem(100)).toEqual(10); - }); - - it('should find the item closest to (but before) the specified offset in the middle', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManager(); - expect(sizeAndPositionManager.findNearestItem(101)).toEqual(10); - }); - }); - - describe('getSizeAndPositionForIndex when itemSize is a function', () => { - it('should error if an invalid index is specified', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManager(); - expect(() => - sizeAndPositionManager.getSizeAndPositionForIndex(-1), - ).toThrow(); - expect(() => - sizeAndPositionManager.getSizeAndPositionForIndex(100), - ).toThrow(); - }); - - it('should return the correct size and position information for the requested item', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManager(); - expect( - sizeAndPositionManager.getSizeAndPositionForIndex(0).offset, - ).toEqual(0); - expect(sizeAndPositionManager.getSizeAndPositionForIndex(0).size).toEqual( - 10, - ); - expect( - sizeAndPositionManager.getSizeAndPositionForIndex(1).offset, - ).toEqual(10); - expect( - sizeAndPositionManager.getSizeAndPositionForIndex(2).offset, - ).toEqual(20); - }); - - it('should only measure the necessary items to return the information requested', () => { - const { - sizeAndPositionManager, - itemSizeGetterCalls, - } = getItemSizeAndPositionManager(); - sizeAndPositionManager.getSizeAndPositionForIndex(0); - expect(itemSizeGetterCalls).toEqual([0]); - }); - - it('should just-in-time measure all items up to the requested item if no items have yet been measured', () => { - const { - sizeAndPositionManager, - itemSizeGetterCalls, - } = getItemSizeAndPositionManager(); - sizeAndPositionManager.getSizeAndPositionForIndex(5); - expect(itemSizeGetterCalls).toEqual([0, 1, 2, 3, 4, 5]); - }); - - it('should just-in-time measure items up to the requested item if some but not all items have been measured', () => { - const { - sizeAndPositionManager, - itemSizeGetterCalls, - } = getItemSizeAndPositionManager(); - sizeAndPositionManager.getSizeAndPositionForIndex(5); - itemSizeGetterCalls.splice(0); - sizeAndPositionManager.getSizeAndPositionForIndex(10); - expect(itemSizeGetterCalls).toEqual([6, 7, 8, 9, 10]); - }); - - it('should return cached size and position data if item has already been measured', () => { - const { - sizeAndPositionManager, - itemSizeGetterCalls, - } = getItemSizeAndPositionManager(); - sizeAndPositionManager.getSizeAndPositionForIndex(5); - itemSizeGetterCalls.splice(0); - sizeAndPositionManager.getSizeAndPositionForIndex(5); - expect(itemSizeGetterCalls).toEqual([]); - }); - }); - - describe('getSizeAndPositionForIndex when itemSize is a number', () => { - it('should error if an invalid index is specified', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManagerNumber(); - expect(() => - sizeAndPositionManager.getSizeAndPositionForIndex(-1), - ).toThrow(); - expect(() => - sizeAndPositionManager.getSizeAndPositionForIndex(100), - ).toThrow(); - }); - - it('should return the correct size and position information for the requested item', () => { - const { - sizeAndPositionManager, - itemSize, - } = getItemSizeAndPositionManagerNumber(); - expect( - sizeAndPositionManager.getSizeAndPositionForIndex(0).offset, - ).toEqual(0); - expect(sizeAndPositionManager.getSizeAndPositionForIndex(0).size).toEqual( - itemSize, - ); - expect( - sizeAndPositionManager.getSizeAndPositionForIndex(1).offset, - ).toEqual(itemSize); - expect( - sizeAndPositionManager.getSizeAndPositionForIndex(2).offset, - ).toEqual(itemSize * 2); - }); - }); - - describe('getSizeAndPositionForIndex when itemSize is an array', () => { - it('should error if an invalid index is specified', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManagerArray(); - expect(() => - sizeAndPositionManager.getSizeAndPositionForIndex(-1), - ).toThrow(); - expect(() => - sizeAndPositionManager.getSizeAndPositionForIndex(100), - ).toThrow(); - }); - - it('should return the correct size and position information for the requested item', () => { - const { - sizeAndPositionManager, - itemSize, - } = getItemSizeAndPositionManagerArray(); - expect( - sizeAndPositionManager.getSizeAndPositionForIndex(0).offset, - ).toEqual(0); - expect(sizeAndPositionManager.getSizeAndPositionForIndex(0).size).toEqual( - itemSize[0], - ); - expect( - sizeAndPositionManager.getSizeAndPositionForIndex(1).offset, - ).toEqual(itemSize[0]); - expect( - sizeAndPositionManager.getSizeAndPositionForIndex(2).offset, - ).toEqual(itemSize[0] + itemSize[1]); - }); - }); - - describe('getSizeAndPositionOfLastMeasuredItem', () => { - it('should return an empty object if no cached items are present', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManager(); - expect( - sizeAndPositionManager.getSizeAndPositionOfLastMeasuredItem(), - ).toEqual({ - offset: 0, - size: 0, - }); - }); - - it('should return size and position data for the highest/last measured item', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManager(); - sizeAndPositionManager.getSizeAndPositionForIndex(5); - expect( - sizeAndPositionManager.getSizeAndPositionOfLastMeasuredItem(), - ).toEqual({ - offset: 50, - size: 10, - }); - }); - }); - - describe('getTotalSize when itemSize is a function', () => { - it('should calculate total size based purely on :estimatedItemSize if no measurements have been done', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManager(); - expect(sizeAndPositionManager.getTotalSize()).toEqual(1500); - }); - - it('should calculate total size based on a mixture of actual item sizes and :estimatedItemSize if some items have been measured', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManager(); - sizeAndPositionManager.getSizeAndPositionForIndex(49); - expect(sizeAndPositionManager.getTotalSize()).toEqual(1250); - }); - - it('should calculate total size based on the actual measured sizes if all items have been measured', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManager(); - sizeAndPositionManager.getSizeAndPositionForIndex(99); - expect(sizeAndPositionManager.getTotalSize()).toEqual(1000); - }); - }); - - describe('getTotalSize when itemSize is a number', () => { - it('should calculate total size by multiplying the itemCount with the itemSize', () => { - const { - sizeAndPositionManager, - totalSize, - } = getItemSizeAndPositionManagerNumber(); - expect(sizeAndPositionManager.getTotalSize()).toEqual(totalSize); - }); - }); - - describe('getTotalSize when itemSize is an array', () => { - it('should calculate total size by counting together all of the itemSize items', () => { - const { - sizeAndPositionManager, - totalSize, - } = getItemSizeAndPositionManagerArray(); - expect(sizeAndPositionManager.getTotalSize()).toEqual(totalSize); - }); - }); - - describe('getUpdatedOffsetForIndex', () => { - - /** - * @param {'auto' | 'start' | 'center' | 'end'} align - * @param {number} itemCount - * @param {number} itemSize - * @param {number} containerSize - * @param {number} currentOffset - * @param {number} estimatedItemSize - * @param {number} targetIndex - * @return {number} - */ - function getUpdatedOffsetForIndexHelper({ - align = ALIGNMENT.START, - itemCount = 10, - itemSize = ITEM_SIZE, - containerSize = 50, - currentOffset = 0, - estimatedItemSize = 15, - targetIndex = 0, - }) { - const sizeAndPositionManager = new SizeAndPositionManager({ - itemCount, - itemSize: () => itemSize, - estimatedItemSize, - }); - - return sizeAndPositionManager.getUpdatedOffsetForIndex({ - align, - containerSize, - currentOffset, - targetIndex, - }); - } - - it('should scroll to the beginning', () => { - expect( - getUpdatedOffsetForIndexHelper({ - currentOffset: 100, - targetIndex: 0, - }), - ).toEqual(0); - }); - - it('should scroll to the end', () => { - expect( - getUpdatedOffsetForIndexHelper({ - currentOffset: 0, - targetIndex: 9, - }), - ).toEqual(50); - }); - - it('should scroll forward to the middle', () => { - const targetIndex = 6; - - expect( - getUpdatedOffsetForIndexHelper({ - currentOffset: 0, - targetIndex, - }), - ).toEqual(ITEM_SIZE * targetIndex); - }); - - it('should scroll backward to the middle', () => { - expect( - getUpdatedOffsetForIndexHelper({ - currentOffset: 50, - targetIndex: 2, - }), - ).toEqual(20); - }); - - it('should not scroll if an item is already visible', () => { - const targetIndex = 3; - const currentOffset = targetIndex * ITEM_SIZE; - - expect( - getUpdatedOffsetForIndexHelper({ - currentOffset, - targetIndex, - }), - ).toEqual(currentOffset); - }); - - it('should honor specified :align values', () => { - expect( - getUpdatedOffsetForIndexHelper({ - align: ALIGNMENT.START, - currentOffset: 0, - targetIndex: 5, - }), - ).toEqual(50); - expect( - getUpdatedOffsetForIndexHelper({ - align: ALIGNMENT.END, - currentOffset: 50, - targetIndex: 5, - }), - ).toEqual(10); - expect( - getUpdatedOffsetForIndexHelper({ - align: ALIGNMENT.CENTER, - currentOffset: 50, - targetIndex: 5, - }), - ).toEqual(30); - }); - - it('should not scroll past the safe bounds even if the specified :align requests it', () => { - expect( - getUpdatedOffsetForIndexHelper({ - align: ALIGNMENT.END, - currentOffset: 50, - targetIndex: 0, - }), - ).toEqual(0); - expect( - getUpdatedOffsetForIndexHelper({ - align: ALIGNMENT.CENTER, - currentOffset: 50, - targetIndex: 1, - }), - ).toEqual(0); - expect( - getUpdatedOffsetForIndexHelper({ - align: ALIGNMENT.START, - currentOffset: 0, - targetIndex: 9, - }), - ).toEqual(50); - - // TRICKY: We would expect this to be positioned at 50. - // But since the :estimatedItemSize is 15 and we only measure up to the 8th item, - // The helper assumes it can scroll farther than it actually can. - // Not sure if this edge case is worth "fixing" or just acknowledging... - expect( - getUpdatedOffsetForIndexHelper({ - align: ALIGNMENT.CENTER, - currentOffset: 0, - targetIndex: 8, - }), - ).toEqual(55); - }); - - it('should always return an offset of 0 when :containerSize is 0', () => { - expect( - getUpdatedOffsetForIndexHelper({ - containerSize: 0, - currentOffset: 50, - targetIndex: 2, - }), - ).toEqual(0); - }); - }); - - describe('getVisibleRange', () => { - it('should not return any indices if :itemCount is 0', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManager(0); - const { start, stop } = sizeAndPositionManager.getVisibleRange({ - containerSize: 50, - offset: 0, - overscanCount: 0, - }); - expect(start).toBeUndefined(); - expect(stop).toBeUndefined(); - }); - - it('should return a visible range of items for the beginning of the list', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManager(); - const { start, stop } = sizeAndPositionManager.getVisibleRange({ - containerSize: 50, - offset: 0, - overscanCount: 0, - }); - expect(start).toEqual(0); - expect(stop).toEqual(4); - }); - - it('should return a visible range of items for the middle of the list where some are partially visible', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManager(); - const { start, stop } = sizeAndPositionManager.getVisibleRange({ - containerSize: 50, - offset: 425, - overscanCount: 0, - }); - // 42 and 47 are partially visible - expect(start).toEqual(42); - expect(stop).toEqual(47); - }); - - it('should return a visible range of items for the end of the list', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManager(); - const { start, stop } = sizeAndPositionManager.getVisibleRange({ - containerSize: 50, - offset: 950, - overscanCount: 0, - }); - expect(start).toEqual(95); - expect(stop).toEqual(99); - }); - }); - - describe('resetItem', () => { - it('should clear size and position metadata for the specified index and all items after it', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManager(); - sizeAndPositionManager.getSizeAndPositionForIndex(5); - sizeAndPositionManager.resetItem(3); - expect(sizeAndPositionManager.getLastMeasuredIndex()).toEqual(2); - sizeAndPositionManager.resetItem(0); - expect(sizeAndPositionManager.getLastMeasuredIndex()).toEqual(-1); - }); - - it('should not clear size and position metadata for items before the specified index', () => { - const { - sizeAndPositionManager, - itemSizeGetterCalls, - } = getItemSizeAndPositionManager(); - sizeAndPositionManager.getSizeAndPositionForIndex(5); - itemSizeGetterCalls.splice(0); - sizeAndPositionManager.resetItem(3); - sizeAndPositionManager.getSizeAndPositionForIndex(4); - expect(itemSizeGetterCalls).toEqual([3, 4]); - }); - - it('should not skip over any unmeasured or previously-cleared items', () => { - const { sizeAndPositionManager } = getItemSizeAndPositionManager(); - sizeAndPositionManager.getSizeAndPositionForIndex(5); - sizeAndPositionManager.resetItem(2); - expect(sizeAndPositionManager.getLastMeasuredIndex()).toEqual(1); - sizeAndPositionManager.resetItem(4); - expect(sizeAndPositionManager.getLastMeasuredIndex()).toEqual(1); - sizeAndPositionManager.resetItem(0); - expect(sizeAndPositionManager.getLastMeasuredIndex()).toEqual(-1); - }); - }); -}); \ No newline at end of file diff --git a/test/VirtualList.spec.js b/test/VirtualList.spec.js deleted file mode 100644 index d3ec8ae..0000000 --- a/test/VirtualList.spec.js +++ /dev/null @@ -1,17 +0,0 @@ -require('@testing-library/jest-dom/extend-expect'); - -const { render /*, fireEvent*/ } = require('@testing-library/svelte'); - -const VirtualList = require('../src/VirtualList.svelte'); - -test('renders successfully', () => { - const { container } = render(VirtualList, { height: 600, itemCount: 1000, itemSize: 50 }); - expect(container).toBeInTheDocument(); -}); - -test('scrollToIndexTest', () => { - const { container } = render(VirtualList, { height: 600, itemCount: 1000, itemSize: 50, scrollToIndex: 20 }); - expect(container).toBeInTheDocument(); -}); - -/* TODO: Add tests */ \ 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 { -} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..8f5b5c8 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,8 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [ + sveltekit() + ] +}); From f708b0e458c90da8d96d8425d1375786e73c1b20 Mon Sep 17 00:00:00 2001 From: Daminski Date: Thu, 18 Jan 2024 00:10:20 +0100 Subject: [PATCH 02/29] readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a704f67..3efd5e8 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ _\* `height` must be a number when `scrollDirection` is `'vertical'`. Similarly, ### Children - `row` - Snippet for each item - - Props: + - Params: - `index: number` - Item index - `style: string` - Item style, must be applied to the slot (look above for example) - `header` - Snippet for the elements that should appear at the top of the list @@ -85,11 +85,11 @@ _\* `height` must be a number when `scrollDirection` is `'vertical'`. Similarly, ### Event handlers - `onAfterScroll` - Called after handling the scroll event - - Props: + - Params: - `event: ScrollEvent` - The original scroll event - `offset: number` - Either the value of `wrapper.scrollTop` or `wrapper.scrollLeft` - `onItemsUpdated` - Called when the visible items are updated - - Props: + - Params: - `start: number` - Index of the first visible item - `end: number` - Index of the last visible item From 02ab91e228e941dd3e4cbf09bddbec40362751b5 Mon Sep 17 00:00:00 2001 From: Daminski Date: Thu, 18 Jan 2024 00:11:57 +0100 Subject: [PATCH 03/29] rename package --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 599dfeb..5787022 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "sveltekit-tiny-virtual-list-tailwind", + "name": "svelte-tiny-virtual-list-tailwind", "version": "0.0.1", "scripts": { "dev": "vite dev", From f80082c4ccce04f1205c14b4a3964a6ce9d4f6e4 Mon Sep 17 00:00:00 2001 From: Daminski Date: Thu, 18 Jan 2024 00:15:05 +0100 Subject: [PATCH 04/29] update license --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 4363a78..c4100ac 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Skayo +Copyright (c) 2024 Daminski Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 0923ac3e7f849a546d62a965fa76e3990df68660 Mon Sep 17 00:00:00 2001 From: Daminski Date: Thu, 18 Jan 2024 00:22:58 +0100 Subject: [PATCH 05/29] readme: spelling --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3efd5e8..aee4618 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ This repo was forked from [skayo/svelte-tiny-virtual-list](https://github.com/sk | estimatedItemSize | `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. | | getKey | `(index: number) => any` | | 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. | | onAfterScroll | `({ index: number, style: string }) => any` | | Function that is called after handling the scroll event | -| onItemsUpdated | `({ start: number, end: number }) => any` | | Function that is called after when the visible items are updated | +| onItemsUpdated | `({ start: number, end: number }) => any` | | Function that is called when the visible items are updated | _\* `height` must be a number when `scrollDirection` is `'vertical'`. Similarly, `width` must be a number if `scrollDirection` is `'horizontal'`_ From 07a7530d12e372d9fd79e5975b81a3ba5aabc88d Mon Sep 17 00:00:00 2001 From: Daminski Date: Thu, 18 Jan 2024 10:10:37 +0100 Subject: [PATCH 06/29] remove useless reactivity --- src/lib/components/VirtualList.svelte | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/lib/components/VirtualList.svelte b/src/lib/components/VirtualList.svelte index 1cb013e..0d8b1b9 100644 --- a/src/lib/components/VirtualList.svelte +++ b/src/lib/components/VirtualList.svelte @@ -112,9 +112,9 @@ }); let wrapper = $state(null); - let items = $state([]); + let items = $state.frozen([]); - let curState = $state(new ListState(scrollOffset || (scrollToIndex != null && itemCount && getOffsetForIndex(scrollToIndex)))); + let curState = $state.frozen(new ListState(scrollOffset || (scrollToIndex != null && itemCount && getOffsetForIndex(scrollToIndex)))); let prevState = new ListState(); let prevProps = new ListProps( @@ -129,7 +129,7 @@ stickyIndices ); - let styleCache = $state({}); + let styleCache = {}; let wrapperStyle = $state(""); let innerStyle = $state(""); @@ -226,7 +226,7 @@ } items = updatedItems; - } + }; function scrollTo(value) { @@ -237,13 +237,13 @@ }); else wrapper[SCROLL_PROP_LEGACY[scrollDirection]] = value; - } + }; export function recomputeSizes(startIndex = 0) { styleCache = {}; sizeAndPositionManager.resetItem(startIndex); refresh(); - } + }; function getOffsetForIndex(index, align = scrollToAlignment, _itemCount = itemCount) { if (index < 0 || index >= _itemCount) @@ -255,7 +255,7 @@ currentOffset: curState.offset || 0, targetIndex: index, }); - } + }; function handleScroll(event) { const offset = getWrapperOffset(); @@ -269,11 +269,11 @@ offset, event }); - } + }; function getWrapperOffset() { return wrapper[SCROLL_PROP_LEGACY[scrollDirection]]; - } + }; function getEstimatedItemSize() { return ( @@ -281,7 +281,7 @@ (typeof itemSize === 'number' && itemSize) || 50 ); - } + }; function getStyle(index, sticky) { if (styleCache[index]) return styleCache[index]; @@ -307,7 +307,7 @@ } return styleCache[index] = style; - } + };
Date: Thu, 18 Jan 2024 10:14:31 +0100 Subject: [PATCH 07/29] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aee4618..2ff6339 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ This repo was forked from [skayo/svelte-tiny-virtual-list](https://github.com/sk ### Features -- **Tiny & dependency free** – Only ~5kb gzipped +- **Tiny & dependency free** - **Render millions of items**, without breaking a sweat - **Scroll to index** or **set the initial scroll offset** - **Supports fixed** or **variable** heights/widths From bf4365057215dc54ae66ea8f31728c63a8060fbd Mon Sep 17 00:00:00 2001 From: Daminski Date: Thu, 18 Jan 2024 11:02:45 +0100 Subject: [PATCH 08/29] add ts types --- package.json | 8 +- src/lib/components/VirtualList.svelte | 61 +-------- src/lib/types/index.d.ts | 177 ++++++++++++++++++++++++++ 3 files changed, 183 insertions(+), 63 deletions(-) create mode 100644 src/lib/types/index.d.ts diff --git a/package.json b/package.json index 5787022..7fba4c2 100644 --- a/package.json +++ b/package.json @@ -26,18 +26,18 @@ }, "devDependencies": { "@sveltejs/adapter-auto": "^3.1.0", - "@sveltejs/kit": "^2.0.0", + "@sveltejs/kit": "^2.3.5", "@sveltejs/package": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", - "@types/eslint": "8.56.0", - "autoprefixer": "^10.4.16", + "@types/eslint": "^8.56.2", + "autoprefixer": "^10.4.17", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.36.0-next.4", "postcss": "^8.4.33", "prettier": "^3.1.1", "prettier-plugin-svelte": "^3.1.2", - "publint": "^0.1.9", + "publint": "^0.2.7", "svelte": "^5.0.0-next.1", "tailwindcss": "^3.4.1", "tslib": "^2.4.1", diff --git a/src/lib/components/VirtualList.svelte b/src/lib/components/VirtualList.svelte index 0d8b1b9..28bc3e5 100644 --- a/src/lib/components/VirtualList.svelte +++ b/src/lib/components/VirtualList.svelte @@ -8,100 +8,43 @@ } from '$lib/utils/constants'; import { ListState } from '$lib/utils/ListState.js'; import { ListProps } from '$lib/utils/ListProps.js'; + let { - /** - * @type {number} - */ height, - /** - * @type {string} - */ width = '100%', - /** - * @type {number} - */ itemCount, - /** - * @type {number} - */ itemSize, - /** - * @type {number} - */ estimatedItemSize = null, - /** - * @type {Number[]} - */ stickyIndices = null, - /** - * @type {function} - * @param {number} index - */ getKey = null, - /** - * @type {'horizontal'|'vertical'} - */ scrollDirection = DIRECTION.VERTICAL, - /** - * @type {number} - */ scrollOffset = null, - /** - * @type {number} - */ scrollToIndex = null, - /** - * @type {'auto'|'start'|'center'|'end'} - */ scrollToAlignment = null, - /** - * @type {'auto'|'instant'|'smooth'} - */ scrollToBehaviour = 'instant', - - /** - * @type {number} - */ + overscanCount = 3, - /** - * @type {function} - * @param {object} object { start, end } - */ onListItemsUpdate = () => null, - /** - * @type {function} - * @param {object} object { offset, event } - */ onAfterScroll = () => null, - /** - * @type {snippet} - */ header = null, - /** - * @type {snippet} - */ footer = null, - /** - * @type {snippet} - * @param {object} item { index, style } - */ row = null } = $props(); diff --git a/src/lib/types/index.d.ts b/src/lib/types/index.d.ts new file mode 100644 index 0000000..d928baf --- /dev/null +++ b/src/lib/types/index.d.ts @@ -0,0 +1,177 @@ +/// +import { SvelteComponent } 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; + + /** + * @param object + */ + onListItemsUpdate?: (object: ItemsUpdatedParams) => any, + + /** + * @param object + */ + onAfterScroll?: (object: AfterScrollParams) => any +} + + +/** + * VirtualList children + * TODO: check type for snippets: https://svelte-5-preview.vercel.app/docs/snippets + */ +export interface VirtualListChildren { + /** + * Snippet for each item + */ + item: (object: VirtualListRowParams) => SvelteComponent; + + /** + * Snippet for the elements that should appear at the top of the list + */ + header?: () => SvelteComponent; + + /** + * Snippet for the elements that should appear at the bottom of the list + */ + footer?: () => SvelteComponent; +} + +export interface VirtualListRowParams { + /** + * Item index + */ + index: number, + + /** + * Item style, must be applied to the snippet (look above for example) + */ + style: string +} + + +export interface ItemsUpdatedParams { + /** + * Index of the first visible item + */ + start: number; + + /** + * Index of the last visible item + */ + end: number; +} + + +export interface AfterScrollParams { + /** + * The original scroll event + */ + event: Event; + + /** + * Either the value of `wrapper.scrollTop` or `wrapper.scrollLeft` + */ + offset: number; +} + + +/** + * VirtualList component + */ +export default class VirtualList extends SvelteComponent { +} \ No newline at end of file From 59bf7cd22d66a42863d0575180fee51370b07b60 Mon Sep 17 00:00:00 2001 From: Daminski Date: Thu, 18 Jan 2024 11:09:18 +0100 Subject: [PATCH 09/29] bump version from source repo --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7fba4c2..f4951c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte-tiny-virtual-list-tailwind", - "version": "0.0.1", + "version": "3.0.0", "scripts": { "dev": "vite dev", "build": "vite build && npm run package", From ae04e08f28d522353e7896d94d7256908de5c876 Mon Sep 17 00:00:00 2001 From: Daminski Date: Thu, 18 Jan 2024 14:16:16 +0100 Subject: [PATCH 10/29] expose npm package --- .gitignore | 1 + README.md | 2 +- package.json | 26 +++++++++++++++++++++----- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index d3085f4..ff5d0f4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ node_modules vite.config.js.timestamp-* vite.config.ts.timestamp-* package-lock.json +svelte-tiny-virtual-list-tailwind-* diff --git a/README.md b/README.md index 2ff6339..496b046 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

svelte-tiny-virtual-list

-

A tiny but mighty list virtualization library for Svelte 5 & Tailwind 💪

+

A tiny but mighty list virtualization library for Svelte 5 & Tailwind

AboutFeatures • diff --git a/package.json b/package.json index f4951c8..76692bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "svelte-tiny-virtual-list-tailwind", "version": "3.0.0", + "description": "A tiny but mighty list virtualization component for svelte 5 & Tailwind", "scripts": { "dev": "vite dev", "build": "vite build && npm run package", @@ -12,7 +13,7 @@ }, "exports": { ".": { - "types": "./dist/index.d.ts", + "types": "./dist/types/index.d.ts", "svelte": "./dist/index.js" } }, @@ -45,9 +46,24 @@ "vite": "^5.0.11" }, "svelte": "./dist/index.js", - "types": "./dist/index.d.ts", + "types": "./dist/types/index.d.ts", "type": "module", - "dependencies": { - "@sveltejs/adapter-static": "^3.0.1" - } + "keywords": [ + "svelte", + "virtual", + "list", + "scroll", + "component", + "plugin", + "svelte-components" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/daminski/svelte-tiny-virtual-list-tailwind.git" + }, + "bugs": { + "url": "https://github.com/daminski/svelte-tiny-virtual-list-tailwind/issues" + }, + "homepage": "https://github.com/daminski/svelte-tiny-virtual-list-tailwind" } From ddff05c083f749608557b798ae996fb1578fd420 Mon Sep 17 00:00:00 2001 From: Daminski Date: Thu, 18 Jan 2024 14:30:26 +0100 Subject: [PATCH 11/29] add installation steps to readme --- README.md | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 86 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 496b046..76d65cf 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -

svelte-tiny-virtual-list

+

svelte-tiny-virtual-list-tailwind

A tiny but mighty list virtualization library for Svelte 5 & Tailwind

AboutFeatures • - Requirements • + InstallationUsageExamplesLicense @@ -15,11 +15,6 @@ Instead of rendering all your data in a huge list, the virtual list component ju This is heavily inspired by [react-tiny-virtual-list](https://github.com/clauderic/react-tiny-virtual-list) and uses most of its code and functionality! This repo was forked from [skayo/svelte-tiny-virtual-list](https://github.com/skayo/svelte-tiny-virtual-list) -## Requirements - -- **Svelte 5** -- Tailwind - ### Features - **Tiny & dependency free** @@ -28,6 +23,90 @@ This repo was forked from [skayo/svelte-tiny-virtual-list](https://github.com/sk - **Supports fixed** or **variable** heights/widths - **Vertical** or **Horizontal** lists +## Installation + +### Requirements + +- **Svelte 5** +- Tailwind + +### Install svelte 5 + +```bash +npm create svelte@latest myapp +``` +Pick svelte 5 + +```bash +cd myapp +npm install +``` + +### Install Tailwind css + +```bash +npm install -D tailwindcss postcss autoprefixer +npx tailwindcss init -p +``` + +### Install svelte-tiny-virtual-list + +```bash +npm install svelte-tiny-virtual-list-tailwind +``` + +### Update svelte.config.js + +```js +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + kit: { + adapter: adapter() + }, + preprocess: vitePreprocess() +}; +export default config; +``` + +### Update tailwind.config.js + +```js +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + './src/**/*.{html,js,svelte,ts}', + './node_modules/svelte-tiny-virtual-list-tailwind/dist/**/*.{html,js,svelte,ts}' + ], + theme: { + extend: {}, + }, + plugins: [], +} +``` + +### Import Tailwind in app.css + +```css +@tailwind base; +@tailwind components; +@tailwind utilities; +``` + +### Import css in +layout.svelte + +```svelte + + + +``` + +### + ## Usage ```svelte From 1ff9c3aed84071279090294d3fde26f9264e02e1 Mon Sep 17 00:00:00 2001 From: Daminski Date: Thu, 18 Jan 2024 14:35:32 +0100 Subject: [PATCH 12/29] fix readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 76d65cf..6ab3bf2 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p ``` -### Install svelte-tiny-virtual-list +### Install svelte-tiny-virtual-list-tailwind ```bash npm install svelte-tiny-virtual-list-tailwind From bf74c9731ab4477df3f0d0922b5d0691be6d9385 Mon Sep 17 00:00:00 2001 From: Daminski Date: Thu, 18 Jan 2024 14:57:24 +0100 Subject: [PATCH 13/29] fix readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6ab3bf2..60ae299 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,9 @@ npx tailwindcss init -p ### Install svelte-tiny-virtual-list-tailwind +Clone repo locally and install ```bash -npm install svelte-tiny-virtual-list-tailwind +npm install path/to/package ``` ### Update svelte.config.js From 4748d4c272e61b2b26365d85c760d8d9112f6886 Mon Sep 17 00:00:00 2001 From: Daminski Date: Thu, 18 Jan 2024 14:59:46 +0100 Subject: [PATCH 14/29] fix readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 60ae299..4d5961a 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ This repo was forked from [skayo/svelte-tiny-virtual-list](https://github.com/sk ### Features -- **Tiny & dependency free** +- **Tiny** - **Render millions of items**, without breaking a sweat - **Scroll to index** or **set the initial scroll offset** - **Supports fixed** or **variable** heights/widths From 002da69e3a9dfb40e1a7fad45bcb54aea5af21aa Mon Sep 17 00:00:00 2001 From: Daminski Date: Wed, 24 Jan 2024 17:28:09 +0100 Subject: [PATCH 15/29] allow user to set classes manually --- package.json | 20 ++++++++++---------- src/lib/components/VirtualList.svelte | 10 +++++++--- src/lib/types/index.d.ts | 16 ++++++++++++++++ 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 76692bf..fa05774 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte-tiny-virtual-list-tailwind", - "version": "3.0.0", + "version": "3.0.1", "description": "A tiny but mighty list virtualization component for svelte 5 & Tailwind", "scripts": { "dev": "vite dev", @@ -23,27 +23,27 @@ "!dist/**/*.spec.*" ], "peerDependencies": { - "svelte": "^4.0.0" + "svelte": "^5.0.0-next.1" }, "devDependencies": { - "@sveltejs/adapter-auto": "^3.1.0", - "@sveltejs/kit": "^2.3.5", - "@sveltejs/package": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@sveltejs/adapter-auto": "^3.1.1", + "@sveltejs/kit": "^2.4.3", + "@sveltejs/package": "^2.2.6", + "@sveltejs/vite-plugin-svelte": "^3.0.1", "@types/eslint": "^8.56.2", "autoprefixer": "^10.4.17", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.36.0-next.4", "postcss": "^8.4.33", - "prettier": "^3.1.1", + "prettier": "^3.2.4", "prettier-plugin-svelte": "^3.1.2", "publint": "^0.2.7", "svelte": "^5.0.0-next.1", "tailwindcss": "^3.4.1", - "tslib": "^2.4.1", - "typescript": "^5.3.2", - "vite": "^5.0.11" + "tslib": "^2.6.2", + "typescript": "^5.3.3", + "vite": "^5.0.12" }, "svelte": "./dist/index.js", "types": "./dist/types/index.d.ts", diff --git a/src/lib/components/VirtualList.svelte b/src/lib/components/VirtualList.svelte index 28bc3e5..dea4654 100644 --- a/src/lib/components/VirtualList.svelte +++ b/src/lib/components/VirtualList.svelte @@ -45,7 +45,11 @@ footer = null, - row = null + row = null, + + dangerously_set_classes_container = "overflow-auto will-change-transform", + + dangerously_set_classes_inner_container = "relative flex w-full" } = $props(); const sizeAndPositionManager = new SizeAndPositionManager({ @@ -255,7 +259,7 @@

@@ -264,7 +268,7 @@ {/if}
{#each items as item (getKey ? getKey(item.index) : item.index)} diff --git a/src/lib/types/index.d.ts b/src/lib/types/index.d.ts index d928baf..4cced89 100644 --- a/src/lib/types/index.d.ts +++ b/src/lib/types/index.d.ts @@ -107,6 +107,22 @@ export interface VirtualListProps { * @param object */ onAfterScroll?: (object: AfterScrollParams) => any + + /** + * Classes for the list container + * + * @default "overflow-auto will-change-transform" + * + */ + dangerously_set_classes_container: string; + + /** + * Classes for the list inner container + * + * @default "relative flex w-full" + * + */ + dangerously_set_classes_inner_container: string; } From b90948ffba9daea60f5a7c9825d5e550cb3feddc Mon Sep 17 00:00:00 2001 From: Daminski Date: Wed, 31 Jan 2024 19:30:14 +0100 Subject: [PATCH 16/29] use derived state for container classes to reduce the chance of breaking the list; logical naming conventions --- package.json | 4 +-- src/lib/components/VirtualList.svelte | 46 +++++++++++++++------------ src/lib/types/index.d.ts | 4 +-- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index fa05774..6b54489 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,9 @@ }, "devDependencies": { "@sveltejs/adapter-auto": "^3.1.1", - "@sveltejs/kit": "^2.4.3", + "@sveltejs/kit": "^2.5.0", "@sveltejs/package": "^2.2.6", - "@sveltejs/vite-plugin-svelte": "^3.0.1", + "@sveltejs/vite-plugin-svelte": "^3.0.2", "@types/eslint": "^8.56.2", "autoprefixer": "^10.4.17", "eslint": "^8.56.0", diff --git a/src/lib/components/VirtualList.svelte b/src/lib/components/VirtualList.svelte index dea4654..728138b 100644 --- a/src/lib/components/VirtualList.svelte +++ b/src/lib/components/VirtualList.svelte @@ -47,9 +47,9 @@ row = null, - dangerously_set_classes_container = "overflow-auto will-change-transform", + dangerously_set_classes_container = "", - dangerously_set_classes_inner_container = "relative flex w-full" + dangerously_set_classes_inner_container = "" } = $props(); const sizeAndPositionManager = new SizeAndPositionManager({ @@ -58,7 +58,7 @@ estimatedItemSize: getEstimatedItemSize() }); - let wrapper = $state(null); + let container = $state(null); let items = $state.frozen([]); let curState = $state.frozen(new ListState(scrollOffset || (scrollToIndex != null && itemCount && getOffsetForIndex(scrollToIndex)))); @@ -77,8 +77,12 @@ ); let styleCache = {}; - let wrapperStyle = $state(""); - let innerStyle = $state(""); + + let containerStyle = $state(""); + let innerContainerStyle = $state(""); + + let cContainer = $derived(`overflow-auto ${dangerously_set_classes_container}`); + let cInnerContainer = $derived(`relative flex w-full ${dangerously_set_classes_inner_container}`); // Listen for updates to props $effect(() => { @@ -137,11 +141,11 @@ const totalSize = sizeAndPositionManager.getTotalSize(); if (scrollDirection === DIRECTION.VERTICAL) { - wrapperStyle = `height:${height}px;width:${width};`; - innerStyle = `flex-direction:column;height:${totalSize}px;`; + containerStyle = `height:${height}px;width:${width};`; + innerContainerStyle = `flex-direction:column;height:${totalSize}px;`; } else { - wrapperStyle = `height:${height};width:${width}px`; - innerStyle = `min-height:100%;width:${totalSize}px;`; + containerStyle = `height:${height};width:${width}px`; + innerContainerStyle = `min-height:100%;width:${totalSize}px;`; } const hasStickyIndices = stickyIndices != null && stickyIndices.length !== 0; @@ -177,13 +181,13 @@ function scrollTo(value) { - if ('scroll' in wrapper) - wrapper.scroll({ + if ('scroll' in container) + container.scroll({ [SCROLL_PROP[scrollDirection]]: value, behavior: scrollToBehaviour }); else - wrapper[SCROLL_PROP_LEGACY[scrollDirection]] = value; + container[SCROLL_PROP_LEGACY[scrollDirection]] = value; }; export function recomputeSizes(startIndex = 0) { @@ -205,9 +209,9 @@ }; function handleScroll(event) { - const offset = getWrapperOffset(); + const offset = getContainerOffset(); - if (offset < 0 || curState.offset === offset || event.target !== wrapper) + if (offset < 0 || curState.offset === offset || event.target !== container) return; curState = new ListState(offset, SCROLL_CHANGE_REASON.OBSERVED); @@ -218,8 +222,8 @@ }); }; - function getWrapperOffset() { - return wrapper[SCROLL_PROP_LEGACY[scrollDirection]]; + function getContainerOffset() { + return container[SCROLL_PROP_LEGACY[scrollDirection]]; }; function getEstimatedItemSize() { @@ -258,9 +262,9 @@
{#if header} @@ -268,8 +272,8 @@ {/if}
{#each items as item (getKey ? getKey(item.index) : item.index)} {@render row({ index: item.index, style: item.style })} diff --git a/src/lib/types/index.d.ts b/src/lib/types/index.d.ts index 4cced89..f85288d 100644 --- a/src/lib/types/index.d.ts +++ b/src/lib/types/index.d.ts @@ -111,7 +111,7 @@ export interface VirtualListProps { /** * Classes for the list container * - * @default "overflow-auto will-change-transform" + * @default "" * */ dangerously_set_classes_container: string; @@ -119,7 +119,7 @@ export interface VirtualListProps { /** * Classes for the list inner container * - * @default "relative flex w-full" + * @default "" * */ dangerously_set_classes_inner_container: string; From febba5d59b6cdb994513c97b3a1272b02639fc59 Mon Sep 17 00:00:00 2001 From: Daminski Date: Wed, 31 Jan 2024 19:30:50 +0100 Subject: [PATCH 17/29] bump to 3.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6b54489..fdd45a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte-tiny-virtual-list-tailwind", - "version": "3.0.1", + "version": "3.0.2", "description": "A tiny but mighty list virtualization component for svelte 5 & Tailwind", "scripts": { "dev": "vite dev", From a375701d13a023e1713850b2cfb57971fcf20a9d Mon Sep 17 00:00:00 2001 From: Daminski Date: Wed, 7 Feb 2024 23:30:08 +0100 Subject: [PATCH 18/29] add tailwind to peer deps; update deps --- package.json | 7 ++++--- src/lib/components/VirtualList.svelte | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index fdd45a5..5e8392a 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "!dist/**/*.spec.*" ], "peerDependencies": { - "svelte": "^5.0.0-next.1" + "svelte": "^5.0.0-next.1", + "tailwindcss": "^3.4.1" }, "devDependencies": { "@sveltejs/adapter-auto": "^3.1.1", @@ -35,8 +36,8 @@ "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.36.0-next.4", - "postcss": "^8.4.33", - "prettier": "^3.2.4", + "postcss": "^8.4.35", + "prettier": "^3.2.5", "prettier-plugin-svelte": "^3.1.2", "publint": "^0.2.7", "svelte": "^5.0.0-next.1", diff --git a/src/lib/components/VirtualList.svelte b/src/lib/components/VirtualList.svelte index 728138b..dfbd197 100644 --- a/src/lib/components/VirtualList.svelte +++ b/src/lib/components/VirtualList.svelte @@ -235,7 +235,8 @@ }; function getStyle(index, sticky) { - if (styleCache[index]) return styleCache[index]; + if (styleCache[index]) + return styleCache[index]; const { size, offset } = sizeAndPositionManager.getSizeAndPositionForIndex(index); From 838ebf23a9197930fb55efea7b59fc47e625ba8d Mon Sep 17 00:00:00 2001 From: Daminski Date: Wed, 7 Feb 2024 23:30:36 +0100 Subject: [PATCH 19/29] 3.0.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5e8392a..784081e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte-tiny-virtual-list-tailwind", - "version": "3.0.2", + "version": "3.0.3", "description": "A tiny but mighty list virtualization component for svelte 5 & Tailwind", "scripts": { "dev": "vite dev", From 0e54cd74619923cfd15b3bea86d921d3f0420ce0 Mon Sep 17 00:00:00 2001 From: Daminski Date: Mon, 18 Mar 2024 19:07:46 +0100 Subject: [PATCH 20/29] update deps --- package.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 784081e..a59f21e 100644 --- a/package.json +++ b/package.json @@ -28,23 +28,23 @@ }, "devDependencies": { "@sveltejs/adapter-auto": "^3.1.1", - "@sveltejs/kit": "^2.5.0", - "@sveltejs/package": "^2.2.6", + "@sveltejs/kit": "^2.5.4", + "@sveltejs/package": "^2.3.0", "@sveltejs/vite-plugin-svelte": "^3.0.2", - "@types/eslint": "^8.56.2", - "autoprefixer": "^10.4.17", - "eslint": "^8.56.0", + "@types/eslint": "^8.56.5", + "autoprefixer": "^10.4.18", + "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.36.0-next.4", - "postcss": "^8.4.35", + "postcss": "^8.4.36", "prettier": "^3.2.5", - "prettier-plugin-svelte": "^3.1.2", + "prettier-plugin-svelte": "^3.2.2", "publint": "^0.2.7", "svelte": "^5.0.0-next.1", "tailwindcss": "^3.4.1", "tslib": "^2.6.2", - "typescript": "^5.3.3", - "vite": "^5.0.12" + "typescript": "^5.4.2", + "vite": "^5.1.6" }, "svelte": "./dist/index.js", "types": "./dist/types/index.d.ts", From 74cd7c35e059445f9b0a047b1f6d55f4398d6b11 Mon Sep 17 00:00:00 2001 From: Daminski Date: Sun, 24 Mar 2024 21:43:17 +0100 Subject: [PATCH 21/29] update deps --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a59f21e..890652b 100644 --- a/package.json +++ b/package.json @@ -31,20 +31,20 @@ "@sveltejs/kit": "^2.5.4", "@sveltejs/package": "^2.3.0", "@sveltejs/vite-plugin-svelte": "^3.0.2", - "@types/eslint": "^8.56.5", - "autoprefixer": "^10.4.18", + "@types/eslint": "^8.56.6", + "autoprefixer": "^10.4.19", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.36.0-next.4", - "postcss": "^8.4.36", + "postcss": "^8.4.38", "prettier": "^3.2.5", "prettier-plugin-svelte": "^3.2.2", "publint": "^0.2.7", "svelte": "^5.0.0-next.1", "tailwindcss": "^3.4.1", "tslib": "^2.6.2", - "typescript": "^5.4.2", - "vite": "^5.1.6" + "typescript": "^5.4.3", + "vite": "^5.2.6" }, "svelte": "./dist/index.js", "types": "./dist/types/index.d.ts", From ef9963ab5e81110155daada476d06e6baa71cb9f Mon Sep 17 00:00:00 2001 From: Daminski Date: Sun, 24 Mar 2024 23:05:38 +0100 Subject: [PATCH 22/29] add horizontal scrolling example; add width as reactive value in example; fix scroll behaviour; svelte 5 passive event declaration: format code --- src/lib/components/VirtualList.svelte | 14 ++- src/lib/utils/SizeAndPositionManager.js | 113 ++++++++++-------------- src/routes/+page.svelte | 66 ++++++++++++-- 3 files changed, 119 insertions(+), 74 deletions(-) diff --git a/src/lib/components/VirtualList.svelte b/src/lib/components/VirtualList.svelte index dfbd197..78b065c 100644 --- a/src/lib/components/VirtualList.svelte +++ b/src/lib/components/VirtualList.svelte @@ -1,4 +1,5 @@
{#if header} {@render header()} diff --git a/src/lib/utils/SizeAndPositionManager.js b/src/lib/utils/SizeAndPositionManager.js index fc70b5b..f2ed8d9 100644 --- a/src/lib/utils/SizeAndPositionManager.js +++ b/src/lib/utils/SizeAndPositionManager.js @@ -74,45 +74,39 @@ export default class SizeAndPositionManager { this.checkForMismatchItemSizeAndItemCount(); - if (!this.justInTime) this.computeTotalSizeAndPositionData(); - } + if (!this.justInTime) + this.computeTotalSizeAndPositionData(); + }; get justInTime() { return typeof this.itemSize === 'function'; - } + }; /** * @param {Options} options */ updateConfig({ itemSize, itemCount, estimatedItemSize }) { - if (itemCount != null) { + if (itemCount != null) this.itemCount = itemCount; - } - if (estimatedItemSize != null) { + if (estimatedItemSize != null) this.estimatedItemSize = estimatedItemSize; - } - if (itemSize != null) { + if (itemSize != null) this.itemSize = itemSize; - } this.checkForMismatchItemSizeAndItemCount(); - if (this.justInTime && this.totalSize != null) { + if (this.justInTime && this.totalSize != null) this.totalSize = undefined; - } else { + else this.computeTotalSizeAndPositionData(); - } - } + }; checkForMismatchItemSizeAndItemCount() { - if (Array.isArray(this.itemSize) && this.itemSize.length < this.itemCount) { - throw Error( - `When itemSize is an array, itemSize.length can't be smaller than itemCount`, - ); - } - } + if (Array.isArray(this.itemSize) && this.itemSize.length < this.itemCount) + throw Error(`When itemSize is an array, itemSize.length can't be smaller than itemCount`); + }; /** * @param {number} index @@ -120,12 +114,11 @@ export default class SizeAndPositionManager { getSize(index) { const { itemSize } = this; - if (typeof itemSize === 'function') { + if (typeof itemSize === 'function') return itemSize(index); - } return Array.isArray(itemSize) ? itemSize[index] : itemSize; - } + }; /** * Compute the totalSize and itemSizeAndPositionData at the start, @@ -145,11 +138,11 @@ export default class SizeAndPositionManager { } this.totalSize = totalSize; - } + }; getLastMeasuredIndex() { return this.lastMeasuredIndex; - } + }; /** @@ -158,16 +151,13 @@ export default class SizeAndPositionManager { * @param {number} index */ getSizeAndPositionForIndex(index) { - if (index < 0 || index >= this.itemCount) { - throw Error( - `Requested index ${index} is outside of range 0..${this.itemCount}`, - ); - } + if (index < 0 || index >= this.itemCount) + throw Error(`Requested index ${index} is outside of range 0..${this.itemCount}`); return this.justInTime ? this.getJustInTimeSizeAndPositionForIndex(index) : this.itemSizeAndPositionData[index]; - } + }; /** * This is used when itemSize is a function. @@ -184,9 +174,8 @@ export default class SizeAndPositionManager { for (let i = this.lastMeasuredIndex + 1; i <= index; i++) { const size = this.getSize(i); - if (size == null || isNaN(size)) { + if (size == null || isNaN(size)) throw Error(`Invalid size returned for index ${i} of value ${size}`); - } this.itemSizeAndPositionData[i] = { offset, @@ -200,13 +189,13 @@ export default class SizeAndPositionManager { } return this.itemSizeAndPositionData[index]; - } + }; getSizeAndPositionOfLastMeasuredItem() { return this.lastMeasuredIndex >= 0 ? this.itemSizeAndPositionData[this.lastMeasuredIndex] : { offset: 0, size: 0 }; - } + }; /** * Total size of all items being measured. @@ -215,7 +204,8 @@ export default class SizeAndPositionManager { */ getTotalSize() { // Return the pre computed totalSize when itemSize is number or array. - if (this.totalSize) return this.totalSize; + if (this.totalSize) + return this.totalSize; /** * When itemSize is a function, @@ -229,7 +219,7 @@ export default class SizeAndPositionManager { lastMeasuredSizeAndPosition.size + (this.itemCount - this.lastMeasuredIndex - 1) * this.estimatedItemSize ); - } + }; /** * Determines a new offset that ensures a certain item is visible, given the alignment. @@ -241,9 +231,8 @@ export default class SizeAndPositionManager { * @return {number} Offset to use to ensure the specified item is visible */ getUpdatedOffsetForIndex({ align = ALIGNMENT.START, containerSize, currentOffset, targetIndex }) { - if (containerSize <= 0) { + if (containerSize <= 0) return 0; - } const datum = this.getSizeAndPositionForIndex(targetIndex); const maxOffset = datum.offset; @@ -263,12 +252,13 @@ export default class SizeAndPositionManager { break; default: idealOffset = Math.max(minOffset, Math.min(maxOffset, currentOffset)); + break; } const totalSize = this.getTotalSize(); return Math.max(0, Math.min(totalSize - containerSize, idealOffset)); - } + }; /** * @param {number} containerSize @@ -279,16 +269,14 @@ export default class SizeAndPositionManager { getVisibleRange({ containerSize = 0, offset, overscanCount }) { const totalSize = this.getTotalSize(); - if (totalSize === 0) { + if (totalSize === 0) return {}; - } const maxOffset = offset + containerSize; let start = this.findNearestItem(offset); - if (start === undefined) { + if (start === undefined) throw Error(`Invalid offset ${offset} specified`); - } const datum = this.getSizeAndPositionForIndex(start); offset = datum.offset + datum.size; @@ -309,7 +297,7 @@ export default class SizeAndPositionManager { start, stop, }; - } + }; /** * Clear all cached values for items after the specified index. @@ -320,7 +308,7 @@ export default class SizeAndPositionManager { */ resetItem(index) { this.lastMeasuredIndex = Math.min(this.lastMeasuredIndex, index - 1); - } + }; /** * Searches for the item (index) nearest the specified offset. @@ -331,9 +319,8 @@ export default class SizeAndPositionManager { * @param {number} offset */ findNearestItem(offset) { - if (isNaN(offset)) { + if (isNaN(offset)) throw Error(`Invalid offset ${offset} specified`); - } // Our search algorithms find the nearest match at or below the specified offset. // So make sure the offset is at least 0 or no match will be found. @@ -342,23 +329,23 @@ export default class SizeAndPositionManager { const lastMeasuredSizeAndPosition = this.getSizeAndPositionOfLastMeasuredItem(); const lastMeasuredIndex = Math.max(0, this.lastMeasuredIndex); - if (lastMeasuredSizeAndPosition.offset >= offset) { - // If we've already measured items within this range just use a binary search as it's faster. + // If we've already measured items within this range just use a binary search as it's faster. + if (lastMeasuredSizeAndPosition.offset >= offset) return this.binarySearch({ high: lastMeasuredIndex, low: 0, offset, }); - } else { - // If we haven't yet measured this high, fallback to an exponential search with an inner binary search. - // The exponential search avoids pre-computing sizes for the full set of items as a binary search would. - // The overall complexity for this approach is O(log n). + + // If we haven't yet measured this high, fallback to an exponential search with an inner binary search. + // The exponential search avoids pre-computing sizes for the full set of items as a binary search would. + // The overall complexity for this approach is O(log n). + else return this.exponentialSearch({ index: lastMeasuredIndex, offset, }); - } - } + }; /** * @private @@ -374,21 +361,19 @@ export default class SizeAndPositionManager { middle = low + Math.floor((high - low) / 2); currentOffset = this.getSizeAndPositionForIndex(middle).offset; - if (currentOffset === offset) { + if (currentOffset === offset) return middle; - } else if (currentOffset < offset) { + else if (currentOffset < offset) low = middle + 1; - } else if (currentOffset > offset) { + else if (currentOffset > offset) high = middle - 1; - } } - if (low > 0) { + if (low > 0) return low - 1; - } return 0; - } + }; /** * @private @@ -411,5 +396,5 @@ export default class SizeAndPositionManager { low: Math.floor(index / 2), offset, }); - } -} \ No newline at end of file + }; +}; \ No newline at end of file diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 5083a65..f3f23f2 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -2,30 +2,44 @@ import VirtualList from "$lib/components/VirtualList.svelte"; const LIST_LENGTH = 2000; - const LIST_HEIGHT = 600; - const ITEM_HEIGHT = 50; + const LIST_HEIGHT = 400; + const ITEM_SIZE = 100; + const LIST_WIDTH = 800; class Row { + constructor (index = 0) { + this.index = index; this.title = "#" + (index + 1); this.content = (Math.floor(Math.random() * 999999999999)).toString(36); - } + + }; + }; const generateFakeData = (ll = 1) => { const dummArray = new Array(ll); - for(let i = 0; i < ll; i++) (dummArray[i] = new Row(i)); + + for(let i = 0; i < ll; i++) + dummArray[i] = new Row(i); + return dummArray; }; - let virtualList = $state(); + let innerWidth = $state(1200); + + let virtualListVertical = $state(); + + let virtualListHorizontal = $state(); let listLength = $state(LIST_LENGTH); let listHeight = $state(LIST_HEIGHT); - let listItemSize = $state(ITEM_HEIGHT); + let listWidth = $state(LIST_WIDTH); + + let listItemSize = $state(ITEM_SIZE); let fakeData = $derived(generateFakeData(listLength)); @@ -37,6 +51,11 @@ e.preventDefault(); }; + const recomputeSizes = () => { + virtualListVertical.recomputeSizes(); + virtualListHorizontal.recomputeSizes(); + }; + const randomBgs = [ "bg-amber-100", "bg-zinc-100", @@ -53,7 +72,10 @@ ]; + +
+

Sveltekit-tiny-virtual-list-tailwind

@@ -68,6 +90,10 @@ Height: {listHeight} +
- +
@@ -90,14 +116,17 @@
+

Vertical

+
{#snippet header()}
Header
@@ -112,4 +141,25 @@ {/snippet}
+ +

Horizontal

+ +
+ + {#snippet row({ index, style })} +
+ {fakeData[index].content}, Row: #{index} +
+ {/snippet} +
+
From 8609a3cd08b2b4a81d596a48d551dd0654ba186d Mon Sep 17 00:00:00 2001 From: Daminski Date: Sun, 24 Mar 2024 23:06:14 +0100 Subject: [PATCH 23/29] 3.0.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 890652b..40b03d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte-tiny-virtual-list-tailwind", - "version": "3.0.3", + "version": "3.0.4", "description": "A tiny but mighty list virtualization component for svelte 5 & Tailwind", "scripts": { "dev": "vite dev", From 166b4527eb4def0108e3e858beffba6ca149e1c2 Mon Sep 17 00:00:00 2001 From: Daminski Date: Mon, 25 Mar 2024 15:46:59 +0100 Subject: [PATCH 24/29] add sensible defaults to VirtualList props; fix types; declare private methods in SizeAndPositionManager --- src/lib/components/VirtualList.svelte | 29 ++-- src/lib/types/index.d.ts | 34 ++++- src/lib/utils/ListProps.js | 28 ++-- src/lib/utils/SizeAndPositionManager.js | 181 ++++++++++++++---------- src/routes/+page.svelte | 28 ++-- 5 files changed, 179 insertions(+), 121 deletions(-) diff --git a/src/lib/components/VirtualList.svelte b/src/lib/components/VirtualList.svelte index 78b065c..979344b 100644 --- a/src/lib/components/VirtualList.svelte +++ b/src/lib/components/VirtualList.svelte @@ -16,23 +16,23 @@ width = '100%', - itemCount, + itemCount = 0, - itemSize, + itemSize = 0, - estimatedItemSize = null, + estimatedItemSize = 0, - stickyIndices = null, + stickyIndices = [], getKey = null, scrollDirection = DIRECTION.VERTICAL, - scrollOffset = null, + scrollOffset = 0, - scrollToIndex = null, + scrollToIndex = -1, - scrollToAlignment = null, + scrollToAlignment = 'start', scrollToBehaviour = 'instant', @@ -48,9 +48,9 @@ row = null, - dangerously_set_classes_container = "", + dangerously_set_classes_container = '', - dangerously_set_classes_inner_container = "" + dangerously_set_classes_inner_container = '' } = $props(); const sizeAndPositionManager = new SizeAndPositionManager({ @@ -62,7 +62,7 @@ let container = $state(null); let items = $state.frozen([]); - let curState = $state.frozen(new ListState(scrollOffset || (scrollToIndex != null && itemCount && getOffsetForIndex(scrollToIndex)))); + let curState = $state.frozen(new ListState(scrollOffset || (scrollToIndex !== -1 && itemCount && getOffsetForIndex(scrollToIndex)))); let prevState = new ListState(); let prevProps = new ListProps( @@ -102,10 +102,10 @@ if (scrollOffsetHasChanged) { curState = new ListState(scrollOffset); - if (typeof scrollToIndex === 'number') + if (scrollToIndex >= 0) scrollTo(scrollOffset); - } else if (typeof scrollToIndex === 'number' && (scrollPropsHaveChanged || itemPropsHaveChanged)) { + } else if (scrollToIndex >= 0 && (scrollPropsHaveChanged || itemPropsHaveChanged)) { const offsetForIndex = getOffsetForIndex( scrollToIndex, scrollToAlignment, @@ -149,8 +149,7 @@ innerContainerStyle = `min-height:100%;width:${totalSize}px;`; } - const hasStickyIndices = stickyIndices != null && stickyIndices.length !== 0; - if (hasStickyIndices) { + if (stickyIndices.length) { for (let i = 0; i < stickyIndices.length; i++) { const index = stickyIndices[i]; updatedItems.push({ @@ -162,7 +161,7 @@ if (start !== undefined && stop !== undefined) { for (let index = start; index <= stop; index++) { - if (hasStickyIndices && stickyIndices.includes(index)) + if (stickyIndices.length && stickyIndices.includes(index)) continue; updatedItems.push({ diff --git a/src/lib/types/index.d.ts b/src/lib/types/index.d.ts index f85288d..3a282bd 100644 --- a/src/lib/types/index.d.ts +++ b/src/lib/types/index.d.ts @@ -22,11 +22,15 @@ export interface VirtualListProps { /** * Height of List. This property will determine the number of rendered items when scrollDirection is `'vertical'`. + * + * @default '100%' */ height: number | string; /** * The number of items you want to render + * + * @default 0 */ itemCount: number; @@ -34,11 +38,13 @@ export interface VirtualListProps { * 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` + * + * @default 0 */ itemSize: ItemSize; /** - * Whether the list should scroll vertically or horizontally. One of `'vertical'` (default) or `'horizontal'`. + * Whether the list should scroll vertically or horizontally. One of `'vertical'` or `'horizontal'`. * * @default 'vertical' */ @@ -46,11 +52,15 @@ export interface VirtualListProps { /** * Can be used to control the scroll offset; Also useful for setting an initial scroll offset + * + * @default 0 */ scrollOffset?: number; /** * Item index to scroll to (by forcefully scrolling if necessary) + * + * @default -1 */ scrollToIndex?: number; @@ -60,17 +70,23 @@ export interface VirtualListProps { * 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. + * + * @default 'start' */ scrollToAlignment?: Alignment; /** * Used in combination with `scrollToIndex`, this prop controls the behaviour of the scrolling. - * One of: `'auto'`, `'smooth'` or `'instant'` (default). + * One of: `'auto'`, `'smooth'` or `'instant'`. + * + * @default 'instant' */ scrollToBehaviour?: ScrollBehaviour; /** * An array of indexes (eg. `[0, 10, 25, 30]`) to make certain items in the list sticky (`position: sticky`) + * + * @default [] */ stickyIndices?: number[]; @@ -85,6 +101,8 @@ export interface VirtualListProps { /** * 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. + * + * @default 0 */ estimatedItemSize?: number; @@ -92,6 +110,8 @@ export interface VirtualListProps { * 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. + * + * @default null * * @param index - The index of the item. * @return - Anything that uniquely identifies the item. @@ -99,11 +119,15 @@ export interface VirtualListProps { getKey?: (index: number) => any; /** + * @default function + * * @param object */ onListItemsUpdate?: (object: ItemsUpdatedParams) => any, /** + * @default function + * * @param object */ onAfterScroll?: (object: AfterScrollParams) => any @@ -111,16 +135,14 @@ export interface VirtualListProps { /** * Classes for the list container * - * @default "" - * + * @default ''' */ dangerously_set_classes_container: string; /** * Classes for the list inner container * - * @default "" - * + * @default ''' */ dangerously_set_classes_inner_container: string; } diff --git a/src/lib/utils/ListProps.js b/src/lib/utils/ListProps.js index 981c1b0..8e1f585 100644 --- a/src/lib/utils/ListProps.js +++ b/src/lib/utils/ListProps.js @@ -6,15 +6,15 @@ export class ListProps { /** * Constructor - * @param {number|null|undefined} scrollToIndex - * @param {string|null|undefined} scrollToAlignment - * @param {number|null|undefined} scrollOffset + * @param {number} scrollToIndex + * @param {'start' | 'center' | 'end' | 'auto'} scrollToAlignment + * @param {number} scrollOffset * @param {number} itemCount - * @param {number} itemSize + * @param {number | number[] | function} itemSize * @param {number} estimatedItemSize * @param {number} height * @param {string} width - * @param {number[]|null|undefined} stickyIndices + * @param {number[]} stickyIndices */ constructor (scrollToIndex = null, scrollToAlignment = null, scrollOffset = null, itemCount = 0, itemSize = 0, estimatedItemSize = 50, height = 600, width = "100%", stickyIndices = null) { this.scrollToIndex = scrollToIndex; @@ -30,15 +30,15 @@ export class ListProps { /** * Check if props have changed and update current object - * @param {number|null|undefined} scrollOffset - * @param {number|null|undefined} scrollToIndex - * @param {string|null|undefined} scrollToAlignment + * @param {number} scrollOffset + * @param {number} scrollToIndex + * @param {'start' | 'center' | 'end' | 'auto'} scrollToAlignment * @param {number} itemCount - * @param {number} itemSize + * @param {number | number[] | function} itemSize * @param {number} estimatedItemSize * @param {number} height * @param {string} width - * @param {number[]|null|undefined} stickyIndices + * @param {number[]} stickyIndices * @returns {Object} */ havePropsChanged (scrollOffset, scrollToIndex, scrollToAlignment, itemCount, itemSize, estimatedItemSize, height, width, stickyIndices) { @@ -52,7 +52,7 @@ export class ListProps { /** * Check if scrollOffset has changed and update - * @param {number|null|undefined} scrollOffset + * @param {number} scrollOffset * @returns {boolean} */ _hasScrollOffsetChanged (scrollOffset) { @@ -66,7 +66,7 @@ export class ListProps { /** * Check if scroll props have changed and update - * @param {number|null|undefined} scrollToIndex + * @param {number} scrollToIndex * @param {string} scrollToAlignment * @returns */ @@ -86,7 +86,7 @@ export class ListProps { /** * Check if item props have changed and update * @param {number} itemCount - * @param {number} itemSize + * @param {number | number[] | function} itemSize * @param {number} estimatedItemSize * @returns {boolean} */ @@ -111,7 +111,7 @@ export class ListProps { * Check if list props have changed and update * @param {number} height * @param {string} width - * @param {number[]|null|undefined} stickyIndices + * @param {number[]} stickyIndices * @returns {boolean} */ _haveListPropsChanged (height, width, stickyIndices) { diff --git a/src/lib/utils/SizeAndPositionManager.js b/src/lib/utils/SizeAndPositionManager.js index f2ed8d9..4c56604 100644 --- a/src/lib/utils/SizeAndPositionManager.js +++ b/src/lib/utils/SizeAndPositionManager.js @@ -35,76 +35,103 @@ import { ALIGNMENT } from './constants'; export default class SizeAndPositionManager { /** - * @param {Options} options + * @private + * @type {ItemSize} */ - constructor({ itemSize, itemCount, estimatedItemSize }) { - /** - * @private - * @type {ItemSize} - */ - this.itemSize = itemSize; + #itemSize = 0; - /** - * @private - * @type {number} - */ - this.itemCount = itemCount; + /** + * @private + * @type {number} + */ + #itemCount = 0; - /** - * @private - * @type {number} - */ - this.estimatedItemSize = estimatedItemSize; + /** + * @private + * @type {number} + */ + #estimatedItemSize = 0; - /** - * Cache of size and position data for items, mapped by item index. - * - * @private - * @type {SizeAndPositionData} - */ - this.itemSizeAndPositionData = {}; + /** + * Cache of size and position data for items, mapped by item index. + * + * @private + * @type {SizeAndPositionData} + */ + #itemSizeAndPositionData = {}; - /** - * Measurements for items up to this index can be trusted; items afterward should be estimated. - * - * @private - * @type {number} - */ - this.lastMeasuredIndex = -1; + /** + * Measurements for items up to this index can be trusted; items afterward should be estimated. + * + * @private + * @type {number} + */ + #lastMeasuredIndex = -1; + + /** + * Total size of items + * + * @private + * @type {number} + */ + #totalSize = 0; + + get #justInTime() { + return typeof this.#itemSize === 'function'; + }; + + /** + * @param {Options} options + */ + constructor({ itemSize, itemCount, estimatedItemSize }) { + this.#itemSize = itemSize; + + this.#itemCount = itemCount; + + this.#estimatedItemSize = estimatedItemSize; this.checkForMismatchItemSizeAndItemCount(); - if (!this.justInTime) + if (!this.#justInTime) this.computeTotalSizeAndPositionData(); - }; - get justInTime() { - return typeof this.itemSize === 'function'; + /** Bind public methods */ + this.updateConfig = this.updateConfig.bind(this); + this.checkForMismatchItemSizeAndItemCount = this.checkForMismatchItemSizeAndItemCount.bind(this); + this.getSize = this.getSize.bind(this); + this.computeTotalSizeAndPositionData = this.computeTotalSizeAndPositionData.bind(this); + this.getLastMeasuredIndex = this.getLastMeasuredIndex.bind(this); + this.getSizeAndPositionForIndex = this.getSizeAndPositionForIndex.bind(this); + this.getJustInTimeSizeAndPositionForIndex = this.getJustInTimeSizeAndPositionForIndex.bind(this); + this.getSizeAndPositionOfLastMeasuredItem = this.getSizeAndPositionOfLastMeasuredItem.bind(this); + this.getTotalSize = this.getTotalSize.bind(this); + this.getUpdatedOffsetForIndex = this.getUpdatedOffsetForIndex.bind(this); + this.getVisibleRange = this.getVisibleRange.bind(this); + this.resetItem = this.resetItem.bind(this); + this.findNearestItem = this.findNearestItem.bind(this); + }; /** * @param {Options} options */ updateConfig({ itemSize, itemCount, estimatedItemSize }) { - if (itemCount != null) - this.itemCount = itemCount; + this.#itemCount = itemCount; - if (estimatedItemSize != null) - this.estimatedItemSize = estimatedItemSize; + this.#estimatedItemSize = estimatedItemSize; - if (itemSize != null) - this.itemSize = itemSize; + this.#itemSize = itemSize; this.checkForMismatchItemSizeAndItemCount(); - if (this.justInTime && this.totalSize != null) - this.totalSize = undefined; + if (this.#justInTime && this.#totalSize) + this.#totalSize = 0; else this.computeTotalSizeAndPositionData(); }; checkForMismatchItemSizeAndItemCount() { - if (Array.isArray(this.itemSize) && this.itemSize.length < this.itemCount) + if (Array.isArray(this.#itemSize) && this.#itemSize.length < this.#itemCount) throw Error(`When itemSize is an array, itemSize.length can't be smaller than itemCount`); }; @@ -112,12 +139,11 @@ export default class SizeAndPositionManager { * @param {number} index */ getSize(index) { - const { itemSize } = this; - if (typeof itemSize === 'function') - return itemSize(index); + if (this.#justInTime) + return this.#itemSize(index); - return Array.isArray(itemSize) ? itemSize[index] : itemSize; + return Array.isArray(this.#itemSize) ? this.#itemSize[index] : this.#itemSize; }; /** @@ -126,37 +152,36 @@ export default class SizeAndPositionManager { */ computeTotalSizeAndPositionData() { let totalSize = 0; - for (let i = 0; i < this.itemCount; i++) { + for (let i = 0; i < this.#itemCount; i++) { const size = this.getSize(i); const offset = totalSize; totalSize += size; - this.itemSizeAndPositionData[i] = { + this.#itemSizeAndPositionData[i] = { offset, size, }; } - this.totalSize = totalSize; + this.#totalSize = totalSize; }; getLastMeasuredIndex() { - return this.lastMeasuredIndex; + return this.#lastMeasuredIndex; }; - /** * This method returns the size and position for the item at the specified index. * * @param {number} index */ getSizeAndPositionForIndex(index) { - if (index < 0 || index >= this.itemCount) - throw Error(`Requested index ${index} is outside of range 0..${this.itemCount}`); + if (index < 0 || index >= this.#itemCount) + throw Error(`Requested index ${index} is outside of range 0..${this.#itemCount}`); - return this.justInTime + return this.#justInTime ? this.getJustInTimeSizeAndPositionForIndex(index) - : this.itemSizeAndPositionData[index]; + : this.#itemSizeAndPositionData[index]; }; /** @@ -166,18 +191,18 @@ export default class SizeAndPositionManager { * @param {number} index */ getJustInTimeSizeAndPositionForIndex(index) { - if (index > this.lastMeasuredIndex) { + if (index > this.#lastMeasuredIndex) { const lastMeasuredSizeAndPosition = this.getSizeAndPositionOfLastMeasuredItem(); let offset = lastMeasuredSizeAndPosition.offset + lastMeasuredSizeAndPosition.size; - for (let i = this.lastMeasuredIndex + 1; i <= index; i++) { + for (let i = this.#lastMeasuredIndex + 1; i <= index; i++) { const size = this.getSize(i); if (size == null || isNaN(size)) throw Error(`Invalid size returned for index ${i} of value ${size}`); - this.itemSizeAndPositionData[i] = { + this.#itemSizeAndPositionData[i] = { offset, size, }; @@ -185,15 +210,15 @@ export default class SizeAndPositionManager { offset += size; } - this.lastMeasuredIndex = index; + this.#lastMeasuredIndex = index; } - return this.itemSizeAndPositionData[index]; + return this.#itemSizeAndPositionData[index]; }; getSizeAndPositionOfLastMeasuredItem() { - return this.lastMeasuredIndex >= 0 - ? this.itemSizeAndPositionData[this.lastMeasuredIndex] + return this.#lastMeasuredIndex >= 0 + ? this.#itemSizeAndPositionData[this.#lastMeasuredIndex] : { offset: 0, size: 0 }; }; @@ -204,8 +229,8 @@ export default class SizeAndPositionManager { */ getTotalSize() { // Return the pre computed totalSize when itemSize is number or array. - if (this.totalSize) - return this.totalSize; + if (this.#totalSize) + return this.#totalSize; /** * When itemSize is a function, @@ -217,7 +242,7 @@ export default class SizeAndPositionManager { return ( lastMeasuredSizeAndPosition.offset + lastMeasuredSizeAndPosition.size + - (this.itemCount - this.lastMeasuredIndex - 1) * this.estimatedItemSize + (this.#itemCount - this.#lastMeasuredIndex - 1) * this.#estimatedItemSize ); }; @@ -283,14 +308,14 @@ export default class SizeAndPositionManager { let stop = start; - while (offset < maxOffset && stop < this.itemCount - 1) { + while (offset < maxOffset && stop < this.#itemCount - 1) { stop++; offset += this.getSizeAndPositionForIndex(stop).size; } if (overscanCount) { start = Math.max(0, start - overscanCount); - stop = Math.min(stop + overscanCount, this.itemCount - 1); + stop = Math.min(stop + overscanCount, this.#itemCount - 1); } return { @@ -307,7 +332,7 @@ export default class SizeAndPositionManager { * @param {number} index */ resetItem(index) { - this.lastMeasuredIndex = Math.min(this.lastMeasuredIndex, index - 1); + this.#lastMeasuredIndex = Math.min(this.#lastMeasuredIndex, index - 1); }; /** @@ -327,11 +352,11 @@ export default class SizeAndPositionManager { offset = Math.max(0, offset); const lastMeasuredSizeAndPosition = this.getSizeAndPositionOfLastMeasuredItem(); - const lastMeasuredIndex = Math.max(0, this.lastMeasuredIndex); + const lastMeasuredIndex = Math.max(0, this.#lastMeasuredIndex); // If we've already measured items within this range just use a binary search as it's faster. if (lastMeasuredSizeAndPosition.offset >= offset) - return this.binarySearch({ + return this.#binarySearch({ high: lastMeasuredIndex, low: 0, offset, @@ -341,7 +366,7 @@ export default class SizeAndPositionManager { // The exponential search avoids pre-computing sizes for the full set of items as a binary search would. // The overall complexity for this approach is O(log n). else - return this.exponentialSearch({ + return this.#exponentialSearch({ index: lastMeasuredIndex, offset, }); @@ -353,7 +378,7 @@ export default class SizeAndPositionManager { * @param {number} high * @param {number} offset */ - binarySearch({ low, high, offset }) { + #binarySearch({ low, high, offset }) { let middle = 0; let currentOffset = 0; @@ -380,19 +405,19 @@ export default class SizeAndPositionManager { * @param {number} index * @param {number} offset */ - exponentialSearch({ index, offset }) { + #exponentialSearch({ index, offset }) { let interval = 1; while ( - index < this.itemCount && + index < this.#itemCount && this.getSizeAndPositionForIndex(index).offset < offset ) { index += interval; interval *= 2; } - return this.binarySearch({ - high: Math.min(index, this.itemCount - 1), + return this.#binarySearch({ + high: Math.min(index, this.#itemCount - 1), low: Math.floor(index / 2), offset, }); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index f3f23f2..86d6e18 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,7 +1,7 @@ @@ -94,23 +96,33 @@ Width: {listWidth} - + {#if typeof listItemSize === "number"} + + {/if} +
+ itemSize +
+ + + +
+
- +
- +
From 6fdb235cddbf5426eed5dafe299366cc81a90b10 Mon Sep 17 00:00:00 2001 From: Daminski Date: Mon, 25 Mar 2024 15:47:30 +0100 Subject: [PATCH 25/29] 3.0.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 40b03d3..1aa9bd9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte-tiny-virtual-list-tailwind", - "version": "3.0.4", + "version": "3.0.5", "description": "A tiny but mighty list virtualization component for svelte 5 & Tailwind", "scripts": { "dev": "vite dev", From 4e35a8b9b3a4fbb3fdc69acedb73803daef2f380 Mon Sep 17 00:00:00 2001 From: Daminski Date: Mon, 25 Mar 2024 15:50:18 +0100 Subject: [PATCH 26/29] typos --- src/lib/types/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/types/index.d.ts b/src/lib/types/index.d.ts index 3a282bd..b8a2efc 100644 --- a/src/lib/types/index.d.ts +++ b/src/lib/types/index.d.ts @@ -135,14 +135,14 @@ export interface VirtualListProps { /** * Classes for the list container * - * @default ''' + * @default '' */ dangerously_set_classes_container: string; /** * Classes for the list inner container * - * @default ''' + * @default '' */ dangerously_set_classes_inner_container: string; } From 7d0cca92a6e297ac10c3eebdff6e3e1a6256cee9 Mon Sep 17 00:00:00 2001 From: Daminski Date: Thu, 4 Apr 2024 08:46:23 +0200 Subject: [PATCH 27/29] update deps --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 1aa9bd9..29466b7 100644 --- a/package.json +++ b/package.json @@ -27,11 +27,11 @@ "tailwindcss": "^3.4.1" }, "devDependencies": { - "@sveltejs/adapter-auto": "^3.1.1", - "@sveltejs/kit": "^2.5.4", + "@sveltejs/adapter-auto": "^3.2.0", + "@sveltejs/kit": "^2.5.5", "@sveltejs/package": "^2.3.0", "@sveltejs/vite-plugin-svelte": "^3.0.2", - "@types/eslint": "^8.56.6", + "@types/eslint": "^8.56.7", "autoprefixer": "^10.4.19", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", @@ -41,10 +41,10 @@ "prettier-plugin-svelte": "^3.2.2", "publint": "^0.2.7", "svelte": "^5.0.0-next.1", - "tailwindcss": "^3.4.1", + "tailwindcss": "^3.4.3", "tslib": "^2.6.2", "typescript": "^5.4.3", - "vite": "^5.2.6" + "vite": "^5.2.8" }, "svelte": "./dist/index.js", "types": "./dist/types/index.d.ts", From 4902ac807b2c10ef6c9a048e4668273dd37328c3 Mon Sep 17 00:00:00 2001 From: Daminski Date: Thu, 4 Apr 2024 09:10:31 +0200 Subject: [PATCH 28/29] remove tailwind from dependencies --- .gitignore | 2 +- README.md | 66 ++---------------- package.json | 14 ++-- postcss.config.js | 6 -- src/lib/components/VirtualList.svelte | 15 ++++ src/lib/styles/app.css | 3 - src/routes/+layout.svelte | 6 +- src/routes/+page.svelte | 98 ++++++++++++++++++++++++++- tailwind.config.js | 9 --- 9 files changed, 124 insertions(+), 95 deletions(-) delete mode 100644 postcss.config.js delete mode 100644 src/lib/styles/app.css delete mode 100644 tailwind.config.js diff --git a/.gitignore b/.gitignore index ff5d0f4..34bc7d9 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,4 @@ node_modules vite.config.js.timestamp-* vite.config.ts.timestamp-* package-lock.json -svelte-tiny-virtual-list-tailwind-* +svelte-tiny-virtual-list-* diff --git a/README.md b/README.md index 4d5961a..c66a4db 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -

svelte-tiny-virtual-list-tailwind

-

A tiny but mighty list virtualization library for Svelte 5 & Tailwind

+

svelte-tiny-virtual-list

+

A tiny but mighty list virtualization library for Svelte 5

AboutFeatures • @@ -28,7 +28,6 @@ This repo was forked from [skayo/svelte-tiny-virtual-list](https://github.com/sk ### Requirements - **Svelte 5** -- Tailwind ### Install svelte 5 @@ -42,70 +41,13 @@ cd myapp npm install ``` -### Install Tailwind css - -```bash -npm install -D tailwindcss postcss autoprefixer -npx tailwindcss init -p -``` - -### Install svelte-tiny-virtual-list-tailwind +### Install svelte-tiny-virtual-list Clone repo locally and install ```bash npm install path/to/package ``` -### Update svelte.config.js - -```js -import adapter from '@sveltejs/adapter-auto'; -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - kit: { - adapter: adapter() - }, - preprocess: vitePreprocess() -}; -export default config; -``` - -### Update tailwind.config.js - -```js -/** @type {import('tailwindcss').Config} */ -export default { - content: [ - './src/**/*.{html,js,svelte,ts}', - './node_modules/svelte-tiny-virtual-list-tailwind/dist/**/*.{html,js,svelte,ts}' - ], - theme: { - extend: {}, - }, - plugins: [], -} -``` - -### Import Tailwind in app.css - -```css -@tailwind base; -@tailwind components; -@tailwind utilities; -``` - -### Import css in +layout.svelte - -```svelte - - - -``` - ### ## Usage @@ -252,4 +194,4 @@ You can style the elements of the virtual list like this: ## License -[MIT License](https://github.com/daminski/svelte-tiny-virtual-list-tailwind/blob/master/LICENSE) +[MIT License](https://github.com/daminski/svelte-tiny-virtual-list/blob/master/LICENSE) diff --git a/package.json b/package.json index 29466b7..bcfedc6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "svelte-tiny-virtual-list-tailwind", + "name": "svelte-tiny-virtual-list", "version": "3.0.5", - "description": "A tiny but mighty list virtualization component for svelte 5 & Tailwind", + "description": "A tiny but mighty list virtualization component for svelte 5", "scripts": { "dev": "vite dev", "build": "vite build && npm run package", @@ -23,8 +23,7 @@ "!dist/**/*.spec.*" ], "peerDependencies": { - "svelte": "^5.0.0-next.1", - "tailwindcss": "^3.4.1" + "svelte": "^5.0.0-next.1" }, "devDependencies": { "@sveltejs/adapter-auto": "^3.2.0", @@ -41,7 +40,6 @@ "prettier-plugin-svelte": "^3.2.2", "publint": "^0.2.7", "svelte": "^5.0.0-next.1", - "tailwindcss": "^3.4.3", "tslib": "^2.6.2", "typescript": "^5.4.3", "vite": "^5.2.8" @@ -61,10 +59,10 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/daminski/svelte-tiny-virtual-list-tailwind.git" + "url": "git+https://github.com/daminski/svelte-tiny-virtual-list.git" }, "bugs": { - "url": "https://github.com/daminski/svelte-tiny-virtual-list-tailwind/issues" + "url": "https://github.com/daminski/svelte-tiny-virtual-list/issues" }, - "homepage": "https://github.com/daminski/svelte-tiny-virtual-list-tailwind" + "homepage": "https://github.com/daminski/svelte-tiny-virtual-list" } diff --git a/postcss.config.js b/postcss.config.js deleted file mode 100644 index 2e7af2b..0000000 --- a/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/src/lib/components/VirtualList.svelte b/src/lib/components/VirtualList.svelte index 979344b..48966ec 100644 --- a/src/lib/components/VirtualList.svelte +++ b/src/lib/components/VirtualList.svelte @@ -294,3 +294,18 @@ {@render footer()} {/if}

+ + \ No newline at end of file diff --git a/src/lib/styles/app.css b/src/lib/styles/app.css deleted file mode 100644 index bd6213e..0000000 --- a/src/lib/styles/app.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 9b4d8c1..cdb33d2 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,9 +1,5 @@ - - - Sveltekit-tiny-virtual-list-tailwind + Sveltekit-tiny-virtual-list \ No newline at end of file diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 86d6e18..e9e70a2 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -78,7 +78,7 @@
-

Sveltekit-tiny-virtual-list-tailwind

+

Sveltekit-tiny-virtual-list

@@ -175,3 +175,99 @@
+ + diff --git a/tailwind.config.js b/tailwind.config.js deleted file mode 100644 index 13207cc..0000000 --- a/tailwind.config.js +++ /dev/null @@ -1,9 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: ['./src/**/*.{html,js,svelte,ts}'], - theme: { - extend: {}, - }, - plugins: [], -} - From 73649242c76dadc7201f8bf9e3416c3d454eee0e Mon Sep 17 00:00:00 2001 From: Daminski Date: Thu, 4 Apr 2024 09:16:07 +0200 Subject: [PATCH 29/29] 3.0.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bcfedc6..7d2f573 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte-tiny-virtual-list", - "version": "3.0.5", + "version": "3.0.6", "description": "A tiny but mighty list virtualization component for svelte 5", "scripts": { "dev": "vite dev",