diff --git a/CHANGELOG.md b/CHANGELOG.md
index 613d1a4..4f6c34c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,14 +1,9 @@
# svelte-virtual-scroll-list changelog
-## 1.3.0
+## 1.0.1
-- Expose `index` from VirtualScroll component
+- Fixed issue where data changes wouldn't refresh the item contents
-## 1.2.0
+## 1.0.0
-- Move example to SvelteKit
-- Package distribution by SvelteKit too
-- Add classes on VS wrappers
-- Add example for horizontal scroll
-- Fix pageMode with SSR
-- Support Svelte 4
\ No newline at end of file
+- Forked and heavily modified from original package svelte-virtual-scroll-list
\ No newline at end of file
diff --git a/README.md b/README.md
index 3b8b4f0..05e9a2b 100644
--- a/README.md
+++ b/README.md
@@ -1,31 +1,17 @@
# svelte-virtual-scroll-list
-[](https://npmjs.com/package/svelte-virtual-scroll-list/)
+[](https://npmjs.com/package/@josesan9/svelte-virtual-scroll-list/)
-Svelte implementation of vue library [vue-virtual-scroll-list](https://github.com/tangbc/vue-virtual-scroll-list)
+fork of same library by v1ack [svelte-virtual-scroll-list](https://github.com/v1ack/svelte-virtual-scroll-list)
-Virtualized scrolling for big lists
-
----
-**Support dynamic both-directional lists** (see example)
-
----
-
-Online demo: [https://v1ack.github.io/svelte-virtual-scroll-list/](https://v1ack.github.io/svelte-virtual-scroll-list/)
-
-[Simple example in Svelte REPL](https://ru.svelte.dev/repl/eae82aab17b04420885851d58de50a2e?version=3.38.2)
+Virtualized scrolling for big lists. For now this does not support bi-directionality (unlike v1ach's implementation)
# Getting started
-## Installing from npm
+`npm i josesan9/svelte-virtual-scroll-list -D`
-`npm i svelte-virtual-scroll-list -D`
-or
-
-`yarn add svelte-virtual-scroll-list -D`
-
-## Using
+## Usage
```html
@@ -61,7 +47,7 @@ More examples available in `example` folder
|---------------------------|----------------------------|---------------------|----------------------------------|
| handle dynamic size data | + | + | - |
| scroll methods (to index) | + | - | + |
-| infinity scrolling | two-directional | - | one-directional with another lib |
+| infinity scrolling | + | - | one-directional with another lib |
| initial scroll position | + | - | + |
| sticky items | - | - | + |
| top/bottom slots | + | - | + |
@@ -125,7 +111,13 @@ Access to methods by component binding
## Additional
-### Get index of current rendering items
+### Params passed down to each virtual list item
+
+| param | description |
+|------------|------------------------------------------------------|
+| data | data item |
+| index | index of item (in relation to full list) |
+| localIndex | index of item (in relation to rendered items only) |
```html
@@ -134,6 +126,7 @@ Access to methods by component binding
key="id"
let:data
let:index
+ let:localIndex
>
{data.text} {index}
diff --git a/package-lock.json b/package-lock.json
index 5b11758..0d33cd0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
- "name": "svelte-virtual-scroll-list",
- "version": "1.2.0",
+ "name": "@josesan9/svelte-virtual-scroll-list",
+ "version": "1.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "svelte-virtual-scroll-list",
- "version": "1.2.0",
+ "name": "@josesan9/svelte-virtual-scroll-list",
+ "version": "1.0.1",
"devDependencies": {
"@sveltejs/adapter-static": "^2.0.2",
"@sveltejs/kit": "^1.20.4",
diff --git a/package.json b/package.json
index 3565a65..1642bca 100644
--- a/package.json
+++ b/package.json
@@ -1,9 +1,9 @@
{
- "name": "svelte-virtual-scroll-list",
+ "name": "@josesan9/svelte-virtual-scroll-list",
"description": "Svelte lib for virtualizing lists",
"author": {
- "name": "v1ack",
- "url": "https://github.com/v1ack"
+ "name": "josesan9",
+ "url": "https://github.com/josesan9"
},
"keywords": [
"svelte",
@@ -11,10 +11,10 @@
"virtual-list",
"virtual-scroll"
],
- "version": "1.3.0",
+ "version": "1.0.1",
"repository": {
"type": "git",
- "url": "git+https://github.com/v1ack/svelte-virtual-scroll-list.git"
+ "url": "git+https://github.com/josesan9/svelte-virtual-scroll-list.git"
},
"scripts": {
"dev": "vite dev",
diff --git a/src/lib/Item.svelte b/src/lib/Item.svelte
index b28728f..102ba6b 100644
--- a/src/lib/Item.svelte
+++ b/src/lib/Item.svelte
@@ -1,13 +1,13 @@
-
@@ -269,13 +257,13 @@
{/if}
- {#each displayItems as dataItem, dataIndex (dataItem[key])}
+ {#each displayItems as dataItem, dataIndex (keyFn(dataItem, dataIndex + range.start))}
-
-
+
{/each}
diff --git a/src/lib/index.js b/src/lib/index.ts
similarity index 100%
rename from src/lib/index.js
rename to src/lib/index.ts
diff --git a/src/lib/virtual.js b/src/lib/virtual.js
deleted file mode 100644
index 034cace..0000000
--- a/src/lib/virtual.js
+++ /dev/null
@@ -1,327 +0,0 @@
-/**
- * virtual list core calculating center
- */
-
-const DIRECTION_TYPE = {
- FRONT: "FRONT", // scroll up or left
- BEHIND: "BEHIND", // scroll down or right
-}
-const CALC_TYPE = {
- INIT: "INIT",
- FIXED: "FIXED",
- DYNAMIC: "DYNAMIC",
-}
-const LEADING_BUFFER = 2
-
-export default class {
- param
- callUpdate
- firstRangeTotalSize = 0
- firstRangeAverageSize = 0
- lastCalcIndex = 0
- fixedSizeValue = 0
- calcType = CALC_TYPE.INIT
- offset = 0
- direction = ""
- range
-
- constructor(param, callUpdate) {
- this.init(param, callUpdate)
- }
-
- init(param, callUpdate) {
- // param data
- this.param = param
- this.callUpdate = callUpdate
-
- // size data
- this.sizes = new Map()
- this.firstRangeTotalSize = 0
- this.firstRangeAverageSize = 0
- this.fixedSizeValue = 0
- this.calcType = CALC_TYPE.INIT
-
- // scroll data
- this.offset = 0
- this.direction = ""
-
- // range data
- this.range = Object.create(null)
- if (param) {
- this.checkRange(0, param.keeps - 1)
- }
-
- // benchmark example data
- // this.__bsearchCalls = 0
- // this.__getIndexOffsetCalls = 0
- }
-
- destroy() {
- this.init(null, null)
- }
-
- // return current render range
- getRange() {
- const range = Object.create(null)
- range.start = this.range.start
- range.end = this.range.end
- range.padFront = this.range.padFront
- range.padBehind = this.range.padBehind
- return range
- }
-
- isBehind() {
- return this.direction === DIRECTION_TYPE.BEHIND
- }
-
- isFront() {
- return this.direction === DIRECTION_TYPE.FRONT
- }
-
- // return start index offset
- getOffset(start) {
- return (start < 1 ? 0 : this.getIndexOffset(start)) + this.param.slotHeaderSize
- }
-
- updateParam(key, value) {
- if (this.param && (key in this.param)) {
- // if uniqueIds change, find out deleted id and remove from size map
- if (key === "uniqueIds") {
- this.sizes.forEach((v, key) => {
- if (!value.includes(key)) {
- this.sizes.delete(key)
- }
- })
- }
- this.param[key] = value
- }
- }
-
- // save each size map by id
- saveSize(id, size) {
- this.sizes.set(id, size)
-
- // we assume size type is fixed at the beginning and remember first size value
- // if there is no size value different from this at next coming saving
- // we think it's a fixed size list, otherwise is dynamic size list
- if (this.calcType === CALC_TYPE.INIT) {
- this.fixedSizeValue = size
- this.calcType = CALC_TYPE.FIXED
- } else if (this.calcType === CALC_TYPE.FIXED && this.fixedSizeValue !== size) {
- this.calcType = CALC_TYPE.DYNAMIC
- // it's no use at all
- delete this.fixedSizeValue
- }
-
- // calculate the average size only in the first range
- if (this.calcType !== CALC_TYPE.FIXED && typeof this.firstRangeTotalSize !== "undefined") {
- if (this.sizes.size < Math.min(this.param.keeps, this.param.uniqueIds.length)) {
- this.firstRangeTotalSize = [...this.sizes.values()].reduce((acc, val) => acc + val, 0)
- this.firstRangeAverageSize = Math.round(this.firstRangeTotalSize / this.sizes.size)
- } else {
- // it's done using
- delete this.firstRangeTotalSize
- }
- }
- }
-
- // in some special situation (e.g. length change) we need to update in a row
- // try going to render next range by a leading buffer according to current direction
- handleDataSourcesChange() {
- let start = this.range.start
-
- if (this.isFront()) {
- start = start - LEADING_BUFFER
- } else if (this.isBehind()) {
- start = start + LEADING_BUFFER
- }
-
- start = Math.max(start, 0)
-
- this.updateRange(this.range.start, this.getEndByStart(start))
- }
-
- // when slot size change, we also need force update
- handleSlotSizeChange() {
- this.handleDataSourcesChange()
- }
-
- // calculating range on scroll
- handleScroll(offset) {
- this.direction = offset < this.offset ? DIRECTION_TYPE.FRONT : DIRECTION_TYPE.BEHIND
- this.offset = offset
-
- if (!this.param) {
- return
- }
-
- if (this.direction === DIRECTION_TYPE.FRONT) {
- this.handleFront()
- } else if (this.direction === DIRECTION_TYPE.BEHIND) {
- this.handleBehind()
- }
- }
-
- // ----------- public method end -----------
-
- handleFront() {
- const overs = this.getScrollOvers()
- // should not change range if start doesn't exceed overs
- if (overs > this.range.start) {
- return
- }
-
- // move up start by a buffer length, and make sure its safety
- const start = Math.max(overs - this.param.buffer, 0)
- this.checkRange(start, this.getEndByStart(start))
- }
-
- handleBehind() {
- const overs = this.getScrollOvers()
- // range should not change if scroll overs within buffer
- if (overs < this.range.start + this.param.buffer) {
- return
- }
-
- this.checkRange(overs, this.getEndByStart(overs))
- }
-
- // return the pass overs according to current scroll offset
- getScrollOvers() {
- // if slot header exist, we need subtract its size
- const offset = this.offset - this.param.slotHeaderSize
- if (offset <= 0) {
- return 0
- }
-
- // if is fixed type, that can be easily
- if (this.isFixedType()) {
- return Math.floor(offset / this.fixedSizeValue)
- }
-
- let low = 0
- let middle = 0
- let middleOffset = 0
- let high = this.param.uniqueIds.length
-
- while (low <= high) {
- // this.__bsearchCalls++
- middle = low + Math.floor((high - low) / 2)
- middleOffset = this.getIndexOffset(middle)
-
- if (middleOffset === offset) {
- return middle
- } else if (middleOffset < offset) {
- low = middle + 1
- } else if (middleOffset > offset) {
- high = middle - 1
- }
- }
-
- return low > 0 ? --low : 0
- }
-
- // return a scroll offset from given index, can efficiency be improved more here?
- // although the call frequency is very high, its only a superposition of numbers
- getIndexOffset(givenIndex) {
- if (!givenIndex) {
- return 0
- }
-
- let offset = 0
- let indexSize = 0
- for (let index = 0; index < givenIndex; index++) {
- // this.__getIndexOffsetCalls++
- indexSize = this.sizes.get(this.param.uniqueIds[index])
- offset = offset + (typeof indexSize === "number" ? indexSize : this.getEstimateSize())
- }
-
- // remember last calculate index
- this.lastCalcIndex = Math.max(this.lastCalcIndex, givenIndex - 1)
- this.lastCalcIndex = Math.min(this.lastCalcIndex, this.getLastIndex())
-
- return offset
- }
-
- // is fixed size type
- isFixedType() {
- return this.calcType === CALC_TYPE.FIXED
- }
-
- // return the real last index
- getLastIndex() {
- return this.param.uniqueIds.length - 1
- }
-
- // in some conditions range is broke, we need correct it
- // and then decide whether need update to next range
- checkRange(start, end) {
- const keeps = this.param.keeps
- const total = this.param.uniqueIds.length
-
- // data less than keeps, render all
- if (total <= keeps) {
- start = 0
- end = this.getLastIndex()
- } else if (end - start < keeps - 1) {
- // if range length is less than keeps, correct it base on end
- start = end - keeps + 1
- }
-
- if (this.range.start !== start) {
- this.updateRange(start, end)
- }
- }
-
- // setting to a new range and rerender
- updateRange(start, end) {
- this.range.start = start
- this.range.end = end
- this.range.padFront = this.getPadFront()
- this.range.padBehind = this.getPadBehind()
- this.callUpdate(this.getRange())
- }
-
- // return end base on start
- getEndByStart(start) {
- const theoryEnd = start + this.param.keeps - 1
- const truelyEnd = Math.min(theoryEnd, this.getLastIndex())
- return truelyEnd
- }
-
- // return total front offset
- getPadFront() {
- if (this.isFixedType()) {
- return this.fixedSizeValue * this.range.start
- } else {
- return this.getIndexOffset(this.range.start)
- }
- }
-
- // return total behind offset
- getPadBehind() {
- const end = this.range.end
- const lastIndex = this.getLastIndex()
-
- if (this.isFixedType()) {
- return (lastIndex - end) * this.fixedSizeValue
- }
-
- // if it's all calculated, return the exactly offset
- if (this.lastCalcIndex === lastIndex) {
- return this.getIndexOffset(lastIndex) - this.getIndexOffset(end)
- } else {
- // if not, use a estimated value
- return (lastIndex - end) * this.getEstimateSize()
- }
- }
-
- // get the item estimate size
- getEstimateSize() {
- return this.isFixedType() ? this.fixedSizeValue : (this.firstRangeAverageSize || this.param.estimateSize)
- }
-}
-
-export function isBrowser() {
- return typeof document !== "undefined"
-}
\ No newline at end of file
diff --git a/src/lib/virtual.ts b/src/lib/virtual.ts
new file mode 100644
index 0000000..27c9a27
--- /dev/null
+++ b/src/lib/virtual.ts
@@ -0,0 +1,155 @@
+type DataKey
= ((item: T, index: number) => any)
+type EstimateSize = ((item: T) => number)
+
+interface IParam {
+ slotHeaderSize: number,
+ slotFooterSize: number,
+ overflow: number,
+ data: T[],
+}
+
+interface IRange {
+ start: number,
+ end: number,
+ padFront: number,
+ padBehind: number,
+}
+
+export class Virtual{
+ param: IParam
+ callUpdate: (range: IRange) => void
+ currOffset = 0
+ clientHeight = 0
+ range: IRange
+ /** array of sizes for each container */
+ sizes = new Map()
+ /** array of offsets for each container */
+ offsets: number[]
+ keyFn: DataKey
+ estimateSize: EstimateSize
+
+ constructor(param: IParam, callUpdate: typeof this.callUpdate, keyFn: DataKey, estimateSize: number | EstimateSize) {
+ // param data
+ this.param = param
+ this.callUpdate = callUpdate
+ this.keyFn = keyFn
+ this.estimateSize = estimateSize instanceof Function ? estimateSize : () => estimateSize
+
+ // size data
+ this.sizes = new Map()
+ this.offsets = []
+ this.param.data.forEach((d, i) => {
+ let estimateSize = this.estimateSize instanceof Function ? this.estimateSize(d) : this.estimateSize
+ this.sizes.set(this.keyFn instanceof Function ? this.keyFn(d, i) : d[this.keyFn], estimateSize)
+ })
+ this.rebuildOffsets()
+
+ this.range = Object.create({ start: -1, end: -1, padFront: 0, padBehind: 0 })
+ }
+
+ // return current render range
+ getRange() {
+ return { ...this.range }
+ }
+
+ // return start index offset
+ getOffset(start: number) {
+ return (start < 1 ? 0 : this.getIndexOffset(start)) + this.param.slotHeaderSize
+ }
+
+ updateParam>(key: K, value: IParam[K]) {
+ if (this.param && (key in this.param)) {
+ this.param[key] = value
+ // if data change, find out deleted id and remove from size map
+ if (key === "data") {
+ let ids = (value as T[]).map(this.keyFn)
+ this.sizes.forEach((v, key) => {
+ if (!ids.includes(key)) {
+ this.sizes.delete(key)
+ }
+ })
+ this.rebuildOffsets()
+ this.handleScroll(this.currOffset, this.clientHeight, true)
+ }
+ }
+ }
+
+ // save each size map by id
+ saveSize(id: any, size: number) {
+ if (this.sizes.get(id) === size) {
+ return
+ }
+ this.sizes.set(id, size)
+ this.rebuildOffsets(this.param.data.findIndex((d, i) => this.keyFn(d, i) === id))
+ }
+ // calculating range on scroll
+ handleScroll(offset: number, clientHeight: number, forceUpdate = false) {
+ this.currOffset = offset
+ this.clientHeight = clientHeight
+ let startIndex = Math.max(this.offsets.findIndex(o => o >= offset) - 1 - this.param.overflow, 0)
+ let endIndex = this.offsets.findIndex(o => o >= offset + clientHeight)
+ if (endIndex === -1) {
+ endIndex = this.param.data.length - 1
+ } else if (endIndex < startIndex) {
+ endIndex = startIndex
+ }
+ endIndex = Math.min(endIndex + this.param.overflow, this.param.data.length - 1)
+ this.updateRange(startIndex, endIndex, forceUpdate)
+ }
+
+ // ----------- public method end -----------
+
+ // rebuilds the offset array
+ rebuildOffsets(startIndex?: number) {
+ if (startIndex === undefined) {
+ this.offsets = [0]
+ startIndex = 0
+ }
+ if (startIndex === -1) return
+ let lastOffset = this.offsets[startIndex] || 0
+ for (let i = startIndex + 1; i < this.param.data.length; i++) {
+ let id = this.keyFn(this.param.data[i - 1], i - 1)
+ lastOffset += this.sizes.get(id)!
+ if (Number.isNaN(lastOffset)) {
+ }
+ this.offsets[i] = lastOffset
+ }
+ }
+
+ // return a scroll offset from given index, can efficiency be improved more here?
+ // although the call frequency is very high, its only a superposition of numbers
+ getIndexOffset(givenIndex: number) {
+ if (!givenIndex) {
+ return 0
+ }
+
+ return this.offsets[givenIndex] || 0
+ }
+
+ // setting to a new range and rerender
+ updateRange(start: number, end: number, forceUpdate = false) {
+ if (!forceUpdate && start === this.range.start && end === this.range.end) return;
+ this.range.start = start
+ this.range.end = end
+ this.range.padFront = this.getPadFront()
+ this.range.padBehind = this.getPadBehind()
+ this.callUpdate(this.range)
+ }
+
+ // return total front offset
+ getPadFront() {
+ return this.offsets[this.range.start] || 0
+ }
+
+ // return total behind offset
+ getPadBehind() {
+ if (this.range.end >= this.param.data.length - 1) {
+ return 0
+ }
+ return this.offsets[this.offsets.length - 1] + this.sizes.get(this.keyFn(this.param.data[this.param.data.length - 1], this.param.data.length - 1))! - (this.offsets[this.range.end + 1])
+ }
+}
+
+export function isBrowser() {
+ return typeof document !== "undefined"
+}
\ No newline at end of file
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 8ab1367..d2359ee 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -35,7 +35,7 @@
class:active={current_path === page.component}
>{page.name}
{/each}
- Source
+ Source