1/* This file is loaded by every important page, so this is a good 2 * place to put the automatic error reporting function. We trap the 3 * error and send back a "carry on" status, but then - after a short 4 * delay to let things carry on nicely - report it */ 5 6//REMOVE BEFORE RELEASE 7//window.onerror = function(msg, url, line, column, err) { 8// return Report_An_Error(msg, url, line, column, err); 9//}; 10 11/* 12window.onunhandledrejection = function(evt) { 13 evt.preventDefault(); 14 // catch for apple which throws these all over the place 15 // if(evt.reason == 'TypeError: Load failed' && navigator.userAgent.indexOf('AppleWebKit/605.1.15' != -1)) 16 // April 2024 - all sorts of promise errors from Safari. Kill them all! 17 if(navigator.userAgent.indexOf('AppleWebKit/605.1.15' != -1)) 18 return false; 19 return Report_An_Error("Error in promise - "+evt.reason, evt.srcElement.location.toString(), 0, 0, evt); 20} 21*/ 22var js_query = document.currentScript.src.substring(document.currentScript.src.indexOf('?')); 23 24function $$(id) { 25 return document.getElementById(id); 26} 27 28// useful for empty callbacks 29function do_nothing() { 30} 31 32//*** Last updated August 2021 33//*** POLYFILLS 34//*** for Safari 10 35if (!Object.values) Object.values = o=>Object.keys(o).map(k=>o[k]); 36 37//** for Mozilla/5.0 (Windows Phone 10.0; Android 6.0.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Mobile Safari/537.36 Edge/15.15254 38 39// Add a getElementsByClassName function if the browser doesn't have one 40// Limitation: only works with one class name 41// Copyright: Eike Send http://eike.se/nd 42// License: MIT License 43 44if (!document.getElementsByClassName) { 45 document.getElementsByClassName = function(search) { 46 var d = document, elements, pattern, i, results = []; 47 if (d.querySelectorAll) { // IE8 48 return d.querySelectorAll("." + search); 49 } 50 if (d.evaluate) { // IE6, IE7 51 pattern = ".//*[contains(concat(' ', @class, ' '), ' " + search + " ')]"; 52 elements = d.evaluate(pattern, d, null, 0, null); 53 while ((i = elements.iterateNext())) { 54 results.push(i); 55 } 56 } else { 57 elements = d.getElementsByTagName("*"); 58 pattern = new RegExp("(^|\\s)" + search + "(\\s|$)"); 59 for (let i = 0; i < elements.length; i++) { 60 if ( pattern.test(elements[i].className) ) { 61 results.push(elements[i]); 62 } 63 } 64 } 65 return results; 66 } 67} 68 69//*** end of POLYFILLS 70 71// a global variable for us to keep useful stuff in when debugging subtle bugs 72if(typeof(cp_debug_data)=='undefined') 73 var cp_debug_data = []; 74 75var errors_reported = {}; 76function Report_An_Error(msg, url, line, column, err) { 77 if(errors_reported.any) 78 return false; // simple check to prevent 1000s from one loop 79 // sometimes my code wants to die silently (often to avoid other bugs) 80 if(msg.indexOf('Ignore this error') != -1) 81 return true; 82 // something (browser plug ins? advert?) can inject buggy code into a page 83 // this will appear in page (not JS file) and in line 1 84 if(url.indexOf('.js') == -1 && line == 1) 85 return false; 86 // trap for something very odd (hundreds of messages, different code, all 87 // same error (including line and number) 88 if(msg.indexOf('userAgent')!=-1 && line==1 && column==293) 89 return false; 90 if(typeof cp_debug_data == 'undefined') 91 cp_debug_data = 'URL is "'+url+'"' 92 if(!url) 93 return false; // some engines give us no useful information 94 if(url.indexOf("bootstrap.min.js") != -1) 95 return; // can't fix bugs in bootstrap! 96 let myurl = new URL(url); // sometimes we have our domain in ad query string 97 if(myurl.hostname.indexOf('canalplan') == -1) 98 return true; // we can't fix bugs in other people's code 99 let agent = navigator.userAgent; 100 let dodgybots = [ 101 "Mediapartners-Google", // runs JS without having allowed it to load 102 "Yeti/1.1", // likewise 103 "Applebot/0.1", // and another 104 "Linespider/1.1", // a new one like the above 105 "YandexBot", // clicks things it shouldn't see 106 "Googlebot", 107 "Adsbot-Google", // FFS Google - fix this 108 "bingbot" // problems with screen size and CSS 109 ]; 110 for(let i=0; i<dodgybots.length; i++) { 111 if(agent.indexOf(dodgybots[i]) != -1) 112 return true; 113 } 114 let ancient_browsers = [ 115 {pattern:/Chrome\/([0-9.]+)/, version:56}, 116 {pattern:/Firefox\/([0-9.]+)/, version:60}, 117 ]; 118 for(let i=0; i<ancient_browsers.length; i++) { 119 let hits = ancient_browsers[i].pattern.exec(agent); 120 if(hits && parseInt(hits[1]) <= ancient_browsers[i].version) { 121 Canalplan_Alert("ancient-browser"); 122 return true; 123 } 124 }
125 // errors that we can do nothing about, so there is no point in reporting 126 let just_ignore = [ 127 // internal thing in Bootstrap, entirely random 128 {type:'error', match:" a is not a function"}, 129 // maybe adblocked, but can't do anything 130 {type:'error',match:"PayPal"}, 131 // a browser extension that breaks things 132 {type:'trace',match:"hookGeo@"}, 133 // Nov 24 - another 134 {type:'trace',match:'/content/gps.js'}, 135 // Nov 25 - strange distributed fetch of photo pages with JS error 136 {type:'url',match:'/can_js/min/jquery-3.4.1.min.js'}, 137 // AWS can't fake a browser properly 138 {type:'error',match:"Incorrect locale"} 139 ]; 140 for(let i=0; i<just_ignore.length; i++) { 141 switch(just_ignore[i].type) { 142 case 'error': 143 if(msg.indexOf(just_ignore[i].match) != -1) 144 return true; 145 break; 146 case 'trace': 147 if(err.stack && err.stack.indexOf(just_ignore[i].match) != -1) 148 return true; 149 break; 150 case 'url': 151 if(url.indexOf(just_ignore[i].match) != -1) 152 return true; 153 break; 154 default: 155 throw 'Unknown type for just_ignore in error trapping' 156 } 157 } 158 159 // finally, we are going to actually report this! 160 var rep = {line:line, 161 column: column, 162 url:url, 163 error_text:msg, 164 trace:err.stack, 165 page:document.location, 166 debug:JSON.stringify(cp_debug_data), 167 agent: navigator.userAgent 168 }; 169// if(typeof re.trace == 'undefined') // if we have no stack trace, debugging 170 // return true; // is just about impossible 171 if(typeof cp_info != 'undefined') { 172 rep.cp_info = JSON.stringify(cp_info); 173 rep.page_code = cp_info.page_code; 174 rep.build_number = cp_info.build; 175 } 176 setTimeout(function(){ 177 Report_Error(rep,'JE'); 178 }, 2000); 179 errors_reported.any = true; 180 // return true; // prevents error and allows continue. pain to have when developing 181 return false; 182}; 183 184// very simple as we don't do anything with the return 185 186function Report_Error(vals, prefix) { 187 var fd = new FormData(); 188 fd.append("mode","JavaScript_Error"); 189 Object.keys(vals).forEach(function(k) { 190 fd.append(k, vals[k]); 191 }); 192 Async_Post("/cgi-bin/callback.cgi", fd, function(resp) { 193 // do nothing 194 }); 195} 196 197function WAS_Report_Error(vals, prefix) { 198 var xmlhttp = new XMLHttpRequest(); 199 var message = ""; 200 for(var name in vals) { 201 if (vals.hasOwnProperty(name)) { 202 if(message) 203 message += '&'; 204 message += name+'='+vals[name]; 205 } 206 } 207 // it's too long to send as a GET - also means we don't need to encode 208 xmlhttp.open("POST","../cgi-bin/callback.cgi?mode="+prefix,true); 209 //Send the proper header information along with the request 210 // xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 211 xmlhttp.send(message); 212 return true; 213} 214 215function hasClass(ele,cls) { 216 return ele.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)')); 217} 218 219function addClass(ele,cls) { 220 if (!this.hasClass(ele,cls)) ele.className += " "+cls; 221} 222 223function removeClass(ele,cls) { 224 if (hasClass(ele,cls)) { 225 var reg = new RegExp('(\\s|^)'+cls+'(\\s|$)'); 226 ele.className=ele.className.replace(reg,' ').replace(/\s+/g,' ').replace(/^\s|\s$/,''); 227 } 228} 229 230function NowBusy() { 231 if(hasClass(document.body,'wait')) { 232 return false; 233 } 234 addClass(document.body,'wait'); 235 return true; 236} 237 238function NotBusy() { 239 removeClass(document.body,'wait'); 240} 241 242function Callback_To_CGI(mode,what,value,ondone) { 243 var params = "mode="+mode+"&what="+what+"&value="+value 244 Async_Text("../cgi-bin/callback.cgi?"+params, function (txt) { 245 if(ondone) 246 ondone(txt); 247 }); 248} 249 250function Reload_Page() { 251 // window.location.href = window.location.href; 252 location.reload(true); 253} 254 255function Master_Startup(opts) { 256 NotBusy(); 257 // if we have PHP options they override the CS ones 258 if(opts.php_vars.indexOf("{") == 0) { 259 var newopts = JSON.parse(opts.php_vars); 260 if(newopts != null) // parsing didn't fail 261 opts = newopts; 262 } 263 if(opts.call_on_load) { 264 setTimeout(function() { 265 Call_On_Load(opts.call_on_load, opts.call_on_load_params); 266 }, 0); 267 } 268 // run all these functions asynchronously 269 setTimeout(function() { 270 Display_Adverts(); 271 },0); 272 if(typeof Logon_Error_Check == 'function') { 273 setTimeout(function() { 274 Logon_Error_Check(); 275 }, 0); 276 } 277 if(opts.geocode && opts.geocode != 'no') { 278 setTimeout(function() { 279 Start_Geolocation(opts.geocode); 280 }, 0); 281// } else { 282// setTimeout(function() { 283// Start_Geolocation("log"); 284// }, 0); 285 } 286 if(typeof Logon_Box == 'function') { 287 setTimeout(function() { 288 Logon_Box(null); 289 }, 0); 290 } 291 setTimeout(function() { 292 Show_Navigational_Photos(); 293 },0); 294 setTimeout(function() { 295 Add_Amenity_Popups(); 296 },0); 297 Any_Messages(); 298} 299 300function Call_On_Load(tocallname, params) { 301 let tocall = window[tocallname]; // get function from name 302 if(typeof tocall != 'function') { // still loading JS 303 setTimeout(function() { 304 Call_On_Load(tocallname, params); 305 }, 100); 306 return; 307 } 308 tocall.apply(null, params); 309} 310 311function Popup(contentsId, focusId, options=[]) { 312 var div = $$('popup_div'); 313 if(div == null) { 314 var div = document.createElement('div'); 315 div.style.display = "none"; 316 div.id = 'popup_div'; 317 document.body.appendChild(div); 318 } 319 if(options.style) // this means the calculations below will work 320 div.className = options.style; 321 else 322 div.className = "popUp"; 323 // create out of sight 324 div.style.left = "-2000px"; 325 div.style.top = "0px"; 326 let header = ''; 327 if(options.noheader) 328 ; 329 else 330 header = '<div style="text-align:right; font-size:70%"><span class="linklike" onclick="Kill_Popup(\'any\')">Close</span></div>'; 331 if(options.fetch) { // contents come from server 332 if(options.fetch == 'html') 333 pth = "/render/html?page="+options.page; 334 else 335 pth = "/render/"+options.fetch; 336 if(options.fetchopt) 337 pth += '&'+options.fetchopt; 338 Async_Text(pth, function(res) { 339 // if lots of hot links, this prevents multiple results 340 if(options.unique_content) 341 div.innerHTML = header; 342 div.innerHTML += res; 343 Popup_After_Load(div, contentsId, focusId, options); 344 }); 345 } else { 346 div.innerHTML = header; 347 if(typeof contentsId == 'string') { 348 var id = $$(contentsId); 349 if(id == null) 350 throw "No block '"+contentsId+"' for popup"; 351 } else 352 var id = contentsId; 353 var innards = id.innerHTML.replace(/INACTIVE_/g,''); 354 if(options && options.format) 355 innards = options.format(innards,options.passthrough);
356 div.innerHTML += innards; 357 Popup_After_Load(div, contentsId, focusId, options); 358 } 359} 360 361function Popup_After_Load(div, contentsId, focusId, options) { 362 div.style.display = "block"; 363 // if anchoring popup to an element 364 if(options && options.anchor) { 365 var anchor = options.anchor; 366 var x =0, y = 0; // work out where to show it 367 y = anchor.offsetHeight; 368 while(anchor.offsetParent) { 369 x += anchor.offsetLeft; 370 y += anchor.offsetTop; 371 anchor = anchor.offsetParent; 372 } 373 var mm = div; 374 var mx = 0; 375 while(div.parentNode && div.parentNode.offsetLeft) { 376 mx += div.parentNode.offsetLeft; 377 div = div.parentNode; 378 } 379 x -= mx; 380 div.style.left = x+"px"; 381 div.style.top = y+"px"; 382 } else if(options && options.anchortomouse) { 383 var x,y; 384 let mouse = options.anchortomouse; // just for clarity in expressions 385 let divbox = div.getBoundingClientRect(); 386 let anchorbox = contentsId.getBoundingClientRect(); 387 if(mouse.clientX < window.innerWidth/2) { // left half of screen 388 x = mouse.clientX; 389 } else { // right half of screen 390 x = mouse.clientX - divbox.width; 391 } 392 // we use the two mouse Y coordinates to work out what offset 393 // the screen has! 394 let yoff = mouse.layerY - mouse.clientY; 395 if(mouse.clientY < window.innerHeight/2) { // top half of screen 396 // vertical has a small offset to move off the text 397 y = anchorbox.bottom + yoff + 10; 398 } else { // bottom half of screen 399 y = anchorbox.bottom + yoff - divbox.height - 10; 400 } 401 div.style.left = x+'px'; 402 div.style.top = y+'px'; 403 } else { 404 // rather naughty - rely on specific selector name 405 if(contentsId == 'selector_popup') { // selectors start at the 406 // left, rest centred 407 div.style.left = "10%"; 408 div.style.top = "10%"; 409 div.style.width="80%"; 410 } else { 411 div.style.left = ((window.innerWidth - div.offsetWidth)/2) + 'px'; 412 if(typeof($) != 'undefined') 413 div.style.top = ($(window).scrollTop()+(window.innerHeight - div.offsetHeight)/2) + 'px'; 414 } 415 } 416 div.style.zIndex = 500; 417 if(focusId && focusId != "") 418 $$(focusId).focus(); 419 if(options && options.nogrey) { 420 } else { 421 greyOut(true); 422 } 423 popupis = contentsId; 424 if(options && options.callback) { 425 options.callback(); 426 } 427 if(options && options.reloadonclose) { 428 div.canalplan_reloadonclose = true; 429 } 430 if(options && options.truncateurl) { 431 div.canalplan_truncateurl = true; 432 } 433 if(options && options.onfinish) 434 options.onfinish(); 435} 436 437window.Kill_Popup =function(what) { 438 if(what != 'any' && typeof popupis != 'undefined') 439 if(what != popupis) // prevents wrong box getting killed 440 return; 441 var div = $$('popup_div'); 442 if(div) { 443 div.innerHTML = ''; 444 div.style.display = 'none'; 445 greyOut(false); 446 if(div.canalplan_reloadonclose) { 447 if(div.canalplan_truncateurl) { 448 window.location = window.location.pathname + window.location.hash; 449 } else { 450 window.location.reload(true); 451 } 452 } 453 } 454} 455 456function greyOut(vis, options) { 457 // Pass true to gray out screen, false to ungray 458 // options are optional. This is a JSON object with the following (optional) properties 459 // opacity:0-100 // Lower number = less grayout higher = more of a blackout 460 // zindex: # // HTML elements with a higher zindex appear on top of the gray out 461 // bgcolor: (#xxxxxx) // Standard RGB Hex color code 462 // grayOut(true, {'zindex':'50', 'bgcolor':'#0000FF', 'opacity':'70'}); 463 // Because options is JSON opacity/zindex/bgcolor are all optional and can appear 464 // in any order. Pass only the properties you need to set. 465 var options = options || {}; 466 var zindex = options.zindex || 50; 467 var opacity = options.opacity || 70; 468 var opaque = (opacity / 100); 469 var bgcolor = options.bgcolor || '#000000'; 470 var dark=document.getElementById('darkenScreenObject'); 471 if (!dark) { 472 // The dark layer doesn't exist, it's never been created. So we'll 473 // create it here and apply some basic styles. 474 // If you are getting errors in IE see: http://support.microsoft.com/default.aspx/kb/927917 475 var tbody = document.getElementsByTagName("body")[0]; 476 var tnode = document.createElement('div'); // Create the layer. 477 tnode.style.position='absolute'; // Position absolutely 478 tnode.style.top='0px'; // In the top
479 tnode.style.left='0px'; // Left corner of the page 480 tnode.style.overflow='hidden'; // Try to avoid making scroll bars 481 tnode.style.display='none'; // Start out Hidden 482 tnode.id='darkenScreenObject'; // Name it so we can find it later 483 tbody.appendChild(tnode); // Add it to the web page 484 dark=document.getElementById('darkenScreenObject'); // Get the object. 485 } 486 if (vis) { 487 // Calculate the page width and height 488 /* I don't know what is going wrong here, but I'm only getting the height of the header bar in Firefox. So fake it */ 489 if( document.body && ( document.body.scrollWidth || document.body.scrollHeight ) ) { 490 var pageWidth = document.body.scrollWidth+'px'; 491 var pageHeight = document.body.scrollHeight+'px'; 492 if(document.body.offsetHeight < 200) 493 pageHeight = '100%'; 494 } else if( document.body.offsetWidth ) { 495 var pageWidth = document.body.offsetWidth+'px'; 496 var pageHeight = document.body.offsetHeight+'px'; 497 if(document.body.offsetHeight < 200) 498 pageHeight = '100%'; 499 } else { 500 var pageWidth='100%'; 501 var pageHeight='100%'; 502 } 503 //set the shader to cover the entire page and make it visible. 504 dark.style.width= pageWidth; 505 dark.style.height= pageHeight; 506 dark.style.opacity=opaque; 507 dark.style.MozOpacity=opaque; 508 dark.style.filter='alpha(opacity='+opacity+')'; 509 dark.style.zIndex=zindex; 510 dark.style.backgroundColor=bgcolor; 511 dark.style.display='block'; 512 } else { 513 dark.style.display='none'; 514 } 515} 516 517// additional function causes proper closure creation and so differing tips 518function Set_Tips(bx,tit) { 519 bx.onmouseover = function() {Tip(tit,FONTSIZE,'1em');}; 520 bx.onmouseout = function() {UnTip();}; 521} 522 523// navnote and other boxes for places, features and waterways 524// rather messy because of a lot of historic decisions 525 526function Add_New_Info(item,elname,iswhat) { 527 var totoggle = true; 528 if(cp_info.whatami == 'waterway') { 529 elname = 'wway_'+elname+'_box'; 530 totoggle = false; 531 } 532 if(cp_info.whatami == 'feature') { 533 elname = 'struct_'+elname+'_box'; 534 } 535 $$(elname).style.display = 'block'; 536 $$(elname).scrollIntoView(true); 537 if(totoggle) 538 Toggle_Parent_Menu(item); 539 Canalplan_Alert("created-"+iswhat); 540} 541 542function Add_Navnote(item) { 543 var elname = 'navnote'; 544 if(cp_info.whatami == 'place') 545 elname = 'plugin_box_navnotes'; 546 Add_New_Info(item,elname,'navnote'); //ALERT-MESSAGE: created-navnote 547} 548 549function Add_Details(item) { 550 var elname = 'detail'; 551 if(cp_info.whatami == 'place') 552 elname = 'place_detail_box'; 553 Add_New_Info(item,elname,'detail'); //ALERT-MESSAGE: created-detail 554} 555 556function Kill_Navnote(item) { 557 if(cp_info.whatami == 'place') 558 Kill_Navnote_Action('pl'); 559 if(cp_info.whatami == 'waterway') 560 Kill_Navnote_Action('ww'); 561 if(cp_info.whatami == 'feature') 562 Kill_Navnote_Action('st'); 563} 564 565//**FIXME - this shouldn't be in "common.js" 566function Add_Photo(id) { 567 if(id) { 568 if(cp_info.userid) { 569 document.location = "../cgi-bin/add_photo.cgi?id="+id; 570 } else { // false or null 571 Canalplan_Alert("logged-on-for-photo"); 572 } 573 } else { 574 Popup('selector_popup','forpopup'); 575 Place_Selected_Callback = function(pn) { 576 Kill_Popup('any'); 577 if(cp_info.userid) { 578 document.location = "../cgi-bin/add_photo.cgi?id="+pn.id; 579 } else { // false or null 580 Canalplan_Alert("logged-on-for-photo"); 581 } 582 }; 583 Selector_Start('place'); 584 } 585} 586 587function getQueryVariable(variable) { 588 var query = window.location.search.substring(1); 589 var vars = query.split("&"); 590 for (var i=0;i<vars.length;i++) { 591 var pair = vars[i].split("="); 592 if(pair[0] == variable){return pair[1];} 593 } 594 return false;
595} 596 597/* Geolocator - January 2010, complete re-write Oct 2012. Tweak for 598 * new sessions June 2013. Moved into common May 2015 for boat tracking 599 600 To use geolocation add "geolocate = js-function-name" to the 'head' 601 section in config.hdf. 602 603 That function will be passed a single object as follows: 604 actual{latitude,longitude} - the actual point on the globe 605 determined and the nearest canalplan place with all the usual 606 fields - look at api.can for details of the structure returned 607*/ 608 609function Start_Geolocation(geo_callback = 'no') { 610 //**** NEXT THREE (FOUR) LINES FOR TESTING WITH NO WIRELESS OR HTTPS 611// var position = {coords:{latitude:51.95,longitude:-2.1674,accuracy:1235.06007890}}; 612// Async_JSON('/api/nearest_place?lat='+position.coords.latitude+'&lng='+position.coords.longitude,Geo_With_Location,{sc:"browser",fn:geo_callback,raw:position.coords}); 613// return; 614// console.log("Restore later!"); 615 //**** REAL CODE HERE 616 // Does the browser do things? 617// console.log("Starting"); 618 if (navigator.geolocation) { 619 var fail = function(err) { 620 if(typeof geo_callback == 'function') { 621 geo_callback({mode:'error',error:err}); 622 } 623 }; 624 navigator.geolocation.getCurrentPosition(function(position) { 625 if(typeof cp_info == 'undefined') 626 cp_info = []; 627 if(typeof position.coords != 'undefined') { 628 cp_info.latitude = position.coords.latitude; 629 cp_info.longitude = position.coords.longitude; 630 if(typeof geo_callback == 'function') 631 Async_JSON('/api/nearest_place?lat='+position.coords.latitude+'&lng='+position.coords.longitude,Geo_With_Location,{sc:"browser",fn:geo_callback,raw:position.coords}); 632 else { 633 Async_Text('../boats/location.php?browser=Y&user='+cp_info.userid+'&latitude='+position.coords.latitude+'&longitude='+position.coords.longitude+'&accuracy='+position.coords.accuracy,do_nothing); 634 var dummy = setTimeout(function(){Start_Geolocation(geo_callback)},5*60*1000); 635 } 636 } 637 },fail,{timeout:10000}); 638 } else { // no browser geolocation at all 639 if(typeof geo_callback == 'function') { 640 geo_callback({mode:'error',error:'nobrowsergeo'}); 641 } 642 } 643} 644 645function Geo_With_Location(geodata,inf) { 646 if(geodata.mode == 'success') { 647 geodata.source = inf.sc; 648 geodata.rawdata = inf.raw; 649 inf.fn(geodata); 650 } 651} 652 653// pulls a message block etc 654function Dynamic_HTML(divid, filename, tocall, passthrough) { 655 var area = $$(divid); 656 if(area == null) { 657 var wksp = document.createElement('div'); 658 wksp.style.display = 'none'; 659 wksp.id = divid; 660 document.body.appendChild(wksp); 661 area = $$(divid); 662 } 663 if(filename.indexOf('/') == -1) 664 filename = "/html/"+filename; 665 Async_Text(filename, function(txt) { 666 area.innerHTML = txt; 667 if(tocall) 668 tocall(passthrough); 669 }); 670} 671 672function Show_Help_Page(pageid) { 673 window.open('/cgi-bin/wiki.cgi?pageid=help:'+encodeURIComponent(pageid),'canalplan_help'); 674} 675 676function HTML_To_Text(ht) { 677 // translate HTML entities etc 678 var wkdiv = document.createElement('div'); 679 wkdiv.innerHTML = ht; 680 return wkdiv.innerText; 681} 682 683/* NB - in a rather nasty fudge the error code can contain (generated 684 by the server code) additional parameters (like place codes) needed 685 to display the message. Ensure you don't break this if extending 686 the call to message.cgi in any way. 687 688 options - 689 - buttons is a list of button names (see 690 data/XX_params.txt) that will be displayed. 691 692 - callback is the *name* (text) of a function that will be 693 called with the index of the button clicked. 694 695 - cbparms - if any - will be passed as a second parameter 696 to callback (meaningless otherwise). Again, should be a 697 string 698 699 - substitution=a list of texts that will be pushed into 700 the message replacing [[index]] 701 702 - reloadonclose - once "close" is clicked, the page will reload 703 - truncateurl - use with the above: removes any query parameters 704*/ 705function Canalplan_Alert(code, options) { 706 cp_debug_data.push({what:"Alerting", code: code, options:options}); 707 if(!cp_info.language) 708 throw("No language set"); 709 // if "code" contains HTML it's a bug at the server end 710 // set sensible defaults 711 if(options == null) options = {}; 712 if(options.buttons == null) options.buttons = ['OK']; 713 // now do stuff 714 var q = document.getElementById('workspace'); 715 if(q == null) { 716 var wksp = document.createElement('div'); 717 wksp.style.display = 'none'; 718 wksp.id = 'workspace'; 719 document.body.appendChild(wksp); 720 q = wksp; 721 } 722 Async_Text("/cgi-bin/message.cgi?lang="+cp_info.language+"&what="+code, function(txt) { 723 cp_debug_data.push({what:"Message",value:txt}); 724 var parts = txt.split("$$"); 725 if(options.substitutions)
726 var restxt = Apply_Alert_Substitutions(parts[1],options.substitutions); 727 else 728 var restxt = parts[1]; 729 if(parts[0].indexOf('{') != -1) { 730 var parms = JSON.parse(parts[0]); 731 } else { 732 throw "No parameter block in text:"+txt 733 } 734 q.innerHTML = '<div class="AlertText">'+restxt+'</div>'; 735 q.innerHTML+='<div class="AlertText">'+Alert_Buttons(options.buttons, parms.buttons, options)+'</div>'; 736 Popup('workspace','workspace',{style:"AlertMessage",noheader:true,reloadonclose:options.reloadonclose,truncateurl:options.truncateurl}); 737 }); 738} 739 740function Alert_Buttons(blist, bdefn, options) { 741 var i; 742 var rets = ''; 743 for(let i=0; i<blist.length; i++) { 744 rets += '<button class="'+bdefn[blist[i]].class+' btn extra-btn" type="button" onclick="'; 745 if(options.nokill) 746 ; // do nothing 747 else 748 rets += 'Kill_Popup(\'any\');'; 749 if(options.callback) { 750 rets += ' '+options.callback+'('+i 751 if(options.cbparms) { 752 rets += ",'"+options.cbparms+"'"; 753 } 754 rets += ');'; 755 } 756 rets += '">'+bdefn[blist[i]].text+'</button> ' 757 } 758 return rets; 759} 760 761function Cause_Bug_Report(text) { 762 Async_Text("/cgi-bin/showstuff.cgi?mode=die_for_bug&msg="+encodeURIComponent(text), function() { 763 Canalplan_Alert('bug-reported&reason='+encodeURIComponent(text)); 764 }); 765} 766 767function Apply_Alert_Substitutions(txt, sublist) { 768 for(let i=0;i<sublist.length;i++) { 769 var ss = '[['+i+']]'; 770 txt = txt.replace(ss,sublist[i]); 771 } 772 return txt; 773} 774 775function Log_Social_Share(context) { 776 // very minimal as don't want any action 777 Async_Text("/cgi-bin/showstuff.cgi?mode=log_social_share&share="+encodeURIComponent(context.share)+"&whatpage="+encodeURIComponent(context.url), do_nothing); 778} 779 780// Called at start-up to check for error and info messages 781function Any_Messages() { 782 if(typeof(cp_info.error_message) != "undefined" && cp_info.error_message != "") { 783 Canalplan_Alert(cp_info.error_message); 784 } else { // don't bother them with two messages 785 let nonag = false; 786 if(typeof cp_info.last_amenity_edit == 'undefined') 787 nonag = true; // page doesn't have info 788 let dte = new Date(cp_info.last_amenity_edit); 789 let today = new Date(); 790 let oneday = 60*60*24*1000; 791 // start nagging after 5 days 792 if(Math.floor((today.getTime() - dte.getTime()) / oneday) < 5) 793 nonag = true; 794 if(!nonag) { // not every single time! 795 if(Math.random() > .85) 796 Canalplan_Alert("amenity-nag"); 797 } 798 } 799} 800 801// A bit of a wrapper for "alert" handling markers of various sorts 802function Show_Message_Alert(msg, options) { 803 if(typeof options == 'undefined') 804 options = []; 805 if(msg.charAt(0) == '!') { // failed - remove marker 806 msg = msg.slice(1); 807 options.reloadonclose = false; 808 } 809 if(msg.charAt(0) == '@') { // reload flag 810 msg = msg.slice(1); 811 options.reloadonclose = true; 812 } 813 Canalplan_Alert(msg, options); 814} 815 816// Advert code - to be run on all pages 817function Display_Adverts() { 818 var el = document.getElementsByClassName('cpadvert'); 819 for(var i=0; i<el.length; i++) { 820 if(typeof el[i].id == 'string' && el[i].id.substring(0,3) == 'ad_') { 821 Apply_Advert(el[i], el[i].id.substr(3)); 822 } 823 } 824} 825 826function Apply_Advert(el, adkey) { 827 Async_Text("/api/showad?id="+encodeURIComponent(adkey), function(txt) { 828 el.innerHTML = txt; 829 el.style.display = 'block'; 830 }); 831} 832 833function JSON_Callback(j_callback, response, passthrough) { 834 response=response.replace('\n','') 835 response=response.replace('\r','') 836 if(response) { 837 // the files can end in commas, making them easy to 838 // concatenate. If so, remove for our purposes 839 if(response.slice(-1) == ',') 840 response = response.slice(0,-1); 841 // suppose that the main website isn't available - in that case 842 // we simply do nothing - better to fail quietly 843 if(response.indexOf("<!DOCTYPE") != -1 && response.indexOf("not available") != -1) 844 return; 845 cp_debug_data.push({what:"JSON parse", txt: response, passthrough: passthrough}); 846 var data = JSON.parse(response); 847 j_callback(data, passthrough); 848 } 849} 850 851function Add_Random_Anticache(path) { 852 let dt = new Date(); 853 let randomish = dt.getTime(); // lots of nanoseconds 854 if(path.indexOf('?') != -1) // already has a QS 855 return path +'&decache='+randomish; 856 return path + '?decache='+randomish; 857} 858 859function Add_Anti_Hack(path) { 860 if(path.indexOf('?') != -1) // already has a QS 861 return path +'&legit=yes'; 862 return path + '?legit=yes'; 863} 864 865function Async_JSON(path, a_callback, passthrough) { 866 path = Add_Random_Anticache(path); 867 path = Add_Anti_Hack(path); 868 cp_debug_data.push({what:"Async_JSON", path: path}); 869 var xmlhttp = new XMLHttpRequest(); 870 xmlhttp.onreadystatechange=function() { 871 if (xmlhttp.readyState==4) { 872 if (xmlhttp.status==200) { 873// cp_debug_data.push({what:"async", from: path, read:xmlhttp.responseText}); 874 JSON_Callback(a_callback, xmlhttp.responseText, passthrough); 875 } 876 } 877 }; 878 xmlhttp.open("GET",path,true); 879 xmlhttp.send(null); 880} 881 882// used when you don't want to parse the results 883 884function Async_Text(path, funct, passthrough) { 885 path = Add_Random_Anticache(path); 886 path = Add_Anti_Hack(path); 887 cp_debug_data.push({what:"Async_Text", path: path}); 888 var xmlhttp = new XMLHttpRequest(); 889 xmlhttp.onreadystatechange=function() { 890 if (xmlhttp.readyState==4) { 891 if (xmlhttp.status==200 && funct) { 892 funct(xmlhttp.responseText, passthrough); 893 } 894 } 895 }; 896 xmlhttp.open("GET",path,true); 897 xmlhttp.send(null); 898} 899 900// used to post formdata 901function Async_Post(path, formd, funct, passthrough) { 902 var xmlhttp = new XMLHttpRequest(); 903 xmlhttp.onreadystatechange=function() { 904 if (xmlhttp.readyState==4) { 905 if (xmlhttp.status==200) { 906 JSON_Callback(funct, xmlhttp.responseText, passthrough); 907 } 908 } 909 }; 910 xmlhttp.open("POST",path,true); 911 xmlhttp.send(formd); 912} 913 914function Show_Navigational_Photos() { 915 var el = document.getElementsByClassName('navigational_photo'); 916 for(let i=0; i<el.length; i++) { 917 var plid = el[i].getAttribute('data-placeid');
918 Async_JSON('/api/guide_photo?plid='+plid, Load_Navigational_Photo, {el: el[i]}); 919 } 920} 921 922function Load_Navigational_Photo(dat, parms) { 923 if(dat.url == '') 924 return; 925 var img = document.createElement('img'); 926 img.src = dat.url; 927 img.className = "shadowed smallshadow"; 928 parms.el.appendChild(img); 929} 930 931function Show_Hide_Item(id, showas) { 932 var it = $$(id); 933 if(it) { 934 if(it.style.display == 'none') 935 it.style.display = showas; 936 else 937 it.style.display = 'none'; 938 } 939} 940 941function Show_Hidden_Menu_Items(ison) { 942 let i = 0; 943 let a = document.getElementsByTagName("div"); 944 while (element = a[i++]) { 945 if(element.className.indexOf('useronly') != -1) { 946 if(ison) 947 element.style.display = 'block'; 948 else 949 element.style.display = 'none'; 950 } 951 } 952} 953 954// track the screen size, and call things when it changes... 955function Check_Screen_Size(swid) { 956 var v = swid.matches; 957 if(typeof screensize_callback_list != 'undefined') { 958 screensize_callback_list.forEach(function(x) { 959 x(v); 960 }); 961 } 962} 963 964var swid = window.matchMedia('(min-width: 1024px)'); 965swid.addListener(Check_Screen_Size); 966 967// call it once everything is ready 968window.addEventListener('DOMContentLoaded', () => { 969 Check_Screen_Size(swid); 970}); 971 972// dynamically load and call another JS file 973var alreadyloaded = {}; 974function Load_And_Run_JS(url, callback_function, callback_params, options) { 975 // it can be conditional on a function not existing (useful if sometimes 976 // you will have loaded the JS file in the header) 977 if(options && options.unlessexists) { 978 if(typeof options.unlessexists == 'string') { 979 if(typeof window[options.unlessexists] == 'function') { 980 callback_function(callback_params); 981 return; 982 } 983 } else 984 throw "options.unlessexists must be a string, not:"+typeof(options.unlessexists); 985 } 986 var furl = url; 987 // if there isn't one, add a query-string to prevent over-caching 988 if(url.indexOf('?') == -1 && js_query.indexOf('?') != -1) 989 furl = url + js_query; // js_query created at top of this file 990 if(!alreadyloaded[url]) { // never seen this file before 991 // give it some storage and queue the function 992 alreadyloaded[url] = {loaded:false, queue:[]}; 993 alreadyloaded[url].queue.push({cbf:callback_function, cbp:callback_params}); 994 var scriptTag = document.createElement('script'); 995 scriptTag.src = furl; 996 scriptTag.onload = function() { 997 for(let i=0; i<alreadyloaded[url].queue.length; i++) { 998 alreadyloaded[url].queue[i].cbf(alreadyloaded[url].queue[i].cbp); 999 } 1000 alreadyloaded[url].loaded = true; 1001 }; 1002 document.head.appendChild(scriptTag); 1003 return; 1004 } 1005 if(!alreadyloaded[url].loaded) { // not loaded yet, so add to queue 1006 alreadyloaded[url].queue.push({cbf:callback_function, cbp:callback_params}); 1007 // in the extreme case that load completed while pushing 1008 if(alreadyloaded[url].loaded) 1009 callback_function(callback_params); 1010 } else { // it was loaded before we got here 1011 callback_function(callback_params); 1012 } 1013} 1014 1015// no callback as will action when loaded 1016function Load_Stylesheet(url) { 1017 if(!alreadyloaded[url]) { 1018 var styleTag = document.createElement('link'); 1019 styleTag.href = url; 1020 styleTag.type = 'text/css'; 1021 styleTag.rel = 'stylesheet'; 1022 document.head.appendChild(styleTag); 1023 alreadyloaded[url] = true; 1024 } 1025} 1026 1027// ads or hides the "explorer" icon on input 1028function Changed_Input(bx) { 1029 var sicon = 'search_'+bx; 1030 var content = $$(bx).value; 1031 var hint = $$('helpfor_'+bx); 1032 content = content.replace(/\s/g, ""); // remove all spaces 1033 if(content) { 1034 $$(sicon).style.visibility = 'visible'; 1035 if(hint) hint.style.display = 'block'; 1036 } else { 1037 $$(sicon).style.visibility = 'hidden'; 1038 if(hint) hint.style.display = 'none'; 1039 } 1040} 1041 1042function Toggle_On_Checkbox(el, id) { 1043 if(el.checked) 1044 $$(id).classList.remove('hidden'); 1045 else 1046 $$(id).classList.add('hidden'); 1047} 1048 1049// Pop ups for amenities 1050var amenity_globals = {popuplive: false, 1051 stickypopup: null 1052 }; 1053 1054function Add_Amenity_Popups() { // add the effect to all boxes with the class 1055 var el = document.getElementsByClassName('amenity'); 1056 if(el.length == 0) return; // bail out immediately if none on page 1057 for(let i=0; i<el.length; i++) { 1058 el[i].addEventListener("mouseover", (e) => { 1059 if(amenity_globals.hoverpopup || amenity_globals.stickypopup !== null) 1060 return; 1061 amenity_globals.hoverpopup = true; 1062 Amenity_Popup(e, false); 1063 1064 }); 1065 el[i].addEventListener("mouseout", () => { 1066 if(amenity_globals.stickypopup !== null) 1067 return; 1068 Kill_Popup('any'); 1069 amenity_globals.hoverpopup = false;
1070 }); 1071 el[i].addEventListener("click", (e) => { 1072 // clear any hover 1073 if(amenity_globals.hoverpopup) { 1074 Kill_Popup('any'); 1075 amenity_globals.hoverpopup = false; 1076 } 1077 if(amenity_globals.stickypopup !== i) { 1078 Amenity_Popup(e, true); 1079 amenity_globals.stickypopup = i; 1080 1081 } else { // clicked again on same one 1082 amenity_globals.stickypopup = null; 1083 Kill_Popup('any'); 1084 } 1085 }); 1086 } 1087} 1088 1089function Show_Single_Amenity(e) { 1090 Amenity_Popup(e); 1091} 1092 1093function Show_Amenity_From_List(e) { 1094 if(amenity_globals.expanded_item) { 1095 let exi = $$(amenity_globals.expanded_item); 1096 if(exi) 1097 exi.classList.add('hidden'); 1098 } 1099 let id = 'amdetails_'+e.target.dataset.amenity_key; 1100 $$(id).classList.remove('hidden'); 1101 amenity_globals.expanded_item = id; 1102} 1103 1104function Amenity_Popup(e, withclose) { 1105 let cmd = (parseInt(e.target.dataset.amenity_count) > 1) ? 'amenity_list' : 'amenity'; 1106 let key = e.target.dataset.amenity_key; 1107 if(e.target.dataset.naptan_key) { 1108 cmd = (parseInt(e.target.dataset.amenity_count) > 1) ? 'naptan_list' : 'naptan'; 1109 key = e.target.dataset.naptan_key; 1110 } 1111 Popup(e.target, null, 1112 {fetch: cmd,
1113 fetchopt: 'key='+key, 1114 nogrey: true, 1115 noheader: !withclose, 1116 anchortomouse: e, 1117 unique_content: true, 1118 onfinish: function() { 1119 // if by the time we've rendered the item we have left, 1120 // then we immediately kill it 1121 if(amenity_globals.hoverpopup == false && amenity_globals.stickypopup == null) 1122 Kill_Popup('any'); 1123 } 1124 }); 1125} 1126 1127// for "recent places" on gazetteer page and in menus 1128function Toggle_Recents(el, searchbox) { 1129 if(typeof el == 'string') // allows literal or "this" in calls from pages 1130 el = $$(el); 1131 if($$('gaz_recents') == null) { 1132 Async_JSON("/api/recents_and_favourites", function (data) { 1133 let ddown = document.createElement('div'); 1134 ddown.id = 'gaz_recents'; 1135 ddown.classList.add('recent-place-list','hidden'); 1136 Add_Dropdown_Lines(ddown, data, 'recent', el, searchbox); 1137 Add_Dropdown_Lines(ddown, data, 'favourite', el, searchbox); 1138 document.body.appendChild(ddown); 1139 Toggle_Recent_Display(el); 1140 }); 1141 } else { 1142 Toggle_Recent_Display(el); 1143 } 1144} 1145 1146function Add_Dropdown_Lines(ddown, data, forwhat, el, searchbox) { 1147 let dhead = document.createElement('div'); 1148 dhead.classList.add('recent_header'); 1149 if(data[forwhat].length) 1150 dhead.innerHTML = `${forwhat} places`; 1151 else 1152 dhead.innerHTML = 'No recent places'; 1153 ddown.appendChild(dhead); 1154 for(let i of data[forwhat]) { 1155 let line = document.createElement('span'); 1156 let icon = document.createElement('i'); 1157 if(forwhat == 'recent') { 1158 icon.classList.add('fa','fa-heart-o'); 1159 } else { 1160 icon.classList.add('fa','fa-heart'); 1161 icon.style.color = 'red'; 1162 } 1163 icon.onclick = function() { 1164 Swap_Recent_And_Favourite(forwhat,i,searchbox,el) 1165 }; 1166 line.appendChild(icon); 1167 let item = document.createElement('span'); 1168 item.classList.add('indexmenu'); 1169 item.innerHTML = ' '+i; 1170 item.onclick = function(){Set_Search_From_Recent(searchbox,item,el)}; 1171 line.appendChild(item); 1172 ddown.appendChild(line); 1173 ddown.appendChild(document.createElement('br')); 1174 } 1175} 1176 1177function Toggle_Recent_Display(el) { 1178 if(el.dataset.showing == 'off') { 1179 el.dataset.showing = 'on'; 1180 el.classList.remove('fa-toggle-down'); 1181 el.classList.add('fa-toggle-up'); 1182 let ddown = $$('gaz_recents'); 1183 ddown.style.left = el.offsetLeft+16+'px'; 1184 ddown.style.top = el.offsetTop+18+'px'; 1185 $$('gaz_recents').classList.remove('hidden'); 1186 } else { 1187 el.dataset.showing = 'off' 1188 el.classList.remove('fa-toggle-up'); 1189 el.classList.add('fa-toggle-down'); 1190 let ddown = $$('gaz_recents'); 1191 $$('gaz_recents').classList.add('hidden'); 1192 } 1193} 1194 1195function Set_Search_From_Recent(sb, item,toggleel) { 1196 let target = $$(sb); 1197 if(target.value) // already has something - give it a comma 1198 target.value += ',' 1199 target.value += item.innerHTML; 1200 Toggle_Recent_Display(toggleel); 1201} 1202 1203function Update_Favourites(text, add) { 1204 let mode = add ? 'add' : 'remove'; 1205 Async_Text("/api/update_favourite?do="+mode+'&pl='+encodeURIComponent(text), function (res) { 1206 }); 1207} 1208 1209function Swap_Recent_And_Favourite(mode, pl, searchbox, el) { 1210 // we do this at the server and then reload 1211 if(mode=='recent') { 1212 Update_Favourites(pl, true); 1213 } else { 1214 Update_Favourites(pl, false); 1215 } 1216 // hide existing dropdown 1217 Toggle_Recent_Display(el); 1218 // remove existing dropdown 1219 $$('gaz_recents').remove(); 1220 // and now redisplay 1221 Toggle_Recents(el,searchbox); 1222} 1223
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.