PageSourceSearch

https://www.findacode.com/includes/v4/_site.js?cachebust=20260211.2

js findacode.com collected 2026-09-24 18:56:53 UTC 43,299 bytes, 963 lines download raw bytes

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">&times;</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 = '&times;';
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()">&times;</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.