1/* /includes/v4/_site.js */ 2 3var dataDatePicker; 4 5// cookiesPageName must be defined before this code (usually in the $page_header code) or it will be assumed that pinning/cookies are not being used 6var useCookies = true; 7if (typeof cookiesPageName === 'undefined') { 8 useCookies = false; 9 var cookiesPageName = ''; 10} 11 12/* video div */ 13function playVimeo(video_id, title, width, height) { // play vimeo video 14 playVideo('vimeo', video_id, title, width, height); 15} 16 17function playYouTube(video_id, title, width, height) { // play youtube video 18 playVideo('youtube', video_id, title, width, height); 19} 20 21function playVideo(video_type, video_id, title, width, height) { // show a video pop-up window and play video 22 23 // add div to document body, if not there already 24 if ($("#videoModal").length == 0) { 25 $(document.body).append('<div class="modal fade" id="videoModal" tabindex="-1" role="dialog" aria-labelledby="videoModalLabel" aria-hidden="true"><div class="modal-dialog modal-dialog-video" role="document"><div class="modal-content"><div class="modal-body"><button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button><div class="embed-responsive embed-responsive-16by9"><iframe class="embed-responsive-item" src="" id="video" allowscriptaccess="always" allow="autoplay" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></div></div></div></div></div>'); 26 $('head').append('<script>$("#videoModal").on("hide.bs.modal", function (e) {$("#video").attr("src", ""); });<\/script>'); 27 } 28 29 // set the video source and open the dialog 30 switch (video_type) { 31 case 'vimeo': 32 $('#video').attr('src', '//player.vimeo.com/video/' + video_id + '?autoplay=1'); 33 break; 34 case 'youtube': 35 $('#video').attr('src', '//www.youtube.com/embed/' + video_id); 36 break; 37 } 38 39 // open the modal div 40 $('#videoModal').modal('show'); 41} 42 43 44/* data date picker */ 45function datePicked() { 46 $('#datepicker_value').val($('#datepicker_dd').find('option:selected').data('y')); 47 if ($('#datepicker_dd').val() == 'specify') { 48 dataDatePicker.open(); 49 } else { 50 dateSelected(); 51 } 52} 53 54function dateSelected() { 55 $.post('/account/ajax_quarter_switcher.php', $('#quarter_date_picker_form').serialize(), function (data) { 56 if (data && data.success) { 57 alert(data.info); 58 window.location = window.location.href; 59 } else if (data && data.info) { 60 alert(data.info); 61 } 62 }, 'json'); 63} 64 65function togglePin(divID, cookieName, pinStatus) { // open or close the body of the section and set the pin status in a cookie 66 if (pinStatus === 1) { 67 var pinnedHTML = '<span class="fa fa-thumb-tack fa-lg section-pinned" style="margin:1px 2px 0 0;" title="pinned open - select to unpin"></span>'; 68 $('#' + divID).find('.section-pin-icon').html(pinnedHTML); 69 setCookie(cookieName, '1'); 70 } else { 71 var unpinnedHTML = '<span class="fa fa-square-o fa-lg section-unpinned" title="unpinned - select to pin open"></span>'; 72 $('#' + divID).find('.section-pin-icon').html(unpinnedHTML); 73 setCookie(cookieName, '0'); 74 } 75 return false; 76} // end function togglePin 77 78 79function toggleOpen(divID, openStatus) { // open or close the body of the section 80 if (openStatus) { 81 $('#' + divID + ' > .section-body').show(); 82 $('#' + divID).removeClass('section-closed').addClass('section-opened'); 83 } else { 84 $('#' + divID + ' > .section-body').hide(); 85 $('#' + divID).removeClass('section-opened').addClass('section-closed'); 86 } 87} // end function toggleOpen 88 89function addBookmark() { 90 $.ajax({method: "POST", url: '/includes/v4/add_bookmark.ajax.php', data: {label: document.title, url: window.location.href}, cache: false}).done(function (result) { 91 location.reload(); 92 }); 93 return false; // don't execute the link click - refresh page - allow link to refresh page? 94} 95function setHomePage() { 96 $.ajax({method: "POST", url: '/includes/v4/set_homepage.ajax.php', data: {url: window.location.href}, cache: false, dataType:'json'}).done(function (result) { 97 alert(result.message ?? "Setting homepage failed."); 98 });
99 return false; 100} 101 102/* utility functions */ 103 104 105function getCookie(c_name) { 106 if (document.cookie.length > 0) { 107 c_start = document.cookie.indexOf(c_name + "="); 108 if (c_start != -1) { 109 c_start = c_start + c_name.length + 1; 110 c_end = document.cookie.indexOf(";", c_start); 111 if (c_end == -1) c_end = document.cookie.length; 112 return unescape(document.cookie.substring(c_start, c_end)); 113 } 114 } 115 return ""; 116} 117 118function setCookie(c_name, value, expiredays, path) { 119 var exdate = new Date(); 120 exdate.setDate(exdate.getDate() + expiredays); 121 var cookie = c_name + "=" + escape(value) + ((expiredays == null) ? "" : ";expires=" + exdate.toGMTString()); 122 if (path) cookie += ";path=" + path; 123 document.cookie = cookie; 124} 125 126 127/* when document is loaded */ 128 129$(document).ready(function () { 130 'use strict'; 131 $('[data-toggle="offcanvas"]').on('click', function () { 132 $('.offcanvas-collapse').toggleClass('open') 133 }) 134 135 // set the search type in the header 136 var searchType = getCookie('hpdd'); 137 var searchOption = $("#searchDropdown a[data-value='" + searchType + "']"); 138 var iconImage = searchOption.data('icon'); 139 var iconLabel = searchOption.data('label'); 140 var formAction = searchOption.data('action'); 141 $('#searchDropdown').find('button > img').attr('src', iconImage).attr('alt', iconLabel).attr('title', iconLabel); 142 $('#searchDropdown').parent().attr('action', formAction); 143 144 // set the large search type in the page body (home page) 145 if ($("#headerSearchBarForm2").length > 0) { 146 var searchOption = $("#searchDropdown2 a[data-value='" + searchType + "']"); 147 var iconImage = searchOption.data('icon'); 148 var iconLabel = searchOption.data('label'); 149 var formAction = searchOption.data('action'); 150 $('#searchDropdown2').find('button > img').attr('src', iconImage).attr('alt', iconLabel).attr('title', iconLabel); 151 $('#searchDropdown2').parent().attr('action', formAction); 152 } 153 154 // get date picker reference and set up actions 155 if ($('#datepicker_value').length > 0) { 156 dataDatePicker = flatpickr('#datepicker_value', {appendTo: document.getElementById('bodyDatePicker')}); 157 dataDatePicker.config.onChange.push(function () { 158 // set the dropdown option back to the "specify" value to process (onClose is run before onChange) 159 $("#datepicker_dd").val($("#datepicker_dd option:last").val()); 160 dateSelected(); 161 }); 162 dataDatePicker.config.onClose.push(function () { 163 // reset the dropdown value to the first (current) value - in case a date was not chosen 164 $("#datepicker_dd").val($("#datepicker_dd option:first").val()); 165 }); 166 } 167 168 /* submenus */ 169 $('.dropdown-submenu > a').on("click", function (e) { 170 var submenu = $(this); 171 $('.dropdown-submenu .dropdown-menu').removeClass('show'); 172 submenu.next('.dropdown-menu').addClass('show'); 173 e.stopPropagation(); 174 }); 175 $('.dropdown').on("hidden.bs.dropdown", function () { 176 // hide any open menus when parent closes 177 $('.dropdown-menu.show').removeClass('show'); 178 }); 179 180 $('.nav-link').hover(function () { // hide current open dropdown menu 181 $(this).closest('.nav-item.dropdown').siblings('.nav-item.dropdown').removeClass('show').find('.dropdown-menu').removeClass('show'); 182 }); 183 184 $('.dropdown-submenu>.dropdown-item').hover(function () { // hide current open submenu 185 $('.dropdown-submenu .dropdown-menu.show').each(function () { 186 $(this).removeClass('show'); 187 }); 188 }); 189 190 /* bookmarks dropdown opens on hover */ 191 $('#myBookmarksButton').hover( 192 function () { 193 $(this).addClass('show'); 194 $(this).find('[data-toggle="dropdown"]').attr('aria-expanded', true); 195 $(this).find('.dropdown-menu').addClass('show'); 196 }, 197 function () { 198 $(this).removeClass('show'); 199 $(this).find('[data-toggle="dropdown"]').attr('aria-expanded', false); 200 $(this).find('.dropdown-menu').removeClass('show'); 201 } 202 ); 203 204 /* header search dropdown */ 205 $('#searchDropdown a').click(function () { 206 var searchValue = $(this).data('value'); 207 var iconImage = $(this).data('icon'); 208 var iconLabel = $(this).data('label'); 209 var formAction = $(this).data('action'); 210 $('#searchDropdown').find('button > img').attr('src', iconImage).attr('alt', iconLabel).attr('title', iconLabel); 211 $('#searchDropdown').parent().attr('action', formAction); 212 $('#headerSearchBarForm #headerSearchStr').focus();
213 setCookie('hpdd', searchValue, 365, '/'); 214 $('#searchDropdown .dropdown-menu').removeClass('show'); 215 }); 216 217 218 /* for index.html (may also be needed for search page */ 219 $('#searchDropdown2 a').click(function () { 220 var searchValue = $(this).data('value'); 221 var iconImage = $(this).data('icon'); 222 var iconLabel = $(this).data('label'); 223 var formAction = $(this).data('action'); 224 $('#searchDropdown2').find('button > img').attr('src', iconImage).attr('alt', iconLabel).attr('title', iconLabel); 225 $('#searchDropdown2').parent().attr('action', formAction); 226 $('#headerSearchBarForm2 #headerSearchStr2').focus(); 227 setCookie('hpdd', searchValue, 365, '/'); 228 }); 229 if ($('#headerSearchStr2').length > 0) { 230 var aimeePromptTimer = null; 231 var searchWasAboveThreshold = (($('#headerSearchStr2').val() || '').trim().length >= 2); 232 233 $('#headerSearchStr2').on('input', function () { 234 var isAboveThreshold = (($(this).val() || '').trim().length >= 2); 235 if (isAboveThreshold && !searchWasAboveThreshold) { 236 if (window.aimeeButtonPrompt && typeof window.aimeeButtonPrompt.show === 'function') { 237 window.aimeeButtonPrompt.show(); 238 } 239 if (aimeePromptTimer) { 240 clearTimeout(aimeePromptTimer); 241 } 242 aimeePromptTimer = setTimeout(function () { 243 if (window.aimeeButtonPrompt && typeof window.aimeeButtonPrompt.hide === 'function') { 244 window.aimeeButtonPrompt.hide(); 245 } 246 aimeePromptTimer = null; 247 }, 30000); // Hide after 30 seconds 248 } 249 searchWasAboveThreshold = isAboveThreshold; 250 }); 251 } 252 $('#headerSearchStr').on('focus', function () { 253 $(this).css('color', 'white'); 254 $(this).css('border-bottom-color', 'white'); 255 }); 256 $('#headerSearchStr').on('blur', function () { 257 $(this).css('color', 'rgba(255,255,255,.5)'); 258 $(this).css('border-bottom-color', 'rgba(255,255,255,.5)'); 259 }); 260 261 262 /* code lists modal windows */ 263 $('#listCodesModal').on('show.bs.modal', function (e) { // user_id is defined at the beginning of the _header.php file (in the HEAD section) 264 var listID = $(e.relatedTarget).data('list-id'); 265 var url = '/ajax/ajax-mycodes-codelist-codes-html.php?list_id=' + listID; 266 $(this).find('.modal-content').load(url); 267 $('#codeListsModal').modal('hide'); 268 }) 269 $('#listCodesModal').on('hide.bs.modal', function (e) { 270 $('#codeListsModal').modal('show'); 271 }) 272 273 /* toggle section open/closed pin-able */ 274 $('.section').each(function () { // initialize each section, check for a cookie indicating the open or close status (pinned open) 275 var divID = $(this).attr('id'); 276 if (useCookies) { 277 var cookieName = cookiesPageName + '|' + $(this).attr('id'); 278 // if cookie/pin is set to open, open it 279 if ($.cookie(cookieName) === '1') { 280 togglePin(divID, cookieName, 1); 281 toggleOpen(divID, 1); 282 } else { 283 togglePin(divID, cookieName, 0); 284 toggleOpen(divID, 0); 285 } 286 } else { 287 if ($(this).hasClass('section-closed')) { 288 toggleOpen(divID, 0); 289 togglePin(divID, cookieName, 0); 290 } else { 291 toggleOpen(divID, 1); 292 togglePin(divID, cookieName, 1); 293 } 294 } 295 }); 296 297 $('div').on('click', '.section-header', function () { // set click action for the section header 298 var divID = $(this).parent().attr('id'); 299 if ($(this).parent().hasClass('section-closed')) { 300 toggleOpen(divID, 1); // open it 301 } else { 302 toggleOpen(divID, 0); // close it 303 } 304 return false; 305 }); 306 307 $('div').on('click', '.section-pin-icon', function (event) { // set the click action for the section pin icon 308 if (useCookies) { 309 event.stopPropagation(); 310 var cookieName = cookiesPageName + "|" + $(this).parent().parent().attr('id'); 311 var divID = $(this).parent().parent().attr('id'); 312 if ($(this).find('span').hasClass('section-unpinned')) { 313 togglePin(divID, cookieName, 1); 314 toggleOpen(divID, 1); 315 } else { 316 togglePin(divID, cookieName, 0); 317 toggleOpen(divID, 0); 318 } 319 } 320 });
321 $('.enlargable-image').click(function () { 322 var height; 323 if ($(this).hasClass('enlargable-image-small')) { 324 // make larger 325 $(this).removeClass('enlargable-image-small').addClass('enlargable-image-large'); 326 if ($(this).data('height-large')) 327 height = $(this).data('height-large'); else height = 'auto'; 328 $(this).find('img').css('height', height).css('border', '1px solid #eee').css('padding', '1rem'); 329 $(this).find('.enlagrable-image-span').html('select to reduce'); 330 } else { 331 // make smaller 332 $(this).removeClass('enlargable-image-large').addClass('enlargable-image-small'); 333 height = $(this).data('height-small'); 334 $(this).find('img').css('height', height).css('border', '0').css('padding', '0'); 335 $(this).find('.enlagrable-image-span').html('select to enlarge'); 336 } 337 }); 338 339}); 340 341function siteSplitTestAction(action_id) { 342 let url = '/ajax/ajax_split_test_action.php'; // calls SplitTest::hit() 343 let data = new FormData(); 344 data.append('action_id', action_id); 345 navigator.sendBeacon(url, data); 346} 347 348function siteToggleDisplay(element, display_type) { 349 // returns true if shown, false if hidden 350 // convert ID to element (if needed) 351 if (typeof element === 'string') element = document.getElementById(element); 352 display_type = display_type || 'block'; 353 // hide 354 if (window.getComputedStyle(element).display !== 'none') { 355 element.style.display = 'none'; 356 return false; 357 // show 358 } else { 359 element.style.display = display_type; 360 return true; 361 } 362} 363 364function addLocation(jump_to) { 365 $('#addLocationDialog').data('location_id', 0).data('jump_to', jump_to).dialog('option', 'title', 'Add Location').dialog('open'); 366} 367 368function editLocation(location_id, jump_to) { 369 $('#addLocationDialog').data('location_id', location_id).data('jump_to', jump_to).dialog('option', 'title', 'Edit Location').dialog('open'); 370} 371 372$(function () { 373 if ($('#addLocationDialog').length === 0) { 374 $('body').append('<div id="addLocationDialog" style="display:none">Loading...</div>'); 375 } 376 $('#addLocationDialog').dialog({ 377 title: 'Location', modal: true, width: 650, height: window.screen.availHeight - 400, autoOpen: false, open: function (event, ui) { 378 $(this).load('/account/client.php?client_id=' + $(this).data('location_id')); 379 }, 380 close: function (event, ui) { 381 $(this).html('Loading...'); 382 }, 383 buttons: { 384 "save": function () { 385 //https://stackoverflow.com/a/24292393/24196650 386 function editParams(key, value) { 387 388 // convert parameters from url-style string to associative array 389 function transformToAssocArray(prmstr) { 390 var params = {}, prmarr = prmstr.split("&"); 391 for (var i = 0; i < prmarr.length; i++) { 392 var tmparr = prmarr[i].split("="); 393 params[tmparr[0]] = tmparr[1]; 394 } 395 return params; 396 } 397 398 function getSearchParameters() { 399 var prmstr = window.location.search.substr(1); 400 return prmstr !== null && prmstr !== "" ? transformToAssocArray(prmstr) : {}; 401 } 402 403 key = encodeURI(key); 404 var params = getSearchParameters(); 405 if (Object.keys(params).length === 0) { 406 return value !== false ? '?' + key + '=' + encodeURI(value) : ''; 407 } 408 if (value !== false) 409 params[key] = encodeURI(value); else delete params[key]; 410 if (Object.keys(params).length === 0) 411 return ''; 412 return '?' + $.map(params, function (value, key) { 413 return key + '=' + value; 414 }).join('&'); 415 } 416 417 var errors = []; 418 $('#client_form').find('[required]').each(function () { 419 if ($(this).val() === '') errors.push($(this).data('title') || this.name); 420 }); 421 if (errors.length > 0) { 422 alert(errors.length === 1 ? "The " + errors[0] + " field is required." : "These fields are required: " + errors.join(', ')) 423 return false; 424 } 425 $.post('/account/ajax_client.php', $('#client_form').serialize(), function (data) { 426 console.log(data); 427 if (data.message) 428 alert(data.message); 429 var debug = false; 430 if (debug && data.debug) 431 alert(data.debug); 432 if (data.success) { 433 $('#addLocationDialog').dialog("close"); 434 if ($(this).data('jump_to')) { 435 window.location.search = editParams($(this).data('jump_to'), jump_to); 436 } else { 437 window.location.reload(); 438 } 439 } 440 }, 'json'); 441 }, 442 "cancel": function () { 443 $(this).dialog("close"); 444 }, 445 }, 446 create: function(){ 447 $(this).closest(".ui-dialog") 448 .find(".ui-dialog-titlebar-close") 449 .addClass("btn") 450 .addClass("btn-primary"); 451 $(this).closest(".ui-dialog") 452 .find(".ui-dialog-buttonset button") 453 .addClass("btn") 454 .addClass("btn-primary"); 455 }, 456 });
457}); 458 459function siteExportToXLS(table_id, file_name) { 460 if (file_name === undefined && table_id.dataset.filename) file_name = table_id.dataset.filename; 461 if (file_name === undefined) file_name = "table_data.xls"; 462 let table_element = document.getElementById(table_id).outerHTML; 463 let excel_content = `<html><body>${table_element}<\/body><\/html>`; 464 let file = new Blob([excel_content], {type:"application\/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}); 465 let dl_anchor = document.createElement("a"); 466 dl_anchor.style.display = "none"; 467 dl_anchor.download = file_name; 468 dl_anchor.href = window.URL.createObjectURL(file); 469 document.body.appendChild(dl_anchor); 470 dl_anchor.click(); 471} 472 473function siteExportToCSV(table_id, file_name) { 474 if (file_name === undefined && table_id.dataset.filename) file_name = table_id.dataset.filename; 475 if (file_name === undefined) file_name = "table_data.csv"; 476 // Escape periods and other special characters in the table_id for CSS selector 477 let escaped_table_id = table_id.replace(/([!"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, "\\$1"); 478 let table_rows = document.querySelectorAll("#"+escaped_table_id+" tr") 479 let csv =""; 480 let rows = [] 481 table_rows.forEach(el => { 482 if (el.style.display === 'none') return; // skip if not visible 483 let row = []; 484 el.querySelectorAll("th, td").forEach(ele => { 485 let ele_clone = ele.cloneNode(true); 486 ele_clone.innerText = (ele_clone.innerText).replace(/"/gi, '\"\"'); 487 ele_clone.innerText = ('"' + ele_clone.innerText + '"'); 488 row.push(ele_clone.innerText); 489 }); 490 rows.push(row.join(",")); 491 }) 492 csv += rows.join(`\r\n`); 493 let file = new Blob([csv], {type:"text/csv"}); 494 var dl_anchor = document.createElement("a"); 495 dl_anchor.style.display = this.nonce; 496 dl_anchor.download = file_name; 497 dl_anchor.href = window.URL.createObjectURL(file); 498 document.body.appendChild(dl_anchor); 499 dl_anchor.click(); 500} 501 502function siteExportToPDF(html, file_name, title, css) { 503 if (file_name === undefined) file_name = "page_data.pdf"; 504 if (title === undefined) title = ''; 505 if (css === undefined) css = ''; 506 507 let form_element = document.createElement("FORM"); 508 form_element.setAttribute("id","PDFExportForm"); 509 document.body.appendChild(form_element); 510 511 // add elements to the form 512 let html_element = document.createElement("INPUT"); 513 html_element.setAttribute("name", "html"); 514 html_element.setAttribute("value", html); 515 document.getElementById("PDFExportForm").appendChild(html_element); 516 let file_name_element = document.createElement("INPUT"); 517 file_name_element.setAttribute("name", "file_name"); 518 file_name_element.setAttribute("value", file_name); 519 document.getElementById("PDFExportForm").appendChild(file_name_element); 520 let title_element = document.createElement("INPUT"); 521 title_element.setAttribute("name", "title"); 522 title_element.setAttribute("value", title); 523 document.getElementById("PDFExportForm").appendChild(title_element); 524 let css_element = document.createElement("INPUT"); 525 css_element.setAttribute("name", "css"); 526 css_element.setAttribute("value", css); 527 document.getElementById("PDFExportForm").appendChild(css_element); 528 529 // submit the form 530 form_element.method = "POST"; 531 form_element.action = "/print_pdf.php"; 532 form_element.target = "_blank"; 533 form_element.submit(); 534 form_element.remove(); 535} 536 537function siteAlert(message) { 538 // create a modal container 539 const modal = document.createElement('div'); 540 modal.style.position = 'fixed'; 541 modal.style.zIndex = 100; 542 modal.style.top = '0'; 543 modal.style.left = '0'; 544 modal.style.width = '100%'; 545 modal.style.height = '100%'; 546 modal.style.backgroundColor = 'rgba(0, 0, 0, 0.1)'; 547 modal.style.display = 'flex'; 548 modal.style.justifyContent = 'center'; 549 modal.style.alignItems = 'center'; 550 modal.style.backdropFilter = 'blur(1px)'; 551 552 // create the alert box 553 const alert_box = document.createElement('div'); 554 alert_box.style.position = 'relative'; 555 alert_box.style.backgroundColor = 'white'; 556 alert_box.style.padding = '1.5rem 2rem 1.5rem 1.5rem'; 557 alert_box.style.borderRadius = '5px'; 558 alert_box.style.fontSize = '20px'; // Customize the font size here 559 alert_box.style.maxWidth = '700px'; 560 alert_box.style.maxHeight = '80%'; 561 alert_box.innerHTML = message; 562 563 // create the x - doesn't do anything special, just gives people a hint of how to dismiss the alert 564 const x_button = document.createElement('div'); 565 x_button.style.position = 'absolute'; 566 x_button.style.top = '0'; 567 x_button.style.right = '10px'; 568 x_button.style.fontSize = '1.5rem'; 569 x_button.style.cursor = 'pointer'; 570 x_button.innerHTML = '×'; 571 alert_box.appendChild(x_button); 572 573 // add the alert box to the modal 574 modal.appendChild(alert_box); 575 576 // add the modal to the document 577 document.body.appendChild(modal); 578 579 // close the modal when the user clicks on it 580 modal.addEventListener('click', function() { 581 document.body.removeChild(modal); 582 });
583 // close the modal when the user presses a key 584 document.addEventListener('keydown', function(event) { 585 document.body.removeChild(modal); 586 }); 587} // end function siteAlert 588 589function siteDialog(content_html, title, width="", height="auto", max_height="60%") { 590 // note: the dialog is removed from the DOM when closed 591 592 // set up css 593 let html = '<style>'; 594 if (width) width = 'width:'+width+'; '; 595 html += 'html:has(dialog[open]) { overflow: hidden; scrollbar-gutter: stable; } '; // prevents user scrolling page and shift when scrollbar disappears 596 html += 'body:has(dialog[open]) { height: 100%;} '; // prevents page scrolling to the top 597 html += 'dialog::backdrop { background-color: rgba(0, 0, 0, 0.1); backdrop-filter: blur(1px); } '; 598 html += '.site-dialog { padding: 0 .1rem .5rem 0; overflow: hidden; border: none; border-radius: .5rem;'; 599 html += width+'; height: '+height+'; max-height: '+max_height+'; } ' 600 html += '.dialog-close-btn { float:right; margin: -.5rem 0 .5rem 0; border: 0; background-color: inherit;'; 601 html += ' font-size: 1.5rem; outline: none;}'; 602 html += '</style>'; 603 604 // set up display div 605 html += '<div style="padding: .5rem .25rem 0 1rem;">'; 606 html += '<button type="button" class="dialog-close-btn" style="outline:none;"'; 607 html += ' onclick="this.closest(\'dialog\').remove()">×</button>'; 608 html += '<div style="font-weight:600;padding-top:.25rem;">'+title+'</div></div>'; 609 html += '<div style="0 1rem 0 1.5rem;width:100%;height:calc(100% - 40px);overflow-y:auto;overflow-x:hidden;">'; 610 const dialog = document.createElement("dialog"); 611 dialog.classList.add('site-dialog'); 612 dialog.innerHTML = html+content_html+'</div>'; 613 document.body.appendChild(dialog); 614 615 // open dialog 616 dialog.showModal(); 617} // end function siteDialog 618 619function siteDivLoader(in_div_id, loader_div_id="loader_div_id", timeout=0, position="top", 620 img_file="/static/images/loading2.gif", image_width="200px") { 621 // use loader_div_id to remove the loader_div when no-longer needed 622 // create a modal container 623 const modal = document.createElement('div'); 624 modal.id = loader_div_id; 625// modal.style.position = 'fixed'; // entire view port 626 modal.style.position = 'absolute'; // relative to the parent container (div) 627 modal.style.zIndex = 100; 628 modal.style.top = '0'; 629 modal.style.left = '0'; 630 modal.style.width = '100%'; 631 modal.style.height = '100%'; 632 modal.style.backgroundColor = 'rgba(0, 0, 0, 0.1)'; 633 modal.style.backdropFilter = 'blur(1px)'; 634 modal.style.display = 'flex'; 635 modal.style.justifyContent = 'center'; 636 modal.style.alignItems = 'center'; 637 638/* not adding image, need to prevent re-requesting of image file 639 // Create the image element 640 const img = document.createElement('img'); 641 if (img_file) 642 img.src = img_file; 643 else 644 img.src = preloadedLoaderImage.src; // defined at top of this script file 645 img.alt = 'Loading image'; 646 647 // Apply CSS styles for centering 648 img.style.display = 'block'; // Make the image a block-level element 649 img.style.margin = '0 auto'; // Auto margins for horizontal centering 650 img.style.width = image_width; 651 img.style.height = 'auto'; 652 img.style.maxWidth = '100%'; // Prevent image from overflowing the div 653 img.style.position = 'absolute'; 654 655 if (position === 'very-top') 656 img.style.top = '10px'; // puts image in upper portion of div 657 if (position === 'top') 658 img.style.top = '100px'; // puts image in upper portion of div 659 if (position === 'middle' || position === 'center') 660 img.style.top = '50%'; // puts image in center/middle portion of div 661 if (position === 'bottom') 662 img.style.bottom = '100px'; // puts image in center/middle portion of div 663 img.style.right = 'calc(50% - 50px)'; // center horizontally 664 665 // Add the image to the div 666 modal.appendChild(img); 667*/ 668 669 // add the modal to the document 670 const in_div = document.getElementById(in_div_id); 671 in_div.style.position = 'relative'; // needed to contain the modal background within the parent div 672 in_div.appendChild(modal); 673 674 if (timeout > 0) 675 setTimeout(function() { modal.remove(); }, timeout); 676 677} // end function siteDivLoader 678 679function siteDivUnLoader(loader_div_id) { 680 const div = document.getElementById(loader_div_id); 681 if (div) div.remove(); // remove loading graphic 682} 683 684function siteFetchOptions(valuesJSON) 685{ // set method:POST and build body from valuesJSON 686 return { 687 method: 'POST', 688 headers: {"Content-Type": "application/json"}, 689 body: JSON.stringify(valuesJSON), 690 }; 691} // end function siteFetchOptions 692 693function siteSendBeacon(url, jsonObject) { 694 const jsonData = JSON.stringify(jsonObject); 695 const blob = new Blob([jsonData], { type: 'application/json' }); 696 navigator.sendBeacon(url, blob); 697} 698 699/* 700tableTools 701David Berky - [email protected] 7022024-04-22 703 704::Initialize Table:: 705A call to the init() function is needed to setup the column sort buttons. If only doing search, calling init() 706isn't required. 707 708To run the init(), call as: 709 tableTools.init("table1"); // call by table ID (one table at a time) 710 tableTools.init(".table-tools"); // call by custom class (include the leading period) 711 tableTools.init(); // assumes the "table-tools" class (same as tableTools.init(".table-tools")) 712 713::Seachable Columns:: 714To specify columns to search on, add a "data-searchcols" attribute to the <table> tag. 715 ex. data-searchcols="1,2,3" to search on contents of the first three columns in the table (as the table is defined) 716
717If "data-searchcols" is not specified it is assumed that all columns may be used to determine the row's searchable text. 718 719::Sortable Columns:: 720To specify columns to add sort buttons to, add a "data-sortcols" attribute to the <table> tag. 721 ex. data-sortcols="1,2,3" to add sort buttons to the first three columns in the table (as the table is defined) 722 723If "data-sortcols" is not specified it is assumed that all columns will be considered sortable. 724 725Add "<span hidden>sort value</span>" to the start of the "td" content to specify the value to use in the sort. 726Sort values for dates and numbers must be the same length to avoid incorrect sorting. They will be sorted as strings. 727 728 ie. display values: 10, 9, 100 => sort values: 010, 009, 100 729 <td><span hidden>010</span>10</td> (.textContent: 01010) 730 <td><span hidden>009</span>9</td> (.textContent: 0099) 731 <td><span hidden>100</span>100</td> (.textContent: 100100) 732 733 ie. display values: 10/5/2024, 9/5/2023, 1/10/2022 => sort values: 20241005, 20230905, 20220110 734 <td><span hidden>20241005</span>10/5/2024</td> (.textContent: 2024100510/5/2024) 735 <td><span hidden>20230905</span>9/5/2023</td> (.textContent: 202309059/5/2023) 736 <td><span hidden>20220110</span>1/10/2022</td> (.textContent: 202201101/10/2022) 737 738The sort button CSS (default class: "sort-btn") will be automatically added to the DOM unless you set the
739insert_button_style JS value to "false"; The default sort button styling is: 740<style> 741 .sort-btn { 742 border:1px solid white; 743 background-color:inherit; 744 padding:0 .25rem; 745 border-radius: .25rem; 746 } 747 .sort-btn:is(:active, :focus, :hover) { 748 border: 1px solid black; 749 background-color: #ccc; 750 } 751</style> 752 753This can be added to your site's CSS file and customized to your needs. 754 755The "sort-btn" class may also be renamed (to avoid conflicts) in the tableTools definition below. 756The sort button icons (or characters by default) can also be overridden in the tableTools definition below. 757 758::Table structure:: 759tableTools works best if the table contains both a "thead" and "tbody" section. A "thead" section is required to use 760the sort buttons. The search function will work without a "thead" section. 761 762Example table code: 763<input type="search" id="terms" oninput="tableTools.search(this, 'table1')" placeholder="Search.."><br /><br /> 764<table class="table-tools" id="table1" data-searchcols="1,2,3,4" data-sortcols="1,2,3,4" border=1 cellspacing=0 cellpadding=3> 765 <thead> 766 <tr><th>Col 1</th><th>Col 2</th><th>Date</th><th>Number</th><th>Action</th></tr> 767 </thead> 768 <tbody> 769 <tr><td>Data 1</td><td>Data C</td><td><span hidden>20240319</span>3/19/2024</td><td style="text-align:right;"><span hidden>010</span>10</td><td><button>Action</button></td></tr> 770 <tr><td>Data 2</td><td>Data B</td><td><span hidden>20230419</span>4/19/2023</td><td style="text-align:right;"><span hidden>009</span>9</td><td><button>Action</button></td></tr> 771 <tr><td>Data 3</td><td>Data A</td><td><span hidden>20220519</span>5/19/2022</td><td style="text-align:right;"><span hidden>100</span>100</td><td><button>Action</button></td></tr> 772 </tbody> 773</table> 774*/ 775 776{ // establish scope 777 tableTools = {}; // library object 778 779 // private variables and functions 780 let insert_button_style = true; // add (or not) the sort-btn css code 781 let sort_button_classes = 'sort-btn'; // set sort button classes here 782 let sort_asc_button = "â"; 783 let sort_desc_button = "â"; 784 785// function onlyVisibleHere() { /* doSomething */ } // private function example 786 787 // functions visible to outside 788 tableTools.init = function(table_id) { 789 // set up table(s) 790 let tables_array = []; 791 if (arguments.length === 0) table_id = '.table-tools'; // default class to identify tables 792 if (table_id.substr(0,1) === '.') { // is class (default or custom), get all instances of class 793 tables_array = document.getElementsByClassName(table_id.substring(1)); 794 } 795 else { // just one table, get reference to table 796 tables_array.push(document.getElementById(table_id)); 797 } 798 let i, j, sort_cols_array, btnUp, btnDn; // other variables 799 800 // setup sort buttons in header of each table 801 for (i = 0; i < tables_array.length; i++) { // each table 802 if (tables_array[i].tHead) { // don't attempt to add sort buttons if no header row 803 sort_cols_array = tableTools.sortCols(tables_array[i]); 804 for (j = 0; j < sort_cols_array.length; j++) { // each table column 805 th = tables_array[i].tHead.rows[0].cells[j]; 806 btnUp = document.createElement('input'); 807 btnUp.type = "button"; 808 btnUp.className = sort_button_classes; 809 btnUp.value = sort_asc_button; 810 btnUp.onclick = (function() {tableTools.sort(this, 'up')}); 811 btnDn = document.createElement('input'); 812 btnDn.type = "button"; 813 btnDn.className = sort_button_classes; 814 btnDn.value = sort_desc_button; 815 btnDn.onclick = (function() {tableTools.sort(this, 'down')}); 816 th.appendChild(btnUp); 817 th.appendChild(btnDn); 818 } 819 } 820 } 821 822 // insert sort button styles (CSS) 823 if (insert_button_style) { 824 let styles = ` 825 .sort-btn { 826 border: 1px solid white; 827 background-color: inherit; 828 padding: 0 .25rem; 829 border-radius: .25rem; 830 } 831 .sort-btn:is(:active, :focus, :hover) { 832 border: 1px solid black; 833 background-color: #ccc; 834 } 835 `; 836 let styleSheet = document.createElement("style"); 837 styleSheet.innerText = styles; 838 document.head.appendChild(styleSheet); 839 } 840 } 841 842 tableTools.sort = function(th_element, direction) { 843 // sort by the column, called by buttons added to table header row columns (see init()) 844 let table_element = th_element.closest('table'); 845 let col_index = th_element.closest('th').cellIndex; 846 let rows = table_element.tBodies[0].rows; // only get rows in tbody (not header/footer - assumes only one tbody) 847 let still_switching_flag, i, sort_data1, sort_data_span, sort_data2, switch_rows_flag; // other variables 848 still_switching_flag = true; 849 // loop until no switching has been done 850 while (still_switching_flag) { 851 still_switching_flag = false; 852 // loop through all table rows in tbody 853 for (i = 0; i < (rows.length - 1); i++) { 854 switch_rows_flag = false; 855 // get the two elements to compare, one from current row and one from the next 856 sort_data1 = rows[i].cells[col_index].textContent; // vs .innerText to pick up hidden text 857 sort_data2 = rows[i+1].cells[col_index].textContent; // vs .innerText to pick up hidden text 858 // check if the two rows should switch places based on sort direction 859 if (direction === 'up') { 860 if (sort_data1.toLowerCase() > sort_data2.toLowerCase()) { 861 switch_rows_flag = true;
861 // mark as a switch and break the loop 862 break; 863 } 864 } 865 else { 866 if (sort_data1.toLowerCase() < sort_data2.toLowerCase()) { 867 switch_rows_flag = true; // mark as a switch and break the loop 868 break; 869 } 870 } 871 } 872 // if switch, switch and mark that a switch has been done 873 if (switch_rows_flag) { 874 rows[i].parentNode.insertBefore(rows[i + 1], rows[i]); 875 still_switching_flag = true; 876 } 877 } 878 } 879 880 tableTools.search = function(input_element, table_id) { 881 // search for text 882 let search_string = input_element.value.toUpperCase(); 883 let table_element = document.getElementById(table_id); 884 let rows = table_element.tBodies[0].rows; // only get rows in tbody (not header/footer - assumes only one tbody) 885 let cols_to_search_array = tableTools.searchCols(table_element); 886 let i, j, content_string, td, marked_hidden; // other variables 887 const hidden_values = ["yes","on","1","hidden"]; // possible values for <td data-hidden="yes"... 888 889 // loop through all table rows, and hide those who don't match the search query 890 for (i = 0; i < rows.length; i++) { 891 // accumulate content for cols to search 892 content_string = ''; 893 for (j = 0; j < cols_to_search_array.length; j++) { 894 td = rows[i].getElementsByTagName("td")[cols_to_search_array[j]]; 895 if (td) { 896 content_string += " " + (td.textContent || td.innerText); 897 } 898 } 899 // check for data-hidden on the row, which can be set by another process 900 marked_hidden = false; 901 if (rows[i].dataset.hasOwnProperty('hidden')) { 902 hidden = rows[i].dataset.hidden; 903 hidden = hidden.toLowerCase(); 904 if (hidden_values.includes(hidden)) 905 marked_hidden = true; 906 } 907 // check content for search string (show if row doesn't have data-hidden="yes" etc.) 908 if (content_string.toUpperCase().indexOf(search_string) > -1 && !marked_hidden) { // show 909 rows[i].style.display = ""; 910 } else { 911 rows[i].style.display = "none"; // hidden 912 } 913 } 914 } 915 916 tableTools.searchCols = function(table_element) { 917 // return specified search columns (starting at 1) from the data-searchcols value or assume all cols are searchable 918 // note: hidden columns may also be used for searching 919 // note: data-searchcols="1,2,3" to search on contents of the first three columns in the table (as the table is defined) 920 if (table_element.dataset.hasOwnProperty('searchcols')) { 921 return tableTools.convertCols(table_element.dataset.searchcols); // converts [1, 2, 3...] to [0, 1, 2...] 922 } 923 else { // assume all columns are searchable 924 let cols_count = tableTools.colCount(table_element); 925 return Array.from({ length: cols_count }, (v, i) => i); // creates an array [0, 1, 2, ...] 926 } 927 } 928 929 tableTools.sortCols = function(table_element) { 930 // return specified sort columns from the data-sortcols (starting at 1) value or assume all cols are sortable 931 // note: data-sortcols="1,2,3" to identify columns (first three) in the table (as the table is defined) 932 if (table_element.dataset.hasOwnProperty('sortcols')) { 933 return tableTools.convertCols(table_element.dataset.sortcols); // converts [1, 2, 3...] to [0, 1, 2...] 934 } 935 else { 936 let cols_count = tableTools.colCount(table_element); 937 return Array.from({ length: cols_count }, (v, i) => i); // creates an array [0, 1, 2, ...] 938 } 939 } 940 941 tableTools.colCount = function(table_element) { 942 // return the number of columns (accounting for colSpan as well) 943 let columnCount = 0; 944 let rows = table_element.rows; 945 if (rows.length > 0) { 946 let cells = rows[0].cells; 947 for (i = 0, len = cells.length; i < len; ++i) { 948 columnCount += cells[i].colSpan; 949 } 950 } 951 return columnCount; 952 } 953 954 tableTools.convertCols = function(cols_string) { 955 // convert string to array, reduce col numbers for 0-based columns 956 let cols_array = cols_string.split(','); 957 for (var i = 0; i < cols_array.length; i++) { 958 cols_array[i] = cols_array[i] - 1; 959 } 960 return cols_array; 961 } 962 963} // end tableTools scope
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.