@glitchr/transparent 1.0.82 → 1.1.1
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.
- package/package.json +1 -1
- package/src/js/transparent.js +737 -478
package/src/js/transparent.js
CHANGED
|
@@ -180,77 +180,44 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
180
180
|
"smoothscroll_speed" : 0,
|
|
181
181
|
"smoothscroll_easing" : "swing",
|
|
182
182
|
"exceptions": [],
|
|
183
|
-
// headlock: list of
|
|
184
|
-
// (e.g. third-party widgets that
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
183
|
+
// headlock: list of URL substrings or regex patterns to preserve in
|
|
184
|
+
// <head> across page transitions (e.g. third-party widgets that
|
|
185
|
+
// inject <style>/<link> dynamically). Anything matching is treated
|
|
186
|
+
// as "locked" and never removed during the head merge.
|
|
187
|
+
//
|
|
188
|
+
// In addition, head nodes injected dynamically AFTER initial
|
|
189
|
+
// DOMContentLoaded are auto-preserved (snapshotted on load → not
|
|
190
|
+
// in the set → preserved).
|
|
191
|
+
//
|
|
192
|
+
// Per-element overrides:
|
|
193
|
+
// <link data-headlock="false"> → opt-out (allow normal removal)
|
|
194
|
+
// <link data-headlock="true"> → opt-in (always preserve)
|
|
195
|
+
//
|
|
196
|
+
// Ported from upstream 1.0.82's `headlock` design (cleaner API than
|
|
197
|
+
// the previous hardcoded SCRIPT/STYLE-never-remove heuristic).
|
|
198
|
+
"headlock": [],
|
|
199
|
+
// ── View Transitions API ────────────────────────────────────────────
|
|
200
|
+
// When true, the DOM swap is wrapped in document.startViewTransition()
|
|
201
|
+
// so the browser captures an OLD snapshot, applies the swap callback,
|
|
202
|
+
// then crossfades to the NEW state natively. Falls back transparently
|
|
203
|
+
// to the CSS-only transition path on browsers without VT support
|
|
204
|
+
// (Firefox < 144, Safari < 18.2, old Chromium). Per-element morph
|
|
205
|
+
// is opt-in via CSS:
|
|
206
|
+
//
|
|
207
|
+
// #page { view-transition-name: page; }
|
|
208
|
+
// .article-hero img { view-transition-name: article-hero; }
|
|
209
|
+
//
|
|
210
|
+
// Pairs the named element across the swap so the browser animates
|
|
211
|
+
// its position/size change instead of crossfading the snapshots.
|
|
212
|
+
//
|
|
213
|
+
// skip_transition_for_cache: when true, in-DOM cache hits bypass VT
|
|
214
|
+
// entirely (for Turbo-style instant-back UX). Default false because
|
|
215
|
+
// VT's 200ms crossfade is fast enough that the consistency win
|
|
216
|
+
// outweighs the saved frames.
|
|
217
|
+
"use_view_transitions": false,
|
|
218
|
+
"skip_transition_for_cache": false
|
|
188
219
|
};
|
|
189
220
|
|
|
190
|
-
// Set of <head> children present on initial load. Anything added after is treated
|
|
191
|
-
// as dynamically injected and preserved across transitions.
|
|
192
|
-
var originalHeadNodes = new WeakSet();
|
|
193
|
-
function snapshotHeadNodes() {
|
|
194
|
-
var head = document.head;
|
|
195
|
-
if(!head) return;
|
|
196
|
-
for(var i = 0; i < head.children.length; i++)
|
|
197
|
-
originalHeadNodes.add(head.children[i]);
|
|
198
|
-
}
|
|
199
|
-
// Snapshot synchronously at module-eval time (scripts at end of <body> run before any
|
|
200
|
-
// async script can inject <style> tags, so the snapshot is clean).
|
|
201
|
-
// A DOMContentLoaded fallback is kept for the rare case where document.head is null
|
|
202
|
-
// (e.g. script loaded inside <head> before it finishes parsing).
|
|
203
|
-
snapshotHeadNodes();
|
|
204
|
-
if(!document.head)
|
|
205
|
-
document.addEventListener("DOMContentLoaded", snapshotHeadNodes, { once: true });
|
|
206
|
-
|
|
207
|
-
Transparent.isHeadlocked = function(el) {
|
|
208
|
-
if(!el || el.nodeType !== 1) return false;
|
|
209
|
-
// Explicit opt-out
|
|
210
|
-
var attr = el.getAttribute && el.getAttribute("data-headlock");
|
|
211
|
-
if(attr === "false") return false;
|
|
212
|
-
// Explicit opt-in via attribute
|
|
213
|
-
if(attr !== null && attr !== undefined) return true;
|
|
214
|
-
// Dynamically injected after initial load
|
|
215
|
-
if(!originalHeadNodes.has(el)) return true;
|
|
216
|
-
// URL pattern match (src/href attributes)
|
|
217
|
-
var patterns = Settings["headlock"] || [];
|
|
218
|
-
if(!patterns.length) return false;
|
|
219
|
-
var url = el.getAttribute && (el.getAttribute("src") || el.getAttribute("href"));
|
|
220
|
-
// <style> elements have no src/href — match against CSS textContent instead
|
|
221
|
-
if(!url && el.tagName === 'STYLE') url = el.textContent || '';
|
|
222
|
-
if(!url) return false;
|
|
223
|
-
for(var i = 0; i < patterns.length; i++) {
|
|
224
|
-
var p = patterns[i];
|
|
225
|
-
if(p instanceof RegExp) { if(p.test(url)) return true; }
|
|
226
|
-
else if(typeof p === "string" && p.length && url.indexOf(p) !== -1) return true;
|
|
227
|
-
}
|
|
228
|
-
return false;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// ─── NAVIGATION TRACE LOG ───────────────────────────────────────
|
|
232
|
-
// Gated on Settings.debug. Set 'debug': true in Transparent.ready({...})
|
|
233
|
-
// to surface a per-step trace prefixed with "[TX]" in the console.
|
|
234
|
-
// Cheap when disabled — single boolean check, no allocation.
|
|
235
|
-
function _tx(tag, extra) {
|
|
236
|
-
if (!Settings || !Settings.debug) return;
|
|
237
|
-
try {
|
|
238
|
-
var cls = document.documentElement.className;
|
|
239
|
-
var t = performance.now().toFixed(1);
|
|
240
|
-
var here = (document.querySelector("#page") || document.documentElement);
|
|
241
|
-
var lay = here.getAttribute && (here.getAttribute("data-layout") || "?");
|
|
242
|
-
var path = location.pathname;
|
|
243
|
-
console.log("%c[TX]", "color:#0a0;font-weight:bold",
|
|
244
|
-
"+" + t + "ms", tag,
|
|
245
|
-
"path=" + path,
|
|
246
|
-
"layout=" + lay,
|
|
247
|
-
"ajaxSem=" + (typeof ajaxSemaphore === "undefined" ? "?" : ajaxSemaphore),
|
|
248
|
-
"classes=[" + cls + "]",
|
|
249
|
-
extra || "");
|
|
250
|
-
} catch(e) {}
|
|
251
|
-
}
|
|
252
|
-
// ────────────────────────────────────────────────────────────────
|
|
253
|
-
|
|
254
221
|
const State = Transparent.state = {
|
|
255
222
|
|
|
256
223
|
ROOT : "transparent",
|
|
@@ -269,53 +236,17 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
269
236
|
CLICK : "click",
|
|
270
237
|
|
|
271
238
|
PREACTIVE : "pre-active",
|
|
272
|
-
|
|
239
|
+
ACTIVEIN : "active-in",
|
|
273
240
|
ACTIVE : "active",
|
|
274
|
-
|
|
241
|
+
ACTIVEOUT : "active-out",
|
|
275
242
|
POSTACTIVE : "post-active",
|
|
276
243
|
|
|
277
|
-
NOTIFICATION: "notification"
|
|
278
|
-
OFFLINE : "offline"
|
|
244
|
+
NOTIFICATION: "notification"
|
|
279
245
|
};
|
|
280
246
|
|
|
281
247
|
var isReady = false;
|
|
282
248
|
var rescueMode = false;
|
|
283
249
|
|
|
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
|
-
|
|
319
250
|
Transparent.html = $($(document).find("html")[0]);
|
|
320
251
|
Transparent.html.addClass(Transparent.state.ROOT+ " " + Transparent.state.LOADING + " " + Transparent.state.FIRST);
|
|
321
252
|
|
|
@@ -324,6 +255,91 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
324
255
|
dispatchEvent(new Event('transparent:'+Transparent.state.ACTIVE));
|
|
325
256
|
}
|
|
326
257
|
|
|
258
|
+
// ── Server-rendered element tracking ───────────────────────────────────
|
|
259
|
+
//
|
|
260
|
+
// SPA head/body merging removes elements not present in the new page's
|
|
261
|
+
// HTML response — correct for server-rendered tags (per-page CSS,
|
|
262
|
+
// <meta>, page-specific <script>), but it ALSO nukes anything that a
|
|
263
|
+
// third-party loader injected at runtime: Brevo's chat widget <style>
|
|
264
|
+
// and <div>, Google Analytics tags, Intercom, Crisp, Hotjar, etc.
|
|
265
|
+
// These elements are never in any page's HTML response, so the merge
|
|
266
|
+
// removes them on every nav → user sees the chat widget lose all its
|
|
267
|
+
// CSS and explode to full screen (visible in the user's screenshot of
|
|
268
|
+
// navigating /articles → /).
|
|
269
|
+
//
|
|
270
|
+
// Fix: maintain a WeakSet of elements known to be server-rendered. The
|
|
271
|
+
// initial document's head + body children are server-rendered. Anything
|
|
272
|
+
// we ADD via the SPA merge later is also server-rendered (it came from
|
|
273
|
+
// a server HTML response). Anything else — third-party script
|
|
274
|
+
// injections that happen AFTER snapshot — is never added to the set
|
|
275
|
+
// and is therefore preserved across navs.
|
|
276
|
+
//
|
|
277
|
+
// WeakSet means removed elements get garbage-collected → no leak.
|
|
278
|
+
//
|
|
279
|
+
// MutationObserver auto-tags elements added by ANY route, so even
|
|
280
|
+
// body-script appends or head-script appends elsewhere in this library
|
|
281
|
+
// stay correctly classified.
|
|
282
|
+
// Ported from upstream 1.0.82 — `headlock` design.
|
|
283
|
+
//
|
|
284
|
+
// Replaces the previous hardcoded "never remove SCRIPT/STYLE" rule
|
|
285
|
+
// with a finer-grained API: snapshot the initial head children, then
|
|
286
|
+
// on the next swap, *preserve* anything that's either (a) not in the
|
|
287
|
+
// snapshot (= dynamically injected after load), or (b) matches a
|
|
288
|
+
// configured URL pattern in Settings.headlock, or (c) has a
|
|
289
|
+
// data-headlock attribute (true/non-empty = lock, "false" = unlock).
|
|
290
|
+
//
|
|
291
|
+
// Two key wins over the previous local impl:
|
|
292
|
+
// 1. The previous impl never removed any <script>/<style> at all,
|
|
293
|
+
// which caused per-page stylesheets to leak across navigations
|
|
294
|
+
// (e.g. layout1's inline <style> persisting on layout2). The new
|
|
295
|
+
// design only locks dynamically-injected ones.
|
|
296
|
+
// 2. Project code can pass `headlock: ["brevo.com", "googletag", /hotjar/]`
|
|
297
|
+
// in Transparent.ready({...}) to explicitly opt-in third-party
|
|
298
|
+
// URLs by substring or regex.
|
|
299
|
+
var originalHeadNodes = new WeakSet();
|
|
300
|
+
function snapshotHeadNodes() {
|
|
301
|
+
if (document.head) {
|
|
302
|
+
for (var i = 0; i < document.head.children.length; i++) {
|
|
303
|
+
originalHeadNodes.add(document.head.children[i]);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Snapshot synchronously at module-eval time. Defer scripts run after
|
|
308
|
+
// the parser has built the <head> but before async third-party loaders
|
|
309
|
+
// execute, so the snapshot captures the server-rendered <head> cleanly.
|
|
310
|
+
// The DOMContentLoaded fallback handles the rare case where the script
|
|
311
|
+
// ran before <head> was complete (e.g. in-head non-defer placement).
|
|
312
|
+
snapshotHeadNodes();
|
|
313
|
+
if (!document.head) {
|
|
314
|
+
document.addEventListener("DOMContentLoaded", snapshotHeadNodes, { once: true });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
Transparent.isHeadlocked = function(el) {
|
|
318
|
+
if (!el || el.nodeType !== 1) return false;
|
|
319
|
+
// Explicit attribute opt-out wins over everything else.
|
|
320
|
+
var attr = el.getAttribute && el.getAttribute("data-headlock");
|
|
321
|
+
if (attr === "false") return false;
|
|
322
|
+
// Explicit attribute opt-in (any non-"false" value).
|
|
323
|
+
if (attr !== null && attr !== undefined) return true;
|
|
324
|
+
// Auto-lock anything injected after the initial snapshot — by
|
|
325
|
+
// definition, third-party widgets and their CSS.
|
|
326
|
+
if (!originalHeadNodes.has(el)) return true;
|
|
327
|
+
// URL-pattern matching: src for <script>/<link>/<iframe>, href for
|
|
328
|
+
// <link>, or full textContent for inline <style> blocks. Substring
|
|
329
|
+
// matches on strings; .test() matches on RegExp.
|
|
330
|
+
var patterns = Settings["headlock"] || [];
|
|
331
|
+
if (!patterns.length) return false;
|
|
332
|
+
var url = el.getAttribute && (el.getAttribute("src") || el.getAttribute("href"));
|
|
333
|
+
if (!url && el.tagName === 'STYLE') url = el.textContent || '';
|
|
334
|
+
if (!url) return false;
|
|
335
|
+
for (var i = 0; i < patterns.length; i++) {
|
|
336
|
+
var p = patterns[i];
|
|
337
|
+
if (p instanceof RegExp) { if (p.test(url)) return true; }
|
|
338
|
+
else if (typeof p === "string" && p.length && url.indexOf(p) !== -1) return true;
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
};
|
|
342
|
+
|
|
327
343
|
window.addEventListener("DOMContentLoaded", function()
|
|
328
344
|
{
|
|
329
345
|
Transparent.loader = $($(document).find(Settings.loader)[0] ?? Transparent.html);
|
|
@@ -414,17 +430,84 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
414
430
|
return this;
|
|
415
431
|
}
|
|
416
432
|
|
|
433
|
+
// ── In-memory live-DOM cache ────────────────────────────────────────────
|
|
434
|
+
//
|
|
435
|
+
// Turbo-style: store cloned <html> Element nodes per uuid so popstate
|
|
436
|
+
// (back/forward) can short-circuit the XHR + sessionStorage round-trip
|
|
437
|
+
// + DOMParser.parseFromString cycle. Result: back/forward feels instant.
|
|
438
|
+
//
|
|
439
|
+
// Key vs sessionStorage flow:
|
|
440
|
+
// - sessionStorage: outerHTML serialize (slow) → string (5-10MB
|
|
441
|
+
// quota) → JSON read → DOMParser parse (slow). Whole cycle on
|
|
442
|
+
// every popstate. Used as the fallback when the live cache misses.
|
|
443
|
+
// - liveDomCache: cloneNode(true) of the rendered <html> element
|
|
444
|
+
// stored in a Map<uuid, {node, scroll, ts}>. Bounded with LRU.
|
|
445
|
+
// No serialization, no parsing — just the live DOM node ready
|
|
446
|
+
// for the swap to consume.
|
|
447
|
+
//
|
|
448
|
+
// setResponse populates BOTH (live cache + sessionStorage). The
|
|
449
|
+
// sessionStorage write is kept so tab reloads and cross-process
|
|
450
|
+
// restores keep working. getLiveResponse() is the new fast-path API
|
|
451
|
+
// used by handleResponse before the DOMParser fallback.
|
|
452
|
+
Transparent._liveDomCache = new Map();
|
|
453
|
+
Transparent.liveDomCacheMax = 25;
|
|
454
|
+
Transparent.liveDomCacheTTL = 5 * 60 * 1000; // 5 min
|
|
455
|
+
|
|
456
|
+
Transparent.getLiveResponse = function(uuid) {
|
|
457
|
+
var entry = Transparent._liveDomCache.get(uuid);
|
|
458
|
+
if (!entry) return null;
|
|
459
|
+
if (Date.now() - entry.ts > Transparent.liveDomCacheTTL) {
|
|
460
|
+
Transparent._liveDomCache.delete(uuid);
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
// LRU bump: re-insert to move to the back of the iteration order.
|
|
464
|
+
Transparent._liveDomCache.delete(uuid);
|
|
465
|
+
Transparent._liveDomCache.set(uuid, entry);
|
|
466
|
+
// Return a CLONE so the caller's swap mutations don't poison
|
|
467
|
+
// the cache for future popstate hits. The clone is detached
|
|
468
|
+
// from any document so it's safe to pass to the swap.
|
|
469
|
+
return entry.node.cloneNode(true);
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
Transparent.setLiveResponse = function(uuid, htmlEl, scrollableXY) {
|
|
473
|
+
if (!htmlEl || htmlEl.nodeType !== 1) return;
|
|
474
|
+
Transparent._liveDomCache.set(uuid, {
|
|
475
|
+
node: htmlEl.cloneNode(true),
|
|
476
|
+
scroll: scrollableXY || [],
|
|
477
|
+
ts: Date.now()
|
|
478
|
+
});
|
|
479
|
+
// LRU evict — Map iteration order is insertion order.
|
|
480
|
+
while (Transparent._liveDomCache.size > Transparent.liveDomCacheMax) {
|
|
481
|
+
var firstKey = Transparent._liveDomCache.keys().next().value;
|
|
482
|
+
Transparent._liveDomCache.delete(firstKey);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
Transparent.clearLiveResponse = function() {
|
|
487
|
+
Transparent._liveDomCache.clear();
|
|
488
|
+
};
|
|
489
|
+
|
|
417
490
|
Transparent.setResponse = function(uuid, responseText, scrollableXY, exceptionRaised = false)
|
|
418
491
|
{
|
|
419
|
-
|
|
492
|
+
// Populate live-DOM cache FIRST while we still have the node.
|
|
493
|
+
// The outerHTML conversion below loses the node identity.
|
|
494
|
+
if (isDomEntity(responseText)) {
|
|
495
|
+
Transparent.setLiveResponse(uuid, responseText, scrollableXY);
|
|
420
496
|
responseText = responseText.outerHTML;
|
|
497
|
+
}
|
|
421
498
|
|
|
422
499
|
var array = JSON.parse(sessionStorage.getItem('transparent')) || [];
|
|
423
500
|
if (!array.includes(uuid)) {
|
|
424
501
|
|
|
425
502
|
array.push(uuid);
|
|
503
|
+
// Enforce the LRU cap. NB: entries are stored under
|
|
504
|
+
// `transparent[response][<uuid>]` / `transparent[position][<uuid>]`,
|
|
505
|
+
// so eviction must remove THOSE keys — the previous code removed
|
|
506
|
+
// `transparent[<uuid>]`, which never existed, leaving the real
|
|
507
|
+
// response/position blobs orphaned. They then accumulated past the
|
|
508
|
+
// cap until QuotaExceededError forced a full sessionStorage.clear().
|
|
426
509
|
while(array.length > Settings["response_limit"])
|
|
427
|
-
|
|
510
|
+
removeResponseEntry(array.shift());
|
|
428
511
|
}
|
|
429
512
|
|
|
430
513
|
try {
|
|
@@ -438,15 +521,42 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
438
521
|
|
|
439
522
|
} catch(e) {
|
|
440
523
|
|
|
524
|
+
// On quota, evict the oldest cached pages (targeted) and retry
|
|
525
|
+
// once, instead of nuking ALL sessionStorage — sessionStorage.clear()
|
|
526
|
+
// also wipes unrelated app state and the entire page cache.
|
|
527
|
+
if (e.name === 'QuotaExceededError' && exceptionRaised === false) {
|
|
528
|
+
evictOldestResponses(Math.max(1, Math.ceil(array.length / 2)));
|
|
529
|
+
return Transparent.setResponse(uuid, responseText, scrollableXY, true);
|
|
530
|
+
}
|
|
531
|
+
// Last resort if a single page is itself too big to ever fit.
|
|
441
532
|
if (e.name === 'QuotaExceededError')
|
|
442
533
|
sessionStorage.clear();
|
|
443
534
|
|
|
444
|
-
return
|
|
535
|
+
return this;
|
|
445
536
|
}
|
|
446
537
|
|
|
447
538
|
return this;
|
|
448
539
|
}
|
|
449
540
|
|
|
541
|
+
// Remove both blobs for a cached page uuid (response HTML + scroll position).
|
|
542
|
+
function removeResponseEntry(uuid) {
|
|
543
|
+
try {
|
|
544
|
+
sessionStorage.removeItem('transparent[response]['+uuid+']');
|
|
545
|
+
sessionStorage.removeItem('transparent[position]['+uuid+']');
|
|
546
|
+
} catch (e) {}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Drop the N oldest cached pages and rewrite the index. Used to recover
|
|
550
|
+
// from a QuotaExceededError without discarding the whole cache.
|
|
551
|
+
function evictOldestResponses(count) {
|
|
552
|
+
try {
|
|
553
|
+
var array = JSON.parse(sessionStorage.getItem('transparent')) || [];
|
|
554
|
+
for (var i = 0; i < count && array.length; i++)
|
|
555
|
+
removeResponseEntry(array.shift());
|
|
556
|
+
sessionStorage.setItem('transparent', JSON.stringify(array));
|
|
557
|
+
} catch (e) {}
|
|
558
|
+
}
|
|
559
|
+
|
|
450
560
|
Transparent.setResponseText = function(uuid, responseText, exceptionRaised = false)
|
|
451
561
|
{
|
|
452
562
|
if(isDomEntity(responseText))
|
|
@@ -554,7 +664,7 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
554
664
|
|
|
555
665
|
if($(Transparent.html).hasClass(Transparent.state.FIRST)) {
|
|
556
666
|
Transparent.scrollToHash(location.hash, {}, function() {
|
|
557
|
-
Transparent.
|
|
667
|
+
Transparent.activeOut(() => Transparent.html.removeClass(Transparent.state.FIRST));
|
|
558
668
|
});
|
|
559
669
|
}
|
|
560
670
|
|
|
@@ -895,37 +1005,28 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
895
1005
|
|
|
896
1006
|
return {delay:delay, duration:duration};
|
|
897
1007
|
}
|
|
898
|
-
var
|
|
899
|
-
var
|
|
900
|
-
|
|
901
|
-
|
|
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
|
-
}
|
|
909
|
-
Transparent.fadeIn = function(activeCallback = function() {}) {
|
|
910
|
-
_tx("fadeIn ENTRY");
|
|
1008
|
+
var activeInTime = 0;
|
|
1009
|
+
var activeInRemainingTime = 0;
|
|
1010
|
+
Transparent.activeIn = function(activeCallback = function() {}) {
|
|
1011
|
+
|
|
911
1012
|
if(!Transparent.html.hasClass(Transparent.state.PREACTIVE)) {
|
|
912
1013
|
Transparent.html.addClass(Transparent.state.PREACTIVE);
|
|
913
1014
|
dispatchEvent(new Event('transparent:'+Transparent.state.PREACTIVE));
|
|
914
1015
|
}
|
|
915
1016
|
|
|
916
1017
|
var active = Transparent.activeTime();
|
|
917
|
-
|
|
918
|
-
|
|
1018
|
+
activeInTime = Date.now();
|
|
1019
|
+
activeInRemainingTime = active.delay+active.duration;
|
|
919
1020
|
|
|
920
1021
|
Transparent.html.removeClass(Transparent.state.PREACTIVE);
|
|
921
|
-
if(!Transparent.html.hasClass(Transparent.state.
|
|
922
|
-
Transparent.html.addClass(Transparent.state.
|
|
923
|
-
dispatchEvent(new Event('transparent:'+Transparent.state.
|
|
1022
|
+
if(!Transparent.html.hasClass(Transparent.state.ACTIVEIN)) {
|
|
1023
|
+
Transparent.html.addClass(Transparent.state.ACTIVEIN);
|
|
1024
|
+
dispatchEvent(new Event('transparent:'+Transparent.state.ACTIVEIN));
|
|
924
1025
|
}
|
|
925
1026
|
|
|
926
1027
|
Transparent.callback(function() {
|
|
927
1028
|
|
|
928
|
-
Transparent.html.removeClass(Transparent.state.
|
|
1029
|
+
Transparent.html.removeClass(Transparent.state.ACTIVEIN);
|
|
929
1030
|
if(!Transparent.html.hasClass(Transparent.state.ACTIVE)) {
|
|
930
1031
|
Transparent.html.addClass(Transparent.state.ACTIVE);
|
|
931
1032
|
dispatchEvent(new Event('transparent:'+Transparent.state.ACTIVE));
|
|
@@ -935,23 +1036,23 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
935
1036
|
Transparent.callback(function() {
|
|
936
1037
|
|
|
937
1038
|
activeCallback();
|
|
938
|
-
|
|
1039
|
+
activeInRemainingTime = 0;
|
|
939
1040
|
|
|
940
1041
|
}.bind(this), active.duration);
|
|
941
1042
|
|
|
942
1043
|
}.bind(this), active.delay);
|
|
943
1044
|
}
|
|
944
1045
|
|
|
945
|
-
Transparent.
|
|
946
|
-
|
|
1046
|
+
Transparent.activeOut = function(activeCallback = function() {}) {
|
|
1047
|
+
|
|
947
1048
|
if(!Transparent.html.hasClass(Transparent.state.ACTIVE)) {
|
|
948
1049
|
Transparent.html.addClass(Transparent.state.ACTIVE);
|
|
949
1050
|
dispatchEvent(new Event('transparent:'+Transparent.state.ACTIVE));
|
|
950
1051
|
}
|
|
951
1052
|
|
|
952
|
-
if(!Transparent.html.hasClass(Transparent.state.
|
|
953
|
-
Transparent.html.addClass(Transparent.state.
|
|
954
|
-
dispatchEvent(new Event('transparent:'+Transparent.state.
|
|
1053
|
+
if(!Transparent.html.hasClass(Transparent.state.ACTIVEOUT)) {
|
|
1054
|
+
Transparent.html.addClass(Transparent.state.ACTIVEOUT);
|
|
1055
|
+
dispatchEvent(new Event('transparent:'+Transparent.state.ACTIVEOUT));
|
|
955
1056
|
}
|
|
956
1057
|
|
|
957
1058
|
var active = Transparent.activeTime();
|
|
@@ -963,7 +1064,7 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
963
1064
|
var active = Transparent.activeTime();
|
|
964
1065
|
Transparent.callback(function() {
|
|
965
1066
|
|
|
966
|
-
Transparent.html.removeClass(Transparent.state.
|
|
1067
|
+
Transparent.html.removeClass(Transparent.state.ACTIVEOUT);
|
|
967
1068
|
if(Transparent.html.hasClass(Transparent.state.LOADING)) {
|
|
968
1069
|
|
|
969
1070
|
dispatchEvent(new Event('transparent:'+Transparent.state.LOADING));
|
|
@@ -1068,25 +1169,10 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
1068
1169
|
Transparent.evalScript($("body")[0]);
|
|
1069
1170
|
}
|
|
1070
1171
|
|
|
1071
|
-
Transparent.
|
|
1172
|
+
Transparent.activeOut();
|
|
1072
1173
|
}
|
|
1073
1174
|
|
|
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
|
-
}
|
|
1175
|
+
Transparent.userScroll = function(el = undefined) { return $(el === undefined ? document.documentElement : el).closestScrollable().prop("user-scroll") ?? true; }
|
|
1090
1176
|
Transparent.scrollTo = function(dict, el = window, callback = function() {})
|
|
1091
1177
|
{
|
|
1092
1178
|
setTimeout(function() {
|
|
@@ -1221,8 +1307,23 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
1221
1307
|
image.onload = function() {
|
|
1222
1308
|
this.classList.add("loaded");
|
|
1223
1309
|
this.classList.remove("loading");
|
|
1310
|
+
this.classList.remove("error");
|
|
1224
1311
|
if(lazybox) lazybox.classList.add("loaded");
|
|
1225
1312
|
if(lazybox) lazybox.classList.remove("loading");
|
|
1313
|
+
if(lazybox) lazybox.classList.remove("error");
|
|
1314
|
+
};
|
|
1315
|
+
|
|
1316
|
+
// Error handler for broken / missing images (404, ACL,
|
|
1317
|
+
// DNS failure, malformed URL). Without this, lazy-loaded
|
|
1318
|
+
// images that fail just stay invisible. The .error
|
|
1319
|
+
// class lets project CSS render a placeholder.
|
|
1320
|
+
image.onerror = function() {
|
|
1321
|
+
this.classList.add("error");
|
|
1322
|
+
this.classList.remove("loading");
|
|
1323
|
+
this.classList.remove("loaded");
|
|
1324
|
+
if(lazybox) lazybox.classList.add("error");
|
|
1325
|
+
if(lazybox) lazybox.classList.remove("loading");
|
|
1326
|
+
if(lazybox) lazybox.classList.remove("loaded");
|
|
1226
1327
|
};
|
|
1227
1328
|
|
|
1228
1329
|
if(lazybox) lazybox.classList.add("loading");
|
|
@@ -1350,42 +1451,60 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
1350
1451
|
$(this).stop();
|
|
1351
1452
|
});
|
|
1352
1453
|
|
|
1353
|
-
|
|
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) {}
|
|
1454
|
+
activeInRemainingTime = activeInRemainingTime - (Date.now() - activeInTime);
|
|
1371
1455
|
|
|
1372
|
-
|
|
1456
|
+
// Whole-swap body extracted so we can dispatch it either through
|
|
1457
|
+
// document.startViewTransition() for browsers that support it (with
|
|
1458
|
+
// Settings.use_view_transitions on), or directly for the legacy path.
|
|
1459
|
+
// Identical behavior in both branches — VT just wraps it so the
|
|
1460
|
+
// browser captures OLD/NEW snapshots and crossfades natively.
|
|
1461
|
+
var _doSwapBody = function() {
|
|
1373
1462
|
|
|
1374
|
-
_tx("onLoad BODY (after " + _swapDelay + "ms)");
|
|
1375
1463
|
// Transfert attributes
|
|
1376
1464
|
Transparent.transferAttributes(dom);
|
|
1377
1465
|
|
|
1466
|
+
// ── Track-reload check ──────────────────────────────────────
|
|
1467
|
+
// Mirrors Turbo's <... data-turbo-track="reload"> mechanism.
|
|
1468
|
+
// Put `data-track="reload"` on critical <script> / <link>
|
|
1469
|
+
// bundles in <head>. On nav, if the set of tracked URLs
|
|
1470
|
+
// differs between current and new HTML, force a full reload
|
|
1471
|
+
// instead of an SPA swap — because the user's loaded JS/CSS
|
|
1472
|
+
// no longer matches what the server is serving (e.g. after
|
|
1473
|
+
// a deploy that bumped asset hashes). The browser then
|
|
1474
|
+
// re-downloads everything cleanly.
|
|
1475
|
+
//
|
|
1476
|
+
// Match key: tagName + src/href (or textContent for inline).
|
|
1477
|
+
// Same logic as Turbo: the SET of tracked URLs must match.
|
|
1478
|
+
(function checkTrackedReload() {
|
|
1479
|
+
function trackedSrcs(root) {
|
|
1480
|
+
var srcs = [];
|
|
1481
|
+
var els = root.querySelectorAll('[data-track="reload"]');
|
|
1482
|
+
for (var i = 0; i < els.length; i++) {
|
|
1483
|
+
var el = els[i];
|
|
1484
|
+
var src = el.getAttribute('src') || el.getAttribute('href') ||
|
|
1485
|
+
(el.textContent || '').slice(0, 200);
|
|
1486
|
+
if (src) srcs.push(el.tagName + ':' + src);
|
|
1487
|
+
}
|
|
1488
|
+
return srcs.sort();
|
|
1489
|
+
}
|
|
1490
|
+
var currentSrcs = trackedSrcs(document.head);
|
|
1491
|
+
if (!currentSrcs.length) return; // nothing tracked → skip
|
|
1492
|
+
var newDoc = dom.documentElement ? dom : (dom[0] || dom);
|
|
1493
|
+
var newHead = newDoc.head || newDoc.querySelector('head');
|
|
1494
|
+
if (!newHead) return;
|
|
1495
|
+
var newSrcs = trackedSrcs(newHead);
|
|
1496
|
+
// Compare as JSON of sorted arrays — order-independent.
|
|
1497
|
+
if (JSON.stringify(currentSrcs) === JSON.stringify(newSrcs)) return;
|
|
1498
|
+
if (Settings.debug) {
|
|
1499
|
+
console.log('Transparent track-reload: asset mismatch, forcing reload',
|
|
1500
|
+
{ current: currentSrcs, new: newSrcs });
|
|
1501
|
+
}
|
|
1502
|
+
// Full reload to the requested URL.
|
|
1503
|
+
window.location.href = window.location.toString();
|
|
1504
|
+
})();
|
|
1505
|
+
|
|
1378
1506
|
// Replace head..
|
|
1379
1507
|
var head = $(dom).find("head");
|
|
1380
|
-
|
|
1381
|
-
// Snapshot hrefs of already-loaded stylesheets so we can detect new ones
|
|
1382
|
-
// added by the head merge and wait for them to finish loading before
|
|
1383
|
-
// making #page visible (prevents FOUC on cold-cache layout transitions).
|
|
1384
|
-
var _existingStyleHrefs = {};
|
|
1385
|
-
$("head").children("link[rel='stylesheet']").each(function() {
|
|
1386
|
-
var h = this.getAttribute("href"); if(h) _existingStyleHrefs[h] = true;
|
|
1387
|
-
});
|
|
1388
|
-
|
|
1389
1508
|
$("head").children().each(function() {
|
|
1390
1509
|
|
|
1391
1510
|
var el = this;
|
|
@@ -1394,16 +1513,16 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
1394
1513
|
head.children().each(function() {
|
|
1395
1514
|
|
|
1396
1515
|
found = this.isEqualNode(el);
|
|
1397
|
-
// Also match identical <style> tags by content
|
|
1398
|
-
if(!found && el.tagName === 'STYLE' && this.tagName === 'STYLE' &&
|
|
1399
|
-
el.textContent && this.textContent &&
|
|
1400
|
-
el.textContent.length > 100 && this.textContent.length === el.textContent.length) {
|
|
1401
|
-
found = this.textContent === el.textContent;
|
|
1402
|
-
}
|
|
1403
1516
|
return !found;
|
|
1404
1517
|
});
|
|
1405
1518
|
|
|
1406
|
-
// Preserve headlocked nodes
|
|
1519
|
+
// Preserve headlocked nodes: anything injected dynamically
|
|
1520
|
+
// after initial load (auto), URL-pattern matches in
|
|
1521
|
+
// Settings.headlock, or explicit data-headlock="true".
|
|
1522
|
+
// This is the win over the previous "never remove SCRIPT/
|
|
1523
|
+
// STYLE" rule — per-page server-rendered <style> blocks
|
|
1524
|
+
// (e.g. layout1 inline CSS) still get swapped, only
|
|
1525
|
+
// third-party / dynamically-injected ones are locked.
|
|
1407
1526
|
if(!found && Transparent.isHeadlocked(el)) found = true;
|
|
1408
1527
|
if(!found) this.remove();
|
|
1409
1528
|
});
|
|
@@ -1416,35 +1535,20 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
1416
1535
|
$("head").children().each(function() { found |= this.isEqualNode(el); });
|
|
1417
1536
|
if(!found) {
|
|
1418
1537
|
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
while ( ++i < N ) {
|
|
1429
|
-
if(attrs[i].name !== 'src') {
|
|
1430
|
-
script.setAttribute( attrs[i].name, attrs[i].value );
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
$("head").append(script);
|
|
1434
|
-
originalHeadNodes.add(script);
|
|
1435
|
-
} else {
|
|
1436
|
-
$("head").append(this);
|
|
1437
|
-
originalHeadNodes.add(this);
|
|
1438
|
-
}
|
|
1538
|
+
|
|
1539
|
+
if(this.tagName != "SCRIPT" || Settings["global_code"] == true) {
|
|
1540
|
+
|
|
1541
|
+
var clone = this.cloneNode(true);
|
|
1542
|
+
$("head").append(clone);
|
|
1543
|
+
// Register the new node as "original" so it falls
|
|
1544
|
+
// through to URL-pattern matching on the next swap
|
|
1545
|
+
// (and isn't auto-locked as third-party content).
|
|
1546
|
+
originalHeadNodes.add(clone);
|
|
1439
1547
|
|
|
1440
1548
|
} else {
|
|
1441
1549
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
// Register as an "original" node so it falls through to URL-pattern
|
|
1445
|
-
// matching on future transitions — prevents layout CSS added by
|
|
1446
|
-
// Transparent itself from being auto-headlocked as third-party content.
|
|
1447
|
-
originalHeadNodes.add(clonedEl);
|
|
1550
|
+
$("head").append(this);
|
|
1551
|
+
originalHeadNodes.add(this);
|
|
1448
1552
|
}
|
|
1449
1553
|
}
|
|
1450
1554
|
});
|
|
@@ -1458,27 +1562,10 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
1458
1562
|
$("body").children().each(function() { found |= this.isEqualNode(el); });
|
|
1459
1563
|
if(!found) {
|
|
1460
1564
|
|
|
1461
|
-
if(this.tagName
|
|
1462
|
-
|
|
1463
|
-
// Same inline-script recreation as for <head>.
|
|
1464
|
-
if(!this.src || this.src === '') {
|
|
1465
|
-
var script = document.createElement("script");
|
|
1466
|
-
script.text = this.innerHTML;
|
|
1467
|
-
var i = -1, attrs = this.attributes, attr;
|
|
1468
|
-
var N = attrs.length;
|
|
1469
|
-
while ( ++i < N ) {
|
|
1470
|
-
if(attrs[i].name !== 'src') {
|
|
1471
|
-
script.setAttribute( attrs[i].name, attrs[i].value );
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
$("body").append(script);
|
|
1475
|
-
} else {
|
|
1476
|
-
$("body").append(this);
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
} else {
|
|
1480
|
-
|
|
1565
|
+
if(this.tagName != "SCRIPT" || Settings["global_code"] == true) {
|
|
1481
1566
|
$("body").append(this.cloneNode(true));
|
|
1567
|
+
} else {
|
|
1568
|
+
$("body").append(this);
|
|
1482
1569
|
}
|
|
1483
1570
|
}
|
|
1484
1571
|
});
|
|
@@ -1495,13 +1582,7 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
1495
1582
|
// Make sure name/layout keep the same after a page change (tolerance for POST or GET requests)
|
|
1496
1583
|
if(oldPage.attr("data-layout") != undefined && page.attr("data-layout") != undefined) {
|
|
1497
1584
|
|
|
1498
|
-
|
|
1499
|
-
// (line ~1852: SWITCH.replace("X", prevLayout).replace("Y", newLayout)).
|
|
1500
|
-
// If these disagreed, the cleanup filter below would not recognize the
|
|
1501
|
-
// switchLayout class that handleResponse added to <html> and would strip
|
|
1502
|
-
// it before its CSS transition could play — visible as a race only in
|
|
1503
|
-
// whichever direction the project's CSS actually styles.
|
|
1504
|
-
var switchLayout = Transparent.state.SWITCH.replace("X", oldPage.attr("data-layout")).replace("Y", page.attr("data-layout"));
|
|
1585
|
+
var switchLayout = Transparent.state.SWITCH.replace("X", page.attr("data-layout")).replace("Y", oldPage.attr("data-layout"));
|
|
1505
1586
|
page.attr("data-layout-prev", oldPage.attr("data-layout"));
|
|
1506
1587
|
}
|
|
1507
1588
|
|
|
@@ -1510,13 +1591,10 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
1510
1591
|
var oldHtmlClass = Array.from(($(Transparent.html).attr("class") || "").split(" "));
|
|
1511
1592
|
var removeHtmlClass = oldHtmlClass.filter(x => !htmlClass.includes(x) && switchLayout != x && !states.includes(x));
|
|
1512
1593
|
|
|
1513
|
-
_tx("onLoad classMgmt", "switchLayout=" + switchLayout + " remove=[" + removeHtmlClass.join(",") + "] add=[" + htmlClass.join(",") + "]");
|
|
1514
1594
|
Transparent.html.removeClass(removeHtmlClass).addClass(htmlClass);
|
|
1515
|
-
_tx("onLoad PAGE_SWAP_BEGIN", "oldLayout=" + (oldPage.attr("data-layout")||"?") + " newLayout=" + (page.attr("data-layout")||"?"));
|
|
1516
1595
|
$(page).insertBefore(oldPage);
|
|
1517
1596
|
|
|
1518
1597
|
oldPage.remove();
|
|
1519
|
-
_tx("onLoad PAGE_SWAP_DONE");
|
|
1520
1598
|
|
|
1521
1599
|
if(Settings["global_code"] == true) Transparent.evalScript($(page)[0]);
|
|
1522
1600
|
document.dispatchEvent(new Event('DOMContentLoaded'));
|
|
@@ -1552,77 +1630,41 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
1552
1630
|
}
|
|
1553
1631
|
}
|
|
1554
1632
|
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1633
|
+
$('head').append(function() {
|
|
1634
|
+
|
|
1635
|
+
$(Settings.identifier).append(function() {
|
|
1636
|
+
|
|
1637
|
+
// Callback if needed, or any other actions
|
|
1638
|
+
callback();
|
|
1639
|
+
|
|
1640
|
+
// Trigger onload event
|
|
1641
|
+
dispatchEvent(new Event('transparent:load'));
|
|
1642
|
+
dispatchEvent(new Event('load'));
|
|
1643
|
+
});
|
|
1560
1644
|
});
|
|
1561
1645
|
|
|
1562
|
-
|
|
1563
|
-
// calling callback() / fadeOut() — otherwise #page becomes visible while
|
|
1564
|
-
// the new CSS is still being parsed, causing a flash of unstyled content.
|
|
1565
|
-
(function() {
|
|
1566
|
-
function doCallback() {
|
|
1567
|
-
_tx("doCallback FIRES → callback() (which starts fadeOut)");
|
|
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
|
-
});
|
|
1581
|
-
});
|
|
1582
|
-
});
|
|
1583
|
-
}
|
|
1584
|
-
// For cached stylesheets, the browser may fire `load` synchronously on
|
|
1585
|
-
// DOM insertion — BEFORE we can attach a listener — so listener-only
|
|
1586
|
-
// waits get stuck on the 3 s guard. `.sheet !== null` indicates the
|
|
1587
|
-
// CSSStyleSheet is already parsed and ready, which is the right
|
|
1588
|
-
// condition to count it as "done." Cross-origin sheets still expose
|
|
1589
|
-
// `.sheet` even though `.cssRules` throws — `.sheet !== null` is
|
|
1590
|
-
// portable.
|
|
1591
|
-
function isStyleLoaded(link) {
|
|
1592
|
-
try { return link.sheet !== null; } catch(e) { return true; }
|
|
1593
|
-
}
|
|
1594
|
-
var pending = _newStyleLinks.filter(function(l) { return !isStyleLoaded(l); });
|
|
1595
|
-
_tx("stylesheet-wait BEGIN", "newLinks=" + _newStyleLinks.length + " cachedSkipped=" + (_newStyleLinks.length - pending.length) + " pending=" + pending.length);
|
|
1596
|
-
if(pending.length === 0) {
|
|
1597
|
-
_tx("stylesheet-wait IMMEDIATE → doCallback");
|
|
1598
|
-
doCallback();
|
|
1599
|
-
} else {
|
|
1600
|
-
var remaining = pending.length;
|
|
1601
|
-
var fired = false;
|
|
1602
|
-
// Safety valve: if a stylesheet fails or stalls, don't block forever.
|
|
1603
|
-
var guard = setTimeout(function() {
|
|
1604
|
-
if(!fired) {
|
|
1605
|
-
_tx("stylesheet-wait GUARD fired (3s)", "remaining=" + remaining);
|
|
1606
|
-
fired = true; doCallback();
|
|
1607
|
-
}
|
|
1608
|
-
}, 3000);
|
|
1609
|
-
pending.forEach(function(link) {
|
|
1610
|
-
function onDone(e) {
|
|
1611
|
-
_tx("stylesheet-wait link.load", "remaining=" + (remaining-1) + " href=" + link.getAttribute("href"));
|
|
1612
|
-
if(--remaining <= 0 && !fired) {
|
|
1613
|
-
fired = true;
|
|
1614
|
-
clearTimeout(guard);
|
|
1615
|
-
_tx("stylesheet-wait ALL_LOADED → doCallback");
|
|
1616
|
-
doCallback();
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
link.addEventListener('load', onDone, {once:true});
|
|
1620
|
-
link.addEventListener('error', onDone, {once:true});
|
|
1621
|
-
});
|
|
1622
|
-
}
|
|
1623
|
-
})();
|
|
1646
|
+
}.bind(this);
|
|
1624
1647
|
|
|
1625
|
-
|
|
1648
|
+
// Dispatch the swap. With VT enabled AND supported, wrap in
|
|
1649
|
+
// document.startViewTransition() so the browser captures OLD/NEW
|
|
1650
|
+
// snapshots and crossfades natively. The setTimeout wait is for
|
|
1651
|
+
// the CSS fade-out to complete BEFORE VT begins, so VT captures
|
|
1652
|
+
// the already-faded state cleanly. Errors inside the callback
|
|
1653
|
+
// don't abort the swap — fall back to direct execution.
|
|
1654
|
+
var _vtEnabled = Settings["use_view_transitions"]
|
|
1655
|
+
&& typeof document.startViewTransition === "function";
|
|
1656
|
+
setTimeout(function() {
|
|
1657
|
+
if (_vtEnabled) {
|
|
1658
|
+
try {
|
|
1659
|
+
document.startViewTransition(_doSwapBody);
|
|
1660
|
+
} catch (e) {
|
|
1661
|
+
if (Settings.debug) console.warn("Transparent VT failed, falling back:", e);
|
|
1662
|
+
_doSwapBody();
|
|
1663
|
+
}
|
|
1664
|
+
} else {
|
|
1665
|
+
_doSwapBody();
|
|
1666
|
+
}
|
|
1667
|
+
}, activeInRemainingTime > 0 ? activeInRemainingTime : 1);
|
|
1626
1668
|
}
|
|
1627
1669
|
|
|
1628
1670
|
function uuidv4() {
|
|
@@ -1733,8 +1775,326 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
1733
1775
|
|
|
1734
1776
|
var ajaxSemaphore = false;
|
|
1735
1777
|
var formSubmission = false;
|
|
1778
|
+
|
|
1779
|
+
// ── User-typed form dirty tracking ──────────────────────────────────────
|
|
1780
|
+
//
|
|
1781
|
+
// True if the user has modified any form input via a real keystroke /
|
|
1782
|
+
// click / select. False after a fresh navigation OR after a form submit.
|
|
1783
|
+
// Used by onbeforeunload to decide whether to confirm.
|
|
1784
|
+
//
|
|
1785
|
+
// `e.isTrusted` filters out programmatic value mutations from JS init
|
|
1786
|
+
// code (Select2 setting the hidden field after dropdown selection,
|
|
1787
|
+
// Editor.js syncing JSON to a hidden <textarea>, datepicker init writing
|
|
1788
|
+
// a normalized value, etc.). Without this filter, the previous value-
|
|
1789
|
+
// comparison logic (formDataBefore vs formDataAfter) flagged every
|
|
1790
|
+
// page-with-form as "dirty" by the time the user hit Ctrl+W, even
|
|
1791
|
+
// when they hadn't typed anything.
|
|
1792
|
+
var formDirty = false;
|
|
1793
|
+
document.addEventListener('input', function(e) {
|
|
1794
|
+
if (!e.isTrusted) return;
|
|
1795
|
+
if (!e.target || !e.target.form) return;
|
|
1796
|
+
formDirty = true;
|
|
1797
|
+
}, true);
|
|
1798
|
+
document.addEventListener('change', function(e) {
|
|
1799
|
+
if (!e.isTrusted) return;
|
|
1800
|
+
if (!e.target || !e.target.form) return;
|
|
1801
|
+
formDirty = true;
|
|
1802
|
+
}, true);
|
|
1803
|
+
// Reset on user-initiated submit so the post-submit redirect doesn't
|
|
1804
|
+
// double-prompt. The existing `formSubmission` flag already handles
|
|
1805
|
+
// the synchronous prompt path; this clears state for any follow-up.
|
|
1806
|
+
document.addEventListener('submit', function() { formDirty = false; }, true);
|
|
1807
|
+
// Reset on every navigation. transparent.js re-dispatches DOMContentLoaded
|
|
1808
|
+
// after each SPA swap (see _doSwap, ~line 1567), so this fires on the
|
|
1809
|
+
// initial load AND on each in-place navigation. Without it the flag leaks
|
|
1810
|
+
// across SPA navigations: typing on page A, then navigating to a form
|
|
1811
|
+
// page B, would leave formDirty=true and wrongly prompt on reload of B
|
|
1812
|
+
// even though the user never touched B. A freshly-loaded page is never
|
|
1813
|
+
// dirty until the user types on it (any restored draft is already saved).
|
|
1814
|
+
document.addEventListener('DOMContentLoaded', function() { formDirty = false; });
|
|
1815
|
+
|
|
1816
|
+
// ── Transparent.formMemory ──────────────────────────────────────────────
|
|
1817
|
+
//
|
|
1818
|
+
// Persistent draft store for forms — survives accidental close, refresh,
|
|
1819
|
+
// power loss, browser crash. Keyed by URL + form identity (name or id).
|
|
1820
|
+
//
|
|
1821
|
+
// Save triggers:
|
|
1822
|
+
// - debounced (500ms) on user `input`/`change` events
|
|
1823
|
+
// - synchronously on beforeunload (last-resort capture for fields
|
|
1824
|
+
// mutated by JS like Editor.js / Select2 that don't fire trusted
|
|
1825
|
+
// `input` events)
|
|
1826
|
+
//
|
|
1827
|
+
// Restore: silently on initial DOMContentLoaded AND after each SPA swap
|
|
1828
|
+
// (transparent.js re-dispatches DOMContentLoaded post-swap, line ~1375).
|
|
1829
|
+
// Restored fields get `data-restored-from-draft=""` for optional
|
|
1830
|
+
// project-level toast / styling.
|
|
1831
|
+
//
|
|
1832
|
+
// Clear: on form submit + on TTL expiry (7 days) + manually via
|
|
1833
|
+
// `Transparent.formMemory.clear(form)`.
|
|
1834
|
+
//
|
|
1835
|
+
// Opt-out:
|
|
1836
|
+
// - `<form data-no-persist>` — entire form skipped
|
|
1837
|
+
// - `<input data-no-persist>` — single field skipped
|
|
1838
|
+
// - Auto-skipped: type="password", type="file", type="submit"/button,
|
|
1839
|
+
// and any hidden field whose name contains `_token` or `csrf`
|
|
1840
|
+
// (Symfony CSRF token field). These are never persisted.
|
|
1841
|
+
//
|
|
1842
|
+
// Editor.js compatibility: Editor.js reads its initial JSON from
|
|
1843
|
+
// `data-edjs` attribute (not `.value`). On restore, if the field is a
|
|
1844
|
+
// <textarea data-edjs>, we mirror the restored value into `data-edjs`
|
|
1845
|
+
// too so Editor.js renders the draft when its init pass runs.
|
|
1846
|
+
Transparent.formMemory = (function() {
|
|
1847
|
+
var KEY_PREFIX = 'tx-form-memory:';
|
|
1848
|
+
var DEFAULT_TTL = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
1849
|
+
var DEBOUNCE = 500;
|
|
1850
|
+
|
|
1851
|
+
var saveTimers = new WeakMap();
|
|
1852
|
+
var api = {
|
|
1853
|
+
enabled : true,
|
|
1854
|
+
ttl : DEFAULT_TTL,
|
|
1855
|
+
debounce: DEBOUNCE,
|
|
1856
|
+
};
|
|
1857
|
+
|
|
1858
|
+
function shouldSkipField(field) {
|
|
1859
|
+
if (!field.name) return true;
|
|
1860
|
+
var type = (field.type || '').toLowerCase();
|
|
1861
|
+
if (type === 'password' || type === 'file') return true;
|
|
1862
|
+
if (type === 'submit' || type === 'button' || type === 'reset') return true;
|
|
1863
|
+
if (field.hasAttribute && field.hasAttribute('data-no-persist')) return true;
|
|
1864
|
+
// CSRF tokens — never persist. Matches Symfony's `_token` and
|
|
1865
|
+
// any other token-named hidden input (`csrf`, `_csrf_token`, ...).
|
|
1866
|
+
if (type === 'hidden') {
|
|
1867
|
+
var n = field.name.toLowerCase();
|
|
1868
|
+
if (n.indexOf('_token') !== -1 || n.indexOf('csrf') !== -1) return true;
|
|
1869
|
+
}
|
|
1870
|
+
return false;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
function shouldSkipForm(form) {
|
|
1874
|
+
if (!form) return true;
|
|
1875
|
+
if (form.hasAttribute && form.hasAttribute('data-no-persist')) return true;
|
|
1876
|
+
if (!form.name && !form.id) return true; // need identity for key
|
|
1877
|
+
return false;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
function getKey(form) {
|
|
1881
|
+
return KEY_PREFIX + location.pathname + ':' + (form.name || form.id);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
function readLS(key) {
|
|
1885
|
+
try { return localStorage.getItem(key); } catch (e) { return null; }
|
|
1886
|
+
}
|
|
1887
|
+
function writeLS(key, val) {
|
|
1888
|
+
try { localStorage.setItem(key, val); } catch (e) {
|
|
1889
|
+
// Quota exceeded or storage disabled — fail silent. The
|
|
1890
|
+
// user keeps their work in memory, just loses persistence.
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
function removeLS(key) {
|
|
1894
|
+
try { localStorage.removeItem(key); } catch (e) {}
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
api.save = function(form) {
|
|
1898
|
+
if (!api.enabled) return;
|
|
1899
|
+
if (shouldSkipForm(form)) return;
|
|
1900
|
+
|
|
1901
|
+
var data = {};
|
|
1902
|
+
var elements = form.elements;
|
|
1903
|
+
for (var i = 0; i < elements.length; i++) {
|
|
1904
|
+
var field = elements[i];
|
|
1905
|
+
if (shouldSkipField(field)) continue;
|
|
1906
|
+
|
|
1907
|
+
var type = (field.type || '').toLowerCase();
|
|
1908
|
+
if (type === 'checkbox' || type === 'radio') {
|
|
1909
|
+
if (!field.checked) continue;
|
|
1910
|
+
if (data[field.name] === undefined) {
|
|
1911
|
+
data[field.name] = field.value;
|
|
1912
|
+
} else if (Array.isArray(data[field.name])) {
|
|
1913
|
+
data[field.name].push(field.value);
|
|
1914
|
+
} else {
|
|
1915
|
+
data[field.name] = [data[field.name], field.value];
|
|
1916
|
+
}
|
|
1917
|
+
} else if (field.tagName === 'SELECT' && field.multiple) {
|
|
1918
|
+
var sel = [];
|
|
1919
|
+
for (var j = 0; j < field.options.length; j++) {
|
|
1920
|
+
if (field.options[j].selected) sel.push(field.options[j].value);
|
|
1921
|
+
}
|
|
1922
|
+
data[field.name] = sel;
|
|
1923
|
+
} else {
|
|
1924
|
+
data[field.name] = field.value;
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
if (Object.keys(data).length === 0) {
|
|
1929
|
+
// No persistable fields with data — clear any stale entry
|
|
1930
|
+
// rather than write an empty record (avoids restoring "all
|
|
1931
|
+
// empty" later and overwriting newly-pre-filled fields).
|
|
1932
|
+
removeLS(getKey(form));
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
writeLS(getKey(form), JSON.stringify({ t: Date.now(), d: data }));
|
|
1937
|
+
};
|
|
1938
|
+
|
|
1939
|
+
api.restore = function(form) {
|
|
1940
|
+
if (!api.enabled) return;
|
|
1941
|
+
if (shouldSkipForm(form)) return;
|
|
1942
|
+
|
|
1943
|
+
var key = getKey(form);
|
|
1944
|
+
var raw = readLS(key);
|
|
1945
|
+
if (!raw) return;
|
|
1946
|
+
|
|
1947
|
+
var entry;
|
|
1948
|
+
try { entry = JSON.parse(raw); }
|
|
1949
|
+
catch (e) { removeLS(key); return; }
|
|
1950
|
+
|
|
1951
|
+
if (!entry || !entry.t || !entry.d) { removeLS(key); return; }
|
|
1952
|
+
if (Date.now() - entry.t > api.ttl) { removeLS(key); return; }
|
|
1953
|
+
|
|
1954
|
+
Object.keys(entry.d).forEach(function(name) {
|
|
1955
|
+
var value = entry.d[name];
|
|
1956
|
+
// Use attr-selector with CSS.escape to handle names like
|
|
1957
|
+
// `Article[content]` that contain brackets.
|
|
1958
|
+
var sel = '[name="' + (typeof CSS !== 'undefined' && CSS.escape
|
|
1959
|
+
? CSS.escape(name)
|
|
1960
|
+
: name.replace(/(["\\\[\]])/g, '\\$1')) + '"]';
|
|
1961
|
+
var fields = form.querySelectorAll(sel);
|
|
1962
|
+
if (fields.length === 0) return;
|
|
1963
|
+
|
|
1964
|
+
for (var k = 0; k < fields.length; k++) {
|
|
1965
|
+
var field = fields[k];
|
|
1966
|
+
if (shouldSkipField(field)) continue;
|
|
1967
|
+
|
|
1968
|
+
var type = (field.type || '').toLowerCase();
|
|
1969
|
+
if (type === 'checkbox' || type === 'radio') {
|
|
1970
|
+
field.checked = Array.isArray(value)
|
|
1971
|
+
? (value.indexOf(field.value) !== -1)
|
|
1972
|
+
: (field.value === value);
|
|
1973
|
+
} else if (field.tagName === 'SELECT' && field.multiple) {
|
|
1974
|
+
if (Array.isArray(value)) {
|
|
1975
|
+
for (var m = 0; m < field.options.length; m++) {
|
|
1976
|
+
field.options[m].selected = (value.indexOf(field.options[m].value) !== -1);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
} else {
|
|
1980
|
+
field.value = value;
|
|
1981
|
+
// Editor.js mirrors: Editor.js reads its initial JSON
|
|
1982
|
+
// from the `data-edjs` attribute, not from .value. So
|
|
1983
|
+
// we have to mirror the restored value into the attr
|
|
1984
|
+
// for the upcoming Editor.js init pass to pick it up.
|
|
1985
|
+
if (field.tagName === 'TEXTAREA' && field.hasAttribute('data-edjs')) {
|
|
1986
|
+
field.setAttribute('data-edjs', value);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
field.setAttribute('data-restored-from-draft', '');
|
|
1990
|
+
}
|
|
1991
|
+
});
|
|
1992
|
+
};
|
|
1993
|
+
|
|
1994
|
+
api.clear = function(form) {
|
|
1995
|
+
if (!form) return;
|
|
1996
|
+
if (shouldSkipForm(form)) return;
|
|
1997
|
+
removeLS(getKey(form));
|
|
1998
|
+
};
|
|
1999
|
+
|
|
2000
|
+
api.restoreAll = function() {
|
|
2001
|
+
var forms = document.querySelectorAll('form');
|
|
2002
|
+
for (var i = 0; i < forms.length; i++) api.restore(forms[i]);
|
|
2003
|
+
};
|
|
2004
|
+
|
|
2005
|
+
api.saveAll = function() {
|
|
2006
|
+
var forms = document.querySelectorAll('form');
|
|
2007
|
+
for (var i = 0; i < forms.length; i++) api.save(forms[i]);
|
|
2008
|
+
};
|
|
2009
|
+
|
|
2010
|
+
api.clearExpired = function() {
|
|
2011
|
+
var ttl = api.ttl;
|
|
2012
|
+
var now = Date.now();
|
|
2013
|
+
try {
|
|
2014
|
+
for (var i = localStorage.length - 1; i >= 0; i--) {
|
|
2015
|
+
var key = localStorage.key(i);
|
|
2016
|
+
if (!key || key.indexOf(KEY_PREFIX) !== 0) continue;
|
|
2017
|
+
var raw;
|
|
2018
|
+
try { raw = localStorage.getItem(key); }
|
|
2019
|
+
catch (e) { continue; }
|
|
2020
|
+
var entry = null;
|
|
2021
|
+
try { entry = JSON.parse(raw); } catch (e) {}
|
|
2022
|
+
if (!entry || !entry.t || (now - entry.t > ttl)) {
|
|
2023
|
+
removeLS(key);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
} catch (e) {}
|
|
2027
|
+
};
|
|
2028
|
+
|
|
2029
|
+
// Debounced per-form save scheduler. WeakMap → no leak when forms
|
|
2030
|
+
// get removed from the DOM (e.g., on SPA swap).
|
|
2031
|
+
function debouncedSave(form) {
|
|
2032
|
+
if (!form || shouldSkipForm(form)) return;
|
|
2033
|
+
var existing = saveTimers.get(form);
|
|
2034
|
+
if (existing) clearTimeout(existing);
|
|
2035
|
+
saveTimers.set(form, setTimeout(function() {
|
|
2036
|
+
api.save(form);
|
|
2037
|
+
}, api.debounce));
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// Event wiring. Capture phase + isTrusted gate matches the
|
|
2041
|
+
// formDirty pattern above — programmatic field mutations from
|
|
2042
|
+
// Select2/Editor.js init code don't trigger a save here, which is
|
|
2043
|
+
// correct (those are not "user changes"; saving them would persist
|
|
2044
|
+
// server-rendered defaults as if they were user input).
|
|
2045
|
+
document.addEventListener('input', function(e) {
|
|
2046
|
+
if (!e.isTrusted) return;
|
|
2047
|
+
if (!e.target || !e.target.form) return;
|
|
2048
|
+
debouncedSave(e.target.form);
|
|
2049
|
+
}, true);
|
|
2050
|
+
document.addEventListener('change', function(e) {
|
|
2051
|
+
if (!e.isTrusted) return;
|
|
2052
|
+
if (!e.target || !e.target.form) return;
|
|
2053
|
+
debouncedSave(e.target.form);
|
|
2054
|
+
}, true);
|
|
2055
|
+
|
|
2056
|
+
// Successful submit clears the draft. We listen in capture so we
|
|
2057
|
+
// run before any user-side submit handler that might cancel.
|
|
2058
|
+
document.addEventListener('submit', function(e) {
|
|
2059
|
+
if (e.target && e.target.tagName === 'FORM') {
|
|
2060
|
+
api.clear(e.target);
|
|
2061
|
+
}
|
|
2062
|
+
}, true);
|
|
2063
|
+
|
|
2064
|
+
// Last-resort save on page exit. Catches state mutated only by JS
|
|
2065
|
+
// (Editor.js content syncs, Select2 hidden-field writes) that
|
|
2066
|
+
// never fired trusted input/change events. Synchronous because
|
|
2067
|
+
// beforeunload doesn't await microtasks.
|
|
2068
|
+
window.addEventListener('beforeunload', function() {
|
|
2069
|
+
if (!api.enabled) return;
|
|
2070
|
+
// Only save if the user has actually typed something — same
|
|
2071
|
+
// gate as the unload-confirm prompt above. If formDirty is
|
|
2072
|
+
// false, drafts written from the debounced handler are stale
|
|
2073
|
+
// and we don't want to refresh them on every page-close.
|
|
2074
|
+
if (formDirty) api.saveAll();
|
|
2075
|
+
});
|
|
2076
|
+
|
|
2077
|
+
// Initial restore. If we're already past DOMContentLoaded (defer
|
|
2078
|
+
// scripts run after parsing but before DCL), run synchronously.
|
|
2079
|
+
// For SPA navs, transparent.js re-dispatches DOMContentLoaded in
|
|
2080
|
+
// _doSwap (around line 1375), so this same listener fires again
|
|
2081
|
+
// on every swap — no extra wiring needed.
|
|
2082
|
+
if (document.readyState === 'loading') {
|
|
2083
|
+
document.addEventListener('DOMContentLoaded', api.restoreAll);
|
|
2084
|
+
} else {
|
|
2085
|
+
api.restoreAll();
|
|
2086
|
+
}
|
|
2087
|
+
document.addEventListener('DOMContentLoaded', api.restoreAll);
|
|
2088
|
+
|
|
2089
|
+
// Cleanup expired entries — once at startup, deferred so it
|
|
2090
|
+
// doesn't block first paint.
|
|
2091
|
+
setTimeout(api.clearExpired, 2000);
|
|
2092
|
+
|
|
2093
|
+
return api;
|
|
2094
|
+
})();
|
|
2095
|
+
|
|
1736
2096
|
function __main__(e) {
|
|
1737
|
-
|
|
2097
|
+
|
|
1738
2098
|
// Disable transparent JS (e.g. during development..)
|
|
1739
2099
|
if(Settings.disable) return;
|
|
1740
2100
|
|
|
@@ -1826,17 +2186,6 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
1826
2186
|
if (ajaxSemaphore) return;
|
|
1827
2187
|
if (url == location) return;
|
|
1828
2188
|
|
|
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
|
-
|
|
1840
2189
|
if((e.type == Transparent.state.CLICK || e.type == Transparent.state.HASHCHANGE) && url.pathname == location.pathname && url.search == location.search && type != "POST") {
|
|
1841
2190
|
|
|
1842
2191
|
if(!url.hash) return;
|
|
@@ -1862,7 +2211,7 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
1862
2211
|
$(Transparent.html).stop();
|
|
1863
2212
|
|
|
1864
2213
|
Transparent.html.addClass(Transparent.state.LOADING);
|
|
1865
|
-
Transparent.
|
|
2214
|
+
Transparent.activeIn();
|
|
1866
2215
|
|
|
1867
2216
|
function isJsonResponse(str) {
|
|
1868
2217
|
try { JSON.parse(str); return true; }
|
|
@@ -1870,7 +2219,6 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
1870
2219
|
}
|
|
1871
2220
|
|
|
1872
2221
|
function handleResponse(uuid, status = 200, method = null, data = null, xhr = null, request = null) {
|
|
1873
|
-
_tx("handleResponse ENTRY", "status=" + status + " method=" + method);
|
|
1874
2222
|
|
|
1875
2223
|
ajaxSemaphore = false;
|
|
1876
2224
|
|
|
@@ -1906,7 +2254,26 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
1906
2254
|
Transparent.setResponse(uuid, responseText);
|
|
1907
2255
|
}
|
|
1908
2256
|
|
|
1909
|
-
|
|
2257
|
+
// Try the in-memory live-DOM cache first. On popstate (back/forward)
|
|
2258
|
+
// we just stored the outgoing page node via setLiveResponse, so the
|
|
2259
|
+
// round-trip is: snapshot → cache → instant retrieve. No serialize,
|
|
2260
|
+
// no DOMParser cost. Falls back to the parse path on miss (first-
|
|
2261
|
+
// time nav, cache eviction, expired entry, etc.).
|
|
2262
|
+
var dom = null;
|
|
2263
|
+
if (Transparent.getLiveResponse) {
|
|
2264
|
+
var liveNode = Transparent.getLiveResponse(uuid);
|
|
2265
|
+
if (liveNode) {
|
|
2266
|
+
// The cache stores the full <html> element. Wrap it in a
|
|
2267
|
+
// minimal Document-shaped object that the swap path can
|
|
2268
|
+
// navigate the same way it navigates a DOMParser result.
|
|
2269
|
+
var docShell = document.implementation.createHTMLDocument('');
|
|
2270
|
+
docShell.replaceChild(liveNode, docShell.documentElement);
|
|
2271
|
+
dom = docShell;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
if (!dom) {
|
|
2275
|
+
dom = new DOMParser().parseFromString(responseText, "text/html");
|
|
2276
|
+
}
|
|
1910
2277
|
if(request && request.getResponseHeader("Content-Type") == "application/json") {
|
|
1911
2278
|
|
|
1912
2279
|
if(!isJsonResponse(responseText)) {
|
|
@@ -1956,18 +2323,12 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
1956
2323
|
history.pushState({uuid: uuid, status:status, method: method, data: {}, href: responseURL}, '', responseURL);
|
|
1957
2324
|
|
|
1958
2325
|
// Page not recognized.. just go fetch by yourself.. no POST information transmitted..
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
if(!Transparent.isPage(dom)) {
|
|
1962
|
-
_waitForFadeIn(function() { window.location.href = url; });
|
|
1963
|
-
return;
|
|
1964
|
-
}
|
|
2326
|
+
if(!Transparent.isPage(dom))
|
|
2327
|
+
return window.location.href = url;
|
|
1965
2328
|
|
|
1966
2329
|
// Layout not compatible.. needs to be reloaded (exception when POST is detected..)
|
|
1967
|
-
if(!Transparent.isCompatiblePage(dom, method, data))
|
|
1968
|
-
|
|
1969
|
-
return;
|
|
1970
|
-
}
|
|
2330
|
+
if(!Transparent.isCompatiblePage(dom, method, data))
|
|
2331
|
+
return window.location.href = url;
|
|
1971
2332
|
|
|
1972
2333
|
// Mark layout as known
|
|
1973
2334
|
if(!Transparent.isKnownLayout(dom)) {
|
|
@@ -1995,46 +2356,16 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
1995
2356
|
Transparent.html.addClass(Transparent.state.SAME);
|
|
1996
2357
|
|
|
1997
2358
|
var switchLayout = Transparent.state.SWITCH.replace("X", prevLayout).replace("Y", newLayout);
|
|
1998
|
-
_tx("handleResponse switchLayout", "prev=" + prevLayout + " new=" + newLayout + " adds=." + switchLayout);
|
|
1999
2359
|
Transparent.html.addClass(switchLayout);
|
|
2000
2360
|
|
|
2001
2361
|
dispatchEvent(new Event('transparent:'+switchLayout));
|
|
2002
2362
|
|
|
2003
|
-
if($(dom).find("html").hasClass(Transparent.state.RELOAD) || $(dom).find("html").hasClass(Transparent.state.DISABLE))
|
|
2004
|
-
|
|
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
|
-
}
|
|
2015
|
-
|
|
2016
|
-
// Kick off preloads for stylesheets the new page needs but aren't yet in <head>.
|
|
2017
|
-
// They download in parallel during the fadeIn animation so onLoad() finds them
|
|
2018
|
-
// already cached — eliminating FOUC on cold-cache layout transitions.
|
|
2019
|
-
(function() {
|
|
2020
|
-
var loaded = {};
|
|
2021
|
-
$("head").children("link[rel='stylesheet']").each(function() {
|
|
2022
|
-
var h = this.getAttribute("href"); if(h) loaded[h] = true;
|
|
2023
|
-
});
|
|
2024
|
-
|
|
2025
|
-
$(dom).find("head").children("link[rel='stylesheet']").each(function() {
|
|
2026
|
-
var h = this.getAttribute("href");
|
|
2027
|
-
if(!h || loaded[h]) return;
|
|
2028
|
-
if($("head").find("link[rel='preload'][href='" + h.replace(/'/g, "\\'") + "']").length) return;
|
|
2029
|
-
var pl = document.createElement("link");
|
|
2030
|
-
pl.rel = "preload"; pl.as = "style"; pl.href = h;
|
|
2031
|
-
document.head.appendChild(pl);
|
|
2032
|
-
});
|
|
2033
|
-
})();
|
|
2363
|
+
if($(dom).find("html").hasClass(Transparent.state.RELOAD) || $(dom).find("html").hasClass(Transparent.state.DISABLE))
|
|
2364
|
+
return window.location.reload();
|
|
2034
2365
|
|
|
2035
2366
|
return Transparent.onLoad(uuid, dom, function() {
|
|
2036
2367
|
|
|
2037
|
-
Transparent.
|
|
2368
|
+
Transparent.activeOut(function() {
|
|
2038
2369
|
|
|
2039
2370
|
Transparent.html
|
|
2040
2371
|
.removeClass(switchLayout)
|
|
@@ -2057,7 +2388,7 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
2057
2388
|
if(history.state)
|
|
2058
2389
|
Transparent.setResponse(history.state.uuid, Transparent.html[0], Transparent.getScrollableElementXY());
|
|
2059
2390
|
|
|
2060
|
-
$(Transparent.html).prop("user-scroll", false); // make sure to avoid page jump during transition (cancelled in
|
|
2391
|
+
$(Transparent.html).prop("user-scroll", false); // make sure to avoid page jump during transition (cancelled in activeIn callback)
|
|
2061
2392
|
|
|
2062
2393
|
// Submit ajax request..
|
|
2063
2394
|
if(form) form.dispatchEvent(new SubmitEvent("submit", { submitter: formTrigger }));
|
|
@@ -2072,18 +2403,8 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
2072
2403
|
processData: false,
|
|
2073
2404
|
headers: Settings["headers"] || {},
|
|
2074
2405
|
xhr: function () { return xhr; },
|
|
2075
|
-
success: function (html, status, 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
|
-
}
|
|
2406
|
+
success: function (html, status, request) { return handleResponse(uuid, request.status, type, data, xhr, request); },
|
|
2407
|
+
error: function (request, ajaxOptions, thrownError) { return handleResponse(uuid, request.status, type, data, xhr, request); }
|
|
2087
2408
|
});
|
|
2088
2409
|
}
|
|
2089
2410
|
|
|
@@ -2114,100 +2435,38 @@ jQuery.event.special.mousewheel = { setup: function( _, ns, handle ) { this.addE
|
|
|
2114
2435
|
window.onpopstate = __main__; // Onpopstate pop out straight to previous page.. this creates a jump while changing pages with hash..
|
|
2115
2436
|
window.onhashchange = __main__;
|
|
2116
2437
|
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
} else formData.append(this.name, this.value);
|
|
2133
|
-
});
|
|
2134
|
-
|
|
2135
|
-
for (var [fieldName,fieldValue] of formData.entries()) {
|
|
2136
|
-
|
|
2137
|
-
if(!fieldName.endsWith("[]") && fieldName != "undefined")
|
|
2138
|
-
formDataBefore[fieldName] = fieldValue;
|
|
2139
|
-
}
|
|
2140
|
-
});
|
|
2141
|
-
});
|
|
2142
|
-
|
|
2143
|
-
window.onbeforeunload = function(e) {
|
|
2144
|
-
|
|
2145
|
-
if(Settings.debug) console.log("Transparent onbeforeunload event called..");
|
|
2146
|
-
|
|
2147
|
-
if(formSubmission) return; // Do not display on form submission
|
|
2438
|
+
// onbeforeunload confirmation REMOVED. It produced spurious "are you
|
|
2439
|
+
// sure you want to leave?" blocks on any page with a form even when the
|
|
2440
|
+
// user never typed: the dirty flag was flipped by any trusted change
|
|
2441
|
+
// event (a <select>/checkbox toggle, Select2/datepicker init firing a
|
|
2442
|
+
// real change, browser autofill, …), and the `e.currentTarget == window`
|
|
2443
|
+
// guard below is unreliable across browsers. More importantly it's now
|
|
2444
|
+
// obsolete: Transparent.formMemory persists every form's content to
|
|
2445
|
+
// localStorage (debounced on input + saved synchronously on unload) and
|
|
2446
|
+
// restores it on the next load, so reloading or closing the tab never
|
|
2447
|
+
// loses typed input. The draft save on beforeunload (in the formMemory
|
|
2448
|
+
// IIFE above) is kept; only the blocking confirmation is gone.
|
|
2449
|
+
var __onbeforeunload_disabled = function(e) {
|
|
2450
|
+
if(Settings.debug) console.log("Transparent onbeforeunload (no-op; drafts auto-saved)");
|
|
2451
|
+
if(formSubmission) return;
|
|
2148
2452
|
if(Settings.disable) return;
|
|
2453
|
+
// No return value → browser never shows the leave/reload confirmation.
|
|
2454
|
+
};
|
|
2149
2455
|
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
} else formData.append(this.name, this.value);
|
|
2166
|
-
});
|
|
2167
|
-
|
|
2168
|
-
for (var [fieldName,fieldValue] of formData.entries()) {
|
|
2169
|
-
|
|
2170
|
-
if(!fieldName.endsWith("[]") && fieldName != "undefined")
|
|
2171
|
-
formDataAfter[fieldName] = fieldValue;
|
|
2172
|
-
}
|
|
2173
|
-
});
|
|
2174
|
-
|
|
2175
|
-
var formDataBeforeKeys = Object.keys(formDataBefore);
|
|
2176
|
-
var formDataAfterKeys = Object.keys(formDataAfter);
|
|
2177
|
-
function same(a, b) { return JSON.stringify(a) === JSON.stringify(b); }
|
|
2178
|
-
function sameKeys(a, b) {
|
|
2179
|
-
|
|
2180
|
-
var aKeys = Object.keys(a).sort();
|
|
2181
|
-
var bKeys = Object.keys(b).sort();
|
|
2182
|
-
return JSON.stringify(aKeys) === JSON.stringify(bKeys);
|
|
2183
|
-
}
|
|
2184
|
-
|
|
2185
|
-
if(!sameKeys(formDataBeforeKeys, formDataAfterKeys)) preventDefault = true;
|
|
2186
|
-
else {
|
|
2187
|
-
|
|
2188
|
-
for (var [fieldName,fieldValueAfter] of Object.entries(formDataAfter)) {
|
|
2189
|
-
|
|
2190
|
-
var fieldValueBefore = formDataBefore[fieldName];
|
|
2191
|
-
if(fieldValueBefore instanceof File) {
|
|
2192
|
-
|
|
2193
|
-
if(!fieldValueAfter instanceof File) preventDefault = true;
|
|
2194
|
-
else if (fieldValueBefore.size != fieldValueAfter.size) preventDefault = true;
|
|
2195
|
-
|
|
2196
|
-
} else if(fieldValueBefore != fieldValueAfter) {
|
|
2197
|
-
preventDefault = true;
|
|
2198
|
-
}
|
|
2199
|
-
}
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
if(Settings.debug || preventDefault) {
|
|
2203
|
-
|
|
2204
|
-
if(preventDefault) Transparent.html.addClass(Transparent.state.READY);
|
|
2205
|
-
if(preventDefault) Transparent.fadeOut();
|
|
2206
|
-
if(preventDefault) dispatchEvent(new Event('load'));
|
|
2207
|
-
|
|
2208
|
-
return "Dude, are you sure you want to leave? Think of the kittens!";
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2456
|
+
// Legacy snapshot-and-compare confirm (formDataBefore vs formDataAfter)
|
|
2457
|
+
// gave
|
|
2458
|
+
// false positives because JS init code mutates form values after
|
|
2459
|
+
// load — Select2 writes selected text to hidden fields, Editor.js
|
|
2460
|
+
// serializes its JSON to a <textarea>, datepicker normalizes
|
|
2461
|
+
// formats, etc. — so by the time the user hit Ctrl+W the snapshot
|
|
2462
|
+
// and the current state always differed, and the browser always
|
|
2463
|
+
// prompted even on read-only pages.
|
|
2464
|
+
//
|
|
2465
|
+
// false positives because JS init code mutated form values after load.
|
|
2466
|
+
// Both that and the formDirty replacement are gone: the content is
|
|
2467
|
+
// already in localStorage (saved on input + on unload), so leaving the
|
|
2468
|
+
// page never loses it and a confirmation only gets in the way.
|
|
2469
|
+
window.onbeforeunload = __onbeforeunload_disabled;
|
|
2211
2470
|
|
|
2212
2471
|
document.addEventListener('click', __main__, false);
|
|
2213
2472
|
|