PageSourceSearch

https://canalplan.org.uk/can_js/link_preview.js?ver=9.45.11

js canalplan.org.uk collected 2026-09-24 08:54:45 UTC 8,484 bytes, 228 lines download raw bytes

1// displays a pop-up when you hover over a link
2
3// at page load, add new mouse events to all internal page links
4document.addEventListener("DOMContentLoaded", function() {
5    Add_Preview_To_Links();
6});
7
8// a separate function so it can be called when marker links are added
9// to pages
10function Add_Preview_To_Links(srcel=document) {
11    let lnks = srcel.getElementsByTagName('a');
12    let base = window.location.origin+'/';  // eg https://canalplan.uk/
13    Object.values(lnks).forEach(function(lnk) {
14        if(lnk.href.indexOf(base) == 0) {  // it's an internal link
15            if(!lnk.dataset.previewenabled) {
16                let lk = lnk.href.substr(base.length).split('/');
17                if(['area','feature','place','waterway','boat','location'].includes(lk[0])) {
18                    if(lk[0] == 'location')
19                        lk = Preview_Resolve_Location(lk[1]);
20                    if(lk[1].indexOf('$')==-1) {  // don't do magic ones
21                        // it's a link to a page
22                        lnk.addEventListener("mouseover", e=> {
23                            Show_Preview(lnk, lk[0], lk[1], e);
24                        });
25                        lnk.addEventListener("mouseout", e=> {
26                            Hide_Preview();
27                        });
28                        lnk.dataset.previewenabled = true;
29                    }
30                }
31            }
32        }
33    });
34    // now add preview to navigational notes
35    document.querySelectorAll('.navnotelink').forEach(function(lnk) {
36        if(!lnk.dataset.previewenabled) {
37            lnk.addEventListener("mouseover", e=> {
38                Show_Preview(lnk, 'navnote', lnk.dataset.nnnum, e);
39            });
40            lnk.addEventListener("mouseout", e=> {
41                Hide_Preview();
42            });
43            lnk.dataset.previewenabled = true;
44        }
45    });
46    // element to pu
47    let preview = document.createElement('div');
48    preview.id = 'preview_pane';
49    preview.classList.add('link_preview_pane');
50    document.body.appendChild(preview);
51}
52
53function Preview_Resolve_Location(loc) {
54    if(loc.indexOf(':') == -1)   // plain place
55        return ['place', loc];
56    let wk = loc.split(':');
57    switch(wk[0]) {
58    case 'a':
59        return ['area', wk[1]];
60    case 'f':
61    case 's':
62        return ['feature', wk[1]];
63    case 'w':
64    case 'ww':
65        return ['waterway', wk[1]];
66    case 'p':
67        return ['place', wk[1]];
68    default:
69        throw 'Unknown location code:'+loc
70    }
71}
72
73// we want to be sure you are actually hovering, not just passing through
74// avoids problem when you move the mouse over a list of links and queue
75// dozens of previews!
76var preview_timeout = null;
77
78function Show_Preview(anchor, type, id, mouse) {
79    let preview = $$('preview_pane');
80    clearTimeout(preview_timeout);   // kill any previous timeouts
81    preview_timeout = setTimeout(function() {
82        Fetch_Preview(type, id).then(txt=>{
83            preview.style.visibility = 'visible';
84            preview.innerHTML = txt;
85            // this means that we position it correctly after all in internals
86            // have been rendered
87            const resizeObserver = new ResizeObserver(entries => {
88                let previewbox = preview.getBoundingClientRect();
89                let anchorbox = anchor.getBoundingClientRect();
90                if(mouse.clientX < window.innerWidth/2) {  // left half of screen
91                    x = mouse.clientX;
92                } else {   // right half of screen
93                    x = mouse.clientX - previewbox.width;
94                }
95                // we use the two mouse Y coordinates to work out what offset
96                // the screen has!
97                let yoff = mouse.layerY - mouse.clientY;
98                if(mouse.clientY < window.innerHeight/2) { // top half of screen
99                    // vertical has a small offset to move off the text
100                    y = anchorbox.bottom + yoff + 10;
101                } else {   // bottom half of screen
102                    y = anchorbox.bottom + yoff - previewbox.height - 10;
103                }
104                preview.style.left = x+"px";
105                preview.style.top = y+"px";
106            });
107            resizeObserver.observe(preview);
108        });
109    }, 200);   // seems to be reasonable short delay
110}
111
112function Hide_Preview() {
113    clearTimeout(preview_timeout);
114    let preview = $$('preview_pane');
115    preview.innerHTML = '';
116    preview.style.visibility = 'hidden';
117}
118
119var link_preview_cache = {};
120
121async function Fetch_Preview(type, id) {
122    if(link_preview_cache[type+':'+id]) {
123        return link_preview_cache[type+':'+id];
124    }
125    var mtab = {
126        area:     {frompage: true},
127        boat:     {frompage: false},
128        feature:  {frompage: true},
129        place:    {frompage: true},
130        waterway: {frompage: true},
131        navnote:  {frompage: false}
132    };
133    if(mtab[type].frompage) {
134        await fetch("/"+type+"/"+id)
135            .then(data => {
136                if(data.ok)
137                    return data.text();
138                throw new Error('Fetch failed for page "'+type+'/'+id+'"');
139            }).then(res => {
140                var parser = new DOMParser();
141                var newdoc = parser.parseFromString(res,"text/html");
142                let parts = Useful_Elements(newdoc);
143                link_preview_cache[type+':'+id] = Format_Preview(parts);
144            });
145        return link_preview_cache[type+':'+id];
146    } else {
147        if(type == 'boat') {
148            await fetch("/render/boat_info?nobutton=yes&id="+id)
149                .then(data => {
150                    if(data.ok)
151                        return data.text();
152                    throw new Error('Fetch failed for remote_render');
153                }).then(res => {
154                    link_preview_cache["boat:"+id] = res;
155                });
156            return link_preview_cache["boat:"+id];
157        } else {   // navnote
158            // we have it in the page, so no need to fetch
159            let nhtml1 = document.getElementById('navnote-header-'+id);
160            let nhtml2 = document.getElementById('navnote-body-'+id);
161            let parts = Useful_Elements_Navnote(nhtml1, nhtml2);
162            link_preview_cache[type+':'+id] = Format_Preview(parts);
163            return link_preview_cache[type+':'+id];
164        }
165    }
166}
167
168function Preview_Remove_Links(el) {
169    let lnks = el.getElementsByTagName('a');
170    Object.values(lnks).forEach(function(lnk) {
171        lnk.classList.remove('quiet');
172        lnk.classList.add('preview_hide_link');
173    });    
174    return el;
175}
176
177// pulls elements we are likely to need from the page HTML
178function Useful_Elements(dom) {
179    var toret = {};
180    [
181        {el:'place_name_show',op:'innerHTML',as:'name'},
182        {el:'place_detail_show',op:'innerHTML',as:'detail'},
183        {el:'best_photo',op:'src',as:'thumbnail'},
184        {el:'pagecloseupmapimg',op:'src',as:'circlemap'},
185        {el:'summary_description',op:'innerHTML',as:'description',call:Preview_Remove_Links}
186    ].forEach(i=> {
187        let item = dom.getElementById(i.el);
188        if(item) {
189            if(i.call)
190                item = i.call(item);
191            toret[i.as] = item[i.op];
192        }
193    });
194    return toret;
195}
196
197function Useful_Elements_Navnote(header, body) {
198    let toret = {};
199
200    let nname = header.getElementsByClassName('navnote-name');
201    toret.name = nname[0].childNodes[0].innerHTML;
202
203    let nmap = body.getElementsByClassName('cc_map');
204    toret.circlemap = nmap[0].src;
205    
206    let nphoto = body.getElementsByClassName('navigational_photo');
207    if(nphoto.length) {
208        toret.thumbnail = nphoto[0].childNodes[0].src;
209        toret.thumbnail = toret.thumbnail.replace('200x200', 'thumbnails');
210    } else
211        toret.thumbnail = null;
212    
213    let ntext = body.getElementsByClassName('vertbox-inner');
214    toret.description = ntext[0].childNodes[0].wholeText;
215    return toret;
216}
217
218function Format_Preview(items) {
219    let txt = `
220<div class="link_preview_flex_row">
221<div class="link_preview_flex_item">${items.thumbnail ? `<img src="${items.thumbnail}">` : ""}</div>
222<div class="link_preview_flex_item"><h1>${items.name}</h1>${items.detail || ''}</div>
223<div class="link_preview_flex_item">${items.circlemap ? `<img class="cc_map link_preview_map" src="${items.circlemap}">` : ""}</div>
224</div>
225<div class="link_preview_text">${items.description}</div>`;
226    return txt
227}
228

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.