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
5 changes: 3 additions & 2 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ <h2 id="project_tagline">
<a href="https://github.com/dhilt/vscroll/blob/main/demo/index-minimal.html">index-minimal.html</a>.
</p>
<p>
Another minimal demo is for table layout: <a href="./table-demo.html">table-demo.html</a>.
A <a href="./toolbar-demo.html">Custom Routines demo</a> shows how a sticky toolbar
can be excluded from the visible area.
</p>
</div>
</div>
Expand All @@ -53,4 +54,4 @@ <h2 id="project_tagline">
<script src="./index.js"></script>
</body>

</html>
</html>
153 changes: 0 additions & 153 deletions demo/table-demo.html

This file was deleted.

128 changes: 128 additions & 0 deletions demo/toolbar-demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<!doctype html>
<html>

<head>
<title>vscroll custom Routines demo</title>
<meta charset="utf-8" />
<style>
#viewport {
height: 300px;
width: 300px;
overflow-y: auto;
scroll-padding-top: 40px;
}

#toolbar {
position: sticky;
top: 0;
height: 40px;
line-height: 40px;
background: #eee;
z-index: 1;
}

#content>[data-sid] {
height: 22px;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/vscroll/dist/bundles/vscroll.umd.js"></script>
</head>

<body>
<h3>VScroll Custom Routines Demo (CDN)</h3>
<p>
Custom Routines exclude the sticky toolbar from the measured viewport. Only rows below it are
reported as visible.
</p>
<p>
<button id="scroll">Scroll down 5 items</button>
<button id="reload">Reload</button>
</p>
<div id="viewport">
<div id="toolbar">Visible items: <span id="visible"></span></div>
<div id="content">
<div data-padding-backward></div>
<div data-padding-forward></div>
</div>
</div>
<script>
const Datasource = VScroll.makeDatasource();
const DATA = Array.from({ length: 1000 }, (_, i) => ({ text: `Item ${i + 1}` }));
const datasource = new Datasource({
get: (index, count, success) => success(DATA.slice(index - 1, index - 1 + count)),
settings: { minIndex: 1, maxIndex: DATA.length }
});

class ToolbarRoutines extends VScroll.Routines {
getElementParams(element) {
const rect = super.getElementParams(element);
// Crop only the viewport; row and content measurements stay unchanged.
if (element === this.viewport) {
// The CSS inset matches the toolbar's height in pixels.
const inset = parseFloat(getComputedStyle(element).scrollPaddingTop) || 0;
rect.y += inset; // Move the top edge below the toolbar.
rect.height -= inset; // Keep the bottom edge unchanged.
}
return rect;
}
}

const contentElt = document.getElementById('content');
const fwdPaddingElt = contentElt.querySelector('[data-padding-forward]');
const visibleHintElt = document.getElementById('visible');
const doScrollElt = document.getElementById('scroll');
const doReloadElt = document.getElementById('reload');

function renderRow(item) {
const row = (item.element ??= document.createElement('div'));
row.dataset.sid = String(item.$index);
row.style.position = item.invisible ? 'fixed' : '';
row.style.top = item.invisible ? '-99999px' : '';
row.textContent = `${item.$index}: ${item.data.text}`;
return row;
}

let previous = [];

function run(items) {
for (const item of previous) {
if (!items.includes(item)) {
item.element?.remove();
}
}
let next = fwdPaddingElt;
for (const item of [...items].reverse()) {
const row = renderRow(item);
if (row.nextElementSibling !== next) {
contentElt.insertBefore(row, next);
}
next = row;
}
previous = [...items];
}

const adapter = datasource.adapter;
const updateVisible = () => {
visibleHintElt.textContent =
`${adapter.firstVisible.$index}–${adapter.lastVisible.$index}`;
};
adapter.firstVisible$.on(updateVisible);
adapter.lastVisible$.on(updateVisible);

const workflow = new VScroll.Workflow({
consumer: { name: 'VScroll Custom Routines Demo (CDN)', version: 'not published' },
element: contentElt,
datasource,
run,
Routines: ToolbarRoutines
});

