@glitchr/transparent 1.0.80 → 1.0.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/js/transparent.js +140 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glitchr/transparent",
3
- "version": "1.0.80",
3
+ "version": "1.0.82",
4
4
  "description": "Transparent SPA Application",
5
5
  "main": "src/index.js",
6
6
  "access": "public",
@@ -274,12 +274,48 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
274
274
  FADEOUT : "fade-out",
275
275
  POSTACTIVE : "post-active",
276
276
 
277
- NOTIFICATION: "notification"
277
+ NOTIFICATION: "notification",
278
+ OFFLINE : "offline"
278
279
  };
279
280
 
280
281
  var isReady = false;
281
282
  var rescueMode = false;
282
283
 
284
+ // ─── OFFLINE DETECTION ──────────────────────────────────────────
285
+ // Two-source signal:
286
+ // 1. window 'online'/'offline' events fired by the browser when the
287
+ // OS-level connectivity changes (Wi-Fi off, airplane mode, etc.)
288
+ // 2. AJAX network errors during navigation — if a request fails with
289
+ // status 0 (and wasn't aborted), the device probably can't reach
290
+ // the server even though navigator.onLine may still be true.
291
+ // The `html.offline` class is the public surface — the project's CSS
292
+ // styles the YouTube-style "Offline" banner from there. Custom events
293
+ // `transparent:offline` and `transparent:online` give JS hooks too.
294
+ var isOnline = (typeof navigator !== "undefined") ? navigator.onLine !== false : true;
295
+ Transparent.isOnline = function() { return isOnline; }
296
+ function setOnlineStatus(online) {
297
+ if (online === isOnline) return; // no change
298
+ isOnline = online;
299
+ if (online) {
300
+ $($(document).find("html")[0]).removeClass(State.OFFLINE);
301
+ dispatchEvent(new Event("transparent:online"));
302
+ } else {
303
+ $($(document).find("html")[0]).addClass(State.OFFLINE);
304
+ dispatchEvent(new Event("transparent:offline"));
305
+ }
306
+ }
307
+ if (typeof window !== "undefined") {
308
+ window.addEventListener("online", function() { setOnlineStatus(true); });
309
+ window.addEventListener("offline", function() { setOnlineStatus(false); });
310
+ }
311
+ // Apply initial state synchronously so first-paint reflects offline if applicable.
312
+ if (!isOnline) {
313
+ // Use the document element directly here — Transparent.html isn't initialized yet at module-eval time.
314
+ var _htmlEl = document.documentElement;
315
+ if (_htmlEl && _htmlEl.classList) _htmlEl.classList.add(State.OFFLINE);
316
+ }
317
+ // ────────────────────────────────────────────────────────────────
318
+
283
319
  Transparent.html = $($(document).find("html")[0]);
284
320
  Transparent.html.addClass(Transparent.state.ROOT+ " " + Transparent.state.LOADING + " " + Transparent.state.FIRST);
285
321
 
@@ -861,6 +897,15 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
861
897
  }
862
898
  var fadeInTime = 0;
863
899
  var fadeInRemainingTime = 0;
