diff --git a/src/gridSystem.ts b/src/gridSystem.ts index 4747894f7..d1b8d94a6 100644 --- a/src/gridSystem.ts +++ b/src/gridSystem.ts @@ -1,20 +1,4 @@ -import { - distinctUntilChanged, - filter, - duc, - combineLatest, - connect, - map, - pipe, - statefulStream, - system, - tup, - withLatestFrom, - subscribe, - streamFromEmitter, - mapTo, - stream, -} from '@virtuoso.dev/urx' +import * as u from '@virtuoso.dev/urx' import { domIOSystem } from './domIOSystem' import { sizeRangeSystem } from './sizeRangeSystem' import { stateFlagsSystem } from './stateFlagsSystem' @@ -67,12 +51,10 @@ const PROBE_GRID_STATE: GridState = { const { ceil, floor, min, max } = Math const hackFloor = (val: number) => (ceil(val) - val < 0.03 ? ceil(val) : floor(val)) -void { subscribe, min, max, hackFloor } - function buildItems(startIndex: number, endIndex: number) { return Array.from({ length: endIndex - startIndex + 1 }).map((_, i) => ({ index: i + startIndex } as GridItem)) } -export const gridSystem = system( +export const gridSystem = u.system( ([ { overscan, visibleRange, listBoundary }, { scrollTop, viewportHeight, scrollBy, scrollTo }, @@ -80,19 +62,19 @@ export const gridSystem = system( scrollSeek, { propsReady, didMount }, ]) => { - const totalCount = statefulStream(0) - const initialItemCount = statefulStream(0) - const gridState = statefulStream(INITIAL_GRID_STATE) - const viewportDimensions = statefulStream({ height: 0, width: 0 }) - const itemDimensions = statefulStream({ height: 0, width: 0 }) - const scrollToIndex = stream() - - connect( - pipe( + const totalCount = u.statefulStream(0) + const initialItemCount = u.statefulStream(0) + const gridState = u.statefulStream(INITIAL_GRID_STATE) + const viewportDimensions = u.statefulStream({ height: 0, width: 0 }) + const itemDimensions = u.statefulStream({ height: 0, width: 0 }) + const scrollToIndex = u.stream() + + u.connect( + u.pipe( didMount, - withLatestFrom(initialItemCount), - filter(([, count]) => count !== 0), - map(([, count]) => { + u.withLatestFrom(initialItemCount), + u.filter(([, count]) => count !== 0), + u.map(([, count]) => { return { items: buildItems(0, count - 1), top: 0, @@ -107,11 +89,11 @@ export const gridSystem = system( gridState ) - connect( - pipe( - combineLatest(duc(totalCount), visibleRange), - withLatestFrom(viewportDimensions, itemDimensions), - map(([[totalCount, [startOffset, endOffset]], viewport, item]) => { + u.connect( + u.pipe( + u.combineLatest(u.duc(totalCount), visibleRange, itemDimensions), + u.withLatestFrom(viewportDimensions), + u.map(([[totalCount, [startOffset, endOffset], item], viewport]) => { const { height: itemHeight, width: itemWidth } = item const { width: viewportWidth } = viewport @@ -140,18 +122,18 @@ export const gridSystem = system( gridState ) - connect( - pipe( + u.connect( + u.pipe( viewportDimensions, - map(({ height }) => height) + u.map(({ height }) => height) ), viewportHeight ) - connect( - pipe( - combineLatest(viewportDimensions, itemDimensions, gridState), - map(([viewport, item, { items }]) => { + u.connect( + u.pipe( + u.combineLatest(viewportDimensions, itemDimensions, gridState), + u.map(([viewport, item, { items }]) => { const { top, bottom } = gridLayout(viewport, item, items) return [top, bottom] }) @@ -159,61 +141,61 @@ export const gridSystem = system( listBoundary ) - connect( - pipe( + u.connect( + u.pipe( listBoundary, - withLatestFrom(gridState), - map(([[, bottom], { offsetBottom }]) => { + u.withLatestFrom(gridState), + u.map(([[, bottom], { offsetBottom }]) => { return { bottom, offsetBottom } }) ), stateFlags.listStateListener ) - const endReached = streamFromEmitter( - pipe( - duc(gridState), - filter(({ items }) => items.length > 0), - withLatestFrom(totalCount), - filter(([{ items }, totalCount]) => items[items.length - 1].index === totalCount - 1), - map(([, totalCount]) => totalCount - 1), - distinctUntilChanged() + const endReached = u.streamFromEmitter( + u.pipe( + u.duc(gridState), + u.filter(({ items }) => items.length > 0), + u.withLatestFrom(totalCount), + u.filter(([{ items }, totalCount]) => items[items.length - 1].index === totalCount - 1), + u.map(([, totalCount]) => totalCount - 1), + u.distinctUntilChanged() ) ) - const startReached = streamFromEmitter( - pipe( - duc(gridState), - filter(({ items }) => { + const startReached = u.streamFromEmitter( + u.pipe( + u.duc(gridState), + u.filter(({ items }) => { return items.length > 0 && items[0].index === 0 }), - mapTo(0) + u.mapTo(0) ) ) - const rangeChanged = streamFromEmitter( - pipe( - duc(gridState), - filter(({ items }) => items.length > 0), - map(({ items }) => { + const rangeChanged = u.streamFromEmitter( + u.pipe( + u.duc(gridState), + u.filter(({ items }) => items.length > 0), + u.map(({ items }) => { return { startIndex: items[0].index, endIndex: items[items.length - 1].index, } }), - distinctUntilChanged((prev, next) => { + u.distinctUntilChanged((prev, next) => { return prev && prev.startIndex === next.startIndex && prev.endIndex === next.endIndex }) ) ) - connect(rangeChanged, scrollSeek.scrollSeekRangeChanged) + u.connect(rangeChanged, scrollSeek.scrollSeekRangeChanged) - connect( - pipe( + u.connect( + u.pipe( scrollToIndex, - withLatestFrom(viewportDimensions, itemDimensions, totalCount), - map(([location, viewport, item, totalCount]) => { + u.withLatestFrom(viewportDimensions, itemDimensions, totalCount), + u.map(([location, viewport, item, totalCount]) => { let { index, align, behavior } = normalizeIndexLocation(location) index = Math.max(0, index, Math.min(totalCount - 1, index)) @@ -254,7 +236,7 @@ export const gridSystem = system( propsReady, } }, - tup(sizeRangeSystem, domIOSystem, stateFlagsSystem, scrollSeekSystem, propsReadySystem) + u.tup(sizeRangeSystem, domIOSystem, stateFlagsSystem, scrollSeekSystem, propsReadySystem) ) function gridLayout(viewport: ElementDimensions, item: ElementDimensions, items: GridItem[]): GridLayout { diff --git a/src/listStateSystem.ts b/src/listStateSystem.ts index 368227019..565096763 100644 --- a/src/listStateSystem.ts +++ b/src/listStateSystem.ts @@ -1,23 +1,4 @@ -import { - combineLatest, - connect, - distinctUntilChanged, - duc, - filter, - map, - mapTo, - pipe, - prop, - statefulStream, - statefulStreamFromEmitter, - stream, - streamFromEmitter, - system, - tap, - tup, - withLatestFrom, - getValue, -} from '@virtuoso.dev/urx' +import * as u from '@virtuoso.dev/urx' import { empty, find, findMaxKeyValue, Range, rangesWithin } from './AATree' import { groupedListSystem } from './groupedListSystem' import { initialTopMostItemIndexSystem } from './initialTopMostItemIndexSystem' @@ -143,7 +124,7 @@ export function buildListState(items: Item[], topItems: Item[], totalCount: numb } } -export const listStateSystem = system( +export const listStateSystem = u.system( ([ { statefulScrollTop }, { sizes, totalCount, data, firstItemIndex }, @@ -154,25 +135,26 @@ export const listStateSystem = system( stateFlags, { didMount }, ]) => { - const topItemsIndexes = statefulStream>([]) - const itemsRendered = stream() + const topItemsIndexes = u.statefulStream>([]) + const itemsRendered = u.stream() - connect(groupedListSystem.topItemsIndexes, topItemsIndexes) - const listState = statefulStreamFromEmitter( - pipe( - combineLatest( + u.connect(groupedListSystem.topItemsIndexes, topItemsIndexes) + + const listState = u.statefulStreamFromEmitter( + u.pipe( + u.combineLatest( didMount, - duc(visibleRange), - duc(totalCount), - duc(sizes), - duc(initialTopMostItemIndex), + u.duc(visibleRange), + u.duc(totalCount), + u.duc(sizes), + u.duc(initialTopMostItemIndex), scrolledToInitialItem, - duc(topItemsIndexes), - duc(firstItemIndex) + u.duc(topItemsIndexes), + u.duc(firstItemIndex) ), - filter(([didMount]) => didMount), - withLatestFrom(data), - map( + u.filter(([didMount]) => didMount), + u.withLatestFrom(data), + u.map( ([ [ , @@ -186,14 +168,15 @@ export const listStateSystem = system( ], data, ]) => { - const { sizeTree, offsetTree } = sizes + const sizesValue = sizes + const { sizeTree, offsetTree } = sizesValue if (totalCount === 0 || (startOffset === 0 && endOffset === 0)) { return EMPTY_LIST_STATE } if (empty(sizeTree)) { - return buildListState(probeItemSet(initialTopMostItemIndex, sizes, data), [], totalCount, sizes, firstItemIndex) + return buildListState(probeItemSet(initialTopMostItemIndex, sizesValue, data), [], totalCount, sizesValue, firstItemIndex) } let topItems = [] as Item[] @@ -202,7 +185,7 @@ export const listStateSystem = system( let startIndex = topItemsIndexes[0] let endIndex = topItemsIndexes[topItemsIndexes.length - 1] let offset = 0 - for (const range of rangesWithin(sizes.sizeTree, startIndex, endIndex)) { + for (const range of rangesWithin(sizeTree, startIndex, endIndex)) { let size = range.value let rangeStartIndex = Math.max(range.start, startIndex) let rangeEndIndex = Math.min(range.end, endIndex) @@ -219,13 +202,13 @@ export const listStateSystem = system( // This is a condition to be avaluated past the probe check, do not merge // with the totalcount check above if (!scrolledToInitialItem) { - return buildListState([], topItems, totalCount, sizes, firstItemIndex) + return buildListState([], topItems, totalCount, sizesValue, firstItemIndex) } // pull a fresh top group, avoids a bug where // scrolling up too fast causes stack overflow - if (!empty(sizes.groupOffsetTree)) { - topItemsIndexes = [findMaxKeyValue(sizes.groupOffsetTree, getValue(statefulScrollTop), 'v')[0]] + if (!empty(sizesValue.groupOffsetTree)) { + topItemsIndexes = [findMaxKeyValue(sizesValue.groupOffsetTree, u.getValue(statefulScrollTop), 'v')[0]] } let minStartIndex = topItemsIndexes.length > 0 ? topItemsIndexes[topItemsIndexes.length - 1] + 1 : 0 @@ -233,7 +216,7 @@ export const listStateSystem = system( let endIndex = findMaxKeyValue(offsetTree, endOffset, 'v')[0]! const maxIndex = totalCount - 1 - const items = tap([] as Item[], result => { + const items = u.tap([] as Item[], result => { for (const range of rangesWithin(offsetTree, startIndex, endIndex)) { let offset = range.value let rangeStartIndex = range.start @@ -262,73 +245,73 @@ export const listStateSystem = system( } }) - return buildListState(items, topItems, totalCount, sizes, firstItemIndex) + return buildListState(items, topItems, totalCount, sizesValue, firstItemIndex) } ), - distinctUntilChanged() + u.distinctUntilChanged() ), EMPTY_LIST_STATE ) - connect( - pipe( + u.connect( + u.pipe( data, - filter(data => data !== undefined), - map(data => data!.length) + u.filter(data => data !== undefined), + u.map(data => data!.length) ), totalCount ) - connect(pipe(listState, map(prop('topListHeight'))), topListHeight) - connect(topListHeight, rangeTopListHeight) - connect(listState, stateFlags.listStateListener) + u.connect(u.pipe(listState, u.map(u.prop('topListHeight'))), topListHeight) + u.connect(topListHeight, rangeTopListHeight) + u.connect(listState, stateFlags.listStateListener) - connect( - pipe( + u.connect( + u.pipe( listState, - map(state => [state.top, state.bottom]) + u.map(state => [state.top, state.bottom]) ), listBoundary ) - connect( - pipe( + u.connect( + u.pipe( listState, - map(state => state.items) + u.map(state => state.items) ), itemsRendered ) - const endReached = streamFromEmitter( - pipe( + const endReached = u.streamFromEmitter( + u.pipe( listState, - filter(({ items }) => items.length > 0), - withLatestFrom(totalCount), - filter(([{ items }, totalCount]) => items[items.length - 1].originalIndex === totalCount - 1), - map(([, totalCount]) => totalCount - 1), - distinctUntilChanged() + u.filter(({ items }) => items.length > 0), + u.withLatestFrom(totalCount), + u.filter(([{ items }, totalCount]) => items[items.length - 1].originalIndex === totalCount - 1), + u.map(([, totalCount]) => totalCount - 1), + u.distinctUntilChanged() ) ) - const startReached = streamFromEmitter( - pipe( + const startReached = u.streamFromEmitter( + u.pipe( listState, - filter(({ items }) => items.length > 0 && items[0].originalIndex === 0), - mapTo(0) + u.filter(({ items }) => items.length > 0 && items[0].originalIndex === 0), + u.mapTo(0) ) ) - const rangeChanged = streamFromEmitter( - pipe( + const rangeChanged = u.streamFromEmitter( + u.pipe( listState, - filter(({ items }) => items.length > 0), - map(({ items }) => { + u.filter(({ items }) => items.length > 0), + u.map(({ items }) => { return { startIndex: items[0].originalIndex, endIndex: items[items.length - 1].originalIndex, } as ListRange }), - distinctUntilChanged((prev, next) => { + u.distinctUntilChanged((prev, next) => { return prev && prev.startIndex === next.startIndex && prev.endIndex === next.endIndex }) ) @@ -336,7 +319,7 @@ export const listStateSystem = system( return { listState, topItemsIndexes, endReached, startReached, rangeChanged, itemsRendered, ...stateFlags } }, - tup( + u.tup( domIOSystem, sizeSystem, groupedListSystem, diff --git a/src/sizeRangeSystem.ts b/src/sizeRangeSystem.ts index b02631e21..20f550a75 100644 --- a/src/sizeRangeSystem.ts +++ b/src/sizeRangeSystem.ts @@ -1,16 +1,4 @@ -import { - combineLatest, - duc, - filter, - map, - pipe, - StatefulStream, - statefulStream, - statefulStreamFromEmitter, - stream, - system, - tup, -} from '@virtuoso.dev/urx' +import * as u from '@virtuoso.dev/urx' import { domIOSystem, DOWN, ScrollDirection, UP } from './domIOSystem' export type NumberTuple = [number, number] @@ -35,26 +23,26 @@ export const getOverscan = (overscan: Overscan, end: ListEnd, direction: ScrollD } } -export const sizeRangeSystem = system( +export const sizeRangeSystem = u.system( ([{ scrollTop, viewportHeight, deviation }]) => { - const listBoundary = stream() - const headerHeight = statefulStream(0) - const footerHeight = statefulStream(0) - const topListHeight = statefulStream(0) - const overscan = statefulStream(0) + const listBoundary = u.stream() + const headerHeight = u.statefulStream(0) + const footerHeight = u.statefulStream(0) + const topListHeight = u.statefulStream(0) + const overscan = u.statefulStream(0) - const visibleRange = (statefulStreamFromEmitter( - pipe( - combineLatest( - duc(scrollTop), - duc(viewportHeight), - duc(headerHeight), - duc(listBoundary, boundryComparator), - duc(overscan), - duc(topListHeight), - duc(deviation) + const visibleRange = (u.statefulStreamFromEmitter( + u.pipe( + u.combineLatest( + u.duc(scrollTop), + u.duc(viewportHeight), + u.duc(headerHeight), + u.duc(listBoundary, boundryComparator), + u.duc(overscan), + u.duc(topListHeight), + u.duc(deviation) ), - map(([scrollTop, viewportHeight, headerHeight, [listTop, listBottom], overscan, topListHeight, deviation]) => { + u.map(([scrollTop, viewportHeight, headerHeight, [listTop, listBottom], overscan, topListHeight, deviation]) => { const top = scrollTop - headerHeight - deviation let direction: ChangeDirection = NONE @@ -78,10 +66,11 @@ export const sizeRangeSystem = system( return null }), - filter(value => value != null) + u.filter(value => value != null), + u.distinctUntilChanged(boundryComparator as any) ), [0, 0] - ) as unknown) as StatefulStream + ) as unknown) as u.StatefulStream return { // input @@ -95,6 +84,6 @@ export const sizeRangeSystem = system( visibleRange, } }, - tup(domIOSystem), + u.tup(domIOSystem), { singleton: true } ) diff --git a/src/sizeSystem.ts b/src/sizeSystem.ts index 4bbe7c2b1..6ac6c9eaf 100644 --- a/src/sizeSystem.ts +++ b/src/sizeSystem.ts @@ -1,18 +1,4 @@ -import { - connect, - distinctUntilChanged, - system, - filter, - map, - pipe, - scan, - statefulStream, - statefulStreamFromEmitter, - stream, - streamFromEmitter, - withLatestFrom, - mapTo, -} from '@virtuoso.dev/urx' +import * as u from '@virtuoso.dev/urx' import { AANode, empty, find, findMaxKeyValue, insert, newTree, Range, rangesWithin, remove, walk } from './AATree' export interface SizeRange { @@ -189,64 +175,71 @@ export function originalIndexFromItemIndex(itemIndex: number, sizes: SizeState) } type OptionalNumber = number | undefined -export const sizeSystem = system( +export const sizeSystem = u.system( () => { - const sizeRanges = stream() - const totalCount = stream() - const unshiftWith = stream() - const firstItemIndex = statefulStream(0) - const groupIndices = statefulStream([] as number[]) - const fixedItemSize = statefulStream(undefined) - const defaultItemSize = statefulStream(undefined) - const data = statefulStream(undefined) + const sizeRanges = u.stream() + const totalCount = u.stream() + const unshiftWith = u.stream() + const firstItemIndex = u.statefulStream(0) + const groupIndices = u.statefulStream([] as number[]) + const fixedItemSize = u.statefulStream(undefined) + const defaultItemSize = u.statefulStream(undefined) + const data = u.statefulStream(undefined) const initial = initialSizeState() - const isGrouped = statefulStream(false) - const sizes = statefulStreamFromEmitter( - pipe(sizeRanges, withLatestFrom(groupIndices), scan(sizeStateReducer, initial), distinctUntilChanged()), + const sizes = u.statefulStreamFromEmitter( + u.pipe(sizeRanges, u.withLatestFrom(groupIndices), u.scan(sizeStateReducer, initial), u.distinctUntilChanged()), initial ) - connect( - pipe( + u.connect( + u.pipe( groupIndices, - withLatestFrom(sizes), - map(([groupIndices, sizes]) => ({ - ...sizes, - groupIndices, - groupOffsetTree: groupIndices.reduce((tree, index) => { - return insert(tree, index, offsetOf(index, sizes)) - }, newTree()), - })) + u.filter(indexes => indexes.length > 0), + u.withLatestFrom(sizes), + u.map(([groupIndices, sizes]) => { + // the initial pass through that finds empty sizes, + // so we record the first group only + const groupOffsetTree = false + ? insert(newTree(), 0, 0) + : groupIndices.reduce((tree, index, idx) => { + return insert(tree, index, offsetOf(index, sizes) || idx) + }, newTree()) + + return { + ...sizes, + groupIndices, + groupOffsetTree, + } + }) ), sizes ) - connect(pipe(groupIndices, mapTo(true)), isGrouped) + u.connect(fixedItemSize, defaultItemSize) - connect(fixedItemSize, defaultItemSize) - const trackItemSizes = statefulStreamFromEmitter( - pipe( + const trackItemSizes = u.statefulStreamFromEmitter( + u.pipe( fixedItemSize, - map(size => size === undefined) + u.map(size => size === undefined) ), true ) - connect( - pipe( + u.connect( + u.pipe( defaultItemSize, - filter(value => value !== undefined), - map(size => [{ startIndex: 0, endIndex: 0, size }] as SizeRange[]) + u.filter(value => value !== undefined), + u.map(size => [{ startIndex: 0, endIndex: 0, size }] as SizeRange[]) ), sizeRanges ) - const listRefresh = streamFromEmitter( - pipe( + const listRefresh = u.streamFromEmitter( + u.pipe( sizeRanges, - withLatestFrom(sizes), - scan( + u.withLatestFrom(sizes), + u.scan( ({ sizes: oldSizes }, [_, newSizes]) => { return { changed: newSizes !== oldSizes, @@ -255,15 +248,15 @@ export const sizeSystem = system( }, { changed: false, sizes: initial } ), - map(value => value.changed) + u.map(value => value.changed) ) ) - connect( - pipe( + u.connect( + u.pipe( unshiftWith, - withLatestFrom(sizes), - map(([unshiftWith, sizes]) => { + u.withLatestFrom(sizes), + u.map(([unshiftWith, sizes]) => { if (sizes.groupIndices.length > 0) { throw new Error('Virtuoso: prepending items does not work with groups') } @@ -287,17 +280,17 @@ export const sizeSystem = system( sizeRanges ) - connect( - pipe( + u.connect( + u.pipe( firstItemIndex, - scan( + u.scan( (prev, next) => { return { diff: prev.prev - next, prev: next } }, { diff: 0, prev: 0 } ), - map(val => val.diff), - filter(value => value > 0) + u.map(val => val.diff), + u.filter(value => value > 0) ), unshiftWith ) diff --git a/test/gridSystem.test.ts b/test/gridSystem.test.ts index d18e7bdb9..4db44b8f0 100644 --- a/test/gridSystem.test.ts +++ b/test/gridSystem.test.ts @@ -25,6 +25,7 @@ describe('grid system', () => { expect(getValue(gridState).items).toHaveLength(1) publish(scrollTop, 0) + publish(viewportDimensions, { width: 1000, height: 500, diff --git a/test/sizeSystem.test.ts b/test/sizeSystem.test.ts index ca140da7a..9f4874d74 100644 --- a/test/sizeSystem.test.ts +++ b/test/sizeSystem.test.ts @@ -341,15 +341,15 @@ describe('size engine', () => { }) describe('group indices', () => { - it('creates 0 valued groupOffsetTree', () => { + it('starts with dummy valued groupOffsetTree', () => { let { groupIndices, sizes } = init(sizeSystem) publish(groupIndices, [0, 6, 11]) expect(getValue(sizes).groupIndices).toEqual([0, 6, 11]) expect(toKV(getValue(sizes).groupOffsetTree)).toEqual([ [0, 0], - [6, 0], - [11, 0], + [6, 1], + [11, 2], ]) })