1/******************************************************************************* 2 * Copyright 2020 Adobe 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 ******************************************************************************/ 16(function() { 17 "use strict"; 18 19 window.CQ = window.CQ || {}; 20 window.CQ.CoreComponents = window.CQ.CoreComponents || {}; 21 window.CQ.CoreComponents.container = window.CQ.CoreComponents.container || {}; 22 window.CQ.CoreComponents.container.utils = {}; 23 24 /** 25 * Utilities for Container Components (accordion, tabs) 26 * 27 * @namespace 28 * @alias CQ.CoreComponents.container.utils 29 * @type {{}} 30 */ 31 window.CQ.CoreComponents.container.utils = { 32 33 /** 34 * Removes the hash from the URL. 35 */ 36 removeUrlHash: function() { 37 history.replaceState(undefined, undefined, " "); 38 }, 39 40 /** 41 * Updates the URL hash with the panel ID without scrolling to it. 42 * 43 * @param {Object} component The container component (e.g. Accordion, Carousel, Tabs). 44 * @param {String} itemType The type of the item as defined in the component. 45 * @param {Number} index The index of the container item 46 */ 47 updateUrlHash: function(component, itemType, index) { 48 if (component && component._elements && component._elements[itemType] && 49 component._elements[itemType][index] && component._elements[itemType][index].id) { 50 var ID = component._elements[itemType][index].id; 51 history.replaceState(undefined, undefined, "#" + ID); 52 } 53 }, 54 55 /** 56 * Returns the index of the component item (accordion, carousel, tabs) that: 57 * - either corresponds to the deep link in the URL fragment 58 * - or contains the element that corresponds to the deep link in the URL fragment 59 * 60 * @param {Object} component The container component (Accordion, Carousel or Tabs). 61 * @param {String} itemType The type of the item as defined in the component. 62 * @param {String} itemContentType The type of the item content as defined in the component. 63 * @returns {Number} the index within the items array if the item exists, -1 otherwise. 64 */ 65 getDeepLinkItemIdx: function(component, itemType, itemContentType) { 66 if (window.location.hash) { 67 var deepLinkId = window.location.hash.substring(1); 68 if (deepLinkId && document.getElementById(deepLinkId) && 69 component && component._config && component._config.element && 70 component._elements[itemType] && 71 component._config.element.querySelector("[id='" + deepLinkId + "']")) { 72 for (var i = 0; i < component._elements[itemType].length; i++) { 73 var item = component._elements[itemType][i]; 74 var itemContentContainsId = false; 75 if (component._elements[itemContentType]) { 76 var itemContent = component._elements[itemContentType][i]; 77 itemContentContainsId = itemContent && itemContent.querySelector("[id='" + deepLinkId + "']"); 78 } 79 if (item.id === deepLinkId || itemContentContainsId) { 80 return i; 81 } 82 } 83 } 84 return -1; 85 } 86 return -1; 87 }, 88 89 /** 90 * Returns the item of the container component (accordion, carousel, tabs) that: 91 * - either corresponds to the deep link in the URL fragment 92 * - or contains the element that corresponds to the deep link in the URL fragment 93 * 94 * @param {Object} component The Accordion or Tabs component. 95 * @param {String} itemType The type of the item as defined in the component. 96 * @param {String} itemContentType The type of the item content as defined in the component. 97 * @returns {Object} the item if it exists, undefined otherwise. 98 */ 99 getDeepLinkItem: function(component, itemType, itemContentType) {
100 var idx = window.CQ.CoreComponents.container.utils.getDeepLinkItemIdx(component, itemType, itemContentType); 101 if (component && component._elements && component._elements[itemType]) { 102 return component._elements[itemType][idx]; 103 } 104 }, 105 106 /** 107 * Scrolls the browser on page reload (if URI contains URI fragment) to the item of the container component (accordion, tabs) 108 that corresponds to the deep link in the URI fragment. 109 * This method fixes the issue existent with Chrome and related browsers, which are not scrolling on page reload (if URI contains URI fragment) 110 to the element that corresponds to the deep link in the URI fragment. 111 * Small setTimeout is needed, otherwise the scrolling will not work on Chrome. 112 */ 113 scrollToAnchor: function() { 114 setTimeout(function() { 115 if (window.location.hash) { 116 var id = decodeURIComponent(window.location.hash.substring(1)); 117 var anchorElement = document.getElementById(id); 118 if (anchorElement && anchorElement.offsetTop) { 119 anchorElement.scrollIntoView(); 120 } 121 } 122 }, 100); 123 } 124 }; 125}()); 126 127var $ = jQuery.noConflict(); 128 129$(document).ready(function(){ 130 var $searchBox = $("#searchInput"); 131 Shadowbox.init({ 132 handleOversize: "drag", 133 modal: true 134 }); 135 // set StateGeo 136 if(google.loader.ClientLocation){ 137 var stateMap = {'ak':'alaska','al':'alabama','ar':'arkansas','az':'arizona','ca':'california','co':'colorado','ct':'connecticut','de':'delaware','fl':'florida','ga':'georgia','hi':'hawaii','ia':'iowa','id':'idaho','il':'illinois','in':'indiana','ks':'kansas','ky':'kentucky','la':'louisiana','ma':'massachusetts','md':'maryland','me':'maine','mi':'michigan','mn':'minnesota','mo':'missouri','ms':'mississippi','mt':'montana','nc':'north_carolina','nd':'north_dakota','ne':'nebraska','nh':'new_hampshire','nj':'new_jersey','nm':'new_mexico','nv':'nevada','ny':'new_york','oh':'ohio','ok':'oklahoma','or':'oregon','pa':'pennsylvania','ri':'rhode_island','sc':'south_carolina','sd':'south_dakota','tn':'tennessee','tx':'texas','ut':'utah','va':'virginia','vt':'vermont','wa':'washington','wi':'wisconsin','wv':'west_virginia','wy':'wyoming','dc':'district of columbia'} 138 $('#stateGeo').val(stateMap[google.loader.ClientLocation.address.region]); 139 }else{ 140 $('#stateGeo').val("none"); 141 } 142 143 // Focus the cursor on the text box 144 $searchBox.focus(); 145 146 // bring cursor to the end of the text 147 cursorToEnd(); 148 149 $searchBox.blur(); 150 151 // Submit the search form when the search button is clicked 152 $("#searchButton").click(function() { $("#searchForm").submit(); }) 153 154 // Disambiguation Dialog 155 $('#disambigSelect').click(function() { 156 $("#disambigDiv").toggle(); 157 $('#disambigSelect > div').toggleClass("disambigArrowRight disambigArrowDown"); 158 159 return false; 160 }); 161 162 $("#searchTooltip").dialog({ autoOpen:false, width: "auto", minHeight: "170px", resizable: false, 163 dialogClass: "searchTooltipWidget", 164 position: { my: "left top", at: "right-365 top", of: "#searchFormContainer" } }); 165 166 167 $("#qaAnnotationTooltip").dialog({ autoOpen:false, width: "auto", minHeight: "170px", resizable: false, 168 dialogClass: "qaAnnotationTooltipWidget", 169 position: { my: "left top", at: "right-365 top", of: "#searchFormContainer" } }); 170 171 $("#infoPanelAnnotationTooltip").dialog({ autoOpen:false, width: "auto", minHeight: "170px", resizable: false, 172 dialogClass: "infoPanelAnnotationTooltipWidget", 173 position: { my: "left top", at: "right-365 top", of: "#searchFormContainer" } }); 174 175 176}); 177 178//bring cursor to the end of the text 179function cursorToEnd(){ 180 var $searchBox = $("#searchInput"); 181 var val = $searchBox.val(); 182 $searchBox.val(''); 183 $searchBox.val(val); 184} 185 186// Note: Plan b for video search. Pull image via ajax 187// http://jquery-howto.blogspot.com/2009/02/how-to-get-youtube-video-screenshot.html 188var SearchDataService = (function ($) { 189 var DEFAULT_QUERY = 'united states'; 190 var DEFAULT_GEO_CODE = '010XX00US'; 191 192 // Root URL of API 193 var dataRoot; 194 var streams; 195 196 /** Maps out the structure of CEDSCI's API. Each object maps to a request for a piece of data, some of which come from the same endpoint. 197 Parameters needed per endpoint are also described. 198 var streams = { 199 entities: { 200 endpoint: 'search', 201 parameters: { 202 q: undefined, 203 services: 'search:featuredresults' 204 } 205 }, 206 search: { 207 endpoint: 'search', 208 parameters: { 209 q: undefined, 210 services: 'search:featuredresults' 211 } 212 }, 213 featuredresults: { 214 endpoint: 'search', 215 parameters: { 216 q: undefined, 217 services: 'search:featuredresults' 218 } 219 }, 220 highlights: { 221 endpoint: 'profile/content/highlights', 222 parameters: { 223 g: undefined 224 } 225 }, 226 // Subset of highlights data specifying the related geo 227 selectedProfile: { 228 endpoint: 'profile/content/highlights', 229 parameters: { 230 g: undefined 231 } 232 }, 233 profiles: { 234 endpoint: 'search', 235 parameters: { 236 services: 'search:featuredresults' 237 } 238 }, 239 typeahead: { 240 endpoint: 'typeahead' 241 } 242 }; 243 **/ 244 245 var initialized = false; 246 var promises = {}; 247 var typeaheadSupportedGeosLevels = {}; 248 249 // Intake root URL 250 function init(root, endpoints, rawTypeaheadSupportedGeos) { 251 typeaheadSupportedGeosLevels = convertGeoLevels(rawTypeaheadSupportedGeos);
252 if (!dataRoot) { 253 dataRoot = root; 254 } 255 if (!streams) { 256 streams = translateRawEndpoints(endpoints); 257 } 258 259 initQueries(); 260 } 261 262 function convertGeoLevels(raw) { 263 let converted = {}; 264 if (raw && raw !== 'null' && raw !== '[]') { 265 let parsed = JSON.parse(raw); 266 267 parsed.forEach(function (geo) { 268 converted[geo.code] = geo.level; 269 }); 270 } 271 272 return converted; 273 } 274 275 function findLocation(entities) { 276 const supportedGeoLevels = { 277 '010': 'nation', 278 '040': 'state', 279 '050': 'county', 280 '160': 'city' 281 }; 282 283 for (let i = 0; i < entities.length; i++) { 284 if (!entities[i].options) { 285 continue; 286 } 287 288 for (let j = 0; j < entities[i].options.length; j++) { 289 const entry = entities[i].options[j]; 290 const locationType = entry?.type; 291 const locationCode = entry?.facets.geography && entry?.facets.geography[0].slice(0, 3); 292 const isSelectedExist = "selected" in entry ? (entry.selected) : null; 293 if (locationType === "geography" && supportedGeoLevels[locationCode] && (isSelectedExist === null || isSelectedExist)) { 294 return entry; 295 } 296 } 297 } 298 } 299 300 // Endpoints comes in as stringified JSON in a format stored by the dialog. 301 // They need to be translated a bit into an object the brain can understand. 302 function translateRawEndpoints(rawEndpoints) { 303 if (rawEndpoints) { 304 var parsedEndpoints = JSON.parse(rawEndpoints); 305 var endpoints = {}; 306 307 parsedEndpoints.forEach(function (parsedEndpoint) { 308 var dataObject = parsedEndpoint.dataObject; 309 delete parsedEndpoint.dataObject; 310 parsedEndpoint.parameters = convertParameters(parsedEndpoint.parameters); 311 312 endpoints[dataObject] = parsedEndpoint; 313 }); 314 315 return endpoints; 316 } 317 } 318 319 function convertParameters(parameters) { 320 var formattedEndpoints = {}; 321 parameters.forEach(function (parameter) { 322 formattedEndpoints[parameter.name] = parameter.value; 323 }); 324 325 return formattedEndpoints; 326 } 327 328 // Initialize query objects 329 function initQueries() { 330 // Only initialize once. This function may be called several times 331 // due to the various components that can initialize it independently. 332 if (!initialized && streams) { 333 // Create list of unique endpoints to serve as structure of promises 334 var endpoints = []; 335 Object.keys(streams).forEach(function (stream) { 336 if (endpoints.indexOf(streams[stream].endpoint) === -1) { 337 endpoints.push(streams[stream].endpoint); 338 } 339 }); 340 341 // Create an object with a promise and a progress flag to each endpoint 342 endpoints.forEach(function (endpointKey) { 343 promises[endpointKey] = { 344 callStarted: undefined, 345 promise: undefined 346 }; 347 }); 348 349 // Remove typeahead init since it will use a different system 350 delete promises.typeahead; 351 352 initialized = true; 353 } 354 } 355 356 // Entry point for components. Returns a promise of the requested data. 357 function subscribe(stream, query) { 358 // Use the override if it exists, otherwise pull the query from the q parameter 359 var q = query ? query : isolateQueryParam(); 360 361 if (streams && streams[stream]) { 362 // Check to see if the requesting data has already been called 363 // If it has, return that as a resolved promise. If not, make the call. 364 var endpointKey = streams[stream].endpoint; 365 if (!promises[endpointKey][q] || 366 !promises[endpointKey][q].callStarted) { 367 368 if (endpointKey === 'search') { 369 promises[endpointKey][q] = { 370 callStarted: true, 371 promise: queryService(stream, { q: q }) 372 }; 373 374 return filterPromise(endpointKey, q, stream); 375 } else { 376 // If the requested data is not from the main search point, further information is needed, usually a recognized geocode 377 // If the search query has not been kicked off already, do it before attaching the subsequent call 378 if (!promises.search[q]) { 379 promises.search[q] = { 380 callStarted: true, 381 promise: queryService('search', { q: q }) 382 }; 383 } 384 385 promises[endpointKey][q] = { 386 callStarted: true, 387 promise: filterPromise('search', q, 'entities').then(function (entities) { 388 // Determine which geo parameter to use based on endpoint 389 var parameters = {}; 390
391 // Only locations support profiles 392 var location = findLocation(entities); 393 parameters.g = location ? location.facets.geography[0] : DEFAULT_GEO_CODE; 394 395 return queryService(stream, parameters); 396 }) 397 }; 398 399 return filterPromise(endpointKey, q, stream); 400 } 401 } else { 402 return filterPromise(endpointKey, q, stream); 403 } 404 } else { 405 // Return rejected promise to activate chained error handling callbacks 406 return Promise.reject(); 407 } 408 } 409 410 // Filter raw response data to only return requested data 411 // Has to account for inconsistent response structures 412 function filterPromise(endpointKey, q, stream) { 413 return promises[endpointKey][q].promise.then(function (response) { 414 if (response && response.response) { 415 if (stream && response.response[stream]) { 416 return response.response[stream]; 417 } else { 418 return response.response; 419 } 420 } else if (response) { 421 // Should only match highlights response structure 422 if (stream && response[stream]) { 423 return response[stream]; 424 } 425 } 426 }); 427 } 428 429 // Parse the query string and return the q value 430 // Doing it manually because IE doesn't support some of the one-line functions things like jQuery and higher forms of JS provide 431 function isolateQueryParam() { 432 var qS = window.location.search; 433 var query = {}; 434 if (qS && qS.length > 0) { 435 // Remove leading ? 436 qS = qS.substring(1); 437 438 // Iterate over query string and build into object 439 var splitQS = qS.split('&'); 440 splitQS.forEach(function (qSFragment) { 441 var splitQSF = qSFragment.split('='); 442 query[splitQSF[0]] = decodeURIComponent(splitQSF[1]); 443 }); 444 } 445 446 // Return search parameter only 447 return query.q ? query.q : DEFAULT_QUERY; 448 } 449 450 // Get data from CEDSCI API 451 function queryService(stream, parameters) { 452 if (streams[stream]) { 453 // Clone parameter template and replace with incoming values 454 var clonedparameters = JSON.parse(JSON.stringify(streams[stream].parameters)); 455 Object.keys(parameters).forEach(function (key) { 456 clonedparameters[key] = parameters[key]; 457 }); 458 459 var url = dataRoot + streams[stream].endpoint; 460 return $.get(url, clonedparameters).then(function (response) { 461 // Filter out various forms of pseudogeographies 462 // This function is probably better suited to be just before final return, but the logic gets more complicated there 463 // and this works for now. 464 if (response && response.response) { 465 // Delete all keys that contain either pseudogeo delimiter 466 // Inelegant, may cause issues in the future. 467 Object.keys(response.response).filter(checkPseudoGeos).forEac
467h(function (pseudoKey) { 468 delete response.response[pseudoKey]; 469 }); 470 471 // Delete all returned pseudogeo location entities 472 if (response.response.entities && response.response.entities[0].options) { 473 var locations = response.response.entities.find(entry => entry.options[0].type === "geography")?.options || []; 474 locations.filter(function (location) { 475 const geoValue = location.facets.geography && location.facets.geography[0]; 476 return geoValue && checkPseudoGeos(location.facets.geography[0]); 477 }).forEach(function (location) { 478 locations.splice(locations.indexOf(location), 1); 479 }); 480 481 // Clean up 482 if (locations.length === 0) { 483 delete response.response.entities[0].options; 484 } 485 } 486 } 487 488 return response; 489 }); 490 } else { 491 // Return rejected promise to activate chained error handling callbacks 492 return Promise.reject(); 493 } 494 } 495 496 // Check to see if the incoming geocode is a pseudogeo, detectable by . (old syntax) or $ (new syntax) delimiters 497 function checkPseudoGeos(code) { 498 return code.indexOf('.') !== -1 || code.indexOf('$') !== -1; 499 } 500 501 // Handle typeahead separately since its structure is so different 502 function typeahead(query) { 503 // var unsupportedEntities = [ 'tables' ]; 504 var unsupportedEntities = []; 505 506 var url = dataRoot + streams.typeahead.endpoint; 507 508 // Cancel existing call if it exists 509 if (promises.typeahead) { 510 promises.typeahead.abort(); 511 } 512 513 // Make new call 514 var payload = { 515 request: { 516 text: query 517 } 518 }; 519 promises.typeahead = $.ajax({ 520 type: 'POST', 521 url: url, 522 data: JSON.stringify(payload), 523 contentType: 'application/json;charset=UTF-8' 524 }); 525 526 // Filter tables results by allowed entity types 527 // Also check for pseudogeography markers 528 return promises.typeahead.then(function (response) { 529 if (response && response.response && response.response.options) { 530 return response.response.options.filter(function (suggestion) { 531 // Remove unsupported level 532 const geoValue = suggestion.facets.geography && suggestion.facets.geography[0]; 533 if (geoValue && !typeaheadSupportedGeosLevels[(suggestion.facets.geography[0].substring(0, 3))]) { 534 return false; 535 } 536 // Remove unsupported entity types 537 if (unsupportedEntities.indexOf(suggestion.type) === -1) { 538 if (suggestion.type === 'geographies') { 539 // DO NOT return if pseudo markers are found. Inverse of normal logic of this function's usage. 540 return !checkPseudoGeos(suggestion.codes[0]); 541 } else { 542 return true; 543 } 544 } 545 }); 546 } 547 }); 548 } 549 550 return { 551 init: init, 552 subscribe: subscribe, 553 subscriptionOptions: function () { return Object.keys(streams); }, 554 typeahead: typeahead, 555 getQuery: isolateQueryParam 556 }; 557 558})($); 559var CensusDataTools = (function() { 560 var supportedGeoLevels = { 561 '040': 'state', 562 '050': 'county', 563 '160': 'city' 564 }; 565 566 var dataTools = { 567 popClock: { 568 title: 'U.S. and World Population Clock', 569 description: 'See U.S. population by date, region, age and sex, and the top 10 areas by people and density. The world view has basic facts, trade, and projections by country.', 570 image: '/content/census/en/data/data-tools/population-clock.listitemgallery.png/1550273046380/popclock_bkg_260.png', 571 link: '/data/data-tools/population-clock.html', 572 ctaText: 'View POPCLOCK' 573 }, 574 quickfacts: { 575 title: 'QuickFacts', 576 description: 'QuickFacts provides frequently requested Census Bureau information at the national, state, county, and city level.', 577 image: '/content/census/en/data/data-tools/quickfacts.listitemgallery.png/1550273049627/quickfacts_160x100.png', 578 link: '//www.census.gov/quickfacts/table/PST045215/', 579 ctaText: 'View QUICKFACTS' 580 } 581 }; 582 583 function findLocation(entities) { 584 for (let i = 0; i < entities.length; i++) { 585 if ( entities[i].options ) { 586 for (let j = 0; j < entities[i].options.length; j++) { 587 const entry = entities[i].options[j]; 588 const locationType = entry?.type; 589 const locationCode = entry?.facets.geography && entry?.facets.geography[0].slice( 0, 3 ); 590 const isSelectedExist = "selected" in entry ? (entry.selected) : null; 591 if (locationType === "geography" && supportedGeoLevels[locationCode] && (isSelectedExist === null || isSelectedExist)) { 592 return entry; 593 } 594 } 595 } 596 } 597 } 598
599 function init() { 600 SearchDataService.subscribe( 'entities' ).then( function( entities ) { 601 const location = findLocation(entities); 602 if (location) { 603 var splitGeoCode = location.facets.geography[0].split( 'US' ); 604 var FIPSCode = splitGeoCode.length > 1 ? splitGeoCode[1] : ''; 605 buildQuickfacts( FIPSCode ); 606 } else { 607 // Pop Clock is mutually exclusive to Quickstats 608 var query = SearchDataService.getQuery(); 609 if ( query.toLowerCase().indexOf( 'population' ) !== -1 ) { 610 buildDataTool( dataTools.popClock ); 611 } 612 } 613 }); 614 } 615 616 function buildQuickfacts( geoLevel ) { 617 // Clone quickfacts component and fully build link URL 618 var quickfactsConfig = JSON.parse( JSON.stringify( dataTools.quickfacts ) ); 619 quickfactsConfig.link += geoLevel; 620 621 buildDataTool( quickfactsConfig ); 622 } 623 624 function buildDataTool( config ) { 625 var $dataTool = $( '<div class="uscb-data-tool-box"></div>' ); 626 $dataTool.html( 627 '<div class="uscb-data-tool-box-top">' + 628 ' <h3 class="uscb-data-tool-box-title">' + config.title + '</h3>' + 629 ' <p class="uscb-data-tool-box-teaser">' + config.description + '</p>' + 630 ' <div class="uscb-data-tool-box-action-button">' + 631 ' <a class="btn btn-primary btn-md" href="' + config.link +'" onclick="linkClick(this, \'Data Tool Container Component\');">' + config.ctaText + '</a>' + 632 ' </div>' + 633 '</div>' + 634 '<div class="inner-box"></div>' ); 635 636 $( '.datatoolcontainer' ).append( $dataTool ); 637 } 638 639 return { 640 init: init 641 }; 642})(); 643var CensusSearchFeaturedResults = (function () { 644 'use strict'; 645 646 function init(overrideQuery, uniqueID, dataTableLink, isEditMode) { 647 const ele = document.getElementById(uniqueID); 648 const loadingIndicator = getNextSibling(ele, '.uscb-loading-circular-indeterminate'); 649
650 // jQuery dependency 651 SearchDataService.subscribe('featuredresults', overrideQuery) 652 .done((result) => { 653 loadingIndicator.classList.add('uscb-hide'); 654 populateFeaturedResult(ele, result, dataTableLink, isEditMode); 655 }).fail((error) => { 656 console.error('[CensusSearchFeaturedResults] error', error); 657 }).always(() => { 658 loadingIndicator.classList.add('uscb-hide'); 659 }); 660 } 661 662 function populateFeaturedResult(ele, featuredResults, dataTableLink, isEditMode) { 663 const enhancedInsightCard = "enhanced-insight-card"; 664 const classicInfoBlock = "classic-info-block"; 665 const selectedContainerName = ele.parentElement.classList.contains('enhanced-insight-card') ? enhancedInsightCard : classicInfoBlock; 666 const containerEle = ele.querySelector(`.uscb-quick-answer-container__${selectedContainerName}`); 667 668 if (isJSONEmpty(featuredResults)) { 669 hideComponent(containerEle, true, isEditMode, 'No data returned from API'); 670 return; 671 } 672 673 let sourceText = ""; 674 if (featuredResults.hasOwnProperty('geoName')) { 675 if (selectedContainerName === enhancedInsightCard) { 676 sourceText = featuredResults.tableName?.includes('.') ? featuredResults.tableName.split('.')[1] : '' 677 } 678 } else { 679 hideComponent(containerEle, true, isEditMode, 'No featuredResults.geoName returned from API'); 680 return; 681 } 682 683 if (featuredResults.hasOwnProperty('label')) { 684 if (selectedContainerName === enhancedInsightCard) { 685 containerEle.querySelector('.uscb-featured-results-quick-header').innerText = getShortTitle(featuredResults.label); 686 containerEle.querySelector('.uscb-data-quick-answer-label').innerText = featuredResults.label; 687 containerEle.querySelector('.uscb-featured-results-quick-col-1').innerHTML = getImageElement(featuredResults.label); 688 } else { 689 containerEle.querySelector('.uscb-data-quick-answer-label').innerHTML = `${featuredResults.label}<span class="uscb-featured-result-geography"> in ${featuredResults.geoName}</span>`; 690 } 691 } else { 692 hideComponent(containerEle, true, isEditMode, 'No featuredResults.label returned from API'); 693 return; 694 } 695 696 if (featuredResults.hasOwnProperty('estimate') && featuredResults.hasOwnProperty('format')) { 697 let marginOfError = ''; 698 if (featuredResults.hasOwnProperty('marginOfError')) { 699 marginOfError = featuredResults.marginOfError; 700 } 701 702 let formattedEstimate = featuredResults.estimate; 703 let formattedMoE = featuredResults.marginOfError; 704 const moeIsNumber = !isNaN(parseFloat(formattedMoE)); 705 706 // If the last character of the value is not a number, treat is as a string. 707 // Otherwise, format it. 708 if (!isNaN(formattedEstimate[formattedEstimate.length - 1])) { 709 const estimateDecimals = determineDecimals(featuredResults.estimate); 710 711 switch (featuredResults.format) { 712 case 'commas,dollar': 713 case 'dollar': 714 formattedEstimate = formatCurrency(featuredResults.estimate, estimateDecimals); 715 if (moeIsNumber) { 716 formattedMoE = formatCurrency(featuredResults.marginOfError, estimateDecimals); 717 } 718 break; 719 case 'percent': 720 formattedEstimate = formatPercent(featuredResults.estimate, estimateDecimals); 721 if (moeIsNumber) { 722 formattedMoE = formatPercent(featuredResults.marginOfError, estimateDecimals); 723 } 724 break; 725 case 'commas': 726 default: 727 formattedEstimate = formatNumber(featuredResults.estimate, estimateDecimals); 728 if (moeIsNumber) { 729 formattedMoE = formatNumber(featuredResults.marginOfError, estimateDecimals); 730 } 731 } 732 } 733 734 if (formattedMoE && formattedMoE.length && (selectedContainerName !== enhancedInsightCard)) { 735 formattedMoE = '+/- ' + formattedMoE; 736 containerEle.querySelector('.uscb-margin-of-error').innerText = formattedMoE; 737 } 738 739 containerEle.querySelector('.uscb-data-quick-answer-value').innerText = formattedEstimate; 740 } else { 741 hideComponent(containerEle, true, isEditMode, 'No featuredResults.estimate/format returned from API'); 742 return; 743 } 744 745 if (featuredResults.hasOwnProperty('sourceLink') && featuredResults.hasOwnProperty('dataSource')) { 746 if (dataTableLink === '') { dataTableLink = 'https://data.census.gov/all?q=population'; } // default to "Population" 747 748 const sourceLinkContainer = containerEle.querySelector('.uscb-data-quick-answer-source'); 749 750 const sourceLink = document.createElement('a'); 751 sourceLink.setAttribute('href', dataTableLink); 752 753 if (sourceLinkContainer) { 754 const sourceDescription = document.createElement('a'); 755 sourceDescription.setAttribute('href', featuredResults.sourceLink); 756 757 if (selectedContainerName === enhancedInsightCard) { 758 759 sourceLink.classList.add('uscb-data-quick-answer-source__hyperlinks');
760 sourceLink.innerText = sourceText; 761 sourceLinkContainer.appendChild(sourceLink); 762 763 const separator = document.createElement('span'); 764 separator.classList.add('uscb-data-quick-answer-source__separator'); 765 separator.innerText = ' | '; 766 sourceLinkContainer.appendChild(separator); 767 768 sourceDescription.innerText = featuredResults.dataSource; 769 770 const sourceDescriptionWrap = document.createElement('span'); 771 sourceDescriptionWrap.classList.add('uscb-data-quick-answer-source__description'); 772 sourceDescriptionWrap.appendChild(sourceDescription); 773 774 sourceLinkContainer.appendChild(sourceDescriptionWrap); 775 } else { 776 sourceDescription.innerText = 'Source: ' + featuredResults.dataSource; 777 sourceLinkContainer.appendChild(sourceDescription); 778 } 779 } 780 781 const dataTableLinkContainer = containerEle.querySelector('.uscb-data-table'); 782 if (dataTableLinkContainer) { 783 if (selectedContainerName === enhancedInsightCard) { 784 dataTableLinkContainer.remove(); // already included in sourceLinkContainer 785 } else { 786 sourceLink.innerText = 'Data Table'; 787 dataTableLinkContainer.appendChild(sourceLink); 788 } 789 } 790 } else { 791 hideComponent(containerEle, true, isEditMode, 'No featuredResults.sourceLink/dataSource returned from API'); 792 return; 793 } 794 795 containerEle.classList.remove('uscb-hide'); 796 } 797 798 function hideComponent(containerEle, forceHide, isEditMode, errorMessage) { 799 // if it's publish mode and needs to hide 800 if (isEditMode === 'false' && forceHide) { 801 containerEle.classList.add('uscb-hide'); 802 } else { 803 containerEle.classList.remove('uscb-hide'); 804 805 if (isEditMode === 'true' && forceHide) { 806 containerEle.innerText = `Error loading results${errorMessage ? ': ' + errorMessage : ''}`; 807 } 808 } 809 } 810 811 function isJSONEmpty(obj) { 812 for (var key in obj) { 813 if (obj.hasOwnProperty(key)) { 814 return false; 815 } 816 } 817 return true; 818 } 819 820 function getNextSibling(elem, selector) { 821 const sibling = elem.nextElementSibling; 822 if (!selector) return sibling; 823 824 while (sibling) { 825 if (sibling.matches(selector)) return sibling; 826 sibling = sibling.nextElementSibling; 827 } 828 829 return null; 830 } 831 832 function determineDecimals(num) { 833 if (Math.floor(num) === num) { 834 return 0; 835 } else { 836 var split = num.toString().split('.'); 837 if (split.length > 1) { 838 return split[1].length; 839 } else { 840 return 0; 841 } 842 } 843 } 844 845 //Assuming only dollars are used 846 function formatCurrency(amount, minimumDigits) { 847 if (amount) { 848 const formatter = new Intl.NumberFormat('en-US', { 849 style: 'currency', 850 currency: 'USD', 851 minimumFractionDigits: minimumDigits || 0 852 }); 853 854 return formatter.format(amount); 855 } 856 } 857 858 function formatPercent(amount, minimumDigits) { 859 if (amount) { 860 return formatNumber(amount, minimumDigits) + '%'; 861 } 862 } 863 864 function formatNumber(amount, minimumDigits) { 865 if (amount) { 866 const formatter = new Intl.NumberFormat('en-US', { 867 minimumFractionDigits: minimumDigits 868 }); 869 870 return formatter.format(amount); 871 } 872 } 873 874 function getShortTitle(label) { 875 const titleMap = { 876 "total population": "Populations and People", 877 "median household income": "Income and Poverty", 878 "bachelor's degree or higher": "Education", 879 "employment rate": "Employment", 880 "total housing units": "Housing", 881 "without health care coverage": "Health", 882 "total employer establishments": "Business and Economy", 883 "total households": "Families and Living Arrangements", 884 "hispanic or latino (of any race)": "Race and Ethnicity" 885 }; 886 887 return titleMap[label.toLowerCase()] || ''; 888 } 889 890 function getImageElement(label) { 891 const iconMap = { 892 "business": [ 893 'agriculture, forestry, fishing and hunting, and mining', 894 'total employer establishments', 895 'total manufactures shipments (in thousands)', 896 ], 897 "education": [ 898 'bachelor\'s degree or higher', 899 'high school graduate or higher', 900 'school enrolled population enrolled in kindergarten to 12th grade', 901 ], 902 "employment": [ 903 'employment rate', 904 'females in computer, engineering and science occupations', 905 'local, state, and federal government workers', 906 'mean usual hours worked for workers', 907 'total employment', 908 ], 909 "family": [ 910 'average family size', 911 'never married',
912 'same sex married couple households', 913 'total households', 914 ], 915 'geography': [ 916 'average travel time to work (in minutes)', 917 ], 918 "health": [ 919 'public health insurance', 920 'without health care coverage', 921 ], 922 "housing": [ 923 "median value for mobile homes", 924 'group quarters population', 925 'homeownership rate', 926 'less than $500', 927 'median gross rent', 928 'median housing value', 929 'moved 2021 or later into occupied housing unit', 930 'moved from a different state in the last year', 931 'occupied housing units', 932 'total housing units', 933 'vacant housing units', 934 ], 935 "income": [ 936 'female median year-round, full-time earnings', 937 'median earnings for workers', 938 'median household income', 939 'per capita income', 940 'poverty, all people', 941 'poverty, under 18 years old', 942 'total annual payroll (in thousands)', 943 ], 944 "people": [ 945 '65 years and older', 946 'disabled population', 947 'median age', 948 'total population', 949 'under 18 years old', 950 'veterans', 951 'women 15 to 50 years old', 952 ], 953 "race": [ 954 'american indian and alaska native alone', 955 'asian alone', 956 'black or african american alone', 957 'foreign-born population', 958 'hispanic or latino (of any race)', 959 'language other than english spoken at home', 960 'native hawaiian and other pacific islander alone', 961 'some other race alone', 962 'spanish speakers', 963 'two or more races', 964 'white alone', 965 'white alone, not hispanic or latino', 966 'without an internet subscription', 967 ], 968 }; 969 970 let iconName; 971 for (const icon of Object.keys(iconMap)) { 972 const labels = iconMap[icon]; 973 if (labels.includes(label.toLowerCase().trim())) { 974 iconName = icon; 975 } 976 } 977 978 if (iconName) { 979 return `<img 980 src="/etc.clientlibs/census/clientlibs/cedsci-icons/resources/${iconName}.svg" 981 alt="Icon"/>`; 982 } 983 984 console.warn(`[search-feature-results] No icon for ${label}`); 985 return ''; 986 } 987 988 return { 989 init: init 990 } 991})(jQuery); 992 993var CensusSearchProfile = ( function() { 994 'use strict'; 995 996 var supportedGeoLevels = {}; 997 998 function init( queryOverride, uniqueID, rawSupportedGeoLevels ) { 999 supportedGeoLevels = convertGeoLevels( rawSupportedGeoLevels ); 1000 SearchDataService.subscribe( 'profiles', queryOverride ).then( function( response ) { 1001 // Hide the loading indicator 1002 var $parentElement = $( '#' + uniqueID ); 1003 $parentElement.find( '.uscb-loading-circular-indeterminate' ).toggleClass( 'uscb-hide' ); 1004
1005 // Only display first profile. Results are ranked by relevance, but multiple may return for searches without specific geographies. 1006 if ( response.length > 0 ) { 1007 var profile = findFirstNonTableResult( response ); 1008 if ( profile ) { 1009 buildProfile( profile, $parentElement ); 1010 } 1011 } 1012 }, function( error ) { 1013 // Until we can ignore IE, we have to do it this way and not use .finally() which would be cleanest 1014 // Hide the loading indicator 1015 var $parentElement = $( '#' + uniqueID ); 1016 $parentElement.find( '.uscb-loading-circular-indeterminate' ).toggleClass( 'uscb-hide' ); 1017 }); 1018 } 1019 1020 function convertGeoLevels( raw ) { 1021 var converted = {}; 1022 // Java returns null as a *string* if the object doesn't exist so that needs to be checked explicitly as it is technically a truthy value 1023 if ( raw && raw !== 'null' ) { 1024 var parsed = JSON.parse( raw ); 1025 parsed.forEach( function( geo ) { 1026 converted[ geo.code ] = geo.level; 1027 }); 1028 } 1029 1030 return converted; 1031 } 1032 1033 function findFirstNonTableResult( profiles ) { 1034 for ( var i = 0; i < profiles.length; i++ ) { 1035 if ( profiles[ i ].suggestionType != 'table' && profiles[ i ].suggestionType != 'tables' ) { 1036 return profiles[ i ]; 1037 } 1038 } 1039 } 1040 1041 function buildProfile( profile, $parentElement ) { 1042 const geoValue = profile.facets.geography && profile.facets.geography[0]; 1043 if ( supportedGeoLevels && geoValue && supportedGeoLevels[ ( profile.facets.geography[0].substring(0, 3) ) ] ) { 1044 var html = '' + 1045 '<div>' + 1046 '<div class="uscb-profile-result-container" tabindex="0">' + 1047 '<div class="uscb-profile-result-title">' + profile.title + ' Profile' + '</div>' + 1048 '<div class="uscb-profile-result-description-container">' + 1049 '<div class="uscb-profile-result-description-text">' + profile.teaser + '</div>' + 1050 '</div>' + 1051 '<a class="uscb-profile-result-view-profile" href="' + 'https://data.census.gov/cedsci/profile?g=' + geoValue + '">View Profile <i class="o-angle-right-circle-2"></i> </a>' + 1052 '</div>' + 1053 '</div>'; 1054 1055 $parentElement.append( html ); 1056 } 1057 } 1058 1059 return { 1060 init: init 1061 } 1062})(jQuery); 1063
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.