//////////////////////////////////////////////////////////// // // 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 = "
" + jsonData.name + "
"; if (jsonData.programNames.length) { for (let i = 0; i < jsonData.programNames.length; i++) { let searchURL = getProgramSearchURL(jsonData.programNames[i]); popupText += "
"; popupText += "
" + jsonData.programNames[i] + "
"; } popupText += "
Click to freeze popup
"; } // Create the popup with the HTML we created above let popup = new mapboxgl.Popup( { offset: 15, } ).setHTML(popupText); // offset how far it pops up from the marker // Create the marker with the popup attached let location = [ coords[1], coords[0] ]; let marker = new mapboxgl.Marker(markerElement) .setLngLat(location) .setPopup(popup) .addTo(globalMap); // If the map scale is large, make the markers small if (gMapScaleBig) { markerElement.classList.add("nd_small_marker"); } else { markerElement.classList.remove("nd_small_marker"); } // Add the marker to the currentMarkers array (***this should be done smarter) currentMarkers.push(marker); // Add listeners to activate the popups when the user hovers over a marker let markerVisibility; let leaveOpen = false; markerElement.addEventListener('mouseenter', function (event) { if (leaveOpen) { leaveOpen = false; marker.togglePopup(); } marker.togglePopup(); }); markerElement.addEventListener('mouseleave', function (event) { if (!leaveOpen) { marker.togglePopup(); markerElement.style.display = markerVisibility; // Restore the previous visibility state } }); markerElement.addEventListener('click', function (event) { leaveOpen = true; event.stopImmediatePropagation(); // Prevent the click from being sent to the accordian }); marker.getPopup().on('close', function (event) { // When the user actively closes the window, turn off flag leaveOpen = false; }); // Activate the popups when the user places the mouse over institution/facility names as well let parent = dataElement.parentElement; // *** This is dangerous to depend on DOM structure let enterListener = function (event) { // This can't be an annoymous function because of cleanup markerElement.style.borderColor = "#FF714A"; markerElement.style.zIndex = 1; if (!leaveOpen) { markerVisibility = markerElement.style.display; } if (markerVisibility == "none") { markerElement.style.display = "block"; } if (leaveOpen) { leaveOpen = false; marker.togglePopup(); markerElement.style.display = markerVisibility; // Restore the previous visibility state } marker.togglePopup(); }; let exitListener = function (event) { // This can't be an annoymous function because of cleanup markerElement.style.removeProperty("border"); markerElement.style.zIndex = "auto"; if (!leaveOpen) { marker.togglePopup(); markerElement.style.display = markerVisibility; // Restore the previous visibility state } }; let clickListener = function (event) { leaveOpen = true; }; // Add the event listeners to activate the popups from the institutions/facility list parent.addEventListener('mouseenter', enterListener); parent.addEventListener('mouseleave', exitListener); parent.addEventListener('click', clickListener); // Save the handlers so we can delete them later recordListener(parent, "mouseenter", enterListener); recordListener(parent, "mouseleave", exitListener); recordListener(parent, "click", clickListener); } ////////////////////////////// // Save Program IDs in the DOM element so we can identify this element later function storeProgramIDs(dataElement, jsonData, replacementInfo) { dataElement.innerText = jsonData[replacementInfo.field]; let programIDs = ""; for (let i = 0; i < jsonData.programNames.length; i++) { let programID = jsonData.programIDs[i]; programIDs += "-" + programID + "-"; } if (programIDs) { dataElement.setAttribute("program-id", programIDs); } } ////////////////////////////// // Set the program name from the template, and attach some listeners to connnect to the // handlers on the Institutions and Facility names function makeSmartProgramName(dataElement, jsonData, replacementInfo) { dataElement.innerText = jsonData[replacementInfo.field]; dataElement.setAttribute("program-id", jsonData.id); dataElement.addEventListener('mouseenter', showProgramPopups); dataElement.addEventListener('mouseleave', hideProgramPopups); recordListener(dataElement, "mouseenter", showProgramPopups); recordListener(dataElement, "mouseleave", hideProgramPopups); } ////////////////////////////// // Save references to event listeners so we can delete them later function recordListener(element, event, eventHandler) { gMapListeners.push({ element: element, event: event, eventHandler: eventHandler }); } // Delete the saved listeners function cleanupListeners() { if (Array.isArray(gMapListeners)) { for (let i = 0; i < gMapListeners.length; i++) { gMapListeners[i].element.removeEventListener(gMapListeners[i].event, gMapListeners[i].eventHandler); } gMapListeners = []; // Does this actually recover resources? } } ////////////////////////////// // Pass events on to other elements function passEvent(programID, mouseEvent, selector) { let elements = document.querySelectorAll(selector); for (let i = 0; i < elements.length; i++) { let ids = elements[i].getAttribute("program-id"); if (ids.includes("-" + programID + "-")) { let event = new Event(mouseEvent); elements[i].parentElement.parentElement.dispatchEvent(event); // This parent of parent is dangerous } } } function passOnProgramEvent(event, mouseEvent) { let element = event.target; let programID = element.getAttribute("program-id"); // Pass the event on to the institution and faciity name elements passEvent(programID, mouseEvent, "[bcd_psidata='Institution Name']"); passEvent(programID, mouseEvent, "[bcd_psfdata='Facility Name']"); } function clickProgramPopups(event) { passOnProgramEvent(event, "click"); } function showProgramPopups(event) { passOnProgramEvent(event, "mouseenter"); } function hideProgramPopups(event) { passOnProgramEvent(event, "mouseleave"); } //////////////////////////////////////////////////////////// // // Return groups of programs, based on field passed in // function createMultiStateGroupBy(programArray, passedState) { let stateInfo = []; let summaryInfo = [ // Summary for all the programs that were passed in { programCount: 0, institutionCount: 0, facilityCount: 0, } ]; let stateStuff = { // Individually by state (by field, actually) name: "", programCount: 0, institutionCount: 0, facilityCount: 0, programNames: [], institutionNames: [], facilityNames: [], programs: [], // All program info institutions: [], // All institution info, de-duped facilities: [], // All facility info }; let allProgramNames = []; let allInstitutions = []; let allFacilities = []; //////////////////////////////////////////////////////////// // Scan each program for (let i = 0; i < programArray.length; i++) { // Keep track of this program's name allProgramNames[i] = { name: programArray[i].name, id: programArray[i].id, }; //////////////////////////////////////////////////////////// // Get the state of the program and add it to the list of states let states = programArray[i].states; for (let s = 0; s < states.length; s++) { let stateName = states[s].name; let index = stateInfo.findIndex(item => item.name == stateName); // If the state doesn't yet exist, add it if (index === -1) { // Add the first element stateInfo.push(JSON.parse(JSON.stringify(stateStuff))); index = stateInfo.length - 1; } //////////////////////////////////////////////////////////// // Begin filling the state info let thisStateInfo = stateInfo[index]; // This is the reference... intentionally thisStateInfo.name = stateName; thisStateInfo.programCount++; // Add the program to this state if (!thisStateInfo.programs) { thisStateInfo.programs = []; } thisStateInfo.programs.push(JSON.parse(JSON.stringify(programArray[i]))); //////////////////////////////////////////////////////////// // Add the institutions in the state to the state info for (let j = 0; j < programArray[i].institutions.length; j++) { if (programArray[i].institutions[j].state == stateName) { thisStateInfo.institutionCount++; if (!thisStateInfo.institutions) { thisStateInfo.institutions = []; } if (programArray[i].institutions.length) { thisStateInfo.institutions.push(JSON.parse(JSON.stringify(programArray[i].institutions[j]))); } } } // ... institutions //////////////////////////////////////////////////////////// // Add the facilities in the state to the state info for (let j = 0; j < programArray[i].facilities.length; j++) { if (programArray[i].facilities[j].state == stateName) { thisStateInfo.facilityCount++; if (!thisStateInfo.facilities) { thisStateInfo.facilities = []; } if (programArray[i].facilities.length) { thisStateInfo.facilities.push(JSON.parse(JSON.stringify(programArray[i].facilities[j]))); } } } // ... facilities } // ... state } // ... for programArray // Sort the list by State name stateInfo.sort((a, b) => (a.name > b.name) ? 1 : -1); for (let i = 0; i < stateInfo.length; i++) { // Sort each group by name stateInfo[i].programs.sort((a, b) => (a.name > b.name) ? 1 : -1); stateInfo[i].institutions.sort((a, b) => (a.name > b.name) ? 1 : -1); stateInfo[i].facilities.sort((a, b) => (a.name > b.name) ? 1 : -1); } // De-dup and create name lists for each group for (let i = 0; i < stateInfo.length; i++) { // Programs if (stateInfo[i].programs.length > 0) { stateInfo[i].programNames.push({name: stateInfo[i].programs[0].name}); if (stateInfo[i].programs.length > 1) { for (let j = 1; j < stateInfo[i].programs.length; j++) { stateInfo[i].programNames.push({name: stateInfo[i].programs[j].name}); /* if (stateInfo[i].programs[j].name == stateInfo[i].programNames[stateInfo[i].programNames.length - 1].name) { console.log("Duplicate program name: ", stateInfo[i].programs[j].name); } */ } } } // Institutions if (stateInfo[i].institutions.length > 0) { stateInfo[i].institutionNames.push(stateInfo[i].institutions[0].name); if (stateInfo[i].institutions.length > 1) { for (let j = 1; j < stateInfo[i].institutions.length; j++) { if (stateInfo[i].institutions[j].name != stateInfo[i].institutionNames[stateInfo[i].institutionNames.length - 1]) { stateInfo[i].institutionNames.push(stateInfo[i].institutions[j].name); } } } } // Facilities if (stateInfo[i].facilities.length > 0) { stateInfo[i].facilityNames.push(stateInfo[i].facilities[0].name); if (stateInfo[i].facilities.length > 1) { for (let j = 1; j < stateInfo[i].facilities.length; j++) { if (stateInfo[i].facilities[j].name != stateInfo[i].facilityNames[stateInfo[i].facilityNames.length - 1]) { stateInfo[i].facilityNames.push(stateInfo[i].facilities[j].name); } } } } // Update summary counts to reflect the de-duped counts // summaryInfo[0].programCount += stateInfo[i].programNames.length; summaryInfo[0].institutionCount += stateInfo[i].institutionNames.length; summaryInfo[0].facilityCount += stateInfo[i].facilityNames.length; } // ... stateInfo allProgramNames.sort((a, b) => (a.name > b.name) ? 1 : -1); allInstitutions.sort((a, b) => (a.name > b.name) ? 1 : -1); allFacilities.sort((a, b) => (a.name > b.name) ? 1 : -1); summaryInfo[0].programCount = programArray.length; // summaryInfo[0].institutionCount = allInstitutions.length; // summaryInfo[0].facilityCount = allFacilities.length; // The next line is a kludge to get the search parms to determine what to set the title string to let searchParms = decodeURIComponent(window.location.search); let titleString = "The National Directory includes:"; if (searchParms) { let fieldCount = 0; let searchTokens = parseParms(searchParms); fieldCount = countFieldTokens(searchTokens); if (fieldCount == 1) { // If there is only one parm, then use a specialized string if (searchTokens.indexOf(stateCommand) > -1) { // If it's a state titleString = "Included in the National Directory, " + searchTokens[searchTokens.indexOf(stateCommand) + 1].substring(exactMatchPrefix.length) + " had..."; } else if (searchTokens.indexOf(regionCommand) > -1) { // If it's a region let region = capitalizeWords(searchTokens[searchTokens.indexOf(regionCommand) + 1].substring(exactMatchPrefix.length)); titleString = "Included in the National Directory, the " + region + " region had..."; } } else { titleString = "Based on your search, the National Directory includes:"; } } summaryInfo[0].titleString = titleString; // Update the summary counts allInstitutions = getInstitutionsInfo(programArray); allFacilities = getFacilityInfo(programArray); summaryInfo[0].institutionCount = allInstitutions.length; // And finally, put it all in a single object let programInfo = { summaryInfo, stateInfo, allProgramNames, allInstitutions, allFacilities }; return programInfo; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// function getInstitutionsInfo(programArray) { // Field is usually state or region let allInstitutions = []; // Collect info for each individual state for (let i = 0; i < programArray.length; i++) { let count = parseInt(programArray[i].institutionCount); if (count) { for (let j = 0; j < programArray[i].institutions.length; j++) { if (programArray[i].institutions.length) { if (programArray[i].institutions[j].name) { let index = allInstitutions.findIndex(item => item.name == programArray[i].institutions[j].name); if (index > -1) { // Not the first time through allInstitutions[index].programNames.push(programArray[i].name); allInstitutions[index].programIDs.push(programArray[i].id); allInstitutions[index].programNames2.push( { name: programArray[i].name } ); } else { // First time through allInstitutions.push( { type: "Institution", name: programArray[i].institutions[j].name, location: programArray[i].institutions[j].geoLocation, collegeNavigator: programArray[i].institutions[j].collegeNavigator, website: programArray[i].institutions[j].website, programNames: [ programArray[i].name ], programIDs: [ programArray[i].id ], outOfState: programArray[i].institutions[j].outOfState, programNames2: [ { name: programArray[i].name } ], }); } } } } } } allInstitutions.sort((a, b) => (a.name > b.name) ? 1 : -1); return allInstitutions; } function getFacilityInfo(programArray) { // Field is usually state or region let allFacilities = []; for (let i = 0; i < programArray.length; i++) { let count = parseInt(programArray[i].onsiteFacilityCount); if (count) { for (let j = 0; j < programArray[i].facilities.length; j++) { if (programArray[i].facilities.length) { let name = programArray[i].facilities[j].name; let location = programArray[i].facilities[j].geoLocation; let website = programArray[i].facilities[j].website; if (name) { let index = allFacilities.findIndex(item => item.name == name); if (index > -1 && allFacilities[index].website == website ) { // If facility is already in array allFacilities[index].programNames.push(programArray[i].name); allFacilities[index].programIDs.push(programArray[i].id); /* if (allFacilities[index].location != location) { console.log("Same facility, different geoLocations: ", allFacilities[index].location + " vs " + location); } */ } else { allFacilities.push( { type: "Facility", name: programArray[i].facilities[j].name, location: location, website: website, programNames: [ programArray[i].name ], programIDs: [ programArray[i].id ], }); } } } } } } allFacilities.sort((a, b) => (a.name > b.name) ? 1 : -1); return allFacilities; } function getTotalDocumentCount() { let count = 0; let masterData = landscapeCollection.find({$and: ""}); for (let i = 0; i < masterData.length; i++) { count += masterData[i].documents.length; } return count; } let documentIconURLs = [ { docType: "Curriculum", iconURL: "https://uploads-ssl.webflow.com/5e3dd3cf0b4b54759e8b1be0/5fadfb7767e47a81d15cc710_Curriculum.png" }, { docType: "MOU", iconURL: "https://uploads-ssl.webflow.com/5e3dd3cf0b4b54759e8b1be0/5fadfb77147645b8d5377a8c_MOU.png" }, { docType: "Org Chart", iconURL: "https://uploads-ssl.webflow.com/5e3dd3cf0b4b54759e8b1be0/5fadfb77d350925eb897c27c_Org%20Chart.png" }, { docType: "Re-entry Guide", iconURL: "https://uploads-ssl.webflow.com/5e3dd3cf0b4b54759e8b1be0/5fadfb77aa2d916ca386fdfb_Re-entry%20Guide.png" }, { docType: "Admissions Application", iconURL: "https://uploads-ssl.webflow.com/5e3dd3cf0b4b54759e8b1be0/5fadfb78aa2d912e3386fdfc_Student%20Admission%20Application.png" }, { docType: "Staff Handbook", iconURL: "https://uploads-ssl.webflow.com/5e3dd3cf0b4b54759e8b1be0/5fadfb7787959b983e476047_Staff%20Handbook.png" }, { docType: "Strategic Plan", iconURL: "https://uploads-ssl.webflow.com/5e3dd3cf0b4b54759e8b1be0/5fadfb77e2bc56e9f67eb1ee_Strategic%20Plan.png" }, { docType: "Student Handbook", iconURL: "https://uploads-ssl.webflow.com/5e3dd3cf0b4b54759e8b1be0/5fadfb776b88a4400cd93294_Student%20Handbook.png" }, { docType: "Faculty Handbook", iconURL: "https://uploads-ssl.webflow.com/5e3dd3cf0b4b54759e8b1be0/5fc6e471a07e5a1d0b3af8af_Faculty%20Handbook.png" }, { docType: "Volunteer Handbook", iconURL: "https://uploads-ssl.webflow.com/5e3dd3cf0b4b54759e8b1be0/5fc6e3f25fc94f114e62b483_VolunteerHandbook.png" }, { docType: "By Laws", iconURL: "https://uploads-ssl.webflow.com/5e3dd3cf0b4b54759e8b1be0/5fc6e3f198766a159eef63e0_ByLaws.png" }, { docType: "FAFSA", iconURL: "https://uploads-ssl.webflow.com/5e3dd3cf0b4b54759e8b1be0/5fc6e3f12990df6583fb5b91_FAFSA.png" }, ]; function getDocumentInfo(programArray) { // Field is usually state or region let allDocuments = []; let docTypes = $("#documents-select").val(); let sortBy = $("#sort-documents").val(); for (let i = 0; i < programArray.length; i++) { for (let j = 0; j < programArray[i].documents.length; j++) { if (programArray[i].documents.length) { let name = programArray[i].documents[j].generalName; if (name && (!docTypes || !docTypes.length || docTypes.includes(name))) { let iconURL = ""; let urlIndex = documentIconURLs.findIndex(x => x.docType == name); if (urlIndex > -1) { iconURL = documentIconURLs[urlIndex].iconURL; } allDocuments.push( { type: name, url: programArray[i].documents[j].url, programName: programArray[i].name, programState: programArray[i].state, programRegion: programArray[i].division, iconURL: iconURL }); } } } } switch (sortBy) { case "Program": allDocuments.sort((a, b) => (a.programName.toUpperCase() > b.programName.toUpperCase()) ? 1 : -1); break; case "State": allDocuments.sort((a, b) => (a.programState.toUpperCase() > b.programState.toUpperCase()) ? 1 : -1); break; case "Region": allDocuments.sort((a, b) => (a.programRegion.toUpperCase() > b.programRegion.toUpperCase()) ? 1 : -1); break; case "Type": default: allDocuments.sort((a, b) => (a.type.toUpperCase() > b.type.toUpperCase()) ? 1 : -1); break; } return allDocuments; } function createDocumentsPageInfo(programArray, field) { let allDocuments = getDocumentInfo(programArray); let summaryInfo = [{ documentCount: allDocuments.length }]; return { summaryInfo, allDocuments }; } //////////////////////////////////////////////////////////////////////////////////////////////////// function getStatesFromSearchParms(searchParms) { let searchTokens = []; let states = []; if (searchParms) { searchTokens = parseParms(searchParms); } else { searchTokens = buildControlSearchParms(); } let fieldCount = 0; fieldCount = countFieldTokens(searchTokens); let count = 0; for (let i = 0; i < searchTokens.length; i++) { if (searchTokens[i] == stateCommand || searchTokens[i] == regionCommand) { if (searchTokens[i] == stateCommand) { let prefix = searchTokens[i + 1].substring(0, 3); let state = searchTokens[i + 1].substring(3); if (prefix == exactMatchPrefix) { states[count++] = state; i++; } } else if (searchTokens[i] == regionCommand) { let prefix = searchTokens[i + 1].substring(0, 3); let state = searchTokens[i + 1].substring(3); if (prefix == exactMatchPrefix) { let stateList = regionStates[state.toLowerCase()]; for (let j = 0; j < stateList.length; j++) { states[count++] = stateList[j]; } i++; } } } } return states; } function removeFieldsOutsideOfStates(programArray, field, states) { if (states && states.length) { programArray = JSON.parse(JSON.stringify(programArray)); // Make a copy before deleting (otherwise the db is affected) for (let i = 0; i < programArray.length; i++) { let fields = programArray[i][field]; for (let j = 0; j < fields.length; j++) { let foundField = false; for (let k = 0; k < states.length; k++) { if (fields[j].state == states[k]) { foundField = true; break; // No need to search farther } } if (!foundField) { fields.splice(j--, 1); } } // ... fields } } return programArray; } function indicateOutOfState(programArray, field, states) { if (states && states.length) { for (let i = 0; i < programArray.length; i++) { let fields = programArray[i][field]; for (let j = 0; j < fields.length; j++) { let foundField = false; for (let k = 0; k < states.length; k++) { if (fields[j].state == states[k]) { foundField = true; break; // No need to search farther } } if (!foundField) { programArray[i][field][j].outOfState = programArray[i][field][j].state; } } // ... fields } } } function removeProgramOtherStates(programArray, statesToKeep) { if (statesToKeep && statesToKeep.length) { for (let i = 0; i < programArray.length; i++) { if (programArray[i].states.length > 1) { let programStates = programArray[i].states; for (let j = 0; j < programStates.length; j++) { if (statesToKeep.indexOf(programStates[j].name) == -1) { programStates.splice(j--, 1); } } } } } } function removeFacilities(programArray, states) { return removeFieldsOutsideOfStates(programArray, "facilities", states); } function indicateInstituionsOutOfState(programArray, states) { indicateOutOfState(programArray, "institutions", states); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Templating tables // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Table used to map fields on the page with data in the database // let smallFontSize = "80%"; let smallNoData = "
- no data -
"; function sayWithSmallText(dataElement, jsonData, replacementInfo) { let theText = jsonData[replacementInfo.field]; if (!theText) { theText = "
" + replacementInfo.emptyField + "
"; } dataElement.innerHTML = theText; } //////////////////////////////////////////////////////////// let fillFieldsTable = { // *** These tables really need to be cleaned up "bcdGroups": { ////////////////////////////// // HTML attribute used to look up the group's field data "bcd_ngroup": ["nationalDirectoryFields"], "bcd_psgroup": ["nationalDirectoryFields"], "bcd_doccgroup": ["documentsPage"], "bcd_docgroup": ["documentsPage"], "bcd_pigsgroup": ["institutionsPage"], "bcd_piggroup": ["institutionsPage"], "bcd_pigpgroup": ["institutionsPage"], "bcd_pspgroup": ["programSummary"], "bcd_psigroup": ["programSummary"], "bcd_psfgroup": ["programSummary"], "bcd_pgroup": ["programFields"], "bcd_ppgroup": ["programOfferingFields"], // Program Offereings "bcd_pagroup": ["additionalFeaturesFields"], // Program Offereings "bcd_pdocgroup": ["documentFields"], // Program Documents "bcd_pdtypegroup": ["degreeTypeFields"], // Program Documents "bcd_pigroup": ["institutionFields"], // Program Institituions "bcd_pcgroup": ["certificateFields"], // Program Certificates "bcd_pcfgroup": ["facilitiesServedFields"], "bcd_pdgroup": ["degreeFields"], // Program degreess "bcd_pdfgroup": ["facilitiesServedFields"], "bcd_pfgroup": ["facilityFields"], // Program facilities "bcd_pfcgroup": ["facilityCertificates"], "bcd_pfd2group": ["facilityDegrees"], // "bcd_pfdgroup": ["facilityFields"], }, ////////////////////////////// // Group field data // // field: name of db field to make html field to // emptyField: the text used to fill the field if there's no data in the db for it // // special and callFunction: option fields that work together to handle special case scenarios // "programSummary": { "Program Name": {field: "name", callFunction: makeSmartProgramName }, "Program URL": {field: "name", callFunction: setProgramSearchHREF}, "Institution Name": {field: "name", callFunction: storeProgramIDs }, "Institution URL": {field: "name", callFunction: setProgramSearchHREF}, "Institution Marker": {field: "name", callFunction: attachToMap}, "Institution State": {field: "state", callFunction: showIfOutOfState}, "Facility Name": {field: "name", callFunction: storeProgramIDs }, "Facility URL": {field: "name", callFunction: setProgramSearchHREF}, "Facility Marker": {field: "name", callFunction: attachToMap}, }, "documentsPage": { "Document Type": {field: "type", emptyField: "xxx" }, "Document URL": {field: "url", callFunction: setHREFNewTab}, "Program Name": {field: "programName", emptyField: "xxx" }, "Program URL": {field: "programName", callFunction: setProgramSearchHREF}, "Program State": {field: "programState", emptyField: "xxx" }, "Program Region": {field: "programRegion", emptyField: "xxx" }, "Document Icon": {field: "iconURL", callFunction: setDocumentIcon }, "Document Count": {field: "name", callFunction: setDocumentCountString }, }, "institutionsPage": { "Program Name": {field: "name", emptyField: "xxx" }, "Program URL": {field: "name", callFunction: setProgramSearchHREF}, "Institution Name": {field: "name", emptyField: "xxx" }, "College Navigator": {field: "collegeNavigator", callFunction: setHREFNewTab}, "Institution Website": {field: "website", callFunction: setHREFNewTab}, "Institution Count": {field: "name", callFunction: setInstitutionCountString }, }, "nationalDirectoryFields": { "Program Count": {field: "programCount", emptyField: "xxx" }, "Institution Count": {field: "institutionCount", emptyField: "yyy" }, "Facility Total": {field: "facilityCount", emptyField: "zzz" }, "Title String": {field: "titleString", emptyField: "zzz" }, "State": {field: "name", emptyField: "zzz" }, "State URL": {field: "name", callFunction: setStateHREF }, "Institutions URL": {field: "url", callFunction: setInstitutionsHREF}, }, // bcd_pdata - program data "programFields": { "Program Name": { field: "name", emptyField: ""}, "Mission Statement": { field: "missionStatement", emptyField: "No mission statement in the dataset"}, "State": { field: "state", emptyField: smallNoData}, "Region": { field: "region", emptyField: smallNoData}, "Map URL" : { field: "state", callFunction: setMapProgramHREF }, // more state info button "State Icon": { field: "state", callFunction: setStateIcon }, "Founded": { field: "yearFounded", emptyField: smallNoData}, "Website": { field: "website", emptyField: smallNoData}, "Website URL": { field: "website", callFunction: setHREFNewTab }, "Phone": { field: "programPhone", emptyField: ""}, "Phone URL": { field: "programPhone", callFunction: setPhoneHREF }, "Email": { field: "programEmail", emptyField: ""}, "Contact First": { field: "contactFirst", emptyField: ""}, "Contact Last": { field: "contactLast", emptyField: "- no contact info -", callFunction: sayWithSmallText}, "Contact Title": { field: "contactTitle", emptyField: ""}, "Contact Email": { field: "contactEmail", emptyField: ""}, "Contact Email URL": { field: "contactEmail", callFunction: setEmailHREF }, "Contact Phone": { field: "contactPhone", emptyField: ""}, "Contact Phone URL": { field: "contactPhone", callFunction: setPhoneHREF }, "Program Student Count": { field: "programEnrollmentCountInside", emptyField: "-"}, ///!!!! does facilityCount exist? "Facility Count": {field: "facilityCount", emptyField: "–"}, "Facility Onsite": {field: "onsiteFacilityCountTotal", emptyField: "
No data collected for
"}, "Facility Onsite Plural": { field: "onsiteFacilityCountTotal", emptyField: "–", singular: "Facility", plural: "Facilities", pluralWord: 1}, "Facility Remote": {field: "remoteFacilityCount", emptyField: "", wordNumber: 1, singular: "Facility", plural: "Facilities", pluralWord: 2}, "Certificate Count": {field: "certificateCount", emptyField: "
No data collected for
"}, "Certificate Count Plural": { field: "certificateCount", singular: "Certificate", plural: "Certificates", pluralWord: 1}, "Yes Count": {field: "licensureYes", emptyField: ""}, "No Count": {field: "licensureNo", emptyField: ""}, "Degree Count": {field: "degreeCount", emptyField: "
No data collected for
"}, "Degree Count Plural": {field: "degreeCount", singular: "Degree", plural: "Degrees", pluralWord: 1}, "Program URL": {field: "name", callFunction: setProgramSearchHREF}, "Program Summary": {field: "summaryStatements", emptyField: ""}, "No Institutions": {field: "institutionCount", emptyField: "No institution data are currently available for this program.", noneStr: "No institution data are currently available for this program."}, "No Facilities": {field: "onsiteFacilityCount", emptyField: "No facility data are currently available for this program.", noneStr: "No facility data are currently available for this program."}, "No Certificates": {field: "certificateCount", emptyField: "No certificate data are currently available for this program.", noneStr: "No certificate data are currently available for this program."}, "No Degrees": {field: "degreeCount", emptyField: "No degree data are currently available for this program.", noneStr: "No degree data are currently available for this program."}, }, // bcd_pdtypedata - program degree types "degreeTypeFields": { "Degree Type": { field: "name", emptyField: "–"}, "Degree Count": { field: "count", emptyField: "–"}, }, // bcd_ppdata - program offerings "programOfferingFields": { "Program Offering": { field: "name", emptyField: ""}, }, // bcd_padata - program additonal features "additionalFeaturesFields": { "Additional Features": { field: "name", emptyField: ""}, }, // bcd_pdocdata - program documents "documentFields": { "Document URL": { field: "url", callFunction: setHREFNewTab}, "Document Name": { field: "generalName", emptyField: ""}, }, // bcd_pidata - program institutions "institutionFields": { "Institution Name": { field: "name", emptyField: smallNoData}, "Institution Type": { field: "type", emptyField: ""}, "Institution Navigator": { field: "collegeNavigator", callFunction: setHREFNewTab }, "Institution Website": { field: "website", callFunction: setHREFNewTab }, "No Institutions": {field: "onsiteFacilityCount", emptyField: "No institution data are currently available for this program.", noneStr: "No institution data are currently available for this program."}, }, // bcd_pcdata - program certificates "certificateFields": { "Certificate Name": { field: "name", emptyField: smallNoData}, "Certificate Degree": { field: "degreeProgram", emptyField: ""}, "Comma Certificate Degree": { field: "degreeProgram", emptyField: "", prefix: ", "}, "Certificate Licensure": { field: "licensure", emptyField: smallNoData}, "Certificate Institution": { field: "institutionName", emptyField: smallNoData}, "Transferable": { field: "transferDegree", emptyField: smallNoData}, "Delivery Method": { field: "deliveryMethod", emptyField: smallNoData}, }, // bcd_pcfdata - program certificate facilities served "facilitiesServedFields": { "Facility": { field: "name", emptyField: smallNoData}, }, // bcd_pddata - program degrees "degreeFields": { "Degree Name": { field: "name", emptyField: "Name: - no data -"}, "Degree Credential": { field: "pathway", emptyField: smallNoData}, "Degree Delivery Mode": { field: "deliveryMethod", emptyField: smallNoData}, "Degree Institution": { field: "institutionName", emptyField: smallNoData}, }, // bcd_pfdata - program facilities "facilityFields": { "Facility Name": { field: "name", emptyField: ""}, "Facility Type": { field: "type", emptyField: smallNoData}, "Website": { field: "website", emptyField: smallNoData}, "Facility Age": { field: "age", emptyField: smallNoData}, "Facility Sex": { field: "sex", emptyField: smallNoData}, "Engagement Mode": { field: "creditPostSecondaryDeliveryMethod", emptyField: smallNoData}, "Enrolement Model": { field: "enrollmentModel", emptyField: smallNoData}, "Admission Cycle": { field: "enrollmentCycle", emptyField: smallNoData}, "Enrollment Count Inside": { field: "enrollmentCountInside", emptyField: "–"}, "No Facilities": {field: "onsiteFacilityCount", emptyField: "No facility data are currently available for this program.", noneStr: "No facility data are currently available for this program."}, }, // bcd_pfcdata - program facility certificates "facilityCertificates": { "Certificate": { field: "name", emptyField: smallNoData}, }, "facilityDegrees": { "Degree": { field: "name", emptyField: smallNoData}, }, }; //////////////////////////////////////////////////////////// // Template for the individual Program page // // group: "bcd_pfgroup", // The attribute name this group applies to // dataType: "bcd_pfdata", // The attribute fields this applies to // suffix: ", ", // Used to seperate sequences of this field // data: "facilities", // The field in the db to retrieve data from // ////////////////////////////// // Program page let programPageTemplate = [ { subLists: [ // Lists of groups { // Programs group: "bcd_pgroup", dataType: "bcd_pdata", data: "", subLists: [ // Program Offerings { group: "bcd_ppgroup", dataType: "bcd_ppdata", data: "programOfferings", }, // Additonal Program Features { group: "bcd_pagroup", dataType: "bcd_padata", data: "additionalProgramFeatures", }, // Program Degree Types { group: "bcd_pdtypegroup", dataType: "bcd_pdtypedata", data: "degreeCounts", }, // Program Documents { group: "bcd_pdocgroup", dataType: "bcd_pdocdata", data: "documents", }, // Program Institutions { group: "bcd_pigroup", dataType: "bcd_pidata", data: "institutions", noneGroup: "bcd_pgroup", }, // Program Facilities { group: "bcd_pfgroup", dataType: "bcd_pfdata", data: "facilities", noneGroup: "bcd_pgroup", subLists: [ // Program facility Certificates { group: "bcd_pfcgroup", dataType: "bcd_pfcdata", suffix: ",", // Used to seperate sequences data: "certificatesOffered", }, { // Program faciity degrees group: "bcd_pfd2group", // For some reason, this has to be pfd2; probably because there is a dataType: "bcd_pfd2data", // residual pfdgroup on the page? suffix: ",", // Used to seperate sequences data: "degreesOffered", }, ], }, // Program Certificates { group: "bcd_pcgroup", dataType: "bcd_pcdata", data: "certificates", noneGroup: "bcd_pgroup", subLists: [ // Program facilities offering certificate { group: "bcd_pcfgroup", dataType: "bcd_pcfdata", suffix: ",", // Used to seperate sequences data: "facilitiesServed", }, ] }, // Program Degrees { group: "bcd_pdgroup", dataType: "bcd_pddata", data: "degrees", noneGroup: "bcd_pgroup", subLists: [ // Program Facilities offering degree { group: "bcd_pdfgroup", dataType: "bcd_pdfdata", suffix: ",", // Used to seperate sequences data: "facilitiesServed", }, ] }, ], // ... Program subGroup }, // ... Programs ], } // ... Top Level ]; ////////////////////////////// // List View page let listViewTemplate = [ { subLists: [ // Lists of groups { // Programs group: "bcd_pgroup", dataType: "bcd_pdata", data: "", subLists: [ // Institutions { group: "bcd_pigroup", dataType: "bcd_pidata", noneGroup: "bcd_pigroup", suffix: ",", // Used to seperate sequences data: "institutions", }, // Facilities { group: "bcd_pfgroup", // The attribute name this group applies to dataType: "bcd_pfdata", // The attribute fields this applies to noneGroup: "bcd_pfgroup", suffix: ",", // Used to seperate sequences data: "facilities", }, // ... Facilities ], // ... Program subGroup }, // ... Programs ], } // ... Top Level ]; ////////////////////////////// // Stats View page let statsViewTemplate = [ { subLists: [ { // Groups programs by state makeGroups: true, groupBy: "state", preProcessFunction: createMultiStateGroupBy, subLists: [ // Lists of groups { group: "bcd_ngroup", dataType: "bcd_ndata", data: "summaryInfo", }, { // States group: "bcd_psgroup", dataType: "bcd_psdata", data: "stateInfo", }, { // Programs group: "bcd_pspgroup", dataType: "bcd_pspdata", data: "allProgramNames", } ], }, // ], } // ... Top Level ]; ////////////////////////////// // Map View page let mapViewTemplate = [ { subLists: [ { // Groups programs by state makeGroups: true, groupBy: "state", preProcessFunction: createMultiStateGroupBy, subLists: [ // Lists of groups { // Programs group: "bcd_pspgroup", dataType: "bcd_pspdata", data: "allProgramNames", }, { // Institutions group: "bcd_psigroup", dataType: "bcd_psidata", data: "allInstitutions", }, { // Facilities group: "bcd_psfgroup", dataType: "bcd_psfdata", data: "allFacilities", } ], }, // ], } // ... Top Level ]; ////////////////////////////// // Institutions page let institutionsTemplate = [ { subLists: [ { // Groups programs by state makeGroups: true, groupBy: "state", preProcessFunction: createMultiStateGroupBy, subLists: [ // Lists of groups { // Summary group: "bcd_pigsgroup", dataType: "bcd_pigsdata", data: "summaryInfo", }, { // Institutions group: "bcd_piggroup", dataType: "bcd_pigdata", data: "allInstitutions", subLists: [ // Lists of groups { // Programs group: "bcd_pigpgroup", dataType: "bcd_pigpdata", data: "programNames2", }, ], }, ], }, // ], } // ... Top Level ]; ////////////////////////////// // Documents page let documentsTemplate = [ { subLists: [ { // Groups programs by state makeGroups: true, groupBy: "state", preProcessFunction: createDocumentsPageInfo, subLists: [ // Lists of groups { // Summary group: "bcd_doccgroup", dataType: "bcd_doccdata", data: "summaryInfo", }, { // Summary group: "bcd_docgroup", dataType: "bcd_docdata", data: "allDocuments", }, ], }, // ], } // ... Top Level ];