doScrollElt.onclick = () => {
const index = Math.min(adapter.firstVisible.$index + 5, DATA.length);
adapter.fix({ scrollToItem: item => item.$index === index });
};
doReloadElt.onclick = () => adapter.reload();
</script>
</body>

</html>
28 changes: 22 additions & 6 deletions docs/routines.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,36 @@ The exported `IRoutines` interface describes the class contract. The following i
| `viewport` | `HTMLElement` | Viewport element determined by `getViewportElement()`. |
| `settings` | `IRoutines['settings']` | Reduced settings: `viewport` is the explicitly configured viewport or `null`; `horizontal` selects horizontal mode; `window` holds the value of `windowViewport`. |

For example, the [table demo](../demo/table-demo.html) uses `tbody` as the content element and explicitly selects an outer scrollable container through `settings.viewportElement`. The `getOffset()` method determines the list's offset relative to the viewport. For this layout, the override sets it to the height of the preceding table header:
For example, a sticky toolbar can cover the top of a vertical element viewport. Default Routines measure the full viewport, so rows behind the toolbar can still be reported as visible. `scroll-padding-top` accounts for a 40px toolbar when the browser scrolls to an item:

```css
#viewport { scroll-padding-top: 40px; }
```

`ToolbarRoutines` applies the same inset to the viewport's measured rectangle:

```js
import { Routines, Workflow } from 'vscroll';

class TableRoutines extends Routines {
getOffset() {
return this.viewport.querySelector('thead')?.offsetHeight || 0;
class ToolbarRoutines extends Routines {
getElementParams(element) {
const rect = super.getElementParams(element);
// Crop only the viewport; row and content measurements stay unchanged.
if (element === this.viewport) {
// The CSS inset matches the toolbar's height in pixels.
const inset = parseFloat(getComputedStyle(element).scrollPaddingTop) || 0;
rect.y += inset; // Move the top edge below the toolbar.
rect.height -= inset; // Keep the bottom edge unchanged.
}
return rect;
}
}

new Workflow({ Routines: TableRoutines, ... });
new Workflow({ Routines: ToolbarRoutines, ... });
```

The adjusted top edge and height describe the uncovered area; row and content measurements remain unchanged. See the [working demo](https://dhilt.github.io/vscroll/toolbar-demo.html).

The inherited constructor initializes `element` and reduced `settings`, assigns `viewport` from `getViewportElement()`, then calls `onInit(settings)`. Both hooks run before subclass fields are initialized; `this.viewport` is not yet available inside `getViewportElement()`. A custom constructor must forward its arguments to `super`; overriding it is usually unnecessary.

## Hiding and revealing new rows
Expand Down Expand Up @@ -127,7 +143,7 @@ Sizes and coordinates are expressed in CSS pixels. The active dimension is heigh
| `setSizeStyle(element: HTMLElement, value: number): void` | Rounds the size, clamps it to zero or greater, and writes inline `height` or `width` in pixels. Used for padding elements. |
| `getEdge(element: HTMLElement, direction: Direction): number` | Returns the corresponding edge from `getElementParams(element)`. |
| `getViewportEdge(direction: Direction): number` | Returns the corresponding edge of the window rectangle in window mode; otherwise, `getEdge(viewport, direction)`. |
| `getOffset(): number` | Returns the content element's `offsetTop` or `offsetLeft`, minus the viewport's corresponding offset. No subtraction is performed in window mode. |
| `getOffset(): number` | Returns the content element's top/left offset from the viewport at scroll position zero, using measured edges and current scroll position. In window mode, returns its document-relative position. |

### Visibility and scrolling

Expand Down
8 changes: 5 additions & 3 deletions src/classes/domRoutines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,11 @@ export class Routines implements IRoutines {
}

getOffset(): number {
const get = (element: HTMLElement) =>
(this.settings.horizontal ? element.offsetLeft : element.offsetTop) || 0;
return get(this.element) - (!this.settings.window ? get(this.viewport) : 0);
return (
this.getEdge(this.element, Direction.backward) -
this.getViewportEdge(Direction.backward) +
this.getScrollPosition()
);
}

scrollTo(element: HTMLElement, argument?: boolean | ScrollIntoViewOptions): void {
Expand Down
Loading
Loading