900
+ // Schedule fn for after the currently in-flight fadeIn animation has
901
+ // finished. Used by handleResponse's three reload/redirect paths to
902
+ // ensure the loader is at full opacity before the browser unloads.
903
+ function _waitForFadeIn(fn) {
904
+ var elapsed = Date.now() - fadeInTime;
905
+ var remaining = fadeInRemainingTime - elapsed;
906
+ if (remaining > 0) setTimeout(fn, remaining + 30);
907
+ else fn();
908
+ }
864
909
  Transparent.fadeIn = function(activeCallback = function() {}) {
865
910
  _tx("fadeIn ENTRY");
866
911
  if(!Transparent.html.hasClass(Transparent.state.PREACTIVE)) {
@@ -1026,7 +1071,22 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
1026
1071
  Transparent.fadeOut();
1027
1072
  }
1028
1073
 
1029
- Transparent.userScroll = function(el = undefined) { return $(el === undefined ? document.documentElement : el).closestScrollable().prop("user-scroll") ?? true; }
1074
+ Transparent.userScroll = function(el = undefined) {
1075
+ // Defensive: closestScrollable() can return a value without .prop
1076
+ // when called from event handlers on transient DOM (e.g. ajaxer
1077
+ // result containers, sticky-scrollpercent triggers fired during
1078
+ // infinite-scroll while the page is transitioning). The app-defer.js
1079
+ // wrapper around $.fn.closestScrollable is supposed to enforce the
1080
+ // jQuery return, but races with timing-sensitive callers can still
1081
+ // hit this. Default to true ("user is scrolling, don't autoscroll").
1082
+ try {
1083
+ var $target = $(el === undefined ? document.documentElement : el);
1084
+ if (!$target || !$target.length) return true;
1085
+ var $scroll = $target.closestScrollable && $target.closestScrollable();
1086
+ if (!$scroll || typeof $scroll.prop !== "function") return true;
1087
+ return $scroll.prop("user-scroll") ?? true;
1088
+ } catch (e) { return true; }
1089
+ }
1030
1090
  Transparent.scrollTo = function(dict, el = window, callback = function() {})
1031
1091
  {
1032
1092
  setTimeout(function() {
@@ -1290,9 +1350,28 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
1290
1350
  $(this).stop();
1291
1351
  });
1292
1352
 
1353
+ // Defer the DOM swap until #page's opacity transition has had a
1354
+ // chance to finish. Read the transition-duration from getComputedStyle
1355
+ // (closure-local — no module state shared between navigations) so
1356
+ // this stays in sync with whatever the project's CSS uses. Without
1357
+ // this delay, a fast/cached AJAX response can land the swap while
1358
+ // #page is still partway through the LOADING-induced fade-out,
1359
+ // making the content change visible to the user (the original
1360
+ // flicker).
1361
+ var _swapDelay = 1;
1362
+ try {
1363
+ var _pageEl = $(Settings.identifier)[0];
1364
+ if (_pageEl) {
1365
+ var _dur = 1000 * Transparent.parseDuration(
1366
+ window.getComputedStyle(_pageEl).transitionDuration || "0"
1367
+ );
1368
+ if (_dur > 1) _swapDelay = _dur;
1369
+ }
1370
+ } catch(e) {}
1371
+
1293
1372
  setTimeout(function() {
1294
1373
 
1295
- _tx("onLoad BODY (after 1ms)");
1374
+ _tx("onLoad BODY (after " + _swapDelay + "ms)");
1296
1375
  // Transfert attributes
1297
1376
  Transparent.transferAttributes(dom);
1298
1377
 
@@ -1486,11 +1565,19 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
1486
1565
  (function() {
1487
1566
  function doCallback() {
1488
1567
  _tx("doCallback FIRES → callback() (which starts fadeOut)");
1489
- $('head').append(function() {
1490
- $(Settings.identifier).append(function() {
1491
- callback();
1492
- dispatchEvent(new Event('transparent:load'));
1493
- dispatchEvent(new Event('load'));
1568
+ // requestAnimationFrame here guarantees the browser has had one
1569
+ // full frame to apply the new page's stylesheets and re-layout
1570
+ // before fadeOut starts. Without it, fadeOut can animate the
1571
+ // loader away while the new page is still rendered with the
1572
+ // previous layout's styles — the article→home flicker the
1573
+ // [TX] trace masked via instrumentation overhead.
1574
+ requestAnimationFrame(function() {
1575
+ $('head').append(function() {
1576
+ $(Settings.identifier).append(function() {
1577
+ callback();
1578
+ dispatchEvent(new Event('transparent:load'));
1579
+ dispatchEvent(new Event('load'));
1580
+ });
1494
1581
  });
1495
1582
  });
1496
1583
  }
@@ -1535,7 +1622,7 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
1535
1622
  }
1536
1623
  })();
1537
1624
 
1538
- }.bind(this), 1);
1625
+ }.bind(this), _swapDelay);
1539
1626
  }
1540
1627
 
1541
1628
  function uuidv4() {
@@ -1739,6 +1826,17 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
1739
1826
  if (ajaxSemaphore) return;
1740
1827
  if (url == location) return;
1741
1828
 
1829
+ // Block navigation when offline. Project CSS / JS can react to
1830
+ // html.offline + the transparent:offline event to surface a banner.
1831
+ // The event is re-dispatched here on each attempted navigation so a
1832
+ // listener can briefly flash/highlight the banner to acknowledge the
1833
+ // click instead of doing nothing silently.
1834
+ if (!isOnline || (typeof navigator !== "undefined" && navigator.onLine === false)) {
1835
+ setOnlineStatus(false);
1836
+ dispatchEvent(new Event("transparent:offline"));
1837
+ return;
1838
+ }
1839
+
1742
1840
  if((e.type == Transparent.state.CLICK || e.type == Transparent.state.HASHCHANGE) && url.pathname == location.pathname && url.search == location.search && type != "POST") {
1743
1841
 
1744
1842
  if(!url.hash) return;
@@ -1858,12 +1956,18 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
1858
1956
  history.pushState({uuid: uuid, status:status, method: method, data: {}, href: responseURL}, '', responseURL);
1859
1957
 
1860
1958
  // Page not recognized.. just go fetch by yourself.. no POST information transmitted..
1861
- if(!Transparent.isPage(dom))
1862
- return window.location.href = url;
1959
+ // Defer the redirect until fadeIn settles, same reasoning as the
1960
+ // html.reload path above.
1961
+ if(!Transparent.isPage(dom)) {
1962
+ _waitForFadeIn(function() { window.location.href = url; });
1963
+ return;
1964
+ }
1863
1965
 
1864
1966
  // Layout not compatible.. needs to be reloaded (exception when POST is detected..)
1865
- if(!Transparent.isCompatiblePage(dom, method, data))
1866
- return window.location.href = url;
1967
+ if(!Transparent.isCompatiblePage(dom, method, data)) {
1968
+ _waitForFadeIn(function() { window.location.href = url; });
1969
+ return;
1970
+ }
1867
1971
 
1868
1972
  // Mark layout as known
1869
1973
  if(!Transparent.isKnownLayout(dom)) {
@@ -1896,8 +2000,18 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
1896
2000
 
1897
2001
  dispatchEvent(new Event('transparent:'+switchLayout));
1898
2002
 
1899
- if($(dom).find("html").hasClass(Transparent.state.RELOAD) || $(dom).find("html").hasClass(Transparent.state.DISABLE))
1900
- return window.location.reload();
2003
+ if($(dom).find("html").hasClass(Transparent.state.RELOAD) || $(dom).find("html").hasClass(Transparent.state.DISABLE)) {
2004
+ // Defer the reload until fadeIn has finished, so the loader is
2005
+ // at full opacity when the browser unloads. Without this, a
2006
+ // fast AJAX response can fire reload() while fadeIn is still
2007
+ // mid-animation: the browser swaps to a page that starts with
2008
+ // .active (loader at 100%), and the user perceives a snap
2009
+ // from in-progress opacity to full. Reading as "fade-out then
2010
+ // fade-in" because the partial loader receded as the browser
2011
+ // swapped frames.
2012
+ _waitForFadeIn(function() { window.location.reload(); });
2013
+ return;
2014
+ }
1901
2015
 
1902
2016
  // Kick off preloads for stylesheets the new page needs but aren't yet in <head>.
1903
2017
  // They download in parallel during the fadeIn animation so onLoad() finds them
@@ -1959,7 +2073,17 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
1959
2073
  headers: Settings["headers"] || {},
1960
2074
  xhr: function () { return xhr; },
1961
2075
  success: function (html, status, request) { _tx("ajax SUCCESS", "status=" + request.status); return handleResponse(uuid, request.status, type, data, xhr, request); },
1962
- error: function (request, ajaxOptions, thrownError) { _tx("ajax ERROR", "status=" + request.status); return handleResponse(uuid, request.status, type, data, xhr, request); }
2076
+ error: function (request, ajaxOptions, thrownError) {
2077
+ _tx("ajax ERROR", "status=" + request.status + " textStatus=" + ajaxOptions);
2078
+ // status=0 with non-abort textStatus typically means the device
2079
+ // couldn't reach the server: dropped connection, DNS failure,
2080
+ // captive portal, etc. Flip to offline so the project's banner
2081
+ // surfaces even if navigator.onLine still reports true.
2082
+ if (request.status === 0 && ajaxOptions !== "abort") {
2083
+ setOnlineStatus(false);
2084
+ }
2085
+ return handleResponse(uuid, request.status, type, data, xhr, request);
2086
+ }
1963
2087
  });
1964
2088
  }
1965
2089