Utilities

IdleTimer

Calls on_idle() after a time without mouse, key, touch or scroll, with a warning dialog and a Stay button first.

#utility#time#security#dialog#interactive

Demo

Preview

Hold still for 4 seconds: a warning appears at the bottom right; 4 more and it says you went idle. Events: none yet.

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/idle_timer.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/idle_timer.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/idle_timer.qwe
component IdleTimerDemoLive() {    state log = []    <div.demo-stack>        <IdleTimer seconds={8} warn={4} title="Still reading?" message="This demo signs you out in {s} seconds." button="I'm here"                   on_warn={() => { log = push(log, "warned") }}                   on_active={() => { log = push(log, "back") }}                   on_idle={() => { log = push(log, "idle") }}/>        <p.demo-note>Hold still for 4 seconds: a warning appears at the bottom right; 4 more and it says you went idle. Events: {len(log) == 0 ? "none yet" : join(log, ", ")}.</p>    </div>}view IdleTimerDemo() =    <div.demo-stack>        <IdleTimerDemoLive/>    </div>

Source

ui/idle_timer.qwe
// IdleTimer: watches for a stretch with no mouse, wheel, key or touch, warns// a while before it is up, and then calls on_idle(): to sign someone out of a// bank's page, to pause a game, to stop polling. The warning is a small dialog// with the seconds left and a button to stay.////   <IdleTimer seconds={900} warn={60} on_idle={() => { navigate("/signed-out") }}/>//   <IdleTimer seconds={300} warn={30} title="Still there?" message="You will be signed out in {s} seconds." button="I'm here"//              on_warn={() => { ... }} on_active={() => { ... }}/>//// seconds: how long without input until it is idle (300). warn: how many// seconds before that the warning shows (60; 0 for none). title, message// ({s} is the seconds left) and button: the warning's words. on_idle(): called// once, with nothing, when the time is up; then it waits for input to start// again. on_warn(): when the warning shows. on_active(): when input comes after// a warning. Inside one of your components. Input is watched on the whole// page; the dialog is role="alertdialog", so screen readers hear it.import ui/theme.qwe use ui_clslet idle_timer_css = css`.ui-idle-host { display: contents; }.ui-idle { position: fixed; z-index: 75; right: 16px; bottom: 16px; width: min(360px, calc(100vw - 32px)); padding: 14px 16px;  border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg);  box-shadow: var(--ui-shadow-lg); font: 14px/1.5 var(--ui-font); animation: ui-idle-in .2s ease-out; }.ui-idle-title { margin: 0 0 4px; font-weight: 600; }.ui-idle-text { margin: 0; color: var(--ui-muted); }.ui-idle-text b { color: var(--ui-fg); font-variant-numeric: tabular-nums; }.ui-idle-bar { height: 4px; margin: 10px 0; overflow: hidden; border-radius: 999px; background: var(--ui-subtle); }.ui-idle-fill { height: 100%; background: var(--ui-warning); transition: width 1s linear; }.ui-idle-btn { height: 34px; padding: 0 14px; border: 0; border-radius: var(--ui-radius-sm); background: var(--ui-accent); color: var(--ui-accent-fg);  font: 500 14px/1 var(--ui-font); cursor: pointer; }.ui-idle-btn:hover { background: color-mix(in srgb, var(--ui-accent) 88%, var(--ui-fg)); }.ui-idle-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }@keyframes ui-idle-in { from { opacity: 0; transform: translateY(8px); } }@media (prefers-reduced-motion: reduce) { .ui-idle { animation: none; } .ui-idle-fill { transition: none; } }`component IdleTimer(seconds: int = 300, warn: int = 60, title: str = "Are you still there?",                    message: str = "For your safety you will be signed out in {s} seconds.", button: str = "Stay signed in",                    on_idle = null, on_warn = null, on_active = null, class: str? = null) {    style global idle_timer_css    state last = now()    state at = now()    state fired = false    state warned = false    let idle = floor((at - last) / 1000)    let left = max([0, seconds - idle])    let warning = !fired && warn > 0 && left <= warn    if !fired { every(1000, () => {        at = now()        if at - last >= seconds * 1000 {            fired = true            warned = false            on_idle && on_idle()        } else if warn > 0 && !warned && (at - last) / 1000 >= seconds - warn {            warned = true            on_warn && on_warn()        }    }) }    let wake = (_e = null) => {        let t = now()        if t - last > 500 || fired {            let was = warned || fired            last = t            at = t            fired = false            warned = false            if was && on_active != null { on_active() }        }    }    <div class="ui-idle-host" @mount={(_el) => {        let d = js("document")        for ev in ["mousemove", "mousedown", "keydown", "touchstart", "wheel", "pointerdown"] {            d.addEventListener(ev, wake, { passive: true, capture: true })        }        () => { for ev in ["mousemove", "mousedown", "keydown", "touchstart", "wheel", "pointerdown"] { d.removeEventListener(ev, wake, { capture: true }) } }    }}>        {warning && <div class={ui_cls(["ui-idle", class])} role="alertdialog" aria-labelledby="ui-idle-title" aria-describedby="ui-idle-text">            <p.ui-idle-title id="ui-idle-title">{title}</p>            <p.ui-idle-text id="ui-idle-text">{raw(escape(message) | replace("{s}", `<b>${left}</b>`))}</p>            <div.ui-idle-bar aria-hidden="true"><div.ui-idle-fill style={`width: ${left * 100 / warn}%`}></div></div>            <button.ui-idle-btn type="button" @click={wake}>{button}</button>        </div>}    </div>}
Thread
67Useful? Like it, or add to its thread below.

Thread 0 posts

On IdleTimer: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.