-
Notifications
You must be signed in to change notification settings - Fork 3
develop to master #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8f5860f
7e40481
450b47a
26bba93
08c03a9
4d2ed64
bf1c692
7a5ee32
797b120
e8fbea0
84c77df
cd54372
725156d
1e13699
32aade5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,37 +1,36 @@ | ||
| import type { Range } from '../../types'; | ||
| import type { LayoutStrategy } from './LayoutStrategy'; | ||
| import { clamp } from '../../utils/clamp'; | ||
| import type { Range } from "../../types"; | ||
| import type { LayoutStrategy } from "./LayoutStrategy"; | ||
| import { clamp } from "../../utils/clamp"; | ||
|
|
||
| export class FixedLayoutStrategy implements LayoutStrategy { | ||
| constructor(private itemSize: number) {} | ||
| readonly #itemSize: number; | ||
|
|
||
| constructor(itemSize: number) { | ||
| this.#itemSize = itemSize; | ||
| } | ||
|
|
||
| getItemOffset(index: number): number { | ||
| return index * this.itemSize; | ||
| return index * this.#itemSize; | ||
| } | ||
|
|
||
| getItemSize(_index: number): number { | ||
| return this.itemSize; | ||
| return this.#itemSize; | ||
| } | ||
|
|
||
| getTotalSize(count: number): number { | ||
| return count * this.itemSize; | ||
| return count * this.#itemSize; | ||
| } | ||
|
|
||
| getVisibleRange( | ||
| scrollOffset: number, | ||
| viewportSize: number, | ||
| count: number | ||
| ): Range { | ||
| const startIndex = clamp( | ||
| 0, | ||
| Math.floor(scrollOffset / this.itemSize), | ||
| count - 1 | ||
| ); | ||
| const startIndex = clamp(0, (scrollOffset / this.#itemSize) | 0, count - 1); | ||
|
|
||
| const visibleCount = Math.ceil(viewportSize / this.itemSize); | ||
| const visibleCount = Math.ceil(viewportSize / this.#itemSize); | ||
| const endIndex = Math.min(count - 1, startIndex + visibleCount); | ||
|
|
||
| return { startIndex, endIndex }; | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,40 +1,40 @@ | ||
| import type { ScrollSource } from "./ScrollSource"; | ||
|
|
||
| export class VirtualScrollSource implements ScrollSource { | ||
| private scrollOffset = 0; | ||
| private viewportSize = 0; | ||
| private listeners = new Set<(offset: number) => void>(); | ||
| #scrollOffset = 0; | ||
| #viewportSize = 0; | ||
| #listeners = new Set<(offset: number) => void>(); | ||
|
|
||
| getScrollOffset(): number { | ||
| return this.scrollOffset; | ||
| return this.#scrollOffset; | ||
| } | ||
|
|
||
| getViewportSize(): number { | ||
| return this.viewportSize; | ||
| return this.#viewportSize; | ||
| } | ||
|
|
||
| setScrollOffset(offset: number): void { | ||
| if (this.scrollOffset !== offset) { | ||
| this.scrollOffset = offset; | ||
| this.notifyListeners(); | ||
| if (this.#scrollOffset !== offset) { | ||
| this.#scrollOffset = offset; | ||
| this.#notifyListeners(); | ||
| } | ||
| } | ||
|
|
||
| setViewportSize(size: number): void { | ||
| if (this.viewportSize !== size) { | ||
| this.viewportSize = size; | ||
| this.notifyListeners(); | ||
| if (this.#viewportSize !== size) { | ||
| this.#viewportSize = size; | ||
| this.#notifyListeners(); | ||
| } | ||
| } | ||
|
|
||
| subscribe(callback: (offset: number) => void): () => void { | ||
| this.listeners.add(callback); | ||
| this.#listeners.add(callback); | ||
| return () => { | ||
| this.listeners.delete(callback); | ||
| this.#listeners.delete(callback); | ||
| }; | ||
| } | ||
|
|
||
| private notifyListeners(): void { | ||
| this.listeners.forEach((listener) => listener(this.scrollOffset)); | ||
| #notifyListeners(): void { | ||
| this.#listeners.forEach((listener) => listener(this.#scrollOffset)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,108 +2,135 @@ import type { | |||||||||||||||||||||||||||||||||||
| VirtualItem, | ||||||||||||||||||||||||||||||||||||
| VirtualizerState, | ||||||||||||||||||||||||||||||||||||
| VirtualizerOptions, | ||||||||||||||||||||||||||||||||||||
| } from '../types'; | ||||||||||||||||||||||||||||||||||||
| import type { LayoutStrategy } from '../strategies/layout/LayoutStrategy'; | ||||||||||||||||||||||||||||||||||||
| import type { ScrollSource } from '../strategies/scroll/ScrollSource'; | ||||||||||||||||||||||||||||||||||||
| import type { Plugin } from '../plugins/Plugin'; | ||||||||||||||||||||||||||||||||||||
| } from "../types"; | ||||||||||||||||||||||||||||||||||||
| import type { LayoutStrategy } from "../strategies/layout/LayoutStrategy"; | ||||||||||||||||||||||||||||||||||||
| import type { ScrollSource } from "../strategies/scroll/ScrollSource"; | ||||||||||||||||||||||||||||||||||||
| import type { Plugin } from "../plugins/Plugin"; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| export class Virtualizer { | ||||||||||||||||||||||||||||||||||||
| private count: number; | ||||||||||||||||||||||||||||||||||||
| private overscan: number; | ||||||||||||||||||||||||||||||||||||
| private plugins: Plugin[]; | ||||||||||||||||||||||||||||||||||||
| private onChange?: (state: VirtualizerState) => void; | ||||||||||||||||||||||||||||||||||||
| #count: number; | ||||||||||||||||||||||||||||||||||||
| readonly #overscan: number; | ||||||||||||||||||||||||||||||||||||
| #plugins: Plugin[]; | ||||||||||||||||||||||||||||||||||||
| readonly #onChange?: (state: VirtualizerState) => void; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| private layoutStrategy: LayoutStrategy; | ||||||||||||||||||||||||||||||||||||
| private scrollSource: ScrollSource; | ||||||||||||||||||||||||||||||||||||
| readonly #layoutStrategy: LayoutStrategy; | ||||||||||||||||||||||||||||||||||||
| readonly #scrollSource: ScrollSource; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| private state: VirtualizerState; | ||||||||||||||||||||||||||||||||||||
| private unsubscribe?: () => void; | ||||||||||||||||||||||||||||||||||||
| #state: VirtualizerState; | ||||||||||||||||||||||||||||||||||||
| readonly #unsubscribe?: () => void; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| #prevRenderRange?: { startIndex: number; endIndex: number }; | ||||||||||||||||||||||||||||||||||||
| #prevVirtualItems?: VirtualItem[]; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| constructor( | ||||||||||||||||||||||||||||||||||||
| layoutStrategy: LayoutStrategy, | ||||||||||||||||||||||||||||||||||||
| scrollSource: ScrollSource, | ||||||||||||||||||||||||||||||||||||
| options: VirtualizerOptions | ||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||
| this.layoutStrategy = layoutStrategy; | ||||||||||||||||||||||||||||||||||||
| this.scrollSource = scrollSource; | ||||||||||||||||||||||||||||||||||||
| this.count = options.count; | ||||||||||||||||||||||||||||||||||||
| this.overscan = options.overscan ?? 4; | ||||||||||||||||||||||||||||||||||||
| this.onChange = options.onChange; | ||||||||||||||||||||||||||||||||||||
| this.plugins = []; | ||||||||||||||||||||||||||||||||||||
| this.#layoutStrategy = layoutStrategy; | ||||||||||||||||||||||||||||||||||||
| this.#scrollSource = scrollSource; | ||||||||||||||||||||||||||||||||||||
| this.#count = options.count; | ||||||||||||||||||||||||||||||||||||
| this.#overscan = options.overscan ?? 4; | ||||||||||||||||||||||||||||||||||||
| this.#onChange = options.onChange; | ||||||||||||||||||||||||||||||||||||
| this.#plugins = []; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| this.state = this.calculateState(); | ||||||||||||||||||||||||||||||||||||
| this.#state = this.#calculateState(); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| this.plugins.forEach((plugin) => plugin.onInit?.()); | ||||||||||||||||||||||||||||||||||||
| this.#plugins.forEach((plugin) => plugin.onInit?.()); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| this.unsubscribe = this.scrollSource.subscribe(() => { | ||||||||||||||||||||||||||||||||||||
| this.#unsubscribe = this.#scrollSource.subscribe(() => { | ||||||||||||||||||||||||||||||||||||
| this.update(); | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| addPlugin(plugin: Plugin): void { | ||||||||||||||||||||||||||||||||||||
| this.plugins.push(plugin); | ||||||||||||||||||||||||||||||||||||
| this.#plugins.push(plugin); | ||||||||||||||||||||||||||||||||||||
| plugin.onInit?.(); | ||||||||||||||||||||||||||||||||||||
| this.#prevRenderRange = undefined; | ||||||||||||||||||||||||||||||||||||
| this.#prevVirtualItems = undefined; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| getState(): VirtualizerState { | ||||||||||||||||||||||||||||||||||||
| return this.state; | ||||||||||||||||||||||||||||||||||||
| return this.#state; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| setCount(count: number): void { | ||||||||||||||||||||||||||||||||||||
| if (this.count !== count) { | ||||||||||||||||||||||||||||||||||||
| this.count = count; | ||||||||||||||||||||||||||||||||||||
| if (this.#count !== count) { | ||||||||||||||||||||||||||||||||||||
| this.#count = count; | ||||||||||||||||||||||||||||||||||||
| this.#prevRenderRange = undefined; | ||||||||||||||||||||||||||||||||||||
| this.#prevVirtualItems = undefined; | ||||||||||||||||||||||||||||||||||||
| this.update(); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| update(): void { | ||||||||||||||||||||||||||||||||||||
| const newState = this.calculateState(); | ||||||||||||||||||||||||||||||||||||
| const newState = this.#calculateState(); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| let finalState = newState; | ||||||||||||||||||||||||||||||||||||
| for (const plugin of this.plugins) { | ||||||||||||||||||||||||||||||||||||
| for (const plugin of this.#plugins) { | ||||||||||||||||||||||||||||||||||||
| const result = plugin.beforeStateChange?.(finalState); | ||||||||||||||||||||||||||||||||||||
| if (result) finalState = result; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| this.state = finalState; | ||||||||||||||||||||||||||||||||||||
| this.#state = finalState; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| this.plugins.forEach((plugin) => plugin.afterStateChange?.(this.state)); | ||||||||||||||||||||||||||||||||||||
| this.#plugins.forEach((plugin) => plugin.afterStateChange?.(this.#state)); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| this.onChange?.(this.state); | ||||||||||||||||||||||||||||||||||||
| this.#onChange?.(this.#state); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| private calculateState(): VirtualizerState { | ||||||||||||||||||||||||||||||||||||
| const scrollOffset = this.scrollSource.getScrollOffset(); | ||||||||||||||||||||||||||||||||||||
| const viewportSize = this.scrollSource.getViewportSize(); | ||||||||||||||||||||||||||||||||||||
| const totalSize = this.layoutStrategy.getTotalSize(this.count); | ||||||||||||||||||||||||||||||||||||
| #calculateState(): VirtualizerState { | ||||||||||||||||||||||||||||||||||||
| const scrollOffset = this.#scrollSource.getScrollOffset(); | ||||||||||||||||||||||||||||||||||||
| const viewportSize = this.#scrollSource.getViewportSize(); | ||||||||||||||||||||||||||||||||||||
| const totalSize = this.#layoutStrategy.getTotalSize(this.#count); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| let visibleRange = this.layoutStrategy.getVisibleRange( | ||||||||||||||||||||||||||||||||||||
| let visibleRange = this.#layoutStrategy.getVisibleRange( | ||||||||||||||||||||||||||||||||||||
| scrollOffset, | ||||||||||||||||||||||||||||||||||||
| viewportSize, | ||||||||||||||||||||||||||||||||||||
| this.count | ||||||||||||||||||||||||||||||||||||
| this.#count | ||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| let renderRange = { | ||||||||||||||||||||||||||||||||||||
| startIndex: Math.max(0, visibleRange.startIndex - this.overscan), | ||||||||||||||||||||||||||||||||||||
| endIndex: Math.min(this.count - 1, visibleRange.endIndex + this.overscan), | ||||||||||||||||||||||||||||||||||||
| startIndex: (visibleRange.startIndex - this.#overscan) | 0, | ||||||||||||||||||||||||||||||||||||
| endIndex: (visibleRange.endIndex + this.#overscan) | 0, | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| for (const plugin of this.plugins) { | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (renderRange.startIndex < 0) renderRange.startIndex = 0; | ||||||||||||||||||||||||||||||||||||
| if (renderRange.endIndex > this.#count - 1) | ||||||||||||||||||||||||||||||||||||
| renderRange.endIndex = this.#count - 1; | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
93
to
+100
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The previous implementation for clamping the
Suggested change
|
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| for (const plugin of this.#plugins) { | ||||||||||||||||||||||||||||||||||||
| if (plugin.onRangeCalculated) { | ||||||||||||||||||||||||||||||||||||
| renderRange = plugin.onRangeCalculated(visibleRange, this.count); | ||||||||||||||||||||||||||||||||||||
| renderRange = plugin.onRangeCalculated(visibleRange, this.#count); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const virtualItems: VirtualItem[] = []; | ||||||||||||||||||||||||||||||||||||
| for (let i = renderRange.startIndex; i <= renderRange.endIndex; i++) { | ||||||||||||||||||||||||||||||||||||
| const start = this.layoutStrategy.getItemOffset(i); | ||||||||||||||||||||||||||||||||||||
| const size = this.layoutStrategy.getItemSize(i); | ||||||||||||||||||||||||||||||||||||
| virtualItems.push({ | ||||||||||||||||||||||||||||||||||||
| index: i, | ||||||||||||||||||||||||||||||||||||
| start, | ||||||||||||||||||||||||||||||||||||
| size, | ||||||||||||||||||||||||||||||||||||
| end: start + size, | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| let virtualItems: VirtualItem[]; | ||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||
| this.#prevRenderRange && | ||||||||||||||||||||||||||||||||||||
| this.#prevVirtualItems && | ||||||||||||||||||||||||||||||||||||
| this.#prevRenderRange.startIndex === renderRange.startIndex && | ||||||||||||||||||||||||||||||||||||
| this.#prevRenderRange.endIndex === renderRange.endIndex | ||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||
| virtualItems = this.#prevVirtualItems; | ||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||
| virtualItems = []; | ||||||||||||||||||||||||||||||||||||
| const startIdx = renderRange.startIndex; | ||||||||||||||||||||||||||||||||||||
| const endIdx = renderRange.endIndex; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| for (let i = startIdx; i <= endIdx; i++) { | ||||||||||||||||||||||||||||||||||||
| const start = this.#layoutStrategy.getItemOffset(i); | ||||||||||||||||||||||||||||||||||||
| const size = this.#layoutStrategy.getItemSize(i); | ||||||||||||||||||||||||||||||||||||
| virtualItems.push({ | ||||||||||||||||||||||||||||||||||||
| index: i, | ||||||||||||||||||||||||||||||||||||
| start, | ||||||||||||||||||||||||||||||||||||
| size, | ||||||||||||||||||||||||||||||||||||
| end: start + size, | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| this.#prevRenderRange = renderRange; | ||||||||||||||||||||||||||||||||||||
| this.#prevVirtualItems = virtualItems; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+108
to
134
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||
|
|
@@ -117,8 +144,7 @@ export class Virtualizer { | |||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| destroy(): void { | ||||||||||||||||||||||||||||||||||||
| this.unsubscribe?.(); | ||||||||||||||||||||||||||||||||||||
| this.plugins.forEach((plugin) => plugin.onDestroy?.()); | ||||||||||||||||||||||||||||||||||||
| this.#unsubscribe?.(); | ||||||||||||||||||||||||||||||||||||
| this.#plugins.forEach((plugin) => plugin.onDestroy?.()); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
readonlywith a private field (#itemSize) is a great improvement for ensuring immutability and true privacy. This is a good modern practice.