aboutsummaryrefslogtreecommitdiff
path: root/src/main/resources/js/prefetch.js
blob: a2394463601b8ec773e54a3869f43473be1c756f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/*! instant.page v1.2.2 - (C) 2019 Alexandre Dieulot - https://instant.page/license */

let urlToPreload;
let mouseoverTimer;
let lastTouchTimestamp;

const prefetcher = document.createElement('link');
const isSupported = prefetcher.relList && prefetcher.relList.supports && prefetcher.relList.supports('prefetch');
const isDataSaverEnabled = navigator.connection && navigator.connection.saveData;
const allowQueryString = 'instantAllowQueryString' in document.body.dataset;
const allowExternalLinks = 'instantAllowExternalLinks' in document.body.dataset;

if (isSupported && !isDataSaverEnabled) {
    prefetcher.rel = 'prefetch';
    document.head.appendChild(prefetcher);

    const eventListenersOptions = {
        capture: true,
        passive: true,
    };
    document.addEventListener('touchstart', touchstartListener, eventListenersOptions);
    document.addEventListener('mouseover', mouseoverListener, eventListenersOptions)
}

function touchstartListener(event) {
    /* Chrome on Android calls mouseover before touchcancel so `lastTouchTimestamp`
     * must be assigned on touchstart to be measured on mouseover. */
    lastTouchTimestamp = performance.now();

    const linkElement = event.target.closest('a');

    if (!isPreloadable(linkElement)) {
        return
    }

    linkElement.addEventListener('touchcancel', touchendAndTouchcancelListener, {passive: true});
    linkElement.addEventListener('touchend', touchendAndTouchcancelListener, {passive: true});

    urlToPreload = linkElement.href;
    preload(linkElement.href)
}

function touchendAndTouchcancelListener() {
    urlToPreload = undefined;
    stopPreloading()
}

function mouseoverListener(event) {
    if (performance.now() - lastTouchTimestamp < 1100) {
        return
    }

    const linkElement = event.target.closest('a');

    if (!isPreloadable(linkElement)) {
        return
    }

    linkElement.addEventListener('mouseout', mouseoutListener, {passive: true});

    urlToPreload = linkElement.href;

    mouseoverTimer = setTimeout(() => {
        preload(linkElement.href);
        mouseoverTimer = undefined
    }, 65)
}

function mouseoutListener(event) {
    if (event.relatedTarget && event.target.closest('a') === event.relatedTarget.closest('a')) {
        return
    }

    if (mouseoverTimer) {
        clearTimeout(mouseoverTimer);
        mouseoverTimer = undefined
    } else {
        urlToPreload = undefined;
        stopPreloading()
    }
}

function isPreloadable(linkElement) {
    if (!linkElement || !linkElement.href) {
        return
    }

    if (urlToPreload === linkElement.href) {
        return
    }

    const preloadLocation = new URL(linkElement.href);

    if (!allowExternalLinks && preloadLocation.origin !== location.origin && !('instant' in linkElement.dataset)) {
        return
    }

    if (!['http:', 'https:'].includes(preloadLocation.protocol)) {
        return
    }

    if (preloadLocation.protocol === 'http:' && location.protocol === 'https:') {
        return
    }

    if (!allowQueryString && preloadLocation.search && !('instant' in linkElement.dataset)) {
        return
    }

    if (preloadLocation.hash && preloadLocation.pathname + preloadLocation.search === location.pathname + location.search) {
        return
    }

    if ('noInstant' in linkElement.dataset) {
        return
    }

    return true
}

function preload(url) {
    prefetcher.href = url
}

function stopPreloading() {
    prefetcher.removeAttribute('href')
}