diff --git a/docs/configuration.md b/docs/configuration.md index fd026c94..43b8d73f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -58,11 +58,11 @@ The viewport is the scrollable area whose position and size the scroller tracks; | Setting | Type | Default | Effect | | --- | --- | --- | --- | | `windowViewport` | `boolean` | `false` | Use the browser window for scrolling and viewport dimensions. | -| `viewportElement` | `HTMLElement`, synchronous function returning one, or `null` | `null` | Use an explicit viewport instead of the content element's parent. Experimental. | +| `viewportElement` | `HTMLElement`, `null`, or a synchronous function returning either | `null` | Use an explicit viewport instead of the content element's parent. Experimental. | | `horizontal` | `boolean` | `false` | Scroll and measure along the horizontal axis; the DOM/CSS layout must also be horizontal. | | `alignShortContentToEnd` | `boolean` | `false` | Align content shorter than the viewport to the bottom/right. | -`windowViewport` takes precedence over `viewportElement`. A viewport factory is evaluated when the scroller is created; it must return an element synchronously. An invalid result falls back to the parent viewport. +`windowViewport` takes precedence over `viewportElement`. A viewport factory is evaluated when the scroller is created; it must return an element or `null` synchronously. A `null` or invalid result falls back to the parent viewport. With `alignShortContentToEnd: true`, content shorter than the viewport is aligned to the bottom in vertical mode or to the right in horizontal mode; `false` aligns it to the top or left. For content that fills or exceeds the viewport, scrolling and the initial position selected by `startIndex` are unchanged. The setting neither reverses item order nor automatically keeps the viewport at the end when items are added. diff --git a/src/classes/adapter.ts b/src/classes/adapter.ts index 54befe8b..ff709ff5 100644 --- a/src/classes/adapter.ts +++ b/src/classes/adapter.ts @@ -442,7 +442,7 @@ export class Adapter implements IAdapter { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - reset(options?: IDatasourceOptional): any { + reset(options?: IDatasourceOptional): any { this.reloadCounter++; this.logger.logAdapterMethod('reset', options, ` of ${this.reloadId}`); this.workflow.call({ diff --git a/src/classes/item.ts b/src/classes/item.ts index 54f9ad9d..216ff119 100644 --- a/src/classes/item.ts +++ b/src/classes/item.ts @@ -36,8 +36,8 @@ export class Item implements _Item { this.container.data = value; } - get element(): HTMLElement { - return this.container.element as HTMLElement; + get element(): HTMLElement | undefined { + return this.container.element; } set element(value: HTMLElement) { this.container.element = value; @@ -68,7 +68,7 @@ export class Item implements _Item { } makeVisible(): void { - this.routines.makeElementVisible(this.element); + this.routines.makeElementVisible(this.element as HTMLElement); this.invisible = false; } diff --git a/src/classes/settings.ts b/src/classes/settings.ts index bb3bf826..d7f77dfa 100644 --- a/src/classes/settings.ts +++ b/src/classes/settings.ts @@ -26,7 +26,7 @@ export class Settings implements ISettings, IDevSettings { infinite: boolean; horizontal: boolean; windowViewport: boolean; - viewportElement: HTMLElement | (() => void) | null; + viewportElement: HTMLElement | (() => HTMLElement | null) | null; alignShortContentToEnd: boolean; // align content shorter than the viewport to the bottom/right onBeforeClip: ItemsProcessor | null; // if set, it will be run before clipping items from Buffer after they are hidden sizeStrategy: SizeStrategy; // "average" | "frequent", determines behavior of unknown items diff --git a/src/classes/viewport.ts b/src/classes/viewport.ts index f2733d06..26622579 100644 --- a/src/classes/viewport.ts +++ b/src/classes/viewport.ts @@ -96,7 +96,7 @@ export class Viewport { bwd ? i <= items.length - 1 : i >= 0; i += bwd ? 1 : -1 ) { - const itemEdge = this.routines.getEdge(items[i].element, opposite); + const itemEdge = this.routines.getEdge(items[i].element as HTMLElement, opposite); diff = itemEdge - viewportEdge; if ((bwd && diff > 0) || (!bwd && diff < 0)) { item = items[i]; diff --git a/src/interfaces/adapter.ts b/src/interfaces/adapter.ts index fee6441f..08f99aea 100644 --- a/src/interfaces/adapter.ts +++ b/src/interfaces/adapter.ts @@ -110,7 +110,6 @@ export interface AdapterInsertOptions { beforeIndex?: number; afterIndex?: number; decrease?: boolean; - virtualize?: boolean; } export interface AdapterReplaceOptions { @@ -164,7 +163,7 @@ export interface IAdapter { readonly eof$: Reactive; readonly paused: boolean; readonly paused$: Reactive; - reset(datasource?: IDatasourceOptional): MethodResult; + reset(datasource?: IDatasourceOptional): MethodResult; reload(reloadIndex?: number | string): MethodResult; append(options: AdapterAppendOptions): MethodResult; append(items: Data | Data[], eof?: boolean): MethodResult; // old signature diff --git a/src/interfaces/datasource.ts b/src/interfaces/datasource.ts index 1e86b8be..e7650612 100644 --- a/src/interfaces/datasource.ts +++ b/src/interfaces/datasource.ts @@ -14,7 +14,7 @@ type DSGetCallback = ( success: SuccessFn, fail?: ErrorFn ) => void; -type DSGetObservable = (index: number, count: number) => ObservableLike; +type DSGetObservable = (index: number, count: number) => ObservableLike; type DSGetPromise = (index: number, count: number) => PromiseLike; export type DatasourceGet = DSGetCallback | DSGetObservable | DSGetPromise; diff --git a/src/interfaces/item.ts b/src/interfaces/item.ts index dd1c4aee..bde21096 100644 --- a/src/interfaces/item.ts +++ b/src/interfaces/item.ts @@ -3,7 +3,7 @@ import { ItemAdapter } from './adapter'; export interface Item { uid: number; $index: number; - element: HTMLElement; + element?: HTMLElement; data: Data; invisible: boolean; get: () => ItemAdapter; diff --git a/src/interfaces/settings.ts b/src/interfaces/settings.ts index 51d2ef19..4870f1ba 100644 --- a/src/interfaces/settings.ts +++ b/src/interfaces/settings.ts @@ -65,11 +65,11 @@ export interface Settings { windowViewport?: boolean; /** - * Explicit viewport element, or a synchronous factory returning one; windowViewport takes precedence. - * An invalid factory result falls back to the content element's parent. + * Explicit viewport element, or a synchronous factory returning an element or null; windowViewport takes precedence. + * A null or invalid factory result falls back to the content element's parent. * Experimental. Default: null (the content element's parent). */ - viewportElement?: HTMLElement | (() => void) | null; + viewportElement?: HTMLElement | (() => HTMLElement | null) | null; /** * Aligns content shorter than the viewport to the bottom vertically or to the right horizontally. diff --git a/src/processes/adapter/insert.ts b/src/processes/adapter/insert.ts index e7c54320..783c9eb1 100644 --- a/src/processes/adapter/insert.ts +++ b/src/processes/adapter/insert.ts @@ -5,6 +5,10 @@ import { Item } from '../../classes/item'; import { Direction } from '../../inputs/index'; import { AdapterInsertOptions, AdapterUpdateOptions } from '../../interfaces/index'; +interface InsertParams extends AdapterInsertOptions { + virtualize?: boolean; +} + export default class Insert extends BaseAdapterProcessFactory(AdapterProcess.insert) { static run(scroller: Scroller, options: AdapterInsertOptions): void { const { params } = Insert.parseInput(scroller, options); @@ -19,7 +23,7 @@ export default class Insert extends BaseAdapterProcessFactory(AdapterProcess.ins }); } - static doInsert(scroller: Scroller, params: AdapterInsertOptions): boolean { + static doInsert(scroller: Scroller, params: InsertParams): boolean { if (!Insert.insertEmpty(scroller, params)) { if (!Insert.insertInBuffer(scroller, params)) { if (!Insert.insertVirtually(scroller, params)) { @@ -52,7 +56,7 @@ export default class Insert extends BaseAdapterProcessFactory(AdapterProcess.ins return true; } - static insertInBuffer(scroller: Scroller, params: AdapterInsertOptions): boolean { + static insertInBuffer(scroller: Scroller, params: InsertParams): boolean { const { before, after, beforeIndex, afterIndex, items, decrease } = params; const indexToInsert = scroller.buffer.getIndexToInsert( before || after, diff --git a/tests/e2e/scaffolding/data.ts b/tests/e2e/scaffolding/data.ts index 4dc7ee6a..0e842fd8 100644 --- a/tests/e2e/scaffolding/data.ts +++ b/tests/e2e/scaffolding/data.ts @@ -11,10 +11,10 @@ export const makeItems = (firstId: number, count: number): TestItem[] => export const starGet = (( index: number, count: number, - success: (items: unknown[]) => void + success: (items: TestItem[]) => void ): void => { success(makeItems(index, count).map(item => ({ ...item, text: `${item.text} *` }))); -}) as DatasourceGet; +}) as DatasourceGet; const minSize = 1; const maxSize = 100; diff --git a/tests/e2e/specs/adapter.reset-persistence.spec.ts b/tests/e2e/specs/adapter.reset-persistence.spec.ts index 5b4f12c6..3802a71b 100644 --- a/tests/e2e/specs/adapter.reset-persistence.spec.ts +++ b/tests/e2e/specs/adapter.reset-persistence.spec.ts @@ -6,14 +6,14 @@ import { makeTest, Misc, starGet, - DatasourceGet, IDatasourceOptional, - TestConfig + TestConfig, + TestItem } from '../scaffolding'; interface ResetVariant { isNew: boolean; - options: IDatasourceOptional; + options: IDatasourceOptional; } type PermanentProp = Adapter.id | Adapter.version; @@ -63,10 +63,8 @@ const reset = (misc: Misc, variant: ResetVariant) => { return misc.adapter.reset(variant.options); } - const datasource = new Datasource({ - get: - variant.options.get ?? - (misc.datasource.get as unknown as DatasourceGet), + const datasource = new Datasource({ + get: variant.options.get ?? misc.datasource.get, settings: variant.options.settings, devSettings: variant.options.devSettings }); diff --git a/tests/e2e/specs/adapter.reset.spec.ts b/tests/e2e/specs/adapter.reset.spec.ts index 0e166dec..c68ce375 100644 --- a/tests/e2e/specs/adapter.reset.spec.ts +++ b/tests/e2e/specs/adapter.reset.spec.ts @@ -6,7 +6,8 @@ import { Routines, starGet, IDatasourceOptional, - TestConfig + TestConfig, + TestItem } from '../scaffolding'; interface ResetSnapshot { @@ -20,7 +21,7 @@ interface ResetSnapshot { interface ResetScenario { config: TestConfig; - options?: IDatasourceOptional; + options?: IDatasourceOptional; scrolls?: number; direction?: Direction; interruption?: boolean; @@ -62,7 +63,7 @@ const settingsScenarios: ResetScenario[] = [baseConfigs[0], baseConfigs[2]].map( settings: { ...config.datasourceSettings, startIndex: 999 - } as IDatasourceOptional['settings'] + } as IDatasourceOptional['settings'] } }) ); @@ -172,7 +173,7 @@ const expectReset = ( misc.expect.uniqueItems(); }; -const reset = (misc: Misc, options?: IDatasourceOptional) => +const reset = (misc: Misc, options?: IDatasourceOptional) => options ? misc.adapter.reset(options) : misc.adapter.reset(); const expectScrollListeners = ( @@ -358,7 +359,7 @@ describe('Adapter Reset Spec', () => { const before = takeSnapshot(misc); const result = await misc.adapter.reset( - options as IDatasourceOptional + options as IDatasourceOptional ); expect(result.success).toBe(false); diff --git a/tests/unit/settings.spec.ts b/tests/unit/settings.spec.ts index b61ba03c..6e5cfb69 100644 --- a/tests/unit/settings.spec.ts +++ b/tests/unit/settings.spec.ts @@ -210,7 +210,7 @@ describe('Settings', () => { expect(calls).toBe(1); }); - it('falls back to null for invalid or asynchronous factory results', () => { + it('falls back to null when a factory does not return an element', () => { [ undefined, null,