PageSourceSearch

https://www1.appliedsystems.com/JavaScript/link-preloader.js?v=9SDlQjnWqmstvBUixYJ-eh2UWr1m79Y56_4DSYAKD1g

js appliedsystems.com collected 2026-09-24 08:39:41 UTC 12,973 bytes, 343 lines download raw bytes

1/*
2 * Link Preloader
3 *
4 * Speculatively loads internal pages so subsequent navigations feel instant.
5 *
6 * Two modes run cooperatively:
7 *
8 * 1. Hover/focus mode (devices with a real pointer):
9 *    - Chromium with Speculation Rules API -> `prerender`: fetches AND renders
10 *      the destination page (subresources, scripts, DOMContentLoaded).
11 *    - Other browsers -> `<link rel="prefetch">`: warms the HTML cache only.
12 *
13 * 2. Viewport mode (touch-only devices by default; opt-in on desktop):
14 *    - Uses IntersectionObserver to watch internal anchors. When a link is
15 *      on-screen (with a ~300px lookahead margin) for at least
16 *      `viewportMinVisibleMs`, a cancellable `<link rel="prefetch">` is added.
17 *    - When the link scrolls off-screen, the prefetch element is removed,
18 *      cancelling any in-flight request and freeing our concurrency slot.
19 *      (If the response already arrived it stays in the HTTP cache.)
20 *    - Refcounted per URL so duplicate links don't double-fetch.
21 *
22 * Per-link opt-out: add `data-no-prefetch`.
23 * Global config: set `window.LinkPreloaderConfig = { ... }` before this script.
24 *
25 * Caveat: prerender executes the destination page's scripts, so pages with
26 * load-time side effects (premature analytics, logout endpoints, etc.) should
27 * opt out via `data-no-prefetch`.
28 */
29{
30    if (typeof window !== 'undefined' && typeof document !== 'undefined') {
31        const DEFAULTS = {
32            debounceDelay: 120,
33            maxConcurrentPrefetches: 3,
34            respectSaveData: true,
35            enableViewportPrefetch: true,
36            enableViewportOnDesktop: false,
37            viewportRootMargin: '300px 0px',
38            viewportMinVisibleMs: 400,
39            maxViewportPrefetches: 4,
40        };
41
42        const config = { ...DEFAULTS, ...(window.LinkPreloaderConfig ?? {}) };
43
44        const prefetched = new Set();
45        const timers = new WeakMap();
46        let inFlight = 0;
47
48        const isTouchOnlyDevice = () =>
49            'ontouchstart' in window && !window.matchMedia('(hover: hover)').matches;
50
51        const isSaveDataConstrained = () => {
52            const { connection } = navigator;
53            if (!connection) return false;
54            return connection.saveData === true || /(^|-)2g$/.test(connection.effectiveType ?? '');
55        };
56
57        const supportsPrefetch = () => {
58            const { relList } = document.createElement('link');
59            return relList && typeof relList.supports === 'function'
60                ? relList.supports('prefetch')
61                : 'prefetch' in (relList ?? {});
62        };
63
64        const supportsSpeculationRules = () =>
65            typeof HTMLScriptElement !== 'undefined' &&
66            typeof HTMLScriptElement.supports === 'function' &&
67            HTMLScriptElement.supports('speculationrules');
68
69        const canPrerender = supportsSpeculationRules();
70
71        const shouldPrefetch = (anchor) => {
72            if (!anchor || anchor.tagName !== 'A') return false;
73            if (anchor.hasAttribute('data-no-prefetch')) return false;
74            if (anchor.target?.toLowerCase() === '_blank') return false;
75
76            const rel = (anchor.getAttribute('rel') ?? '').toLowerCase();
77            if (rel.includes('external') || rel.includes('nofollow')) return false;
78
79            const rawHref = anchor.getAttribute('href');
80            if (!rawHref) return false;
81
82            const trimmed = rawHref.trim();
83            if (!trimmed || trimmed.startsWith('#')) return false;
84
85            const lower = trimmed.toLowerCase();
86            if (
87                lower.startsWith('mailto:') ||
88                lower.startsWith('tel:') ||
89                lower.startsWith('javascript:') ||
90                lower.startsWith('sms:')
91            ) {
92                return false;
93            }
94
95            const resolved = anchor.href;
96            if (!resolved || !resolved.startsWith(window.location.origin)) return false;
97
98            const [current] = window.location.href.split('#');
99            const [target] = resolved.split('#');
100            if (target === current) return false;
101
102            return !prefetched.has(target);
103        };
104
105        const addPrerenderRule = (url) => {
106            const script = document.createElement('script');
107            script.type = 'speculationrules';
108            script.textContent = JSON.stringify({
109                prerender: [{ source: 'list', urls: [url] }],
110            });
111            document.head.appendChild(script);
112        };
113
114        const addPrefetchLink = (url, asHint) => {
115            const link = document.createElement('link');
116            link.rel = 'prefetch';
117            link.href = url;
118            if (asHint) link.as = asHint;
119
120            const release = () => {
121                inFlight = Math.max(0, inFlight - 1);
122            };
123
124            link.addEventListener('load', release, { once: true });
125            link.addEventListener('error', release, { once: true });
126            document.head.appendChild(link);
127        };
128
129        const prefetch = (url, asHint) => {
130            if (prefetched.has(url)) return;
131            if (inFlight >= config.maxConcurrentPrefetches) return;
132
133            prefetched.add(url);
134            inFlight += 1;
135
136            if (canPrerender) {
137                try {
138                    addPrerenderRule(url);
139                } catch {
140                    addPrefetchLink(url, asHint);
141                    return;
142                }
143                // Prerender doesn't emit a load event we can listen on; release
144                // the slot shortly after so we don't starve other hovers.
145                window.setTimeout(() => {
146                    inFlight = Math.max(0, inFlight - 1);
147                }, 1500);
148                return;
149            }
150
151            addPrefetchLink(url, asHint);
152        };
153
154        const scheduleFromAnchor = (anchor) => {
155            if (!shouldPrefetch(anchor)) return;
156            if (timers.has(anchor)) return;
157
158            const timerId = window.setTimeout(() => {
159                timers.delete(anchor);
160                if (!shouldPrefetch(anchor)) return;
161                const [resolved] = anchor.href.split('#');
162                const asHint = /\.pdf(\?|$)/i.test(resolved) ? 'document' : 'document';
163                prefetch(resolved, asHint);
164            }, config.debounceDelay);
165
166            timers.set(anchor, timerId);
167        };
168
169        const cancelFromAnchor = (anchor) => {
170            const timerId = timers.get(anchor);
171            if (timerId) {
172                window.clearTimeout(timerId);
173                timers.delete(anchor);
174            }
175        };
176
177        const anchorFromEvent = (event) => event.target?.closest?.('a') ?? null;
178
179        const onPointerOver = (event) => {
180            const anchor = anchorFromEvent(event);
181            if (anchor) scheduleFromAnchor(anchor);
182        };
183
184        const onPointerOut = (event) => {
185            const anchor = anchorFromEvent(event);
186            if (anchor) cancelFromAnchor(anchor);
187        };
188
189        const onFocusIn = (event) => {
190            if (event.target?.tagName === 'A') scheduleFromAnchor(event.target);
191        };
192
193        const onFocusOut = (event) => {
194            if (event.target?.tagName === 'A') cancelFromAnchor(event.target);
195        };
196
197        // ---- Viewport mode (mobile / opt-in desktop) ----
198
199        const viewportHandles = new Map(); // url -> { element, refCount }
200        const viewportTimers = new WeakMap(); // anchor -> timerId
201        const viewportUrls = new WeakMap(); // anchor -> url currently held open
202        let viewportInFlight = 0;
203
204        const startViewportPrefetch = (url) => {
205            const existing = viewportHandles.get(url);
206            if (existing) {
207                existing.refCount += 1;
208                return true;
209            }
210            if (viewportInFlight >= config.maxViewportPrefetches) return false;
211            if (prefetched.has(url)) {
212                // Already prefetched via hover/prerender — just track refs so a
213                // later release call balances cleanly without removing anything.
214                viewportHandles.set(url, { element: null, refCount: 1 });
215                return true;
216            }
217
218            const link = document.createElement('link');
219            link.rel = 'prefetch';
220            link.href = url;
221            link.as = 'document';
222            document.head.appendChild(link);
223
224            viewportInFlight += 1;
225            viewportHandles.set(url, { element: link, refCount: 1 });
226            return true;
227        };
228
229        const stopViewportPrefetch = (url) => {
230            const handle = viewportHandles.get(url);
231            if (!handle) return;
232            handle.refCount -= 1;
233            if (handle.refCount > 0) return;
234
235            if (handle.element) {
236                handle.element.remove();
237                viewportInFlight = Math.max(0, viewportInFlight - 1);
238            }
239            viewportHandles.delete(url);
240        };
241
242        const onIntersect = (entries) => {
243            for (const entry of entries) {
244                const anchor = entry.target;
245
246                if (entry.isIntersecting) {
247                    if (viewportTimers.has(anchor)) continue;
248                    if (!shouldPrefetch(anchor)) continue;
249
250                    const timerId = window.setTimeout(() => {
251                        viewportTimers.delete(anchor);
252                        if (!shouldPrefetch(anchor)) return;
253                        const [url] = anchor.href.split('#');
254                        if (startViewportPrefetch(url)) {
255                            viewportUrls.set(anchor, url);
256                        }
257                    }, config.viewportMinVisibleMs);
258
259                    viewportTimers.set(anchor, timerId);
260                } else {
261                    const timerId = viewportTimers.get(anchor);
262                    if (timerId) {
263                        window.clearTimeout(timerId);
264                        viewportTimers.delete(anchor);
265                    }
266                    const url = viewportUrls.get(anchor);
267                    if (url) {
268                        stopViewportPrefetch(url);
269                        viewportUrls.delete(anchor);
270                    }
271                }
272            }
273        };
274
275        const observedAnchors = new WeakSet();
276        let intersectionObserver = null;
277
278        const observeAnchor = (anchor) => {
279            if (!intersectionObserver) return;
280            if (!(anchor instanceof HTMLAnchorElement)) return;
281            if (observedAnchors.has(anchor)) return;
282            if (!anchor.hasAttribute('href')) return;
283            observedAnchors.add(anchor);
284            intersectionObserver.observe(anchor);
285        };
286
287        const scanForAnchors = (root) => {
288            if (root.nodeType !== 1 && root.nodeType !== 9) return;
289            if (root instanceof HTMLAnchorElement) observeAnchor(root);
290            root.querySelectorAll?.('a[href]').forEach(observeAnchor);
291        };
292
293        const initViewportMode = () => {
294            if (typeof IntersectionObserver === 'undefined') return;
295            if (typeof MutationObserver === 'undefined') return;
296
297            intersectionObserver = new IntersectionObserver(onIntersect, {
298                rootMargin: config.viewportRootMargin,
299                threshold: 0.01,
300            });
301
302            const start = () => {
303                scanForAnchors(document.body);
304                new MutationObserver((mutations) => {
305                    for (const mutation of mutations) {
306                        for (const node of mutation.addedNodes) {
307                            scanForAnchors(node);
308                        }
309                    }
310                }).observe(document.body, { childList: true, subtree: true });
311            };
312
313            if (document.readyState === 'loading') {
314                document.addEventListener('DOMContentLoaded', start, { once: true });
315            } else {
316                start();
317            }
318        };
319
320        // ---- Bootstrap ----
321
322        const hasHover = !isTouchOnlyDevice();
323        const bailForData = config.respectSaveData && isSaveDataConstrained();
324        const canDoAnyPrefetch = supportsPrefetch() || canPrerender;
325
326        if (!bailForData && canDoAnyPrefetch) {
327            if (hasHover) {
328                document.addEventListener('pointerover', onPointerOver, { passive: true });
329                document.addEventListener('pointerout', onPointerOut, { passive: true });
330                document.addEventListener('focusin', onFocusIn, { passive: true });
331                document.addEventListener('focusout', onFocusOut, { passive: true });
332            }
333
334            const enableViewport =
335                config.enableViewportPrefetch &&
336                (!hasHover || config.enableViewportOnDesktop);
337
338            if (enableViewport) {
339                initViewportMode();
340            }
341        }
342    }
343}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.