//////////////////////////////////////////////////////////// // // National Directory Webflow code // const accordionSelector = ".accordionitemtrigger-3"; const mapViewSlug = "national-directory/map-view"; const programProfileSlug = "national-directory/program-profile"; const institutionsSlug = "national-directory/academic-institutions"; const documentLibrarySlug = "national-directory/document"; const exactMatchPrefix = "e--"; const programCommand = "program"; const institutionCommand = "institution"; const facilityCommand = "facility"; const stateCommand = "state"; const regionCommand = "region"; const sexCommand = "sex"; const ageCommand = "age"; const modeCommand = "mode"; const docCommand = "doc"; const divisionIDCommand = "divisionID"; const regionIDCommand = "regionID"; const fAgeCommand = "fAge"; const fSexCommand = "fSex"; const fTypeCommand = "fType"; const fCreditCommand = "credit"; // credit const fNonCreditCommand = "noncredit"; //noncredit const pathwayCommand = "pathway"; // credit // Globals let institutionNames = []; let searchCookie = "bcd_search_cookie: " + bcd_webPage; let urlSearchParms = decodeURIComponent(window.location.search); if (urlSearchParms) { urlSearchParms = urlSearchParms.substring(1); } // Remove the leading question mark // Load the National Directory json data let ndData = getNDData(); let ndPrograms = ndData.miscInfo.programs; function getNDData() { return masterNDData; } // Add the data to the Loki database let db = new loki('loki.db'); let landscapeCollection = db.addCollection('landscape'); landscapeCollection.insert(ndPrograms); //////////////////////////////////////////////////////////// // // Install event listeners // //////////////////////////////////////////////////////////// // Add event listener for after the DOM is loaded, to install handlers and update UI document.addEventListener('DOMContentLoaded', (event) => { // If we are coming from the user selecting the back button, use cookies to set up UI if (window.performance && window.performance.navigation.type == window.performance.navigation.TYPE_BACK_FORWARD) { let searchParms = decodeURIComponent(document.cookie.split('; ').find(row => row.startsWith(searchCookie))); if (searchParms) { searchParms = decodeURIComponent(searchParms.split('=')[1]); searchParms = searchParms.replace(/&/g, "&"); if (searchParms) { // Set the global with the parms and delete the cookie urlSearchParms = searchParms; document.cookie = searchCookie + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT"; // Delete the cookie } } } // Reset the "reset search" control setSearchControlHandlers(searchControllerParms); setResetSearchParmsHandler(); // Build the Academic Institutions select control let selectBox = document.querySelector('#institution-select'); if (selectBox) { buildInstitutionNames(); buildSelectBox(selectBox, institutionNames); } // The following is necessary for List and Program views (but not Stats view) Why? let searchEvent = new CustomEvent("updateSearch", {detail: {searchParms: urlSearchParms}}); document.dispatchEvent(searchEvent); // Initialize the US map if (bcd_webPage == "Stats View") { // Add click event listeners for each states let states = document.getElementsByClassName("nd_state_box"); for (let i = 0; i < states.length; i++) { states[i].addEventListener('click', selectState); } // Add click event listeners for the regions let regions = document.getElementsByClassName("nd_region_box"); for (let i = 0; i < regions.length; i++) { regions[i].addEventListener('click', selectRegion); } } if (bcd_webPage == "Documents") { $("#" + "sort-documents").on("change", function(event, params) { let searchEvent = new CustomEvent("updateSearch", {detail: {searchExpression: ""}}); document.dispatchEvent(searchEvent); }); } // Add a listener for the "View All" button let viewAll = document.getElementsByClassName("nd_view_all"); if (viewAll && viewAll.length) { viewAll[0].addEventListener('click', resetSearchUI); } // Un-hide the region of the screeen the will contain the search results setTimeout(function() { // We do the setTimeout as a hack to wait until the DOM is updated before showing the search block // let searchResultsDOM = document.querySelector(".nd_search_results"); // if (searchResultsDOM) { // searchResultsDOM.style.display = "block"; // } let searchResultsElements = document.querySelectorAll(".nd_load_hidden"); for (let i = 0; i < searchResultsElements.length; i++) { searchResultsElements[i].style.display = "block"; } }, 1); }); // Update the UI controls with previous state window.addEventListener('load', function(){ resetSearchParms(); // Fix bug with Chosen dropdowns setSearchControlValues(urlSearchParms); if (bcd_webPage == "Stats View" || bcd_webPage == "Map View" || bcd_webPage == "List View") { // Move the status bar to the top of the page $('html, body').animate({ scrollTop: $("#Top_Of_View").offset().top }, 500); let activeButton = 0; switch (bcd_webPage) { case "Stats View": activeButton = 0; break; case "Map View": activeButton = 1; break; case "List View": activeButton = 2; break; } buildRadioButtons(activeButton); filterOnRadioButtons(); } if (bcd_webPage == "Map View") { // Add listeners for the Hide Institutions / Facilities buttons let hideInstitutions = document.getElementById("Hide_Institutions"); let toggleInsitutionsVisible = true; hideInstitutions.addEventListener('click', function (event) { if (toggleInsitutionsVisible) { showMarkerType("nd_institution_indicator", false); toggleInsitutionsVisible = false; hideInstitutions.innerText = "show institutions"; } else { showMarkerType("nd_institution_indicator", true); toggleInsitutionsVisible = true; hideInstitutions.innerText = "hide institutions"; } }); let hideFacilities = document.getElementById("Hide_Facilities"); let toggleFacilitiesVisible = true; hideFacilities.addEventListener('click', function (event) { if (toggleFacilitiesVisible) { showMarkerType("nd_facility_indicator", false); toggleFacilitiesVisible = false; hideFacilities.innerText = "show facilities"; } else { showMarkerType("nd_facility_indicator", true); toggleFacilitiesVisible = true; hideFacilities.innerText = "hide facilities"; } }); } // ... Map View }); function showMarkerType(markerID, bool) { let allMarkers = document.getElementsByClassName(markerID); for (let i = 0; i < allMarkers.length; i++) { if (bool) { allMarkers[i].style.display = "block"; } else { allMarkers[i].style.display = "none"; } } } // Save state when we navigate away from this page window.onbeforeunload = function(){ // let searchParms = document.getElementById("search_string").innerHTML; // Should this be innerText? saveSearchState(window.location.search); }; // Force Chosen to resize responsively $(document).ready(function(){ resizeChosen(); jQuery(window).on('resize', resizeChosen); // removed Nov 27 9:30 pm... because I don't think it's necessary // let stateList = getStateList(urlSearchParms); // highlightStates(stateList); }); // Listen for the search events document.addEventListener('updateSearch', function (event) { filterProgramsAndUpdateUI(landscapeCollection, event.detail.searchParms); }); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// function buildRadioButtons(activeButton) { // For each group of radio buttons, build a radioButtonGroup and add event listeners // to manage pseudo-radio button selection var radioButtonGroups = document.querySelectorAll('.s_radio_buttons'); radioButtonGroups.forEach(function(radioButtonGroup) { var radioButtons = radioButtonGroup.querySelectorAll('.s_radio_button'); var radioButtonControl = { radioButtonElements: radioButtons, focusStyles: [], activeButton: -1 }; // Add listener to each button for (var i = 0; i < radioButtons.length; i++) { radioButtonControl.focusStyles[i] = radioButtonControl.radioButtonElements[i].querySelector('[hidden_style="focus"]'); radioButtonControl.radioButtonElements[i].addEventListener('click', (function(i) { return function() { radioButtonClicked(radioButtonControl, i); }; })(i)); } setActiveRadioButton(radioButtonControl, activeButton); }); } // Manage radio button selection function setActiveRadioButton(radioButtonControl, buttonNumber) { if (buttonNumber != radioButtonControl.activeButton) { // Unset old button if (radioButtonControl.activeButton >= 0) { radioButtonControl.focusStyles[radioButtonControl.activeButton].style.display = "none"; } // Set new button radioButtonControl.focusStyles[buttonNumber].style.display = "block"; radioButtonControl.activeButton = buttonNumber; } } function radioButtonClicked(radioButtonControl, buttonNumber) { setActiveRadioButton(radioButtonControl, buttonNumber); } // Filter cards based on radio button selection function filterOnRadioButtons() { let filterElements = document.querySelectorAll('[f_radio_button]'); filterElements.forEach(function(element) { element.addEventListener('click', function() { let buttonValue = element.getAttribute("f_radio_button").toLowerCase(); let destPage = ""; switch (buttonValue) { case "stats": destPage = "stats-view"; break; case "map": destPage = "map-view"; break; case "list": destPage = "list-view"; break; } let encodedParms = window.location.search; let origin = window.location.origin; let destURL = origin + "/national-directory/" + destPage + encodedParms; window.location = destURL; console.log("Dest URL: " + destURL); }); }); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// let globalMap; let mapInitialized = false; let gMapScaleBig = true; let currentMarkers = []; let gMapListeners = []; function showMap(showStates, statesToHighlight) { if (globalMap) { // Remove markers and handlers pointing to them if (currentMarkers) { for (let i = 0; i < currentMarkers.length; i++) { currentMarkers[i].remove(); } currentMarkers = []; cleanupListeners(); } // globalMap.remove(); // We remove the entire map to handle resource cleanup } mapboxgl.accessToken = "pk.eyJ1IjoiYmx1ZW1hcmJsZWNocmlzdGluZSIsImEiOiJja2dvNW03YnUwOGdmMnNxaGs1NnJpcGZwIn0.vR6oX3Mo1wX8T18KwjjLHw"; // mapboxgl.accessToken = 'pk.eyJ1IjoidHJpc3RyYW0iLCJhIjoiY2syZHU0bnVhMDJsOTNibDZ0Mjh4eGtuciJ9.bo90X-elHNAof32ZkKRqpg'; // Find the bounding rect that encompasses all of the passed in states let minXBound = 0; let maxXBound = -180; let minYBound = 180; let maxYBound = 0; for (let i = 0; i < showStates.length; i++) { let showState = showStates[i]; let stateIndex = -1; for (let j = 0; j < stateBounds.length; j++) { if (stateBounds[j].NAME == showState) { stateIndex = j; break; } } if (stateIndex > -1) { let minX = stateBounds[stateIndex].xmin; let maxX = stateBounds[stateIndex].xmax; let minY = stateBounds[stateIndex].ymin; let maxY = stateBounds[stateIndex].ymax; if (minX < minXBound) { minXBound = minX; } if (maxX > maxXBound) { maxXBound = maxX; } if (minY < minYBound) { minYBound = minY; } if (maxY > maxYBound) { maxYBound = maxY; } } } let borderFactor = .05; // Leave some space around the state let width = maxXBound - minXBound; let height = maxYBound - minYBound; minXBound = minXBound - width * borderFactor; maxXBound = maxXBound + width * borderFactor; minYBound = minYBound - height * borderFactor; maxYBound = maxYBound + height * borderFactor; // State center let centerX = minXBound + (maxXBound - minXBound)/2.0; let centerY = minYBound + (maxYBound - minYBound)/2.0; let bounds = [ [minXBound, minYBound], [maxXBound, maxYBound], ]; if (showStates.length == 0) { let top = 49.3457868; // # north lat let left = -124.7844079; // # west long let right = -66.9513812; //# east long let bottom = 24.7433195; // # south lat bounds = [ [left, bottom], [right, top], ]; } // Create the map if (!globalMap) { globalMap = new mapboxgl.Map( { container: 'ND-Map', style: 'mapbox://styles/mapbox/light-v10', //streets-v11', // style: "mapbox://styles/bluemarblechristine/ckgo42c0l0z3w19juvqjs0qg4", // center: [-98.5795, 39.8283], // Center of US center: [centerX, centerY], // Center of state zoom: 3.5, maxZoom: 12, } ); } else { globalMap.fitBounds(bounds, { duration: 500 } ); } // globalMap.scrollZoom.disable(); globalMap.dragRotate.disable(); globalMap.dragPan.disable(); globalMap.touchZoomRotate.enable(); // For touch screens // globalMap.doubleClickZoom.disable(); /* globalMap.addControl(new mapboxgl.NavigationControl( { showCompass: false, } )); */ globalMap.on("wheel", event => { if (event.originalEvent.ctrlKey || event.originalEvent.metaKey || event.originalEvent.altKey) { return; } event.preventDefault(); }); // Hack to manage double-click zooming. Use the default Mapbox doubleClickZoom to do the // first zoom so we can get the initial zoom factor. After the first double click, we // turn off doubleClickZoom and use that initial zoom factor to calc our own very limited // double-click zoom in and out. let maxZoom = 14; // Max zoom in factor let maxZoomLevels = 2; // Number of double clicks before zooming all the way back out let savedZoom = 0; let zoomLevel = 0; let zoomIncrement = 0; let firstPass = true; let localZoomLevel = 3.5; globalMap.on('dblclick', function(e) { if (firstPass && zoomLevel == 1) { // After using the default doubleClickZoom once, turn it off firstPass = false; globalMap.doubleClickZoom.disable(); savedZoom = globalMap.getZoom(); zoomIncrement = (maxZoom - savedZoom) / (maxZoomLevels); } zoomLevel++; if (!firstPass) { globalMap.setCenter(e.lngLat); if (zoomLevel <= maxZoomLevels) { // Zoom in localZoomLevel = savedZoom + (zoomIncrement * (zoomLevel - 1)); globalMap.zoomTo(localZoomLevel); } else { // Zoom out to initial state globalMap.fitBounds(bounds, { duration: 500 } ); if (gMapScaleBig) { localZoomLevel = 3; } else { localZoomLevel = savedZoom; } zoomLevel = 0; } } // Update the marker size if (Array.isArray(currentMarkers)) { for (let i = 0; i < currentMarkers.length; i++) { if (localZoomLevel <= 4) { currentMarkers[i].getElement().classList.add("nd_small_marker"); } else { currentMarkers[i].getElement().classList.remove("nd_small_marker"); } } } }); // Set up the map after is loads from MapBox let firstIdlePass = true; globalMap.on('idle', function() { if (firstIdlePass) { // globalMap.setMaxBounds(globalMap.getBounds()); firstIdlePass = false; } }); globalMap.on('load', function () { globalMap.fitBounds(bounds, { duration: 500 } ); let states = stateOutlines.features; let theStates = statesToHighlight; if (!theStates.length) { theStates = showStates; } // Loop through all the states and gray out everything but the active state(s) for (let i = 0; i < states.length; i++) { globalMap.addSource( states[i].properties.NAME, { 'type': 'geojson', 'data': stateOutlines.features[i] } ); // Set the non-active state "background" color. Make Canada and Mexico darker let opacity = .1; /* if (statesToHighlight.indexOf(states[i].properties.NAME) > -1) { opacity = 0; } else if (states[i].properties.NAME == "Mexico" || states[i].properties.NAME == "Canada") { opacity = .3; } */ if (theStates.length && theStates.indexOf(states[i].properties.NAME) == -1) { // Set the non-active state "background" color. Make Canada and Mexico darker if (states[i].properties.NAME == "Mexico" || states[i].properties.NAME == "Canada") { opacity = .3; } } else { opacity = 0; } // Build the layer globalMap.addLayer({ 'id': states[i].properties.NAME, 'type': 'fill', 'source': states[i].properties.NAME, 'layout': {}, 'paint': { 'fill-color': 'gray', 'fill-opacity': opacity, } }); // ...addLayer } // ...for state mapInitialized = true; }); // ...globalMap.on // Use the size of the map to determine how big to draw the markers let boundsWidth = maxXBound - minXBound; if (boundsWidth > 40) { gMapScaleBig = true; } else { gMapScaleBig = false; } if (mapInitialized) { globalMap.fitBounds(bounds, { duration: 500 } ); let states = stateOutlines.features; let theStates = statesToHighlight; if (!theStates.length) { theStates = showStates; } // Loop through all the states and gray out everything but the active state(s) for (let i = 0; i < states.length; i++) { let opacity = .1; if (theStates.length && theStates.indexOf(states[i].properties.NAME) == -1) { // Set the non-active state "background" color. Make Canada and Mexico darker if (states[i].properties.NAME == "Mexico" || states[i].properties.NAME == "Canada") { opacity = .3; } } else { opacity = 0; } globalMap.setPaintProperty(states[i].properties.NAME, 'fill-opacity', opacity); } } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////// // // Helper functions // //////////////////////////////////////////////////////////// // Force the Chosen select controls to resize correctly upon window resize function resizeChosen() { $(".chosen-container").each(function() { $(this).attr('style', 'width: 100%'); }); } // Escape a string so it can be included in a mongo regex search expression function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string } // Save the current search state to a cookie function saveSearchState(searchParms) { let encodedParms = encodeURIComponent(searchParms); document.cookie = searchCookie + "=" + encodedParms; } // Replace the URL parameters with string function updateURL(string) { let refreshURL = window.location.protocol + "//" + window.location.host + window.location.pathname; if (string) { refreshURL = refreshURL + "?" + string; } window.history.pushState({ path: refreshURL }, '', refreshURL); } // Build array of institution names (used to the build institution popdown) function buildInstitutionNames() { let institutionObjects = []; for (let i = 0; i < ndPrograms.length; i++) { // Scan each Program if (ndPrograms[i].institutionCount) { for (let j = 0; j < ndPrograms[i].institutionCount; j++) { let name = institutionObjects[ndPrograms[i].institutions[j].name]; if (name) { // Prevent duplicates (but count them) institutionObjects[ndPrograms[i].institutions[j].name]++; } else { institutionObjects[ndPrograms[i].institutions[j].name] = 1; } } } } let count = 0; for (const name in institutionObjects) { institutionNames[count++] = name; } institutionNames.sort(); if (institutionNames[0] == "") { institutionNames.splice(0, 1); } } // Capitalize the words in the passed string function capitalizeWords(string) { let words = string.split(" "); for (let i = 0; i < words.length; i++) { words[i] = words[i][0].toUpperCase() + words[i].substr(1); } return words.join(" "); } // Not used - perhaps used to sanatize URL from cross-scripting issues? function getURLParm(name) { name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); var regex = new RegExp("[\\?&]" + name + "=([^]*)"), results = regex.exec(location.search); return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); } //////////////////////////////////////////////////////////// // Select controls functions //////////////////////////////////////////////////////////// // Reset the "reset search" control function setResetSearchParmsHandler() { $("#reset_search_parms").click(function(event, params) { let object = document.querySelector("#reset_search_parms"); object.checked = false; resetSearchUI(event); }); } // Given a select control, add the options to the Chosen control function buildSelectBox(selectBox, options) { removeAll(selectBox); for (let i = 0; i < options.length; i++) { let newOption = new Option(options[i], options[i]); selectBox.add(newOption,undefined); } $("#institution-select").trigger("chosen:updated"); } // Remove all of the element from a select box function removeAll(selectBox) { while (selectBox.options.length > 0) { selectBox.remove(0); } } // Deselect all of the elements in the select control function deselectAll(objectID) { let selectBox = document.querySelector("#" + objectID); if (selectBox) { for (let i = 0; i < selectBox.length; i++) { selectBox[i].selected = false; } $("#" + objectID).trigger("chosen:updated"); } } //////////////////////////////////////////////////////////// // // Search UI Controls // //////////////////////////////////////////////////////////// let searchControllerParms = [ { objectID: "nd_search_box_control", type: "search", events: "keyup,click", field: "s_splitValues", altFields: programCommand + "," + facilityCommand + "," + institutionCommand + "," + stateCommand, bool: "and" }, { objectID: "location-select", type: "select", events: "change", field: "s_splitValues", bool: "or", altFields: regionCommand + "," + stateCommand }, { objectID: "credential-select", type: "select", events: "change", field: pathwayCommand, bool: "or" }, { objectID: "mode-of-delivery-select", type: "select", events: "change", field: modeCommand, bool: "or" }, { objectID: "offerings-select", type: "select", events: "change", field: "p_o", bool: "or" }, { objectID: "additional-features-select", type: "select", events: "change", field: "p_a", bool: "or" }, { objectID: "documents-select", type: "select", events: "change", field: docCommand, bool: "or" }, { objectID: "sex-select", type: "select", events: "change", field: sexCommand, bool: "or" }, { objectID: "age-select", type: "select", events: "change", field: ageCommand, bool: "or" }, { objectID: "institution-type-select", type: "select", events: "change", field: "pi_t", bool: "or" }, { objectID: "institution-select", type: "select", events: "change", field: institutionCommand, bool: "or" }, { objectID: "facility-type-select", type: "select", events: "change", field: fTypeCommand, bool: "or" }, { objectID: "credit-offered-select", type: "select", events: "change", field: "s_splitValues", bool: "or", altFields: fNonCreditCommand + "," + fCreditCommand }, ]; //////////////////////////////////////////////////////////// // Set the Search UI control handliers function setSearchControlHandlers(searchControllers) { for (let i = 0; i < searchControllers.length; i++) { let events = searchControllers[i].events.split(","); // Allow for more than one event type for (let j = 0; j < events.length; j++) { $("#" + searchControllers[i].objectID).on(events[j], function(event, params) { if (event.type == "click") { event.stopImmediatePropagation(); // Prevent the click from being sent to the accordian } else { let searchEvent = new CustomEvent("updateSearch", {detail: {searchExpression: ""}}); document.dispatchEvent(searchEvent); } }); } } } // Reset all of the search UI elements and send an update event function resetSearchUI(event) { event.stopImmediatePropagation(); // Prevent the click from being sent to the accordian resetSearchParms(); let searchEvent = new CustomEvent("updateSearch", {detail: {searchParms: ""}}); document.dispatchEvent(searchEvent); // Added 12/3/2020 to prevent the UI from being pushed down $('html, body').animate({ scrollTop: $("#Top_Of_View").offset().top }, 500); } // Reset each Search UI control function resetSearchParms() { for (let i = 0; i < searchControllerParms.length; i++) { let objectID = searchControllerParms[i].objectID; let type = searchControllerParms[i].type; switch(type) { case "select": deselectAll(objectID); break; case "search": let object = document.querySelector("#" + objectID); if (object) { object.value = ""; } break; } } } // Count the number of item selected in the Select search controls function countSelectParms() { let itemCount = 0; for (let i = 0; i < searchControllerParms.length; i++) { let objectID = searchControllerParms[i].objectID; let type = searchControllerParms[i].type; switch(type) { case "select": let values = $("#" + objectID).val(); if (values) { itemCount += values.length; } break; case "search": break; } } return itemCount; } //////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////// // Populate the search controls with the passed in parameters //////////////////////////////////////////////////////////// function setSearchControlValues(passedSearchParms) { if (!passedSearchParms) { return (""); } if (passedSearchParms[0] == "?") { passedSearchParms = passedSearchParms.substring(1); } let passedSearchTokens = parseParms(passedSearchParms); let tokens = [...passedSearchTokens]; for (let i = 0; i < tokens.length; i++) { // Remove all but the fields and strings let firstChar = tokens[i][0]; switch (firstChar) { case "$": case "(": case ")": tokens.splice(i--, 1); // Move the index back before removed item break; } } for (let i = 0; i < tokens.length; i++) { let command = tokens[i++]; let value = tokens[i]; let prefix = value.substr(0, 3); if (prefix.substr(1,2) == "--") { value = value.substr(3); } let searchObject = searchControllerParms.find(x => x.field == command); if (!searchObject) { for (let j = 0; j < searchControllerParms.length; j++) { if (prefix == "e--") { if (searchControllerParms[j].field == "s_splitValues" && searchControllerParms[j].type == "select") { let fields = searchControllerParms[j].altFields.split(","); for (let k = 0; k < fields.length; k++) { if (command == fields[k]) { value = command + ":" + value; searchObject = searchControllerParms[j]; break; } } } } } } if (searchObject) { let objectID = searchObject.objectID; if (prefix == exactMatchPrefix) { // Exact match let selectBox = document.querySelector("#" + objectID); if (selectBox) { for (let j = 0; j < selectBox.length; j++) { if (selectBox[j].value.toLowerCase() == value.toLowerCase()) { selectBox[j].selected = true; $("#" + objectID).trigger("chosen:updated"); } } } } else if (searchObject.type == "search") { let object = document.querySelector("#" + objectID); if (object) { object.value = tokens[i]; } } else { console.log("Unknown command/control type: ", searchObject.type + " : " + command); } } else { console.log("Command not found: ", command); } } // ... for i } //////////////////////////////////////////////////////////// // Build the Mongo search expression from the passed search parms //////////////////////////////////////////////////////////// function createSearchExpression(searchParms) { // If there are URL search parameters, use those for the search let searchExpression = []; if (searchParms) { let searchParms3 = parseParms(searchParms); searchExpression = buildSearchExpression(searchParms3); } else { let searchParms3 = buildControlSearchParms(); searchExpression = buildSearchExpression(searchParms3); } return searchExpression; } //////////////////////////////////////////////////////////// // Build the search parms by reading the current state of the UI controls //////////////////////////////////////////////////////////// function buildControlSearchParms() { // Get the values from all of the search controls let searchString = ""; for (let i = 0; i < searchControllerParms.length; i++) { let objectID = searchControllerParms[i].objectID; let field = searchControllerParms[i].field; let type = searchControllerParms[i].type; switch(type) { case "select": let values = $("#" + objectID).val(); let separator = " "; let prefix = ""; if (values && values.length > 0) { if (values.length > 1) { prefix = "$or("; } for (let j = 0; j < values.length; j++) { let currentField = field; let value = values[j]; if (j > 0) { separator = " "; prefix = ""; } // If it is field specific option, parse the parms if (searchControllerParms[i].field == "s_splitValues") { let fields = value.split(":"); // Expected format: fieldname:value currentField = fields[0]; value = fields[1]; } if (searchString) { // If part of the search string has already be contructed searchString = searchString + separator; } searchString = searchString + prefix + currentField + "\"" + exactMatchPrefix + value + "\""; // Indicate exact match } // ... for j if (values.length > 1) { searchString = searchString + ")"; } if (searchString) { if (document.getElementById("search_string")) { document.getElementById("search_string").innerHTML = searchString; } } } break; case "search": let inputSearch = document.querySelector("#" + objectID); // the Search box let searchValue = ""; if (inputSearch && inputSearch.value.length > 0) { // Start sorting immediately after first key is pressed searchValue = inputSearch.value.toLowerCase().trim(); if (searchValue.startsWith("&")) { // Allow search parms to be used in the search string searchString = searchString + searchValue; } else { // Get the field(s) to search for the string let fields = searchControllerParms[i].altFields.split(","); if (fields.length > 1) { searchString = searchString + "$or("; } for (let k = 0; k < fields.length; k++) { searchString = searchString + fields[k] + "\"" + searchValue + "\""; } if (fields.length > 1) { searchString = searchString + ")"; } } if (searchString && document.getElementById("search_string")) { document.getElementById("search_string").innerText = searchString; // was innerHTML? } } break; } //... switch } // ... for updateURL(searchString); let tokens = parseParms(searchString); return tokens; } //////////////////////////////////////////////////////////// // Parse a parm string into tokens //////////////////////////////////////////////////////////// function parseParms(parms) { if (parms[0] == "?") { parms = parms.substring(1); } // Just in case it's there, remove leading "?" let tokens = []; let index = -1; // Start here because the index gets incremented when a token is found let foundToken = true; for (let i = 0; i < parms.length; i++) { let char = parms[i]; let remaining = parms.substring(i); switch (char) { case "$": { // Start of a logical token index++; foundToken = true; let nextObjectIndex = remaining.indexOf("("); let bool = remaining.substring(0, nextObjectIndex); tokens[index] = bool; i += nextObjectIndex - 1; } break; case "(": // Logical group start index++; foundToken = true; tokens[index] = "("; break; case ")": // Logical group end index++; foundToken = true; tokens[index] = ")"; break; case "\"": { // Start of string index++; foundToken = true; remaining = remaining.substring(1); let nextObjectIndex = remaining.indexOf("\""); // Find the closing quote let string = remaining.substring(0, nextObjectIndex); tokens[index] = string; i += nextObjectIndex + 1; // Move past the closing quote } break; default: // This should be the field name if (foundToken) { // If previous char string was a token, start a new token foundToken = false; index++; } if (!tokens[index]) { // If start of token array, add first element tokens[index] = char; } else { tokens[index] = tokens[index] + char; } } } // Clean up the tokens for (let i = 0; i < tokens.length; i++) { tokens[i] = tokens[i].trim(); if (!tokens[i]) { // Remove blank tokens tokens.splice(i--, 1); // Decriment index to be sure to process next element } } return tokens; } function countFieldTokens(tokens) { let fieldCount = 0; for (let i = 0; i < tokens.length; i++) { if (tokens[i][0] == "$" || tokens[i][0] == "(" || tokens[i][0] == ")") { tokens.splice(i--, 1); // Remove the token and reset the index } } if (tokens.length > 1) { fieldCount = tokens.length / 2; } // Half the command/value pair return fieldCount; } //////////////////////////////////////////////////////////// // // The command to field mapping table (the descriptor to field mapping table) // //////////////////////////////////////////////////////////// let searchParmsTable = [ { command: ageCommand, field: "ageTotal", bool: "or" }, { command: regionCommand, field: "division", bool: "or" }, { command: divisionIDCommand, field: "divisionID", bool: "or" }, { command: regionIDCommand, field: "regionID", bool: "or" }, { command: docCommand, field: "documents.generalName", bool: "or" }, { command: modeCommand, field: "modeOfDelivery", bool: "or" }, { command: programCommand, field: "name", bool: "and" }, { command: stateCommand, field: "state", bool: "or" }, { command: sexCommand, field: "sexTotal", bool: "or" }, { command: fAgeCommand, field: "facilities.age", bool: "or" }, { command: fSexCommand, field: "facilities.sex", bool: "or" }, { command: fTypeCommand, field: "facilities.type", bool: "or" }, { command: pathwayCommand, field: "degrees.pathway", bool: "or" }, { command: facilityCommand, field: "facilities.name", bool: "or" }, { command: fCreditCommand, field: "facilities.creditPostSecondaryOffered", bool: "or" }, { command: fNonCreditCommand, field: "facilities.nonCreditCourseworkOffered", bool: "or" }, { command: institutionCommand, field: "institutions.name", bool: "or" }, { command: "p_a", field: "additionalProgramFeatures.name", bool: "or" }, { command: "p_r", field: "region", bool: "or" }, // Actual region... but we are using divison as a region { command: "p_o", field: "programOfferings.name", bool: "or" }, { command: "pi_dm", field: "institutions.deliveryMethod", bool: "or" }, { command: "pi_dp", field: "institutions.degreeProgram", bool: "or" }, { command: "pi_t", field: "institutions.type", bool: "or" }, { command: "pc_n", field: "certificates.name", bool: "or" }, ]; ////////////////////////////// function buildSearchExpression(passedSearchTokens) { if (passedSearchTokens == "") return (""); // Search for all possible parameters let index = 0; let passedBool = "$and"; let searchExpression = []; let commands = []; let commandIndex = 0; let parmCount = 0; let command = ""; let value = ""; let bool = ""; for (let i = index; i < passedSearchTokens.length; i++) { if (passedSearchTokens[i][0] == "$") { // Boolean bool = passedSearchTokens[i++]; if (commands) { for (let j = 0; j < commands.length; j++) { searchExpression[parmCount++] = commands[j]; } commands = []; commandIndex = 0; } if (passedSearchTokens[i] != "(") { console.log("collectPairs Error: Missing opening paren"); } } else { if (passedSearchTokens[i] == ")") { if (bool) { searchExpression[parmCount++] = {[bool]: commands}; bool = ""; commands = []; commandIndex = 0; } else { console.log("Missing bool"); } } else { command = passedSearchTokens[i++]; value = passedSearchTokens[i]; let searchObject = searchParmsTable.find(x => x.command == command); if (searchObject) { let field = searchObject.field; let startsWith = value.substring(0, 3); if (startsWith == exactMatchPrefix) { // Exact match value = value.substring(3); if (field == "state") { commands[commandIndex++] = {"states.name": {$regex: new RegExp("^" + escapeRegExp(value) + "$", "i")}}; } else { commands[commandIndex++] = {[field]: {$regex: new RegExp("^" + escapeRegExp(value) + "$", "i")}}; } } else if (startsWith == "s--") { // Starts with value = value.substring(3); commands[commandIndex++] = {[field]: {$regex: new RegExp("^" + escapeRegExp(value), "i")}}; } else{ commands[commandIndex++] = {[field]: {$regex: new RegExp(escapeRegExp(value), "i")}}; } } else { console.log("Command not found: ", command); } } } } if (commands.length) { for (let j = 0; j < commands.length; j++) { searchExpression[parmCount++] = commands[j]; } } return [{[passedBool]: searchExpression}]; } ////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Given passed search parameters, filter the DB and update the UI. // If no parameters are passed, build them from the current state of the Search UI // //////////////////////////////////////////////////////////// function filterProgramsAndUpdateUI(collection, searchParms) { let searchExpression = createSearchExpression(searchParms); // Retrieve the results by ANDing the string of search expressions let results = collection.chain().find({$and: searchExpression}).simplesort("name").data(); // Sort by program name //////////////////////////////////////////////////////////// // Filter facilities and institutions from the results if (bcd_webPage == "Stats View" || bcd_webPage == "Map View") { let states = getStatesFromSearchParms(searchParms); if (states) { indicateInstituionsOutOfState(results, states); // This be here because removeFacilities changes the var results results = removeFacilities(results, states); removeProgramOtherStates(results, states); } } //////////////////////////////////////////////////////////// let keyupTimeout = setTimeout(function() { // This is a hack to give the DOM time to update (an async function and await might be the right way to do this) // Use a template to update the fields let template = programPageTemplate; // Default if (bcd_webPage == "Documents") { template = documentsTemplate; } else if (bcd_webPage == "Institutions") { template = institutionsTemplate; } else if (bcd_webPage == "List View") { template = listViewTemplate; } else if (bcd_webPage == "Map View") { template = mapViewTemplate; let stateList = getStateList(searchParms); let statesToHighlight = getStatesFromSearchParms(searchParms); if (statesToHighlight && statesToHighlight.length) { showMap(statesToHighlight, statesToHighlight); } else { showMap(stateList, statesToHighlight); } } else if (bcd_webPage == "Stats View") { template = statsViewTemplate; // todo: Could add a template choice based on searchParms (to handle different types of pages displays)? // Reset the UI based upon search parameters let clickText = document.getElementsByClassName("nd_click_to_explore"); let viewAll = document.getElementsByClassName("nd_view_all"); if (searchParms == "") { clickText[0].style.display = "block"; viewAll[0].style.display = "none"; } else { clickText[0].style.display = "none"; viewAll[0].style.display = "block"; } let stateList = getStateList(searchParms); let statesToHighlight = getStatesFromSearchParms(searchParms); highlightStates(stateList, statesToHighlight); // highlightStates(statesToHighlight, statesToHighlight); } setSearchControlValues(searchParms); let itemCount = countSelectParms(); let searchCount = document.querySelector("#ND_Options_Selected"); if (searchCount) { if (itemCount) { searchCount.innerText = ("( " + String(itemCount) + " selected )"); } else { searchCount.innerText = ""; } } fillGroupFields(document, template[0].subLists, results); }, 1); // This delay is necessary, see above // Update generic UI with the found count let count = results.length.toString(); if (document.getElementById("program_count")) { document.getElementById("program_count").innerText = "Displaying " + count + " Programs of " + collection.data.length; // Should this be innerText? } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// let regionStates = { "pacific": ["Alaska", "Washington", "Oregon", "California", "Hawaii"], "mountain": ["Idaho", "Nevada", "Utah", "Arizona", "Montana", "Wyoming", "Colorado", "New Mexico"], "west north central": ["North Dakota", "South Dakota", "Nebraska", "Kansas", "Minnesota", "Iowa", "Missouri"], "east north central": ["Wisconsin", "Illinois", "Indiana", "Michigan", "Ohio"], "west south central": ["Oklahoma", "Texas", "Arkansas", "Louisiana"], "east south central": ["Kentucky", "Tennessee", "Mississippi", "Alabama"], "south atlantic": ["West Virginia", "North Carolina", "Virginia", "South Carolina", "Georgia", "Maryland", "Washington D.C.", "Florida", "Delaware"], "middle atlantic": ["Pennsylvania", "New York", "New Jersey"], "new england": ["Vermont", "Massachusetts", "Connecticut", "Maine", "New Hampshire", "Rhode Island"], "u.s. territories": [ "American Samoa", "Guam", "Northern Mariana Islands", "Puerto Rico", "Virgin Islands"] }; let stateShortToLong = { // Table used to convert from short state names to long ones "AL": "Alabama", "AK": "Alaska", "AZ": "Arizona", "AR": "Arkansas", "CO": "Colorado", "CA": "California", "CT": "Connecticut", "DE": "Delaware", "FL": "Florida", "GA": "Georgia", "HI": "Hawaii", "ID": "Idaho", "IL": "Illinois", "IN": "Indiana", "IA": "Iowa", "KS": "Kansas", "KY": "Kentucky", "LA": "Louisiana", "ME": "Maine", "MD": "Maryland", "MA": "Massachusetts", "MI": "Michigan", "MN": "Minnesota", "MS": "Mississippi", "MO": "Missouri", "MT": "Montana", "NE": "Nebraska", "NV": "Nevada", "NH": "New Hampshire", "NJ": "New Jersey", "NM": "New Mexico", "NY": "New York", "NC": "North Carolina", "ND": "North Dakota", "OH": "Ohio", "OK": "Oklahoma", "OR": "Oregon", "PA": "Pennsylvania", "RI": "Rhode Island", "SC": "South Carolina", "SD": "South Dakota", "TN": "Tennessee", "TX": "Texas", "UT": "Utah", "VT": "Vermont", "VA": "Virginia", "WA": "Washington", "WV": "West Virginia", "WI": "Wisconsin", "WY": "Wyoming", "AS": "American Samoa", "GU": "Guam", "MP": "Northern Mariana Islands", "DC": "Washington D.C.", "PR": "Puerto Rico", "VI": "Virgin Islands", }; let pacificColor = "#6056b6"; let mountainColor = "#700548"; let wnCentralColor = "#ff714a"; let enCentralColor = "#ff3700"; let wsCentralColor = "#9c4260"; let esCentralColor = "#e691a7"; let sAtlanticColor = "#f5a800"; let mAtlanticColor = "#84a7b2"; let newEnglandColor = "#546f74"; let territoriesColor = "#b1a7d2"; let multipleColor = "gray"; let unknownColor = "black"; let stateNames = [ // Table used to convert from short state names to long ones ["AL", "Alabama", esCentralColor], ["AK", "Alaska", pacificColor], ["AZ", "Arizona", mountainColor], ["AR", "Arkansas", wsCentralColor], ["CO", "Colorado", mountainColor], ["CA", "California", pacificColor], ["CT", "Connecticut", newEnglandColor], ["DE", "Delaware", sAtlanticColor], ["FL", "Florida", sAtlanticColor], ["GA", "Georgia", sAtlanticColor], ["HI", "Hawaii", pacificColor], ["ID", "Idaho", mountainColor], ["IL", "Illinois", enCentralColor], ["IN", "Indiana", enCentralColor], ["IA", "Iowa", wnCentralColor], ["KS", "Kansas", wnCentralColor], ["KY", "Kentucky", esCentralColor], ["LA", "Louisiana", wsCentralColor], ["ME", "Maine", newEnglandColor], ["MD", "Maryland", sAtlanticColor], ["MA", "Massachusetts", newEnglandColor], ["MI", "Michigan", enCentralColor], ["MN", "Minnesota", wnCentralColor], ["MS", "Mississippi", esCentralColor], ["MO", "Missouri", wnCentralColor], ["MT", "Montana", mountainColor], ["NE", "Nebraska", wnCentralColor], ["NV", "Nevada", mountainColor], ["NH", "New Hampshire", newEnglandColor], ["NJ", "New Jersey", mAtlanticColor], ["NM", "New Mexico", mountainColor], ["NY", "New York", mAtlanticColor], ["NC", "North Carolina", sAtlanticColor], ["ND", "North Dakota", wnCentralColor], ["OH", "Ohio", enCentralColor], ["OK", "Oklahoma", wsCentralColor], ["OR", "Oregon", pacificColor], ["PA", "Pennsylvania", mAtlanticColor], ["RI", "Rhode Island", newEnglandColor], ["SC", "South Carolina", sAtlanticColor], ["SD", "South Dakota", wnCentralColor], ["TN", "Tennessee", esCentralColor], ["TX", "Texas", wsCentralColor], ["UT", "Utah", mountainColor], ["VT", "Vermont", newEnglandColor], ["VA", "Virginia", sAtlanticColor], ["WA", "Washington", pacificColor], ["WV", "West Virginia", sAtlanticColor], ["WI", "Wisconsin", enCentralColor], ["WY", "Wyoming", mountainColor], ["AS", "American Samoa", territoriesColor], ["GU", "Guam", territoriesColor], ["MP", "Northern Mariana Islands", territoriesColor], ["DC", "Washington D.C.", sAtlanticColor], ["PR", "Puerto Rico", territoriesColor], ["VI", "Virgin Islands", territoriesColor], ["S+", "Multiple", multipleColor], ]; // Convert an abreviated state name to a unabreviated one function getLongStateName(abbreviation) { let long = stateShortToLong[abbreviation]; let short = getShortStateName(long); return stateShortToLong[abbreviation]; } function getShortStateName(state) { let foundState = stateNames.find(x => x[1] == state); if (foundState) { return foundState[0]; } else { console.log("State not found: ", foundState); return ""; } } function getStateColor(state) { let foundState = stateNames.find(x => x[1] == state); if (foundState) { return foundState[2]; } else { console.log("State not found: ", foundState); return unknownColor; } } function highlightStates(stateList, statesToHighlight) { let stateElements = document.querySelectorAll(".nd_state_box"); for (let i = 0; i < stateElements.length; i++) { let element = stateElements[i].querySelector(".nd_state_name"); if (element) { let state = getLongStateName(element.innerHTML); if (statesToHighlight.length) { if (statesToHighlight.includes(state)) { stateElements[i].style.removeProperty("opacity"); } else { stateElements[i].style.opacity = 0.15; } } else if (stateList.includes(state)) { stateElements[i].style.removeProperty("opacity"); } else { stateElements[i].style.opacity = 0.15; } } } } // Return the list of states that are found in the DB using the search parms // This list may contain more states that in the parameter list because it searches // the db for programs that are in that state ... and some programs are in // multiple state, thus potentially returning mulitiple states function getStateList(searchParms) { let searchExpression = createSearchExpression(searchParms); // Retrieve the results by ANDing the string of search expressions let results = landscapeCollection.find({$and: searchExpression}); let stateList = []; for (let i = 0; i < results.length; i++) { let states = results[i].states; for (let j = 0; j < states.length; j++) { stateList[i] = states[j].name.trim(); i++; } i--; // stateList[i] = results[i].state; } if (stateList.length > 1) { stateList.sort(); for (let i = 1; i < stateList.length; i++) { if (stateList[i] == stateList[i - 1]) { stateList.splice(i--, 1); // Remove the duplicate state and reset the index } } } return stateList; } //////////////////////////////////////////////////////////// // Handle state and region selection in the UI let stateAndRegionParms = ""; // *** This probably will need to be initialized during reset function selectState(event) { let state = event.currentTarget.innerText; let stateFull = getLongStateName(state); let searchParms = stateCommand + "\"" + exactMatchPrefix + stateFull + "\""; if (stateAndRegionParms && event.metaKey) { let index = stateAndRegionParms.indexOf("\"" + exactMatchPrefix + stateFull + "\""); if (index > -1) { stateAndRegionParms = stateAndRegionParms.replace(searchParms, ""); if (stateAndRegionParms == "$or()") { stateAndRegionParms = ""; } } else { stateAndRegionParms = stateAndRegionParms.substring(0, stateAndRegionParms.length - 1) + searchParms + ")"; } } else { stateAndRegionParms = "$or(" + searchParms + ")"; } // Clicking a state on the Map resets all the search criterion resetSearchParms(); filterProgramsAndUpdateUI(landscapeCollection, stateAndRegionParms); updateURL(stateAndRegionParms); } ////////////////////////////// function selectRegion(event) { let region = event.currentTarget.innerText.toLowerCase(); let searchParms = regionCommand + "\"" + exactMatchPrefix + region + "\""; if (stateAndRegionParms && event.metaKey) { let index = stateAndRegionParms.indexOf("\"" + exactMatchPrefix + region + "\""); if (index > -1) { stateAndRegionParms = stateAndRegionParms.replace(searchParms, ""); if (stateAndRegionParms == "$or()") { stateAndRegionParms = ""; } } else { stateAndRegionParms = stateAndRegionParms.substring(0, stateAndRegionParms.length - 1) + searchParms + ")"; } } else { stateAndRegionParms = "$or(" + searchParms + ")"; } resetSearchParms(); filterProgramsAndUpdateUI(landscapeCollection, stateAndRegionParms); updateURL(stateAndRegionParms); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Main templating code // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////// // // fillFields: // // Search parentElement for elements with attributeName, and use ruleTable rules to // replace their text with data from jsonData // function fillFields(parentElement, attributeName, ruleTable, suffix, jsonData) { // Find the data elements to fill let dataElements = parentElement.querySelectorAll("[" + attributeName + "]"); // Fill each element, fill the field based upon the rule table for (let i = 0; i < dataElements.length; i++) { // Based on the attribute, retieve the rule information let attribute = dataElements[i].getAttribute(attributeName); let replacementInfo = ruleTable[attribute]; // If there's a rule, replace the element's dummy string with real data if (replacementInfo) { if (replacementInfo.callFunction) { replacementInfo.callFunction(dataElements[i], jsonData, replacementInfo); } else { let replacementString = jsonData[replacementInfo.field]; // Figure out what the replacement text should be if (replacementString === "") { replacementString = replacementInfo.emptyField; // Use the "empty" phrase if there's no data } else if (replacementInfo.noneStr) { replacementString = replacementInfo.noneStr; // Add the string for no objects } else if (replacementInfo.prefix) { replacementString = replacementInfo.prefix + replacementString; // Add a prefix string } else if (replacementInfo.suffix) { replacementString = replacementString + replacementInfo.suffix; // Add a suffix string } // Check to see if replacing a word if (replacementInfo.wordNumber || replacementInfo.singular) { if (replacementInfo.wordNumber) { let textToReplace = dataElements[i].innerHTML; // Should this be innerText? let words = textToReplace.split(" "); dataElements[i].innerHTML = textToReplace.replace(words[replacementInfo.wordNumber - 1], replacementString); // Should this be innerText? } if (replacementInfo.singular) { let textToReplace = dataElements[i].innerHTML; // Should this be innerText? let words = textToReplace.split(" "); if (parseInt(replacementString) == 1) { replacementString = replacementInfo.singular; } else { replacementString = replacementInfo.plural; } let replaceText = textToReplace.replace(words[replacementInfo.pluralWord - 1], replacementString); replacementString = replaceText; dataElements[i].innerHTML = replacementString; // Should this be innerText? } } else { dataElements[i].innerHTML = replacementString + suffix; // Should this be innerText? } } } else { console.log("Missing:", attribute); } } } //////////////////////////////////////////////////////////// // // fillGroupFields: // // Walk through the passed in page "template" to find groups of elements and fill in their values // "Groups" are groups of elements that share the same basic data type (such as Certificate) // function fillGroupFields(parentElement, template, fieldDataArray) { // Walk through the template to find and fill the DOM elements for (let i = 0; i < template.length; i++) { // Handle special case where the data will be handled in batches if (template[i].makeGroups) { let programGroups = template[i].preProcessFunction(fieldDataArray, template[i].groupBy); if (1) { fillGroupFields(parentElement, template[i].subLists, programGroups); } else { for (const programGroup in programGroups) { // almost ready to test?? if (template[i].subLists) { // Process each group fillGroupFields(parentElement, template[i].subLists, programGroup); } else { console.log("Error: Expected subList: ", template[i]); } } } } // Get the correct data segment for this segment of the page (template) let fieldData = fieldDataArray; if (template[i].otherData) { // Special case to change data source (used by summarize groups code) fieldData = template[i].otherData; if (template[i].preProcessFunction) { fieldData = template[i].preProcessFunction(fieldDataArray); } } // Use the field data as the data source and add sub attributes if they are required let subData = fieldData; if (template[i].data) { subData = fieldData[template[i].data]; } // If there's no data for this group, hide the groups and display the "no data for this group" element let expectedLength = subData.length; // The number objects in the data let showFullGroup = "block"; // Display the groups let showEmptyGroup = "none"; // ... and hide the "no info" group let noneGroup = 0; if (!expectedLength) { // If there isn't any data, then hide the groups, and show the "None" group showFullGroup = "none"; showEmptyGroup = "block"; noneGroup = 1; } let groupSelector = "[" + template[i].group + "]"; let foundGroups = parentElement.querySelectorAll(groupSelector); // Groups of the current type if (foundGroups.length > 0) { for (let j = 0; j < foundGroups.length; j++) { if (foundGroups[j].getAttribute(template[i].group) == "None") { // Start by showing the groups, and hiding the "none" group foundGroups[j].style.display = showEmptyGroup; } else foundGroups[j].style.display = showFullGroup; } } // If there's data, add it to the groups to be displayed if (expectedLength || noneGroup) { // Select all the non-special groups (that is, don't select groups with the "None") let groupSelector = "[" + template[i].group + "='" + "-" + "']"; if (noneGroup) { groupSelector = "[" + template[i].group + "='" + "None" + "']"; } // Find the group elements in the DOM and then add or pare down the number of // elements for this group to match the number that of groups in the data. Then fill // the fields in groups with data. let foundGroups = parentElement.querySelectorAll(groupSelector); if (foundGroups.length > 0) { // Add/remove group elements if (foundGroups.length < expectedLength) { // Add missing elements let addCount = expectedLength - foundGroups.length; for (let j = 0; j < addCount; j++) { let clone = foundGroups[0].cloneNode(true); foundGroups[0].insertAdjacentElement("afterend", clone); } } else if (foundGroups.length > expectedLength) { // Delete extra elements (but always leave one - because it is used a template) if (expectedLength == 0) { expectedLength = 1; } for (let j = foundGroups.length - 1; j >= expectedLength; j--) { foundGroups[j].remove(); } } // Using a rule table specific to this group, fill each group with data foundGroups = parentElement.querySelectorAll(groupSelector); // We add/removed elements above, so an extra call here is necessary for (let j = 0; j < expectedLength; j++) { // Add a suffix between sequences (if it is specified in the template) let suffix = ""; if (template[i].suffix && expectedLength > 1 && j < (expectedLength - 1)) { suffix = template[i].suffix; } if (noneGroup) { if (template[i].noneGroup) { let ruleTable = fillFieldsTable.bcdGroups[template[i].noneGroup][0]; fillFields(foundGroups[j], template[i].dataType, fillFieldsTable[ruleTable], suffix, fieldData); } } else { let ruleTable = fillFieldsTable.bcdGroups[template[i].group][0]; fillFields(foundGroups[j], template[i].dataType, fillFieldsTable[ruleTable], suffix, subData[j]); if (template[i].subLists) { if (j == 0) { if (template[i].subLists[0].useCallerData) { subData = fieldDataArray; } } // Recursively call this function for each sub group fillGroupFields(foundGroups[j], template[i].subLists, subData[j]); } } } } else { console.log("Missing:", groupSelector); } } // ... if expectedLength } // ... for each template segment } //////////////////////////////////////////////////////////////////////////// // Template helper functions //////////////////////////////////////////////////////////// function setInstitutionCountString(dataElement, jsonData, replacementInfo) { let count = jsonData.institutionCount; if (!institutionNames.length) { buildInstitutionNames(); } let totalCount = institutionNames.length; let string = "Displaying " + count + " Institutions of " + totalCount; dataElement.innerText = string; } function setDocumentCountString(dataElement, jsonData, replacementInfo) { let count = jsonData.documentCount; let totalDocumentCount = getTotalDocumentCount(); let string = "Displaying " + count + " Documents of " + totalDocumentCount; dataElement.innerText = string; } function setDocumentIcon(dataElement, jsonData, replacementInfo) { let url = jsonData.iconURL; dataElement.setAttribute("src", url); } //////////////////////////////////////////////////////////// // Helper code setting URLs in templates //////////////////////////////////////////////////////////// function setHREF(dataElement, url, prefix) { if (url) { if (prefix) { url = prefix + url; } dataElement.setAttribute("href", url); } else { dataElement.setAttribute("href", "#"); } } function setHREFNewTab(dataElement, jsonData, replacementInfo) { let url = jsonData[replacementInfo.field]; setHREF(dataElement, url); if (url) { dataElement.setAttribute("target", "_blank"); // Open in a new tab } } function setPhoneHREF(dataElement, jsonData, replacementInfo) { let url = jsonData[replacementInfo.field]; setHREF(dataElement, url, "tel:"); } function setEmailHREF(dataElement, jsonData, replacementInfo) { let url = jsonData[replacementInfo.field]; setHREF(dataElement, url, "mailto:"); } ////////////////////////////// function setInstitutionsHREF(dataElement, jsonData, replacementInfo) { let encodedParms = window.location.search; let url = "/" + institutionsSlug; if (encodedParms) { url = "/" + institutionsSlug + "?" + encodedParms; } setHREF(dataElement, url); } function setProgramSearchHREF(dataElement, jsonData, replacementInfo) { let url = getProgramSearchURL(jsonData[replacementInfo.field]); setHREF(dataElement, url); } function setStateHREF(dataElement, jsonData, replacementInfo) { let url = "/" + mapViewSlug + "?" + stateCommand + "\"e--" + jsonData[replacementInfo.field] + "\""; let states = jsonData[replacementInfo.field].split(","); let command = "$or("; if (states.length > 1) { for (let i = 0; i < states.length; i++) { command += stateCommand + "\"e--" + states[i].trim() + "\""; } url = "/" + mapViewSlug + "?" + command + ")"; } setHREF(dataElement, url); } function setMapProgramHREF(dataElement, jsonData, replacementInfo) { let url = "/" + mapViewSlug + "?" + programCommand + "\"" + encodeURIComponent(jsonData.name) + "\""; setHREF(dataElement, url); } function setStateIcon(dataElement, jsonData, replacementInfo) { let states = jsonData[replacementInfo.field].split(","); let state = states; if (states.length > 1) { state = "Multiple"; } let stateAbbreviation = getShortStateName(state); let stateColor = getStateColor(state); dataElement.innerText = stateAbbreviation; dataElement.parentElement.style.backgroundColor = stateColor; } ////////////////////////////// function getProgramSearchURL(programName) { return "/" + programProfileSlug + "?" + programCommand + "\"" + encodeURIComponent(programName) + "\""; } ////////////////////////////// // Show a state name if out of state, other hide function showIfOutOfState(dataElement, jsonData, replacementInfo) { if (jsonData.outOfState) { dataElement.innerText = "(" + jsonData.outOfState + ")"; } else { dataElement.innerText = ""; } } //////////////////////////////////////////////////////////// // This attaches a marker and popup to the map var markerCount = 0; // ***This is a hack counter. Markers should be managed better function attachToMap(dataElement, jsonData, replacementInfo) { // Do basic error check on the passed in geoCoords let coords = jsonData.location.split(","); if (!coords[0] || !coords[1]) { console.log("Bad coord in AttachToMap: ", JSON.stringify(coords)); return; } // create new DOM element for the marker and assign it a unique ID let markerElement = document.createElement('div'); let markerID = 'marker' + markerCount++; markerElement.id = markerID; // Set the new element to be either for Institutions or Facilities if (jsonData.type == "Institution") { markerElement.classList.add("nd_institution_indicator"); } else if (jsonData.type == "Facility") { markerElement.classList.add("nd_facility_indicator"); } // Create the text to go into the popup let popupText = "