Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,10 @@ Every `Workflow` requires a datasource object to supply items on request and, op
| [`bufferSize`](docs/configuration.md#settings) | `5` | Minimum fetch batch target, not a limit on buffered items. |
| [`itemSize`](docs/configuration.md#size-estimates-and-layout) | `NaN` | Initial item-size estimate in pixels; measured automatically when omitted. |
| [`sizeStrategy`](docs/configuration.md#size-estimates-and-layout) | `'average'` | Estimate unknown item sizes using `'average'`, `'frequent'` or `'constant'`. |
| [`viewportElement`](docs/configuration.md#viewport-and-horizontal-scrolling) | `null` | Custom viewport element or element factory; defaults to the content element's parent. Experimental. |
| [`windowViewport`](docs/configuration.md#viewport-and-horizontal-scrolling) | `false` | Use the browser window as the viewport. |
| [`horizontal`](docs/configuration.md#viewport-and-horizontal-scrolling) | `false` | Scroll horizontally instead of vertically. |
| [`inverse`](docs/configuration.md#other-settings) | `false` | Align short content to the bottom or right without reversing item order. Experimental. |
| [`viewportElement`](docs/configuration.md#viewport-scrolling-axis-and-alignment) | `null` | Custom viewport element or element factory; defaults to the content element's parent. Experimental. |
| [`windowViewport`](docs/configuration.md#viewport-scrolling-axis-and-alignment) | `false` | Use the browser window as the viewport. |
| [`horizontal`](docs/configuration.md#viewport-scrolling-axis-and-alignment) | `false` | Scroll horizontally instead of vertically. |
| [`alignShortContentToEnd`](docs/configuration.md#viewport-scrolling-axis-and-alignment) | `false` | Align short content to the bottom or right without reversing item order. |
| [`infinite`](docs/configuration.md#settings) | `false` | Keep loaded items instead of clipping them automatically. |
| [`onBeforeClip`](docs/configuration.md#settings) | `null` | Receive clipped items just before they leave the buffer. Experimental. |

Expand Down
8 changes: 5 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,23 +51,25 @@ Before the first render, the scroller estimates how many rows to request for the

After rendering, the scroller measures actual row sizes. With `'average'` (the default) or `'frequent'`, later requests use those measurements rather than the initial `itemSize` estimate. With `'constant'`, `itemSize` remains the estimate for unseen rows; if omitted, the first measured size takes its place. No strategy forces equal CSS sizes. In TypeScript, use the exported `SizeStrategy` enum (for example, `SizeStrategy.Frequent`). See [Rendering](rendering.md) for the DOM and layout requirements.

### Viewport and horizontal scrolling
### Viewport, scrolling axis and alignment

The viewport is the scrollable area whose position and size the scroller tracks; by default, it is the content element's parent. `windowViewport` and `viewportElement` choose a different scroll target, while `horizontal` changes the scroll axis.
The viewport is the scrollable area whose position and size the scroller tracks; by default, it is the content element's parent. `windowViewport` and `viewportElement` choose a different scroll target, while `horizontal` changes the scroll axis and `alignShortContentToEnd` controls the alignment of short content.

| 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. |
| `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.

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.

### Other settings

| Setting | Type | Default | Effect |
| --- | --- | --- | --- |
| `inverse` | `boolean` | `false` | Align content shorter than the viewport to the bottom/right without reversing item order. Experimental. |
| `onBeforeClip` | `(items: IAdapterItem<T>[]) => void` or `null` | `null` | Called synchronously after clipped rows are hidden, before their items leave the buffer. Experimental. |

## Development settings
Expand Down
2 changes: 1 addition & 1 deletion docs/rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const element = document.getElementById('content');
const workflow = new Workflow({ element, ... });
```

By default, the Scroller uses the content element's parent as its viewport; [Configuration](configuration.md#viewport-and-horizontal-scrolling) explains how to select a different scroll target.
By default, the Scroller uses the content element's parent as its viewport; [Configuration](configuration.md#viewport-scrolling-axis-and-alignment) explains how to select a different scroll target.

The padding elements remain mounted as rows come and go; the Scroller sizes them to represent virtual space. Rows must be measurable. By default, their size comes from `getBoundingClientRect()`, which excludes margins; unaccounted margins or gaps distort the virtual geometry. Row measurement can be customized through [Routines](routines.md#geometry-and-padding-sizes).

Expand Down
22 changes: 6 additions & 16 deletions src/classes/paddings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,12 @@ export class Paddings {
reset(viewportSize: number, startIndex: number, offset: number): void {
const positive = this.getPositiveSize(startIndex, viewportSize, offset);
const negative = this.getNegativeSize(startIndex);
if (this.settings.inverse) {
this.forward.reset(negative);
this.backward.reset(positive);
const diff = viewportSize - this.backward.size - offset;
if (diff > 0) {
this.backward.size += diff;
this.forward.size -= diff;
}
} else {
this.forward.reset(positive);
this.backward.reset(negative);
const diff = viewportSize - this.forward.size - offset;
if (diff > 0) {
this.backward.size -= diff;
this.forward.size += diff;
}
this.forward.reset(positive);
this.backward.reset(negative);
const diff = viewportSize - this.forward.size - offset;
if (diff > 0) {
this.backward.size -= diff;
this.forward.size += diff;
}
}

Expand Down
28 changes: 26 additions & 2 deletions src/classes/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class Settings<Data = unknown> implements ISettings, IDevSettings {
horizontal: boolean;
windowViewport: boolean;
viewportElement: HTMLElement | (() => void) | null;
inverse: boolean; // if true, bwd padding element will have a priority when filling the viewport (if lack of items)
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

Expand Down Expand Up @@ -150,6 +150,15 @@ export class Settings<Data = unknown> implements ISettings, IDevSettings {
*/
viewport: HTMLElement | null;

/** @deprecated Use alignShortContentToEnd. */
get inverse(): boolean {
return this.alignShortContentToEnd;
}

set inverse(value: boolean) {
this.alignShortContentToEnd = value;
}

constructor(
settings: ISettings<Data> | undefined,
devSettings: IDevSettings | undefined,
Expand All @@ -160,6 +169,7 @@ export class Settings<Data = unknown> implements ISettings, IDevSettings {
this.instanceIndex = instanceIndex;
this.initializeDelay = this.getInitializeDelay();
this.viewport = this.getViewport();
this.alignShortContentToEnd = this.getAlignShortContentToEnd(settings);
// todo: min/max indexes must be ignored if infinite mode is enabled ??
}

Expand Down Expand Up @@ -192,10 +202,24 @@ export class Settings<Data = unknown> implements ISettings, IDevSettings {
return this.viewportElement;
}
const value = this.viewportElement();
const result = validateOne({ value }, 'value', { validators: [VALIDATORS.ELEMENT] });
const result = validateOne({ value }, 'value', {
validators: [VALIDATORS.ELEMENT],
mandatory: true
});
if (!result.isValid) {
return null; // fallback to default (null) if Function didn't return HTML element synchronously
}
return result.value as HTMLElement;
}

getAlignShortContentToEnd(settings: ISettings<Data> | undefined): boolean {
if (settings?.alignShortContentToEnd !== undefined || settings?.inverse === undefined) {
return this.alignShortContentToEnd;
}
return validateOne(
{ inverse: settings.inverse },
'inverse',
SETTINGS.alignShortContentToEnd
).value as boolean;
}
}
4 changes: 2 additions & 2 deletions src/inputs/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ enum Settings {
horizontal = 'horizontal',
windowViewport = 'windowViewport',
viewportElement = 'viewportElement',
inverse = 'inverse',
alignShortContentToEnd = 'alignShortContentToEnd',
onBeforeClip = 'onBeforeClip',
sizeStrategy = 'sizeStrategy'
}
Expand Down Expand Up @@ -91,7 +91,7 @@ export const SETTINGS: ICommonProps<Settings> = {
validators: [OR([ELEMENT, FUNC])],
defaultValue: null
},
[Settings.inverse]: {
[Settings.alignShortContentToEnd]: {
validators: [BOOLEAN],
defaultValue: false
},
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface Settings<T = unknown> {
horizontal?: boolean;
windowViewport?: boolean;
viewportElement?: HTMLElement | (() => void) | null;
alignShortContentToEnd?: boolean;
/** @deprecated Use alignShortContentToEnd. */
inverse?: boolean;
onBeforeClip?: ItemsProcessor<T> | null;
sizeStrategy?: SizeStrategy;
Expand Down
4 changes: 2 additions & 2 deletions src/processes/adjust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,13 @@ export default class Adjust extends BaseProcessFactory(CommonProcess.adjust) {
const scrollSize = bwdSize + bufferSize + fwdSize;
const viewportSizeDiff = viewport.getSize() - scrollSize;
if (viewportSizeDiff > 0) {
if (settings.inverse) {
if (settings.alignShortContentToEnd) {
bwdSize += viewportSizeDiff;
} else {
fwdSize += viewportSizeDiff;
}
scroller.logger.log(() =>
settings.inverse
settings.alignShortContentToEnd
? 'backward'
: 'forward' + ` padding will be increased by ${viewportSizeDiff} to fill the viewport`
);
Expand Down
Loading
Loading