CHANGELOG.md · read at bc6ed6a
Changelog
1.6.1 - 2026-10-30
Fixed
-
ajaxer - a channel without a url sends nothing and says
No target URL provided.: the test was=== undefinedwhereAjaxer.get()answersnullfor a key never set, so the data went to the page's own address. -
ajaxer - a channel without a target, called as
Ajaxer.send(...), no longer throws (Cannot read properties of undefined (reading 'createDocumentFragment'), before any request, no callback:thiswas the Ajaxer object, taken for the target and the loader's container). The caller stands in when it is an element or a jQuery object (Ajaxer.send.call(element, ...)); otherwise nothing is marked, no loader is shown, the request goes and the callbacks are called once,thisan empty jQuery object. -
ajaxer -
retry_limitcounts the tries after a failure, as the README and the options table said:1, the default, sends a failed request once more. A retry was made only when the failed request was not the first of its channel in flight, and the number was the tries in all, so a request sent alone was never retried. A failure is retried when there was no answer or an answer of 500 or more (not under 500, not an abort, not aparsererror); each retry is in the channel's list of requests in flight (query_limitcounts it,abort()stops it);completeis called once, at the end of the last try. -
ajaxer - a value set without a name is the default of every channel for
url,target,loader_containerandquery_limittoo, as for the other keys:Ajaxer.set('url', '/api')andconfigure({query_limit: 1})were stored and read by nothing.tests/ajaxer-channels.html.
-
navigation - a script of the swapped block runs once, as on a page load. With
global_codeon (the default) an inline script ran twice: given toeval()(insideevalScript(), where itsvarstayed local and a module could not parse), then natively when its fresh copy was put in. Only the copy runs now; one that throws is the browser's uncaught error, and the swap goes on. -
navigation - a link with a
downloadattribute is left to the browser, which saves the file under the attribute's name. It was taken by the navigation: its file fetched by XHR, then again bylocation.hrefas "no page", the name lost. -
navigation - the
<previous>-to-<next>layout class (main-to-admin) stays on<html>through the swap, as the swap meant: it spared the class built the other way round (admin-to-main) and took the real one off before the new block was in. -
navigation -
lazyload: falseswitches the lazy loading off (its runs atDOMContentLoaded, atready(), at each swap, and the pictures already watched); the option was read by nothing. -
navigation -
skip_transition_for_cache(withuse_view_transitions): a page given back from the history's cache (Back, Forward) is swapped without the view transition; the option was read by nothing. -
navigation -
rescue_reload: a failure that comes with an empty page (a 500 with nothing in it) no longer replaces the document with nothing - the block says so (.error) and the page reloadsrescue_reloadlater. The test compared the body with the string"undefined", and a parsed document always has a head: the reload never happened. The value is a duration read as the others (5000,"5s","300ms"; it went throughparseDuration, in seconds, so"5s"was 5 ms).tests/navigation-swap.html, tests/navigation-options.html, tests/navigation-rescue.html.
-
core -
Flow.scrollToat aspeed: a movement up (or left) to anywhere but 0 took the time of the whole position (3000 -> 2000 at 1000 px/s: 3 s); each axis now takes its way to go over the speed, up as down. -
core - the reader's wheel, finger or press on an element scroller stops its
Flow.scrollTomovement, as on the page: the movement listens on its scroller for its own time. Only the window was heard, so an element's movement went on under a wheel (ended "arrived") unless a script had registeredFlow.onInterrupt(el, Flow.release). -
sortable - a press released before the drag threshold is forgotten wherever it ends: a
pointerupanywhere in the document, apointercancel, a move with no button held. Only apointerupon the container forgot it: released a few px outside the list, the next move of the pointer over it dragged with no button held. A drag the browser cancels (pointercancel) puts the item back where it was, withoutonChangeoronMerge. -
sticky -
scrolllock: during a held way to a magnet the wheel and the finger move nothing, through realwheel(ormousewheel) andtouchmovelisteners, not passive, on the window, taken off when the page is given back;Sticky.allowScrolling()is false meanwhile. The names were jQuery-style namespaced ones (wheel.preventDefault) that the nativeaddEventListenertook for events that never fire,enableScroll()anddisableScroll()were inverted, aDOMMouseScrolllistener was added at each way's end and never removed, andallowScrolling()always answered true. The keys are left alone (the page's overflow is hidden meanwhile, and a magnet key past the last magnet is the browser's). Written against the DOM spec and checked in Chromium; not checked in Firefox.tests/core-scroll.html, tests/sortable-press.html, tests/sticky-scrolllock.html; Flow.version checked against package.json in tests/units-and-listeners.html.
Removed
- ajaxer -
Ajaxer.ready(list, options)no longer stores a non-emptylistunderAjaxer.settings.list, which nothing read. The parameter stays, not read, so that a call writtenready(list, options)keeps its options. - sticky - the options
tick("1ms") andscrollsnap_start("start") are no longer declared inSticky.settings(public API): nothing readtick, andscrollsnap_startwas read into a variable only printed withdebug(the snap always meets a magnet by its top under the scroll padding). A value given for them is stored as any unknown key and read by nothing, as before.
1.6.0 - 2026-10-26
- navigation - the progress bar never blinks. It had been drawn for as
long as
html.loadingwas set, a few frames on a page from the cache or a static host: a flash at the top of the screen at each click (the flowjs site on GitHub Pages). A navigation now shows it only once it has lastedprogress_bar_delay(200ms), keeps it at leastprogress_bar_min(400ms), then finishes it - filled, faded overprogress_bar_finish(250ms) - on classes of its own (html.progress-bar-shown,progress-bar-done), and says each step intransparent:progress. Its look is custom properties (--transparent-bar-height,-color,-background,-size,-duration,-done,-z): a rainbow in docs/navigation.md. The cursor'sprogressfollows the same timing. tests/navigation-progress.html (a fast navigation: no bar at any moment; a slow one: shown after the delay, at least the minimum, finished, gone; once shown never shorter than the minimum). - navigation - a website in the website:
FlowFrame(src/js/frame.js, in the navigation entry, alsoTransparent.frame; no jQuery). A frame of the page ([data-flow-frame]) whose links and forms load their page in the background and swap its block into the frame alone; its own history (back, forward, the address field, the scroll given back) - the window's is not touched, so the browser's Back leaves the page that holds it; Transparent never hears its clicks;flow:frame:load,flow:frame:error,.flow-frame-loadingon the progress bar's timing. Inline, needing nothing of the server, unlikenest. tests/frame.html. - scenes - a new module,
StickyScenes(src/js/scenes.js, entry@glitchr/flowjs/scenes, stylesheet css/scenes.scss; no jQuery): a section the page scrolls through while its stage stays pinned, its progress 0 to 1 in--scene-progressand inscene:progress, steps (--scene-step,--scene-step-progress), an image sequence on a canvas; prefers-reduced-motion shows its end, static; a scene that is a stop is a tall stop, free across, its keys a screen at a time; the progress is read 400ms past the last scroll event for iOS's late positions. docs/scenes.md. tests/scenes.html, tests/scenes-reduced.html. - scenes - the keyboard: while a scene is pinned, ArrowDown, PageDown and
Space go to where its next step begins, then its end, then the section
after it; ArrowUp, PageUp and Shift+Space back, then the section before -
and from the top of the section after, back to its end. Keys in a field,
on a widget, with a modifier or taken by the page are left alone; the scene
listens on the document, before StickyStops, which leaves it a key it took.
keysoption,data-scene-keys="false",scene:key. tests/scenes-keys.html. - scenes - sideways:
data-scene-axis="x", a scene read by a horizontal scroll of its track ([data-scene-track]), a native overflow-x scroller with scroll-snap on its panels ([data-scene-panel], the steps unless named): trackpad, Shift+wheel, swipe, ArrowLeft/ArrowRight/Home/End on the focused track (made a named region, tabindex 0); progress scrollLeft / (scrollWidth - clientWidth); no length, the page's vertical scroll never held.data-scene-touchedat the first movement, for good;[data-scene-hint]blinks until then (still under reduced motion). - sortable - a hierarchy:
StickySortable.Tree(and$.fn.stickySortableTree) for nested lists. Dragged by its handle, an item lands where a marker shows, at the level the pointer's horizontal place asks for within what its neighbours allow (indent),maxDepth(its own subtree counted) and the items that take children (accepts,data-sortable-leaf); on a touch screen after a long press (longPress). On a handle: ArrowUp/ArrowDown among siblings, ArrowRight into the sibling above, ArrowLeft out of the parent, each said in an aria-live region (labels).[data-sortable-toggle]folds and unfolds (aria-expanded).onChangeandsortable:changewith{item, parent, index, depth, tree};serialize()gives nested ids. tests/sortable-tree.html. - cookie - the default row of a category no longer glues its name to
"Always on" ("The site itselfAlways on"): its parts are separated as
written HTML is, and css/cookie.scss sets
[data-cookie-required]apart (a margin, smaller). A site's own row (<template data-cookie-row>, as omnibase/consent's) is untouched. tests/cookie-banner.html. - cookie - where the banner floats, configurable:
position(the six corners and edges,center), also asCookie.defaults.positionfor the whole site and as the banner's owndata-cookie-position;gutter(--cookie-gap);follow, the cookie button the banner opens next to while that button floats (data-cookie-following,--cookie-follow-block,--cookie-follow-inline, placed again on resize). Nothing named: no attribute, the stylesheet's corner, as before - omnibase/consent'sconsent.positionis written as it was. tests/cookie-position.html. - core - a percentage is a hundredth:
Flow.parseToPixel('15%')(andSticky.percentToPixel,Transparent.percentToPixel) gave 15 times the width, so ascroll-padding-topin % read as thousands of pixels in every module that reads it.Flow.toMilliseconds()reads "1s", "250ms" and plain ms;Flow.versionsays 1.6.0 (it said 1.3.1). - sticky -
swipe_timegate: "1s"is a second: read with parseInt it was 1 ms, and no swipe ever triggered with the default. Sticky's unload no longer calls$(window).off("hashchange"), which dropped a site's own jQuery hashchange handler at the first navigation (the module binds none). tests/units-and-listeners.html.
1.5.4 - 2026-10-26
- stops - a glide told its position late is still ours. iOS Safari scrolls its page in another process, which tells the position back a frame or more after a script set it: read in the next frames, window.scrollY may still be one the glide went through. The glide took it for another's scroll and gave up at the first frame that moved more than 2px; the rest that followed glided again from there, and the page went to its stop by 10-20px steps for seconds without a finger on the screen (anaelletourret's home on an iPhone). A position the glide went through in the last 150ms is now its own, and a scroll that is not - nor a reader's (a finger, a wheel, a press stop a glide as before) - leaves the page where it put it, not glided again from there. tests/stops-lag.html (scrollY told 40ms late: one glide, all the way; a wheel during it still takes over).
1.5.3 - 2026-10-25
- stops - a stop barely taller than the screen (
shortEnd) is a stop the screen holds for the settle too: its band below counts from its top, and so does the halfway bound of its neighbours' bands. A scroll heading down that comes to rest past its end within that band glides back to its top; further down, on to the next stop, within thereach. With 1.5.2 such a rest went on to the next stop whatever the band, and with areachthat stop was out of it: the page stayed between two sections, 78px past the end of panoboutique's third section (31px taller than a screen, reach and bands 140px). Its extra pixels are still read freely. tests/stops-ends.html (reach and bands 140px: 31px and 123px taller, back to its top inside its band, staying past it with the next stop out of reach, on without a reach).
1.5.2 - 2026-10-24
- stops - a stop barely taller than the screen has no end of its own: at
most a quarter of a screen taller (the new
shortEndoption, a share of the screen,data-sticky-short-endon a stop), it rests at its top only. A scroll heading down that comes to rest past it goes on to the next stop - inside its band below it glided back up to its end -, one heading up goes to its top (unlessupis off), and no key stops at its end: an arrow withends(it skipped only a stop at most a line taller), a page key, a key withscreens(whose quarter of a screen is the same threshold, one helper). Its few extra pixels are still read freely. A taller stop keeps its end as before. Withends, omniscient's home took a key, or a wheel's rest, to stop 100-200px below a section's top before going on. tests/stops-ends.html (a stop 150px taller, the keys and the wheel each way; one 60% taller unchanged; the attribute).
1.5.1 - 2026-10-24
- stops -
screens: down, from less than a quarter of a screen before a tall stop's end, a key goes on to the next stop rather than making that short step to the end; up, a step that would rest less than a quarter of a screen under the stop's top goes to the top. With 1.5.0 a section 173px taller than a screen took a screen, then 173px, then the next section (anaelletourret's albums). tests/stops-screens.html.
1.5.0 - 2026-10-24
- stops - the
screensoption (falseby default, so nothing changes until a site asks): withkeys, arrows and page keys read a tall stop a whole screen at a time, from its top to its end (its last screen) exactly, then go on to the next stop's top, and back the same way up. A step that would rest just short of a section's end or of the next section goes on to it: withendsa tall stop's end could lie a few pixels from its top, or from where the reader was, and a key moved the page by those pixels and stopped there. tests/stops-screens.html.
1.4.0 - 2026-10-17
- stops - the
endsoption (falseby default, so nothing changes until a site asks): withkeys, an arrow shows a tall stop's two ends. Down, from its top (or from anywhere inside it) to its end, its last screen, then on to the next stop's top; up, from its end back to its top, and from the top of the stop below to the end of a tall one before its top. Without it an arrow goes from top to top, however tall the stop, and a section taller than the screen had its bottom skipped by the keyboard. tests/stops-ends.html.
1.3.1 - 2026-10-16
- cookie - the
ignoreoption: the elements whose clicks are not on the page ([data-cookie-ignore]by default; a selector, an element or a list of them,falsefor none). A click inside one is no click outside the banner: it answers nothing bydismissand does not count as leaving the question. A site that setdismissto an answer had it given, and the question closed, by a click on a developer's toolbar (Symfony's web debug toolbar), the visitor never knowing what was kept.Cookie.ignored(element)says it for a site's own script. Nothing changes for a site that does not use it. tests/cookie-countdown.html.
1.3.0 - 2026-10-16
- stops - each way on its own: the
upanddownoptions (trueby default, so nothing changes until a site asks). Withup: falsea scroll heading up comes to rest where the reader left it - not carried on to the stop above, not settled back down - while a scroll heading down still goes on to its stop;down: falsethe other way round. A stop says it for itself withdata-sticky-up/data-sticky-down("false", or"true"against the option: a full sky that keeps its pull upwards on a page where going back up is free); a sideways row takes the same two attributes. The keys go from stop to stop whatever the options. tests/stops-ways.html.
1.2.1 - 2026-10-14
What plugging the cookie module under a site's own consent panel showed, and a navigation that takes a refused form for what it is.
- navigation - a page of the site is shown whatever its status. A form
the server refuses comes back with its errors under 422 (what Symfony
answers for an invalid form): it was handed to
Transparent.rescue()-Rescue mode.. calledin the console, the whole document replaced - like every answer that was not a 200. Now an answer in which the navigation finds the page's block, in a layout it can swap, is swapped in like any other, at its address, without a word in the console: 422, 201, the site's own page for a 404, a 403, a 500 or a 503. An answer that is no page of the site (a proxy's 502, a debug trace) is rescued and said as before. changes:Transparent.isRescueMode()staysfalseafter such a page. - cookie
- How long an entry is kept is said in the site's words: an entry's
durationLabel, else the newdurationsoption - a table by duration as declared ({"30d": "30 jours"}) or a function(duration, entry, seconds). The detailed section printed30das declared. The element keeps the declared duration indata-cookie-entry-duration. - An answer that expired is asked again with
legacy: true(the default). The record's cookie gone at the end of itslifetime, the answer was taken back from thelocalStoragecookie/<KEY>the module keeps in step, and the visitor was never asked again. Those keys are now an answer to take over only for a visitor the module has no record of yet (localStorage<record.name>.seensays it has had one); they are erased once the record is gone, too old or of anotherversion. Cookie.use()before the category is declared: a category declared afterwards asdefault: trueorrequired: truestarts the feature (onGrant,cookie:grant), one declared off stops it. The options of a configuration are applied before its categories.- Several places for the banner in a page: of several
[data-cookie-banner], the first whose parent the visitor can see is taken, not the first of the document.Cookie.mount(target)puts the one live banner in the place of another (which takes the place it leaves) or at the end of any element, as it is; therootoption names the banner (an element, a selector, a function asked again at each opening). A banner left where it cannot be seen opens in a place that can. - No banner where the site put none: a site that has a banner of its
own (found in a page, or named by
rootortemplate) no longer gets the bare default on a page without one - the question waits for the next page that has.bare: trueasks for the default there,bare: falsenever builds it;"auto", the default, builds it for a site that has no banner at all, as before. - The countdown waits while the banner cannot be seen - a parent that
is
display: none,hidden,visibility: hiddenor transparent, a page that shows no banner - and not only while the tab is hidden. - A page's configuration (
<script type="application/json" data-cookie-config>) is found under the library's prefix whatever the site's - a page can announce its prefix there -, under the site's, and where the newconfigoption (a selector) says.
- How long an entry is kept is said in the site's words: an entry's
tests/navigation-status.html and tests/cookie-places.html are new;
tests/cookie.html, cookie-banner.html, cookie-countdown.html and
cookie-navigation.html have the rest. Suite green (22 pages).
1.2.0 - 2026-10-14
A sixth module: the cookies and the visitor's consent, @glitchr/cookie
1.0.53 brought in with its history. Nothing changes for a page that does not
load it; it is in the whole library's entry and has one of its own.
- cookie (new) -
@glitchr/flowjs/cookie, theCookieglobal, on the core, without jQuery. docs/cookie.md.- Categories of consent are the site's to declare (
categories,Cookie.category()): a key, a label and a description in the site's words, on or off before any answer (default), refusable or not (required), what each stores and governs. None is written in the library. - What is stored is declared under a category - cookies,
localStorageandsessionStorageentries, by name or pattern, with a duration and a purpose.Cookie.cookies,Cookie.localandCookie.sessionread and write by that declaration: nothing under a category that is not granted (said in the console withdebug), nothing undeclared unlessundeclared: "allow", and everything a category declares is erased when it is refused or withdrawn. - Features start at a grant and stop at a withdrawal:
Cookie.use(category, [options], onGrant, onRevoke)(the shape of omnibase/consent'sConsent.use()), or inert elements of the page -<script type="text/plain" data-cookie-category>,<template>, anything withdata-cookie-src,data-cookie-placeholder. Events onwindow:cookie:consent,cookie:grant,cookie:revoke,cookie:open,cookie:close. - The interface: a banner (accept, refuse, customize) and a detailed
section (a switch per category, accept all, refuse all, save), opened
again at any time by anything with
data-cookie-open. Its HTML is the site's (an element or a<template>found by itsdata-cookie-*attributes, or thetemplateoption) or a bare default; the attributes' prefix and names, the classes, the CSS variables, the position and every word (texts) are options. The detailed section is a modal dialog: focus kept inside, Escape, focus given back;prefers-reduced-motionis followed. - The countdown is an option, off by default - a silence is not a
consent:
countdown: {duration, action}withaction"default","accept","refuse"or"none".dismissdoes the same for a click outside the banner. - The answer is a record (version, date, a yes or no per category) kept
in a cookie whose name, lifetime, path, domain,
SameSiteandSecureare options, or inlocalStorage, or in a store of the site's. The visitor is asked again whenversionchanges, when a category is new, or when the answer is older than its lifetime.signals: {gpc, dnt}makes Global Privacy Control and Do Not Track a refusal, if the site asks. - A configuration can be handed by the page (
<script type="application/json" data-cookie-config>), read at each page the navigation swaps in; the banner stays across those pages and their inert elements are woken or left asleep as they arrive. - From
@glitchr/cookie: every method of 1.0.53 keeps its name and its meaning (ready,configure,set,get,delete,checkConsent,setConsent,hasConsents,getConsents,declare,addGroup,refresh,onConfirm,onDeny,onCheck, the option accessors), the consents stay inlocalStoragecookie/<GROUP>and the cookies areGROUP/NAME. changes: jQuery is no longer needed -cookie:confirm,cookie:denyandcookie:checkare native events onwindow, which$(window).on()hears as before; a group whose name is a declared category's key follows that category (written only while it is granted, where a bare group is written until refused).
- Categories of consent are the site's to declare (
src/css/cookie.scss- an optional, minimal look for the banner, drawn from--cookie-*properties; no entry imports it.
tests/cookie.html (the 1.0.53 methods, categories, storage, features, inert
elements, the record), tests/cookie-banner.html (the banner, the detailed
section, the keyboard, asking again, signals, the site's own HTML, no sentence
in the module), tests/cookie-countdown.html (none, and its four endings),
tests/cookie-navigation.html (with the navigation).
1.1.0 - 2026-10-14
Two options for the magnets (.sticky-magnet), both leaving a site as it was
until it sets them.
- sticky -
scrollsnap_hold(true): the way to a magnet holds the scroll, as in 1.0.4.falsemakes it the reader's to take: nothing is set on<html>(no.sticky-prevent-scroll), a wheel, a finger, a press or a key on the way stops the page where it is, and the snap waits for the scroll to have been quiet fordebouncebefore it acts again - it does not start against the gesture that stopped it. - sticky -
scrollsnap_keys(false): the arrow keys go from magnet to magnet, down the page (ArrowDown, ArrowUp) and along a row that scrolls sideways (ArrowRight, ArrowLeft, pressed in the row or with the pointer over it), under the scroller'sscroll-padding. Left to the browser past the last magnet, along an axis without magnets, in a field, on a widget, in a dialog, with a modifier, inside an element that scrolls by itself, and when the page took the key - the rules of the stops'keys. A key held down goes one magnet at a time; two presses lead two magnets further. A key's way followsscrollsnap_hold. - sticky - new:
Sticky.toMagnet(scroller, {top, left}), the way both take, andSticky.getMagnetPositions(scroller, axis). One keydown listener on the window. - The stops already went from stop to stop along a
data-sticky-axis="x"row with ArrowLeft and ArrowRight (keys);tests/stops-row-keys.htmlnow says so.
tests/sticky-magnets-interruptible.html (a wheel, a key, a finger on the
way: stopped in place, nothing held, no restart against the reader, the snap
back after a rest; a modifier alone stops nothing), tests/sticky-magnet-keys.html
(down and up the page, along a row, the bounds, what is left alone, a key
held, two presses, the held and the taken way), tests/stops-row-keys.html.
The held way of 1.0.4 is unchanged: tests/sticky-magnets.html as it was.
1.0.4 - 2026-10-14
A page that stopped answering, fixed
- core, sticky - with
scrollsnapand.sticky-magnetsections, a wheel froze the page: the way to a magnet sets.sticky-prevent-scroll(overflow: hidden) on<html>and lifted it in the callback of its scroll, and a wheel on the way stopped the animation without calling back. (1.0.2 did not show it: its magnets went nowhere. 1.0.3 listed it as a callback lost on interruption, left as it was.) Two things now:Flow.scrollToalways ends and calls back once, with{completed, reason, top, left}-"arrived"(also with no way to go),"interrupted"(the reader took over),"replaced"(another scroll of the same scroller began); an animation stopped from outside (a site's own.stop()) is put where it was going a second past its time.Sticky.scrollTocalls back with"refused"when a script has the scroll already, where it returned without a word.- the way to a magnet holds the scroll (
{hold: true}, new), as in stickyjs where the hidden overflow swallowed the wheel: the page lands on the magnet, a quick gesture leads one magnet further and not two, and the class is lifted however the movement ended.
- changes for a callback of
Flow.scrollTo/Sticky.scrollTo/Transparent.scrollTo: it is now called when the movement was interrupted or replaced too - checkoutcome.completedbefore doing what belongs to an arrival. The library's own callers read again: the autoscroll does not bounce back or start over against a reader who took the scroll; a link to an anchor of the page writes the hash on arrival only (written after an interruption, it would jump the page there); the first page's scroll to its hash takestransparent-firstoff even when the reader interrupts it (it stayed for ever). - core -
user-scrollstaysfalsewhile a movement started from a callback runs (the autoscroll's way back): it was given back to the reader as that movement began.
tests/sticky-magnets.html: a trackpad gesture (40 x 25px), three quick
notches down and up, a wheel during a magnet button's scroll - no
.sticky-prevent-scroll left and the page answers afterwards - and the
callback's four endings.
1.0.3 - 2026-10-13
The merge read again, module by module, against stickyjs 1.7.0, transparentjs 3.0.31 and ajaxer 1.0.56. No public name changes; what behaves differently is marked changes.
Lost in the merge (1.0.0 - 1.0.2), given back
- core - a jQuery object where an element is meant is unwrapped
(
Sticky.userScroll($el),Sticky.getScrollPadding($scroller)- Sticky hands itself some at every scroll - read the page's scroller instead of the object's); an element out of the page has no scroller again;$.fn.isScrollableX/isScrollableYcountoverflow: auto. - core - the reader stopping an animated scroll of a script (a wheel, a
press, a second click on an anchor) left it "running" for ever:
Flow.scripted()stayed true and the stops never settled again on that page. - core, stops - what a scroll draws is drawn in the frame of the scroll:
the progress (
--sticky-progress,sticky:progress) was written one frame late. - sortable - a list inside a box that scrolls sideways only (a table in an
overflow-xwrapper) autoscrolls the page again while an item is dragged.
Defects the originals shipped, fixed
- sticky - changes:
.sticky-tophides as the page scrolls down and shows on the way up. Every scroll wrotearia-scrollhide="false"on it first (since 2022), so no header ever hid. A site that counts on its header staying setsscrollhide: falseoraria-scrollhide="false". - sticky -
Sticky.getMagnets()and the snaps read each magnet offthisin anArray.mapcallback (the window): the magnets are the magnets,.sticky-magnet-next/-prev/-first/-lastgo where they say. - sticky -
.sticky-autoscrollno longer adds amouseleavehandler per page loaded. - navigation - Back to a long page gives it back where it was left: the window's position is kept on its own (it was dropped, with all the others, when the page had one scroller less than when it was left) and held while the page grows (it was clamped to what the page had a frame after the swap). See docs/navigation.md.
- navigation - Forward gives a page back where it was left, and Back a second time where it was last read: the page left by Back or Forward is kept.
- navigation - changes: a request that gets no answer (status 0)
leaves the page as it is and announces it again (
DOMContentLoaded,transparent:load,load); it replaced the document with an empty one. - navigation -
replaceCanvases()keeps a canvas with an id in the block the site named (identifier), not only under#page. - ajaxer -
loader_throttle(setLoaderThrottle) is read: once shown, the loader stays at least that long. None by default, as before.
Between the modules
- A script's scroll rests where it was put even when its scroll event is heard late (a heavy page just swapped in) or lands in the middle of the reader's own scroll: the stops no longer glide off a position the navigation has just given back.
- Scrollers that left the page with it let go of the core's listeners at
transparent:load. Flow.scrollToanimates only the axes that move: the callback of a scroll down the page (an anchor's hash) no longer waits the sideways duration out.
Cost
On a page of forty sections (tests/perf/scroll.html, processor slowed four
times), per scroll frame: the stops and the navigation alone, 1.55 ms of
script, 159 getComputedStyle and 79 getBoundingClientRect -> 0.14 ms, 0
and 2; with the sticky effects, 2.9 ms, 202 and 124 -> 1.5 ms, 5 and 46. The
window's stops are measured once and kept while the page keeps its shape
(stops.measure(true); measure() always measures).
Tests
Served over HTTP by the runner, against a small site (tests/site.mjs): the navigation browsed (links, anchors, Back and Forward, forms, the head and the classes, a canvas, an overtaken navigation, a dropped connection, a failing page), Back to a page that is not whole yet, the nest; a page without jQuery; the sortable's scroller, autoscroll, grid and merge; Ajaxer's debounce, loader, limits and failures. 11 pages where there were 7.
1.0.2 - 2026-10-13
- A
.sticky-autoscrollelement initialises (it handedFlow.onInterrupta jQuery object: a TypeError per element). - Scrollers with
overflow: autocount again (Flow.isScrollableNode). - A stop rests under its own
scroll-margin-top, added to the scroller'sscroll-padding-top. - The swipe hint's and the reverse autoscroll's delays are read as durations.
1.0.1 - 2026-10-13
- Ajaxer's durations say what they mean:
throttle,debounceandloader_debouncetake a number of milliseconds, or a string with its unit ("250ms","1s");Ajaxer.parseTimereturns milliseconds. ("250ms"switched the throttle off,"1s"made it 1 ms.)
1.0.0 - 2026-10-12
@glitchr/flowjs, successor of @glitchr/transparentjs 3.0.31,
@glitchr/stickyjs 1.7.0 and @glitchr/ajaxer 1.0.56: one library, one
module per feature (/navigation, /sticky, /stops, /sortable,
/ajaxer) on a shared core (Flow). The globals, options, CSS classes and
data-* attributes keep their names.
- One native listener per scroller and kind for all modules; the sticky effects computed once per frame, bound once however many pages the navigation swaps in.
- The stops attach themselves again after every page swapped in; a scroll the navigation makes rests where it was put.
- The sticky effects run once when the page is loaded, without waiting for a first scroll.
- The sortable is the back office's evolved copy: the drop marker oriented to
the grid (its size as
--sortable-item-height/-width, no inline height), the autoscroll band under a sticky header, a wider band for a finger; its jQuery plugin optional.