@glitchr/transparent 1.0.81 → 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 +127 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glitchr/transparent",
3
- "version": "1.0.81",
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
 
@@ -1543,7 +1622,7 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
1543
1622
  }
1544
1623
  })();
1545
1624
 
1546
- }.bind(this), 1);
1625
+ }.bind(this), _swapDelay);
1547
1626
  }
1548
1627
 
1549
1628
  function uuidv4() {
@@ -1747,6 +1826,17 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
1747
1826
  if (ajaxSemaphore) return;
1748
1827
  if (url == location) return;
1749
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
+
1750
1840
  if((e.type == Transparent.state.CLICK || e.type == Transparent.state.HASHCHANGE) && url.pathname == location.pathname && url.search == location.search && type != "POST") {
1751
1841
 
1752
1842
  if(!url.hash) return;
@@ -1866,12 +1956,18 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
1866
1956
  history.pushState({uuid: uuid, status:status, method: method, data: {}, href: responseURL}, '', responseURL);
1867
1957
 
1868
1958
  // Page not recognized.. just go fetch by yourself.. no POST information transmitted..
1869
- if(!Transparent.isPage(dom))
1870
- 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
+ }
1871
1965
 
1872
1966
  // Layout not compatible.. needs to be reloaded (exception when POST is detected..)
1873
- if(!Transparent.isCompatiblePage(dom, method, data))
1874
- return window.location.href = url;
1967
+ if(!Transparent.isCompatiblePage(dom, method, data)) {
1968
+ _waitForFadeIn(function() { window.location.href = url; });
1969
+ return;
1970
+ }
1875
1971
 
1876
1972
  // Mark layout as known
1877
1973
  if(!Transparent.isKnownLayout(dom)) {
@@ -1904,8 +2000,18 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
1904
2000
 
1905
2001
  dispatchEvent(new Event('transparent:'+switchLayout));
1906
2002
 
1907
- if($(dom).find("html").hasClass(Transparent.state.RELOAD) || $(dom).find("html").hasClass(Transparent.state.DISABLE))
1908
- 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
+ }
1909
2015
 
1910
2016
  // Kick off preloads for stylesheets the new page needs but aren't yet in <head>.
1911
2017
  // They download in parallel during the fadeIn animation so onLoad() finds them
@@ -1967,7 +2073,17 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
1967
2073
  headers: Settings["headers"] || {},
1968
2074
  xhr: function () { return xhr; },
1969
2075
  success: function (html, status, request) { _tx("ajax SUCCESS", "status=" + request.status); return handleResponse(uuid, request.status, type, data, xhr, request); },
1970
- 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
+ }
1971
2087
  });
1972
2088
  }
1973
2089