1/** 2 * If a variable (likely a function parameter) is undefined / wasn't supplied, then 3 * coalesce to a different value. 4 * @param arg 5 * @param defaultToVal 6 * @returns {*} 7 */ 8function defaultTo(arg, defaultToVal) { 9 return typeof arg !== 'undefined' ? arg : defaultToVal; 10} 11 12// See https://stackoverflow.com/questions/6756583/prevent-browser-from-loading-a-drag-and-dropped-file/6756680#6756680 13function disableBrowserFilePreview() { 14 15 window.addEventListener("dragover", function(e){ 16 e = e || event; 17 e.preventDefault(); 18 }, false); 19 20 window.addEventListener("drop", function(e){ 21 e = e || event; 22 e.preventDefault(); 23 }, false); 24} 25 26 27/** 28 * Implementation of Java's String.hashCode() in JS. 29 * http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ 30 * @param absolute 31 * @returns {number} 32 */ 33String.prototype.hashCode = function(absolute){ 34 35 absolute = defaultTo(absolute, false); 36 37 var hash = 0; 38 if (this.length === 0) return hash; 39 for (let i = 0; i < this.length; i++) { 40 let char = this.charCodeAt(i); 41 hash = ((hash<<5)-hash)+char; 42 hash = hash & hash; // Convert to 32bit integer 43 } 44 45 if (absolute) { 46 hash = Math.abs(hash); 47 } 48 return hash; 49}; 50 51 52/** 53 * Recursively build a HTML form compatible key-value set from a potentially multidimensional 54 * key-value set 55 * @param string name POST key/name. 56 * @param mixed val POST value, which may be an object or array. 57 * @returns {unresolved} 58 */ 59function buildHTMLFormAttributes(name, val) { 60 var postKeyVals = {}; 61 if (typeof val === 'string') { 62 postKeyVals[name] = val; 63 } else { 64 $.each(val , function(valEntryName, valEntryVal) { 65 $.extend( 66 postKeyVals, 67 buildHTMLFormAttributes( 68 name+'['+valEntryName+']', 69 valEntryVal 70 ) 71 ); 72 }); 73 } 74 75 return postKeyVals; 76} 77 78/** 79 * Create a deep-clone of a simple object. 80 * 81 * Should only be used for simple JSON-compliant objects and shouldn't be used for objects with prototypes, 82 * functions or circular references. 83 * @return Object 84 */ 85function deepClone(yourObj) { 86 return JSON.parse(JSON.stringify(yourObj)); 87} 88 89/** 90 * Toggle browser fullscreen, if it's available. 91 * Taken from: https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#Toggling_fullscreen_mode 92 */ 93function toggleFullScreen(forceEnable) { 94 95 forceEnable = defaultTo(forceEnable, null); 96 97 var doc = window.document; 98 var docEl = doc.documentElement; 99 100 var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen || docEl.msRequestFullscreen; 101 var cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen; 102 103 if (forceEnable === null) { 104 forceEnable = !doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement && !doc.msFullscreenElement 105 } 106 107 if(forceEnable) { 108 requestFullScreen.call(docEl); 109 } else { 110 cancelFullScreen.call(doc); 111 } 112} 113 114/** 115 * Very simple pluralizer. 116 */ 117function pluralize(word, quantity) { 118 return quantity + ' ' + (quantity === 1 ? word : word + 's'); 119} 120 121/** 122 * 123 * @param {Date} [date] 124 */ 125function getWelcomeMessage(date) { 126 date = defaultTo(date, new Date()) 127 128 var hours = date.getHours(); 129 if (hours < 12) { 130 return 'Good morning'; 131 } else if (hours < 17) { 132 return 'Good afternoon'; 133 } else { 134 return 'Good evening'; 135 } 136} 137 138function round(num, decimalPlaces) { 139 decimalPlaces = defaultTo(decimalPlaces, 2); 140 var coefficient = Math.pow(10, decimalPlaces); 141 return (Math.round(num * coefficient) / coefficient); 142} 143 144 145function formatDate(date, incDate, incTime) { 146 147 incDate = defaultTo(incDate, true); 148 incTime = defaultTo(incTime, true); 149 150 if (!(date instanceof Date) || (!incDate && !incTime)) { 151 return null; 152 } 153 154 var opts = {}; 155 156 if (incDate) { 157 Object.assign(opts, {day: 'numeric', month: 'short', year:'numeric'}); 158 } 159 if (incTime) { 160 Object.assign(opts, {hour:'2-digit', minute:'2-digit'}); 161 } 162
163 return date.toLocaleString('en-GB', opts); 164} 165 166/** 167 * Convert time string to equivalent total number of seconds. E.g. "16:30" -> 59400. 168 * An invalid time string e.g. "25:30" would be rejected. 169 * @param {string} timeString e.g. "16:30", or "4:15" etc. 170 * @returns {?int} Total seconds, or NULL if supplied arg is invalid. 171 */ 172function timeToSeconds(timeString) { 173 const [hours, minutes] = timeString.split(':').map(val => parseInt(val)); 174 // Values which are not-a-number will fail this check: 175 if (!(hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59)) { 176 return null; 177 } 178 return (hours * 3600) + (minutes * 60); 179} 180 181 182/** 183 * Convert HTML metacharacters into to their HTML entities. 184 * @param {string} str 185 * @return {string} 186 */ 187function escapeHtml(str) { 188 var entityMap = { 189 '&': '&', 190 '<': '<', 191 '>': '>', 192 '"': '"', 193 "'": ''', 194 '/': '/', 195 '`': '`', 196 '=': '=' 197 }; 198 199 return String(str).replace(/[&<>"'`=\/]/g, function(s) { 200 return entityMap[s]; 201 }); 202} 203 204/** 205 * Return string to given length + ellipsis. 206 * @todo Enhance to prevent word breaking. 207 * @param {string} str 208 * @param {number} len 209 * @return {string} 210 */ 211function makeNiceString(str, len) { 212 if (str.length <= len) { 213 return str; 214 } else { 215 return str.substr(0, len - 3) + '...'; 216 } 217} 218 219/** 220 * Parse a PHP or JS JSON encoded DateTime value and return a corresponding JS Date object. 221 * @param {(Object|string)} d 222 */ 223function parseDate(d) { 224 // Encoded from JS via JSON.stringify 225 if (typeof d === 'string') { 226 return new Date(d); 227 // Encoded from PHP via json_encode: 228 } else { 229 // return new Date(Date.parse(d.date)); 230 // Altered for Safari support. See https://stackoverflow.com/questions/5324178 231 var arr = d.date.split(/[- :]/); 232 return new Date(arr[0], arr[1]-1, arr[2], arr[3], arr[4], arr[5]); 233 } 234 235} 236 237/** 238 * See https://stackoverflow.com/a/14706877 239 * @param {*} val 240 * @return {boolean} TRUE if the argument is an Object or an Array. 241 */ 242function isObject(val) { 243 var type = typeof val; 244 return type === 'function' || type === 'object' && !!val; 245} 246 247/** 248 * Upper-case the first character in a string. 249 * @param {string} str 250 * @return {string} 251 */ 252function ucFirst(str) { 253 return str.charAt(0).toUpperCase() + str.substring(1); 254} 255 256/** 257 * Remove HTML from a string. 258 * Similar to `strip_tags(..)` in PHP. 259 * Source: https://stackoverflow.com/a/47140708 260 * @param {string} html e.g. "<p>Hello</p>" 261 * @return {string} e.g. "Hello" 262 */ 263function stripTags(html) { 264 var doc = new DOMParser().parseFromString(html, 'text/html'); 265 return doc.body.textContent || ""; 266} 267 268function setClipboard(text){ 269 var input = document.createElement('input'); 270 document.body.appendChild(input); 271 input.value = text; 272 input.select(); 273 document.execCommand('copy', false); 274 input.remove(); 275} 276 277function createTooltip(tooltipProps) { 278 const {id, heading, content, allowHtml, hideMobile, delayMs, hideOnBlur, autoload, maxWidth} = tooltipProps; 279 280 // Optionally hide on mobile: 281 if (isMobileDevice() && hideMobile) { 282 return; 283 } 284 285 let title = heading ? "<p class='tippy-header'>" + heading + "</p>" : ''; 286 287 let contentWithTitle = 288 "<div class='tippy-wrapper'>" + 289 title + 290 "<div class='tippy-inner-content'>" + content + "</div>" + 291 "</div>"; 292 293 let parent = $("#" + id).parent(); 294 295 // Don't apply tooltip if the wrapper is tablesorter - this creates duplicates! 296 if (parent.hasClass("tablesorter-header-inner")) { 297 return; 298 } 299 300 // Set tooltip props: 301 parent.attr('data-tippy-content', contentWithTitle); 302 parent.attr('data-tippy-allowHTML', allowHtml); 303 parent.attr('data-tippy-delay', delayMs); 304 parent.attr('data-tippy-hideOnClick', hideOnBlur); 305 parent.attr('data-tippy-showOnCreate', autoload); 306 parent.attr('data-tippy-maxWidth', maxWidth); 307 parent.attr('aria-label', heading); 308} 309 310/** 311 * @return {number} Time in milliseconds since initial page load. 312 */ 313function uptime() 314{ 315 // List of performance types - see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/entryType 316 const [currPage] = performance.getEntriesByType("navigation"); 317 return (performance.now() - currPage.startTime); 318} 319 320 321/** 322 * If `arrOrVal` is an array, return it as it is. 323 * If `arrOrVal` is anything else, box it inside an array and return that. 324 * 325 * @param {*|Array} arrOrVal 326 */ 327Array.box = function(arrOrVal) { 328 if (Array.isArray(arrOrVal)) { 329 return arrOrVal; 330 } else { 331 return [arrOrVal]; 332 } 333}; 334 335// Based on addHours from: https://stackoverflow.com/a/1050782 336Date.prototype.addMinutes = function(m) { 337 this.setTime(this.getTime() + (m*60*1000)); 338 return this; 339};
340 341 342/** IE11 Polyfills: **/ 343 344/* 345 `Element.closest` https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill 346*/ 347 348if (!Element.prototype.matches) { 349 Element.prototype.matches = Element.prototype.msMatchesSelector || 350 Element.prototype.webkitMatchesSelector; 351} 352 353if (!Element.prototype.closest) { 354 Element.prototype.closest = function(s) { 355 var el = this; 356 357 do { 358 if (el.matches(s)) return el; 359 el = el.parentElement || el.parentNode; 360 } while (el !== null && el.nodeType === 1); 361 return null; 362 }; 363} 364 365/* 366 `NodeList.forEach` https://developer.mozilla.org/en-US/docs/Web/API/NodeList/forEach#Polyfill 367 */ 368if (window.NodeList && !NodeList.prototype.forEach) { 369 NodeList.prototype.forEach = Array.prototype.forEach; 370} 371 372/* 373 `ChildNode.remove` https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove#Polyfill 374 */ 375(function (arr) { 376 arr.forEach(function (item) { 377 if (item.hasOwnProperty('remove')) { 378 return; 379 } 380 Object.defineProperty(item, 'remove', { 381 configurable: true, 382 enumerable: true, 383 writable: true, 384 value: function remove() { 385 if (this.parentNode === null) { 386 return; 387 } 388 this.parentNode.removeChild(this); 389 } 390 }); 391 }); 392})([Element.prototype, CharacterData.prototype, DocumentType.prototype]); 393 394 395// Function to load the image when it's about to come into the viewport 396const lazyLoadImages = () => { 397 const images = document.querySelectorAll('img.lazy'); 398 const observer = new IntersectionObserver((entries, observer) => { 399 entries.forEach(entry => { 400 if (entry.isIntersecting) { 401 const img = entry.target; 402 const cacheBuster = new Date().getTime(); // or a server-side version string 403 const imgSrc = img.getAttribute('data-src'); 404 if (imgSrc) { 405 // Add a fallback for broken or missing images 406 img.onerror = () => { 407 img.src = '/images/placeholder.jpg'; // â path to your default placeholder 408 img.classList.add('broken-image'); 409 }; 410 411 // Trigger actual image load with cache-busting param 412 img.src = `${imgSrc}?v=${cacheBuster}`; 413 } 414 img.classList.remove('lazy'); 415 observer.unobserve(img); 416 } 417 }); 418 }, { threshold: 0.1 }); // Load earlier (10% visible) 419 420 images.forEach(img => observer.observe(img)); 421 422 // ð Watch for dynamically added images 423 const mutationObserver = new MutationObserver((mutations) => { 424 mutations.forEach(mutation => { 425 mutation.addedNodes.forEach(node => { 426 if (node.nodeType === 1) { 427 if (node.tagName === 'IMG' && node.classList.contains('lazy')) { 428 observer.observe(node); 429 } else { 430 // Check inside the added node (e.g., in modal) 431 node.querySelectorAll && node.querySelectorAll('img.lazy').forEach(img => observer.observe(img)); 432 } 433 } 434 }); 435 }); 436 }); 437 438 mutationObserver.observe(document.body, { 439 childList: true, 440 subtree: true 441 }); 442}; 443 444// Run on page load 445document.addEventListener('DOMContentLoaded', lazyLoadImages);
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.