Hi,
I have a remote TV displaying data in table component and I would like to scroll thru records automatically to display all the records if they overflowing. Is there a script or build-in method I can use to do that ?
Hi,
I have a remote TV displaying data in table component and I would like to scroll thru records automatically to display all the records if they overflowing. Is there a script or build-in method I can use to do that ?
One simple method would be to enable pagination on the table (configure the 'pager' section of component props) and then use a timer in an expression binding on the 'activePage' property to update which page is displayed at a pre-specified interval (assuming you know exactly how many pages will exist based on the total number of rows and the number of rows per page. If not, you could still calculate this by looking at the number of rows in your dataset at runtime and use a custom prop to store the page count for reference). This wouldn't really be "scrolling", but I'd argue that it's hard to track data in a scrolling table and I'd prefer the rows to be static and jump between pages anyway.
I like @djhammett77's suggestion, but actual scrolling might be possible by triggering a .focus() on an embedded view of a table cell. There was a similar question not long ago for an Accordion list, and I was able to get it working in that context:
I'd be interested to see if anyone gets this method to work in a table, given that it originally relied on an onStartup event when a new item is added to an Accordion and the data rows in a table are typically all available when the table first populates rather than being added one at a time... I'm sure it would be possible to append to the table data once piece at a time to enable something like this, but I'd be concerned that it would impact performance (churn) in obtaining the data from the source. Just rough thoughts, though
I have a hacky use-at-your-own-risk solution which uses the Markdown component with JavaScript injection. IA has said that they plan on patching script injection, but for the time being it works. It could be worth looking into @bmusson's Embr-Periscope module which provides a way of executing JavaScript.
To get it to work you'll have to:
meta.domId to match the ID in the script, currently "scrolling-table" (or change the ID)props.virtualized to false on the tableMarkdown component with script:
[
{
"type": "ia.display.markdown",
"version": 0,
"props": {
"markdown": {
"sourcePos": true,
"escapeHtml": false
},
"source": "<img src=\"x\" style=\"display:none\" onerror=\"(() => {\n const ID = '#scrolling-table';\n const PAUSE = 3000;\n const init = () => {\n const el = document.querySelector(ID);\n if (!el) return setTimeout(init, 500);\n if (el.initialized) return;\n el.initialized = true;\n el.scrollIsReset = false;\n const startScroll = () => {\n if (el.scrollInterval || el.scrollIsReset) return;\n el.scrollInterval = setInterval(() => {\n const body = el.querySelector('.flexBody');\n if (!body || el.scrollIsReset) return;\n if (body.scrollTop + body.offsetHeight >= body.scrollHeight - 2) {\n el.scrollIsReset = true;\n el.scrollTimeout = setTimeout(() => {\n body.scrollTo({ top: 0, behavior: 'smooth' });\n body.addEventListener('scrollend', function onEnd() {\n body.removeEventListener('scrollend', onEnd);\n el.scrollTimeout = setTimeout(() => { el.scrollIsReset = false; }, PAUSE);\n }, { once: true });\n }, PAUSE);\n } else {\n body.scrollTop += 1;\n }\n }, 30);\n };\n el.addEventListener('mouseover', () => { \n clearInterval(el.scrollInterval); el.scrollInterval = null;\n clearTimeout(el.scrollTimeout); el.scrollIsReset = false;\n const body = el.querySelector('.flexBody');\n if (body) { body.scrollTop = body.scrollTop; }\n }, true);\n el.addEventListener('mouseout', (e) => {\n if (!el.contains(e.relatedTarget)) startScroll();\n }, true);\n el.scrollIsReset = true;\n el.scrollTimeout = setTimeout(() => { el.scrollIsReset = false; startScroll(); }, PAUSE);\n };\n init();\n})()\">"
},
"meta": {
"name": "AutoScrollTable"
},
"position": {
"shrink": 0,
"display": false
},
"custom": {},
"propConfig": {
"props.source": {
"access": "PUBLIC"
}
}
}
]
Yup, you can easily do this with Periscope. This example uses a React component to drive the scroll behavior on the Table.
embr-testing_2026-09-26_0054.zip (26.9 KB)
The meat and potatoes:
// Examples/AutoScroll
const { useEffect } = React;
type AutoScrollTableProps = {
/**
* The native Perspective table to scroll.
* Either a bare DOM id (the table's meta.domId, e.g. "scrolling-table")
* or any CSS selector (e.g. "#scrolling-table", ".my-table-class").
*/
table: string;
/** Pause (ms) before scrolling starts, at the bottom, and after returning to the top. */
pause?: number;
/** Tick rate (ms) of the scroll loop. */
interval?: number;
/** Pixels scrolled per tick. Fractional values are fine. */
step?: number;
/** Stop scrolling while the mouse is over the table. */
pauseOnHover?: boolean;
/** Log what the component finds to the browser console. */
debug?: boolean;
};
const toSelector = (target: string): string =>
/^[#.[]/.test(target) || /[\s>:~+]/.test(target) ? target : `#${CSS.escape(target)}`;
export default function AutoScrollTable({
table,
pause = 3000,
interval = 30,
step = 1,
pauseOnHover = true,
debug = false,
}: AutoScrollTableProps) {
useEffect(() => {
if (!table) return;
const selector = toSelector(table);
let el: HTMLElement | null = null;
let hovered = false;
let pos = 0; // float accumulator so small steps still move at non-100% zoom
let tickTimer: number | undefined;
let waitTimer: number | undefined;
let pollTimer: number | undefined;
const log = (...args: unknown[]) => {
if (debug) console.log("[AutoScrollTable]", ...args);
};
const isScrollable = (node: HTMLElement) => {
const overflowY = getComputedStyle(node).overflowY;
return (
(overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") &&
node.scrollHeight > node.clientHeight + 1
);
};
// The element that actually scrolls isn't always '.flexBody' (it varies by
// Perspective version/table config), so find the first vertically scrollable
// descendant. Cached, and re-found if Perspective replaces it.
let scroller: HTMLElement | null = null;
const findScroller = (root: HTMLElement): HTMLElement | null => {
const preferred = root.querySelector<HTMLElement>(".flexBody");
if (preferred && isScrollable(preferred)) return preferred;
if (isScrollable(root)) return root;
for (const node of root.querySelectorAll<HTMLElement>("*")) {
if (isScrollable(node)) return node;
}
return null;
};
const getBody = () => {
if (!el) return null;
if (scroller && scroller.isConnected && el.contains(scroller) && isScrollable(scroller)) {
return scroller;
}
const next = findScroller(el);
if (next !== scroller) {
log(next ? "scroll container:" : "no scrollable container found (rows may fit)", next);
if (next) log("scrollHeight", next.scrollHeight, "clientHeight", next.clientHeight);
}
scroller = next;
return scroller;
};
const clearTimers = () => {
window.clearInterval(tickTimer);
window.clearTimeout(waitTimer);
window.clearInterval(pollTimer);
tickTimer = waitTimer = pollTimer = undefined;
};
const startScrolling = () => {
clearTimers();
if (hovered) return;
pos = getBody()?.scrollTop ?? 0;
tickTimer = window.setInterval(tick, interval);
};
const resumeAfter = (ms: number) => {
clearTimers();
waitTimer = window.setTimeout(startScrolling, ms);
};
const resetToTop = () => {
clearTimers();
waitTimer = window.setTimeout(() => {
const body = getBody();
if (!body) return startScrolling();
body.scrollTo({ top: 0, behavior: "smooth" });
// Poll instead of relying on 'scrollend' (not supported everywhere).
const started = Date.now();
pollTimer = window.setInterval(() => {
const current = getBody();
const timedOut = Date.now() - started > 3000;
if (!current || current.scrollTop <= 1 || timedOut) {
if (current && timedOut) current.scrollTop = 0;
resumeAfter(pause);
}
}, 50);
}, pause);
};
const tick = () => {
const body = getBody();
if (!body) return;
const max = body.scrollHeight - body.clientHeight;
if (max <= 1) return; // content fits, nothing to scroll
// Resync if the user scrolled or the rows changed underneath us.
if (Math.abs(body.scrollTop - pos) > 2) pos = body.scrollTop;
if (body.scrollTop >= max - 2) {
resetToTop();
return;
}
pos = Math.min(pos + step, max);
body.scrollTop = pos;
};
const onEnter = () => {
hovered = true;
clearTimers();
const body = getBody();
if (body) body.scrollTop = body.scrollTop; // halts an in-flight smooth scroll
};
const onLeave = () => {
hovered = false;
startScrolling();
};
const attach = (node: HTMLElement) => {
el = node;
log("attached to", selector, el);
if (pauseOnHover) {
el.addEventListener("mouseenter", onEnter);
el.addEventListener("mouseleave", onLeave);
hovered = el.matches(":hover");
if (hovered) log("mouse is over the table; waiting for it to leave");
}
resumeAfter(pause);
};
const detach = () => {
clearTimers();
if (el) {
el.removeEventListener("mouseenter", onEnter);
el.removeEventListener("mouseleave", onLeave);
}
el = null;
scroller = null;
hovered = false;
};
// Find the table, and re-find it if Perspective remounts it.
const watch = () => {
if (el?.isConnected) return;
if (el) detach();
const found = document.querySelector<HTMLElement>(selector);
if (found) attach(found);
else if (!warned) {
warned = true;
log("no element matches", selector, "- still looking");
}
};
let warned = false;
watch();
const watchTimer = window.setInterval(watch, 500);
return () => {
window.clearInterval(watchTimer);
detach();
};
}, [table, pause, interval, step, pauseOnHover, debug]);
// Behaviour-only component: renders nothing.
return null;
}
@nminchin Claude authored ![]()
@bmusson Very interesting! It's quite annoying that when reading a table (set to 50 rows), moving to the next page leaves you at the bottom. Is it possible to intercept the page change to automatically scroll back to the top row?
Haha nice one! Sorry if I converted you ![]()