PageSourceSearch

https://me.micahrl.com/js/instantpage.js

js micahrl.com collected 2026-10-03 23:08:43 UTC 11,672 bytes, 330 lines download raw bytes

vendor: 11,672 bytes, lines 1-330
1/*! instant.page v5.2.0 - (C) 2019-2023 Alexandre Dieulot - https://instant.page/license */
2
3let _chromiumMajorVersionInUserAgent = null
4  , _allowQueryString
5  , _allowExternalLinks
6  , _useWhitelist
7  , _delayOnHover = 65
8  , _lastTouchTimestamp
9  , _mouseoverTimer
10  , _preloadedList = new Set()
11
12const DELAY_TO_NOT_BE_CONSIDERED_A_TOUCH_INITIATED_ACTION = 1111
13
14init()
15
16function init() {
17  const isSupported = document.createElement('link').relList.supports('prefetch')
18  // instant.page is meant to be loaded with <script type=module>
19  // (though sometimes webmasters load it as a regular script).
20  // So it’s normally executed (and must not cause JavaScript errors) in:
21  // - Chromium 61+
22  // - Gecko in Firefox 60+
23  // - WebKit in Safari 10.1+ (iOS 10.3+, macOS 10.10+)
24  //
25  // The check above used to check for IntersectionObserverEntry.isIntersecting
26  // but module scripts support implies this compatibility — except in Safari
27  // 10.1–12.0, but this prefetch check takes care of it.
28
29  if (!isSupported) {
30    return
31  }
32
33  const handleVaryAcceptHeader = 'instantVaryAccept' in document.body.dataset || 'Shopify' in window
34  // The `Vary: Accept` header when received in Chromium 79–109 makes prefetches
35  // unusable, as Chromium used to send a different `Accept` header.
36  // It’s applied on all Shopify sites by default, as Shopify is very popular
37  // and is the main source of this problem.
38  // `window.Shopify` only exists on “classic” Shopify sites. Those using
39  // Hydrogen (Remix SPA) aren’t concerned.
40
41  const chromiumUserAgentIndex = navigator.userAgent.indexOf('Chrome/')
42  if (chromiumUserAgentIndex > -1) {
43    _chromiumMajorVersionInUserAgent = parseInt(navigator.userAgent.substring(chromiumUserAgentIndex + 'Chrome/'.length))
44  }
45  // The user agent client hints API is a theoretically more reliable way to
46  // get Chromium’s version… but it’s not available in Samsung Internet 20.
47  // It also requires a secure context, which would make debugging harder,
48  // and is only available in recent Chromium versions.
49  // In practice, Chromium browsers never shy from announcing "Chrome" in
50  // their regular user agent string, as that maximizes their compatibility.
51
52  if (handleVaryAcceptHeader && _chromiumMajorVersionInUserAgent && _chromiumMajorVersionInUserAgent < 110) {
53    return
54  }
55
56  const mousedownShortcut = 'instantMousedownShortcut' in document.body.dataset
57  _allowQueryString = 'instantAllowQueryString' in document.body.dataset
58  _allowExternalLinks = 'instantAllowExternalLinks' in document.body.dataset
59  _useWhitelist = 'instantWhitelist' in document.body.dataset
60
61  const eventListenersOptions = {
62    capture: true,
63    passive: true,
64  }
65
66  let useMousedown = false
67  let useMousedownOnly = false
68  let useViewport = false
69  if ('instantIntensity' in document.body.dataset) {
70    const intensity = document.body.dataset.instantIntensity
71
72    if (intensity.startsWith('mousedown')) {
73      useMousedown = true
74      if (intensity == 'mousedown-only') {
75        useMousedownOnly = true
76      }
77    }
78    else if (intensity.startsWith('viewport')) {
79      const isNavigatorConnectionSaveDataEnabled = navigator.connection && navigator.connection.saveData
80      const isNavigatorConnectionLike2g = navigator.connection && navigator.connection.effectiveType && navigator.connection.effectiveType.includes('2g')
81      if (!isNavigatorConnectionSaveDataEnabled && !isNavigatorConnectionLike2g) {
82        if (intensity == "viewport") {
83          if (document.documentElement.clientWidth * document.documentElement.clientHeight < 450000) {
84            useViewport = true
85            // Smartphones are the most likely to have a slow connection, and
86            // their small screen size limits the number of links (and thus
87            // server load).
88            //
89            // Foldable phones (being expensive as of 2023), tablets and PCs
90            // generally have a decent connection, and a big screen displaying
91            // more links that would put more load on the server.
92            //
93            // iPhone 14 Pro Max (want): 430×932 = 400 760
94            // Samsung Galaxy S22 Ultra with display size set to 80% (want):
95            // 450×965 = 434 250
96            // Small tablet (don’t want): 600×960 = 576 000
97            // Those number are virtual screen size, the viewport (used for
98            // the check above) will be smaller with the browser’s interface.
99          }
100        }
101        else if (intensity == "viewport-all") {
102          useViewport = true
103        }
104      }
105    }
106    else {
107      const milliseconds = parseInt(intensity)
108      if (!isNaN(milliseconds)) {
109        _delayOnHover = milliseconds
110      }
111    }
112  }
113
114  if (!useMousedownOnly) {
115    document.addEventListener('touchstart', touchstartListener, eventListenersOptions)
116  }
117
118  if (!useMousedown) {
119    document.addEventListener('mouseover', mouseoverListener, eventListenersOptions)
120  }
121  else if (!mousedownShortcut) {
122      document.addEventListener('mousedown', mousedownListener, eventListenersOptions)
123  }
124
125  if (mousedownShortcut) {
126    document.addEventListener('mousedown', mousedownShortcutListener, eventListenersOptions)
127  }
128
129  if (useViewport) {
130    let requestIdleCallbackOrFallback = window.requestIdleCallback
131    // Safari has no support as of 16.3: https://webkit.org/b/164193
132    if (!requestIdleCallbackOrFallback) {
133      requestIdleCallbackOrFallback = (callback) => {
134        callback()
135        // A smarter fallback like setTimeout is not used because devices that
136        // may eventually be eligible to a Safari version supporting prefetch
137        // will be very powerful.
138        // The weakest devices that could be eligible are the 2017 iPad and
139        // the 2016 MacBook.
140      }
141    }
142
143    requestIdleCallbackOrFallback(function observeIntersection() {
144      const intersectionObserver = new IntersectionObserver((entries) => {
145        entries.forEach((entry) => {
146          if (entry.isIntersecting) {
147            const anchorElement = entry.target
148            intersectionObserver.unobserve(anchorElement)
149            preload(anchorElement.href)
150          }
151        })
152      })
153
154      document.querySelectorAll('a').forEach((anchorElement) => {
155        if (isPreloadable(anchorElement)) {
156          intersectionObserver.observe(anchorElement)
157        }
158      })
159    }, {
160      timeout: 1500,
161    })
162  }
163}
164
165function touchstartListener(event) {
166  _lastTouchTimestamp = performance.now()
167  // Chrome on Android triggers mouseover before touchcancel, so
168  // `_lastTouchTimestamp` must be assigned on touchstart to be measured
169  // on mouseover.
170
171  const anchorElement = event.target.closest('a')
172
173  if (!isPreloadable(anchorElement)) {
174    return
175  }
176
177  preload(anchorElement.href, 'high')
178}
179
180function mouseoverListener(event) {
181  if (performance.now() - _lastTouchTimestamp < DELAY_TO_NOT_BE_CONSIDERED_A_TOUCH_INITIATED_ACTION) {
182    return
183  }
184
185  if (!('closest' in event.target)) {
186    return
187    // Without this check sometimes an error “event.target.closest is not a function” is thrown, for unknown reasons
188    // That error denotes that `event.target` isn’t undefined. My best guess is that it’s the Document.
189    //
190    // Details could be gleaned from throwing such an error:
191    //throw new TypeError(`instant.page non-element event target: timeStamp=${~~event.timeStamp}, type=${event.type}, typeof=${typeof event.target}, nodeType=${event.target.nodeType}, nodeName=${event.target.nodeName}, viewport=${innerWidth}x${innerHeight}, coords=${event.clientX}x${event.clientY}, scroll=${scrollX}x${scrollY}`)
192  }
193  const anchorElement = event.target.closest('a')
194
195  if (!isPreloadable(anchorElement)) {
196    return
197  }
198
199  anchorElement.addEventListener('mouseout', mouseoutListener, {passive: true})
200
201  _mouseoverTimer = setTimeout(() => {
202    preload(anchorElement.href, 'high')
203    _mouseoverTimer = undefined
204  }, _delayOnHover)
205}
206
207function mousedownListener(event) {
208  const anchorElement = event.target.closest('a')
209
210  if (!isPreloadable(anchorElement)) {
211    return
212  }
213
214  preload(anchorElement.href, 'high')
215}
216
217function mouseoutListener(event) {
218  if (event.relatedTarget && event.target.closest('a') == event.relatedTarget.closest('a')) {
219    return
220  }
221
222  if (_mouseoverTimer) {
223    clearTimeout(_mouseoverTimer)
224    _mouseoverTimer = undefined
225  }
226}
227
228function mousedownShortcutListener(event) {
229  if (performance.now() - _lastTouchTimestamp < DELAY_TO_NOT_BE_CONSIDERED_A_TOUCH_INITIATED_ACTION) {
230    return
231  }
232
233  const anchorElement = event.target.closest('a')
234
235  if (event.which > 1 || event.metaKey || event.ctrlKey) {
236    return
237  }
238
239  if (!anchorElement) {
240    return
241  }
242
243  anchorElement.addEventListener('click', function (event) {
244    if (event.detail == 1337) {
245      return
246    }
247
248    event.preventDefault()
249  }, {capture: true, passive: false, once: true})
250
251  const customEvent = new MouseEvent('click', {view: window, bubbles: true, cancelable: false, detail: 1337})
252  anchorElement.dispatchEvent(customEvent)
253}
254
255function isPreloadable(anchorElement) {
256  if (!anchorElement || !anchorElement.href) {
257    return
258  }
259
260  if (_useWhitelist && !('instant' in anchorElement.dataset)) {
261    return
262  }
263
264  if (anchorElement.origin != location.origin) {
265    let allowed = _allowExternalLinks || 'instant' in anchorElement.dataset
266    if (!allowed || !_chromiumMajorVersionInUserAgent) {
267      // Chromium-only: see comment on “restrictive prefetch”
268      return
269    }
270  }
271
272  if (!['http:', 'https:'].includes(anchorElement.protocol)) {
273    return
274  }
275
276  if (anchorElement.protocol == 'http:' && location.protocol == 'https:') {
277    return
278  }
279
280  if (!_allowQueryString && anchorElement.search && !('instant' in anchorElement.dataset)) {
281    return
282  }
283
284  if (anchorElement.hash && anchorElement.pathname + anchorElement.search == location.pathname + location.search) {
285    return
286  }
287
288  if ('noInstant' in anchorElement.dataset) {
289    return
290  }
291
292  return true
293}
294
295function preload(url, fetchPriority = 'auto') {
296  if (_preloadedList.has(url)) {
297    return
298  }
299
300  const linkElement = document.createElement('link')
301  linkElement.rel = 'prefetch'
302  linkElement.href = url
303
304  linkElement.fetchPriority = fetchPriority
305  // By default, a prefetch is loaded with a low priority.
306  // When there’s a fair chance that this prefetch is going to be used in the
307  // near term (= after a touch/mouse event), giving it a high priority helps
308  // make the page load faster in case there are other resources loading.
309  // Prioritizing it implicitly means deprioritizing every other resource
310  // that’s loading on the page. Due to HTML documents usually being much
311  // smaller than other resources (notably images and JavaScript), and
312  // prefetches happening once the initial page is sufficiently loaded,
313  // this theft of bandwidth should rarely be detrimental.
314
315  linkElement.as = 'document'
316  // as=document is Chromium-only and allows cross-origin prefetches to be
317  // usable for navigation. They call it “restrictive prefetch” and intend
318  // to remove it: https://crbug.com/1352371
319  //
320  // This document from the Chrome team dated 2022-08-10
321  // https://docs.google.com/document/d/1x232KJUIwIf-k08vpNfV85sVCRHkAxldfuIA5KOqi6M
322  // claims (I haven’t tested) that data- and battery-saver modes as well as
323  // the setting to disable preloading do not disable restrictive prefetch,
324  // unlike regular prefetch. That’s good for prefetching on a touch/mouse
325  // event, but might be bad when prefetching every link in the viewport.
326
327  document.head.appendChild(linkElement)
328
329  _preloadedList.add(url)
330}

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.