1(function ($, mapsUtil, ol, abbr) { 2 "use strict"; 3 4 (function main() { 5 exportConfig(); 6 })(); 7 8 /** 9 * 10 */ 11 function initialize() { 12 const placeholders = _findPlaceholders(); 13 placeholders.forEach((p) => _initTable(p)); 14 } 15 16 /** 17 * 18 * @returns 19 */ 20 function _findPlaceholders() { 21 const placeholders = [...document.querySelectorAll(".tab-widget-placeholder")]; 22 return placeholders.filter((placeholder) => { 23 return placeholder.dataset.type === "t"; 24 }); 25 } 26 27 /** 28 * 29 * @param {*} placeholder 30 */ 31 function _initTable(placeholder) { 32 const mapEl = mapsUtil.findMap(placeholder); 33 const map = $(mapEl).data("map"); 34 if (!map) { 35 return; 36 } 37 _checkSourceType(map); 38 const table = createTableReaderTable(); 39 map.on("postrender", () => { 40 const features = retrieveFeatures(map); 41 features.sourceType = map.sourceType; 42 renderVariousFeatures(table, features); 43 placeholder.replaceChildren(table); 44 }); 45 } 46 47 /** 48 * 49 * @param {*} map 50 */ 51 function _checkSourceType(map) { 52 const source = map.dataLayer.getSource(); 53 if (typeof map.dataLayer.getSource().getUrl === "function") { 54 const url = source.getUrl(); 55 if (url.indexOf("nws-dgt-pnts") !== -1) { 56 map.sourceType = "nws"; 57 } 58 if (url.indexOf("strmflow") !== -1) { 59 map.sourceType = "strmflow"; 60 } 61 } 62 } 63 64 /** 65 * Deprecated 66 * Was triggered by ol-maps.js; calls button rendering if appropriate after features are loaded. 67 * @param {object} map - The OpenLayers map object 68 * @param {HTMLDivElement} wrapper - The div where the target map is placed 69 */ 70 function addTableReader(map, wrapper) { 71 const source = map.dataLayer.getSource(); 72 if (!map.sourceType) { 73 _checkSourceType(map); 74 } 75 if (map.sourceType && document.title === "Map Feature Tests | Drought.gov") { 76 source.on("featuresloadend", () => { 77 addTableReaderButton(map, wrapper); 78 }); 79 } 80 } 81 82 // ************************* 83 // HTML Generation Functions 84 // ************************* 85 86 /** 87 * Builds an HTML element with designated attributes. 88 * @param {String} tag - The type of element to build 89 * @param {Array} classList - The optional array of classes to assign to the element 90 * @param {Object} aria - The optional object of ARIA properties and values ({LabelledBy: 'id'}) 91 * @param {Object} misc - The optional object of attributes to assign to the element ({value: 'none'}) 92 * @return {HTMLElement} The required HTML element 93 */ 94 function buildWithAria(tag, classList = null, aria = null, misc = null) { 95 const el = document.createElement(tag); 96 if (classList) { 97 el.classList.add(...classList); 98 } 99 if (aria) { 100 Object.entries(aria).forEach(([key, val]) => { 101 el.setAttribute(`aria-${key}`, val); 102 }); 103 } 104 if (misc) { 105 Object.entries(misc).forEach(([key, val]) => { 106 el.setAttribute(key, val); 107 }); 108 } 109 return el; 110 } 111 112 /** 113 * Builds an HTML button element with designated attributes. 114 * @param {Array} classList - The optional array of classes to assign to the element 115 * @param {String} alabel - The ARIA label to attach to the button 116 * @param {String} atitle - The title to attach to the button 117 * @param {String} spanText - The text to include in the child span element 118 * @return {HTMLButtonElement} The required HTML element 119 */ 120 function buildDecoButton(classList = null, alabel, atitle, spanText) { 121 const button = buildWithAria("button", classList, { label: alabel }, { title: atitle }); 122 const span = document.createElement("span"); 123 span.ariaHidden = true; 124 span.innerHTML = spanText; // allows for html entities 125 button.appendChild(span); 126 return button; 127 } 128 129 /** 130 * Builds an HTML th element with designated text and scope. 131 * @param {string} text - The text to include in the element 132 * @param {string} s - The scope ("row" or "col") 133 * @return {HTMLTableHeaderCellElement} The th element 134 */ 135 function buildTh(text, s) { 136 let ariaRole; 137 if (s === "row") { 138 ariaRole = "rowheader"; 139 } else { 140 ariaRole = "columnheader"; 141 } 142 const th = buildWithAria("th", null, null, { scope: s, role: ariaRole }); 143 if (typeof text === "string") { 144 th.textContent = text; 145 } else { 146 th.append(text); 147 } 148 th.setAttribute("data-sortable", ""); 149 return th; 150 } 151 152 /** 153 * Builds an HTML td element with designated text. 154 * @param {any} content - The text to include in the element 155 * @return {HTMLTableHeaderCellElement} The th element 156 */ 157 function buildTd(content) { 158 const td = document.createElement("td"); 159 if (typeof content === "string") { 160 td.textContent = content; 161 } else { 162 td.append(content); 163 } 164 return td; 165 } 166 167 /** 168 * Builds an HTML option element with designated text. 169 * @param {string} text - The text to include in the element 170 * @return {HTMLOptionElement} A populated option element 171 */ 172 function buildOption(text) { 173 const element = document.createElement("option"); 174 if (text === "(All)") { 175 element.value = ""; 176 element.selected = true; 177 } else { 178 element.value = text; 179 } 180 element.textContent = text; 181 return element; 182 } 183 184 /** 185 * Sees if a button is already generated on a map and apply one if not. 186 * @param {object} map - The OpenLayers map object 187 * @param {HTMLDivElement} wrapper - The div where the target map is placed 188 */ 189 function addTableReaderButton(map, wrapper) { 190 const buttonContainer = buildWithAria("div", ["tablereader-buttons", "ol-unselectable"]); 191 buttonContainer.style.pointerEvents = "auto"; 192 const button = createTableReaderButton(); 193 buttonContainer.append(button); 194 195 wrapper.querySelector(".ol-selectable").ariaLabel = "Map with keyboard accessible options."; 196 wrapper.querySelector(".ol-overlaycontainer-stopevent").firstElementChild.after(buttonContainer); 197 button.addEventListener("click", initiateTableReader.bind(map)); 198 } 199 200 /** 201 * Generates the HTML for the Table Reader button. 202 * @return {HTMLButtonElement} - A fully rendered button 203 */ 204 function createTableReaderButton() { 205 const svgNS = "http://www.w3.org/2000/svg"; 206 const button = buildWithAria( 207 "button", 208 ["tablereader-button"], 209 { label: "Keyboard accessible format." }, 210 { title: "Keyboard accessible format" } 211 ); 212 const svg = document.createElementNS(svgNS, "svg"); 213 svg.setAttribute("viewBox", "0 0 24 24"); 214 svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink"); 215 svg.setAttribute("focusable", false); 216 const path = document.createElementNS(svgNS, "path"); 217 path.setAttribute( 218 "d", 219 "M20 5H4c-1.1 0-1.99.9-1.99 2L2 17c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-9 3h2v2h-2V8zm0 3h2v2h-2v-2zM8 8h2v2H8V8zm0 3h2v2H8v-2zm-1 2H5v-2h2v2zm0-3H5V8h2v2zm9 7H8v-2h8v2zm0-4h-2v-2h2v2zm0-3h-2V8h2v2zm3 3h-2v-2h2v2zm0-3h-2V8h2v2z" 220 ); 221 222 svg.appendChild(path); 223 button.appendChild(svg); 224 return button; 225 } 226 227 /** 228 * Generates the HTML for the Table Reader container div. 229 * @param {object} map - The OpenLayers map object 230 * @return {HTMLDivElement} The container for the Table Reader div 231 */ 232 function createTableReaderDiv(map) { 233 const container = buildWithAria("div", [ 234 "tablereader-container", 235 "bg-base-lightest", 236 "height-full", 237 "width-full", 238 "padding-2", 239 ]); 240 container.collapsed = false;
241 const titleDiv = buildWithAria("div", ["tablereader-title-div"]); // add toolbar role in future 242 const mainDiv = buildWithAria("div", ["overflow-y-scroll"]); 243 const table = createTableReaderTable(); 244 const zoomDiv = buildWithAria("div", ["tablereader-button-group", "tablereader-zoom"]); 245 const scopeDiv = buildWithAria("div", ["tablereader-button-group", "tablereader-scope", "display-none"]); 246 // const miniDiv = buildWithAria("div", ["tablereader-mini-map"]); 247 const actionDiv = buildWithAria("div", ["tablereader-button-group"]); 248 const toggleButton = buildDecoButton( 249 ["tablereader-toggle-button", "bg-white"], 250 "Collapse the map.", 251 "collapse map", 252 "↑" 253 ); 254 const closeButton = buildDecoButton(["tablereader-close-button", "bg-white"], "Close the table.", "close", "X"); 255 const zoomInButton = buildDecoButton(["tablereader-zoom-button", "bg-white"], "Zoom in map.", "zoom in", "+"); 256 const zoomOutButton = buildDecoButton(["tablereader-zoom-button", "bg-white"], "Zoom out map.", "zoom out", "-"); 257 const scopeLabel = buildWithAria("label", null, null, { for: "trscope" }); 258 scopeLabel.textContent = "Data Scope: "; 259 const scopeSelect = buildWithAria("select", ["display-inline-block"]); 260 scopeSelect.id = "trscope"; 261 actionDiv.append(closeButton, toggleButton); 262 zoomDiv.append(zoomInButton, zoomOutButton); 263 scopeDiv.append(scopeLabel, scopeSelect); 264 mainDiv.append(table); 265 titleDiv.append(actionDiv, scopeDiv, zoomDiv); // miniDiv 266 container.append(titleDiv, mainDiv); 267 268 closeButton.addEventListener("click", (e) => { 269 closeTableReader(e.target, map); 270 }); 271 toggleButton.addEventListener("click", (e) => { 272 toggleTable(container, map); 273 }); 274 zoomInButton.addEventListener("click", () => { 275 zoomInMap(map); 276 }); 277 zoomOutButton.addEventListener("click", () => { 278 zoomOutMap(map); 279 }); 280 281 return container; 282 } 283 284 /** 285 * Resizes the Table Reader container to be appropriate for the window. 286 */ 287 function resizeTableReaderContainers() { 288 const windowHeight = document.documentElement.clientHeight; 289 const containers = document.querySelectorAll(".tablereader-container"); 290 containers.forEach((container) => { 291 const map = mapsUtil.findMap(container); 292 let idealHeight = windowHeight - map.clientHeight - 96; // 24 293 if (idealHeight < 220) { 294 idealHeight = 220; 295 } 296 container.setAttribute("style", `height: ${idealHeight}px; top: ${map.clientHeight}px`); 297 }); 298 } 299 300 /** 301 * Generates the HTML for the Table Reader table. 302 * @return {HTMLDivElement} A div containing a table 303 */ 304 function createTableReaderTable() { 305 const div = buildWithAria("div", [ 306 "tablereader-table", 307 "overflow-y-scroll", 308 "height-mobile", 309 "usa-table-container", 310 ]); 311 const table = buildWithAria("table", ["border-1px", "usa-table", "width-full"]); 312 div.append(table); 313 return div; 314 } 315 316 /** 317 * Generates the HTML for the Table Reader overlay. 318 * @param {object} map - The OpenLayers map object 319 * @return {HTMLDivElement} The overlay div 320 */ 321 function createTableReaderOverlay(map) { 322 const overlay = buildWithAria("div", ["tablereader-overlay", "width-full", "height-full"]); 323 const actionContainer = document.createElement("div"); 324 const focus = buildWithAria( 325 "div", 326 ["tablereader-focus", "border-2px", "border-black", "width-full"], 327 { label: "Map focus." }, 328 { tabIndex: 0 } 329 ); 330 const directions = ["right", "down", "left", "up"]; 331 const arrows = []; 332 directions.forEach((el, i) => { 333 const deg = i * 90; 334 const button = buildWithAria( 335 "button", 336 ["tablereader-arrow-button", "text-white", "display-block", "text-bold", "margin-0"], 337 { label: `Pan map ${el}.` }, 338 { title: `Pan ${el}` } 339 ); 340 const span = buildWithAria("span", null, { hidden: true }); 341 span.textContent = ">"; 342 button.style = `transform: rotate(${deg}deg)`; 343 const arrow = document.createElement("div"); 344 arrow.id = el; 345 button.append(span); 346 arrow.append(button); 347 arrows.push(arrow); 348 button.addEventListener("click", (e) => { 349 e.preventDefault(); 350 panMap(el, map); 351 }); 352 }); 353 354 overlay.append(actionContainer); 355 actionContainer.append(arrows[3], arrows[2], arrows[0], arrows[1], focus); 356 return overlay; 357 } 358 359 /** 360 * Syncs the overlay with the OL map as padding. 361 * @param {HTMLDivElement} overlay - The tablereader overlay element 362 * @param {object} map - The OpenLayers map object 363 */ 364 function bindOverlay(overlay, map) { 365 const view = map.getView(); 366 const focus = overlay.querySelector(".tablereader-focus"); 367 const container = overlay.parentElement.querySelector(".tablereader-container"); 368 const n = (overlay.offsetHeight - focus.offsetHeight) / 2;
369 const w = (overlay.offsetWidth - focus.offsetWidth) / 2; 370 const e = w + container.offsetWidth; 371 const s = n; 372 view.set("padding", [n, e, s, w]); 373 map.focusSize = [focus.offsetWidth, focus.offsetHeight]; 374 } 375 376 // Not in use - retaining code to report as reoccurance of closed OL bug (blank unless collapsed and uncollapsed); 377 // /** 378 // * Generates an overview map for the collapsed tablereader 379 // * @param {HTMLDivElement} container - The tablereader container div 380 // * @param {object} map - the OpenLayers map object 381 // */ 382 // function createMiniMap(container, map) { 383 // const miniMapDiv = container.querySelector(".tablereader-mini-map"); 384 // const miniMap = new ol.control.OverviewMap({ 385 // layers: map.getAllLayers(), 386 // collapsed: false, 387 // collapsible: false, 388 // target: miniMapDiv, 389 // }); 390 // map.addControl(miniMap); 391 // map.on("postrender", () => { 392 // miniMap.setCollapsible(true); 393 // miniMap.setCollapsed(true); 394 // miniMap.setCollapsed(false); 395 // miniMap.setCollapsible(false); 396 // }); 397 // } 398 399 // ***************************** 400 // Table Reader DOM Interactions 401 // ***************************** 402 403 /** 404 * Processes the button click action and initiate the table reader. 405 * @param {event} e - The button click event which triggers the function 406 * @this map 407 */ 408 function initiateTableReader(e) { 409 // manipulate DOM && make elements 410 const button = e.target; 411 const wrapper = mapsUtil.findMap(button); 412 const mapElem = mapsUtil.findMap(button); 413 let tableContainer = $(wrapper).find(".tablereader-container"), 414 overlay; 415 if (!tableContainer.length) { 416 tableContainer = createTableReaderDiv(this); 417 overlay = createTableReaderOverlay(this); 418 mapElem.prepend(tableContainer); 419 mapElem.prepend(overlay); 420 bindOverlay(overlay, this); 421 let features = retrieveFeatures(this); 422 features.sourceType = this.sourceType; 423 424 // scroll and resize the tablereader container and other elems 425 const position = overlay.getBoundingClientRect().top + window.scrollY - 48; 426 window.scroll({ 427 top: position, 428 behavior: "smooth", 429 }); 430 resizeTableReaderContainers(); 431 mapElem.classList.add("border-2px", "border-base-dark"); 432 433 renderVariousFeatures(tableContainer, features); 434 poulateScopeOptions(tableContainer, features); 435 makeTableNavigable(tableContainer); 436 controlOverride(mapElem, false); 437 animateButtons(mapElem); 438 $(overlay).find(".tablereader-focus").focus(); 439 440 // rerender on map movement 441 this.on("moveend", () => { 442 features = retrieveFeatures(this); 443 features.sourceType = this.sourceType; 444 renderVariousFeatures(tableContainer, features); 445 }); 446 447 // inital collapse 448 if (tableContainer.clientHeight <= 220) { 449 toggleTable(tableContainer, this); 450 $(tableContainer).find(".tablereader-close").focus(); 451 } 452 handleResize(tableContainer, this); 453 } else { 454 closeTableReader(button, this); 455 } 456 } 457 458 /** 459 * Collapses or expands the table's display over the map. 460 * @param {HTMLDivElement} container - The tablereader-container object 461 * @param {object} map - The OL map object 462 */ 463 function toggleTable(container, map) { 464 const mapElem = mapsUtil.findMap(container); 465 const scopeDiv = container.querySelector(".tablereader-scope"); 466 const zoomDiv = container.querySelector(".tablereader-zoom"); 467 const toggleButton = container.querySelector(".tablereader-toggle-button"); 468 const toggleButtonSpan = toggleButton.querySelector("span"); 469 const overlayButtons = mapElem.querySelectorAll(".tablereader-overlay button, .tablereader-focus"); 470 if (container.collapsed) { 471 expandTable(container, mapElem); 472 scopeDiv.style.display = "none"; 473 scopeDiv.ariaHidden = true; 474 zoomDiv.style.display = "flex"; 475 zoomDiv.ariaHidden = false; 476 toggleButtonSpan.innerHTML = "↑"; 477 toggleButton.title = "collapse map"; 478 toggleButton.ariaLabel = "Collapse the map."; 479 overlayButtons.forEach((el) => { 480 el.tabIndex = -1; 481 }); 482 const features = retrieveFeatures(map); 483 features.sourceType = map.sourceType; 484 renderVariousFeatures(container, features); 485 } else { 486 collapseTable(container, mapElem); 487 const features = retrieveAllFeatures(map); 488 features.sourceType = map.sourceType; 489 renderVariousFeatures(container, features); 490 scopeDiv.style.display = "block"; 491 scopeDiv.ariaHidden = false;
492 zoomDiv.style.display = "none"; 493 zoomDiv.ariaHidden = true; 494 toggleButtonSpan.innerHTML = "↓"; 495 toggleButton.title = "expand map"; 496 toggleButton.ariaLabel = "Expand the map."; 497 overlayButtons.forEach((el) => { 498 el.tabIndex = 0; 499 }); 500 toggleButton.focus(); 501 } 502 container.collapsed = !container.collapsed; 503 togglePanning(container); 504 } 505 506 /** 507 * Expands the tablereader container div and places it under the map. 508 * @param {HTMLDivElement} container - The table reader container div 509 * @param {HTMLDivElement} mapElem = The map container div 510 */ 511 function expandTable(container, mapElem) { 512 const windowHeight = document.documentElement.clientHeight; 513 let idealHeight = windowHeight - mapElem.clientHeight - 96; // 24 514 if (idealHeight < 220) { 515 idealHeight = 220; 516 } 517 container.setAttribute("style", `height: ${idealHeight}px; top: ${mapElem.clientHeight}px`); 518 } 519 520 /** 521 * Shrinks the tablereader container and places it over the map. 522 * @param {HTMLDivElement} container - The tablereader container div 523 * @param {HTMLDivElement} mapElem - The map container div 524 */ 525 function collapseTable(container, mapElem) { 526 const mapHeight = mapElem.clientHeight; 527 container.setAttribute("style", `height: ${mapHeight}px; top: 0px`); 528 container.querySelector(".tablereader-close-button").focus(); 529 } 530 531 /** 532 * Prepares event listener and timeout for window resizing. 533 * @param {HTMLDivElement} container - Div containing the table 534 * @param {object} map - OpenLayers map object 535 */ 536 function handleResize(container, map) { 537 let resizeTR; 538 539 window.addEventListener("resize", () => { 540 clearTimeout(resizeTR); 541 resizeTR = setTimeout(function () { 542 resizeTableReaderContainers(); 543 if (container.clientHeight <= 220 && !container.collapsed) { 544 toggleTable(container, map); 545 $(container).find(".tablereader-close").focus(); 546 } 547 }, 200); 548 }); 549 } 550 // *************************** 551 // Feature Rendering Functions 552 // *************************** 553 554 /** 555 * Captures the features currently in view. 556 * @param {object} map - The OL map object 557 * @return {object} The OL features collection 558 */ 559 function retrieveFeatures(map) { 560 const extent = map.getView().calculateExtent(map.getSize()); 561 const newExtent = map.getView().calculateExtent(map.focusSize); 562 const source = map.dataLayer.getSource(); 563 source.on("featuresloadend", () => { 564 return source.getFeatures(); 565 }); 566 return source.getFeaturesInExtent(extent); 567 } 568 569 /** 570 * Captures all features present on the map. 571 * @param {object} map - The OL map object 572 * @return {object} OL features object 573 */ 574 function retrieveAllFeatures(map) { 575 const source = map.dataLayer.getSource(); 576 return source.getFeatures(); 577 } 578 579 /** 580 * Tests the url source and calls the appropriate feature rendering function. 581 * @param {HTMLDivElement} element - The div the table exists in 582 * @param {object} features - The features object collected from map metadata 583 */ 584 function renderVariousFeatures(element, features) { 585 switch (features.sourceType) { 586 case "nws": 587 renderNWSFeatures(element, features); 588 break; 589 case "strmflow": 590 renderStrmflowFeatures(element, features); 591 break; 592 default: 593 break; 594 } 595 } 596 597 /** 598 * Appends all NWS Statements to the table. 599 * @param {object} element - The div the table exists in 600 * @param {*} features - The features collected from map metadata 601 */ 602 function renderNWSFeatures(element, features) { 603 const table = element.tagName === "table" ? element : element.querySelector("table"); 604 if (!table) { 605 return; 606 } 607 table.tabIndex = 0; 608 const caption = document.createElement("caption"); 609 caption.innerText = "National Weather Service Statements From The Map"; 610 const thead = document.createElement("thead"); 611 const trh = document.createElement("tr"); 612 const th1 = buildTh("Office", "col"); 613 const th2 = buildTh("City", "col"); 614 const th3 = buildTh("State", "col"); 615 const th4 = buildTh("Date", "col"); 616 const th5 = buildTh("URL", "col"); 617 const tbody = document.createElement("tbody"); 618 trh.append(th1, th2, th3, th4, th5); 619 thead.append(trh); 620
621 features.forEach((feature) => { 622 if (feature.get("DGT") !== "0") { 623 const props = feature.getProperties(); 624 const trn = document.createElement("tr"); 625 const td1 = buildTh(abbr.buildElement(props.WFO, "wfo"), "row"); 626 const td2 = buildTd(props.CITY); 627 const td3 = buildTd(abbr.buildElement(props.STATE, "state")); 628 const td4 = buildTd(props.DATE); 629 const a = buildWithAria( 630 "a", 631 null, 632 { label: `Open ${props.WFO} statement in new tab.` }, 633 { target: "_blank", href: feature.getProperties().URL } 634 ); 635 a.innerText = `View ${props.WFO} statement`; 636 const td5 = buildTd(a); 637 trn.append(td1, td2, td3, td4, td5); 638 tbody.append(trn); 639 } 640 }); 641 table.replaceChildren(caption, thead, tbody); 642 } 643 644 /** 645 * Appends all streamflow statements to the table. 646 * @param {object} element - The div the table exists in 647 * @param {*} features - The features collected from map metadata 648 */ 649 function render
649StrmflowFeatures(element, features) { 650 const table = element.querySelector("table"); 651 table.tabIndex = 0; 652 const caption = document.createElement("caption"); 653 caption.innerText = "Streamflow Statements From The Map"; 654 const thead = document.createElement("thead"); 655 const trh = document.createElement("tr"); 656 const th1 = buildTh("Location", "col"); 657 const th3 = buildTh("Status", "col"); 658 const tbody = document.createElement("tbody"); 659 trh.append(th1, th3); 660 thead.append(trh); 661 662 features.forEach((feature) => { 663 if (feature.get("clickable") === "true") { 664 const props = feature.getProperties(); 665 props.location = props.STANAME || props.NAME; 666 props.shortName = props.location.split(" ", 2).join(" "); 667 props.id = props.STAID; 668 let status; 669 switch (true) { 670 case props.CLASS === 8: 671 status = "High"; 672 break; 673 case props.CLASS === 7: 674 status = "Much Above"; 675 break; 676 case props.CLASS === 6: 677 status = "Above"; 678 break; 679 case props.CLASS === 5: 680 status = "Normal"; 681 break; 682 case props.CLASS === 4: 683 status = "Below"; 684 break; 685 case props.CLASS === 2 || props.CLASS === 3: 686 status = "Much Below"; 687 break; 688 case props.CLASS === 1: 689 status = "Low"; 690 break; 691 default: 692 status = "Not Ranked"; 693 break; 694 } 695 696 const trn = document.createElement("tr"); 697 const td3 = buildTd(status); 698 const a = buildWithAria( 699 "a", 700 null, 701 { label: `Opens page for ${props.shortName} in a new tab` }, 702 { href: `https://waterdata.usgs.gov/monitoring-location/${props.id}`, target: "_blank" } 703 ); 704 a.innerText = `${props.location}`; 705 const td1 = buildTh(a, "row"); 706 trn.append(td1, td3); 707 tbody.append(trn); 708 } 709 }); 710 const cloneTable = table.cloneNode(); 711 cloneTable.replaceChildren(caption, thead, tbody); 712 table.replaceWith(cloneTable); 713 table.remove(); 714 } 715 716 /** 717 * Returns label text for a given source type 718 * @param {*} sourceType - "nws" or "strmflow" (from metadata) 719 * @return {string} The label text 720 */ 721 function getScopeString(sourceType) { 722 switch (sourceType) { 723 case "nws": 724 return "State: "; 725 case "strmflow": 726 return "Class: "; 727 default: 728 return ""; 729 } 730 } 731 732 /** 733 * Generates an array of scope options from map features. 734 * @param {Array} features - The list of current features 735 * @return {Array} The list of options 736 */ 737 function makeScopeOptions(features) { 738 const options = []; 739 options[0] = "(All)"; 740 if (features.sourceType === "nws") { 741 features.forEach((feature) => { 742 if (feature.get("DGT") !== "0") { 743 const state = feature.get("STATE"); 744 if (!options.includes(state)) { 745 options.push(state); 746 } 747 } 748 }); 749 } else if (features.sourceType === "strmflow") { 750 features.forEach((feature) => { 751 if (feature.get("clickable") === "true") { 752 const classStr = feature.get("CLASS"); 753 if (!options.includes(classStr)) { 754 options.push(classStr); 755 } 756 } 757 }); 758 } 759 options.sort(); 760 return options.map(buildOption); 761 } 762 763 /** 764 * Populates the scope selection box with provided features. 765 * @param {HTMLDivElement} container - The tablereader container that is the parent of the select element 766 * @param {Array} features - The list of current features 767 */ 768 function poulateScopeOptions(container, features) { 769 const scopeSelect = container.querySelector("select"); 770 container.querySelector("label[for=trscope]").textContent = getScopeString(features.sourceType); 771 scopeSelect.replaceChildren(...makeScopeOptions(features)); 772 scopeSelect.addEventListener("change", () => { 773 if (scopeSelect.value === "") { 774 renderVariousFeatures(container, features); 775 } else { 776 let newFeatures; 777 if (features.sourceType === "nws") { 778 newFeatures = [...features].filter((feature) => feature.get("STATE") === scopeSelect.value); 779 } else if (features.sourceType === "strmflow") { 780 newFeatures = [...features].filter((feature) => feature.get("CLASS") === scopeSelect.value); 781 } 782 newFeatures.sourceType = features.sourceType; 783 renderVariousFeatures(container, newFeatures); 784 } 785 }); 786 } 787 788 // ********************************** 789 // Keyboard event capturing functions 790 // ********************************** 791 792 /** 793 * Zooms out on the map. 794 * @param {object} map - The target OpenLayers map object 795 */ 796 function zoomOutMap(map) { 797 const view = map.getView(); 798 const zoom = view.getZoom(); 799 view.setZoom(zoom - 1); 800 } 801 802 /** 803 * Zooms in on the map. 804 * @param {object} map - The target OpenLayers map object 805 */ 806 function zoomInMap(map) { 807 const view = map.getView(); 808 const zoom = view.getZoom(); 809 view.setZoom(zoom + 1); 810 } 811 812 /** 813 * Pans the map in a given direction. 814 * @param {string} direction - The direction to pan (left, right, up, or down) 815 * @param {object} map - The target OpenLayers map object 816 */ 817 function panMap(direction, map) { 818 const view = map.getView();
819 const center = map.getPixelFromCoordinate(view.getCenter()); 820 const buffer = 128; // standard for OL 821 822 switch (direction) { 823 case "left": 824 center[0] -= buffer; 825 break; 826 case "up": 827 center[1] -= buffer; 828 break; 829 case "right": 830 center[0] += buffer; 831 break; 832 case "down": 833 center[1] += buffer; 834 break; 835 default: 836 break; 837 } 838 view.animate({ 839 center: map.getCoordinateFromPixel(center), 840 duration: 150, 841 }); 842 } 843 844 /** 845 * Renders a given table keyboard navigable. 846 * @param {HTMLDivElement} table - Parent div of the table 847 */ 848 function makeTableNavigable(table) { 849 const tableElement = table.querySelector("table"); 850 const links = table.querySelectorAll("a"); 851 if (links) { 852 links.forEach((link) => { 853 link.tabIndex = "-1"; // only change if JS/SR allow 854 }); 855 } 856 const rows = table.querySelectorAll("tr"); 857 const arrow = { 858 left: "ArrowLeft", 859 up: "ArrowUp", 860 right: "ArrowRight", 861 down: "ArrowDown", 862 }; 863 const enter = "Enter"; 864 const space = "Space"; 865 const tab = "Tab"; 866 tableElement.addEventListener("focus", function (e) { 867 if (rows) { 868 rows.forEach((row) => { 869 row.tabIndex = "0"; 870 }); 871 } 872 }); 873 tableElement.addEventListener("keydown", function (e) { 874 if ([arrow.left, arrow.up, arrow.right, arrow.down].includes(e.code)) { 875 e.stopPropagation(); 876 } 877 if ((e.code === enter || e.code === space) && e.target === tableElement) { 878 rows[0].focus(); 879 rows.forEach(function (row, index) { 880 row.tabIndex = "0"; 881 row.tabHandle = index; 882 }); 883 } else if (e.code === arrow.down && e.target.tagName === "TR") { 884 if (e.target.tabHandle + 1 < rows.length) { 885 e.preventDefault(); 886 rows[e.target.tabHandle + 1].focus(); 887 } 888 } else if (e.code === arrow.up && e.target.tagName === "TR") { 889 if (e.target.tabHandle - 1 >= 0) { 890 e.preventDefault(); 891 rows[e.target.tabHandle - 1].focus(); 892 } 893 } else if ((e.code === enter || e.code === space) && e.target.tagName === "TR") { 894 e.target.querySelector("a").focus(); 895 } else if (e.code === tab && e.target === tableElement) { 896 // actions if tab out of table 897 rows.forEach((row) => { 898 row.tabIndex = "-1"; 899 }); 900 links.forEach((link) => { 901 link.tabIndex = "-1"; 902 }); 903 } 904 }); 905 tableElement.addEventListener("blur", function (e) { 906 // reset tabIndexes 907 if (rows) { 908 rows.forEach((row) => { 909 row.tabIndex = "-1"; 910 }); 911 } 912 if (links) { 913 links.forEach((link) => { 914 link.tabIndex = "-1"; 915 }); 916 } 917 }); 918 } 919 920 /** 921 * Adds or removes the blockPanning event listener based on container state. 922 * @param {HTMLDivElement} container The tablereader container 923 */ 924 function togglePanning(container) { 925 if (container.collapsed) { 926 container.addEventListener("keydown", blockPanning); 927 } 928 if (!container.collapsed) { 929 container.removeEventListener("keydown", blockPanning); 930 } 931 } 932 933 /** 934 * Prevents event propogation on arrow keys. 935 * @param {Event} e - The keydown event 936 */ 937 function blockPanning(e) { 938 const arrow = { 939 left: "ArrowLeft", 940 up: "ArrowUp", 941 right: "ArrowRight", 942 down: "ArrowDown", 943 }; 944 if ([arrow.left, arrow.up, arrow.right, arrow.down].includes(e.code)) { 945 e.stopPropagation(); 946 } 947 } 948 949 // *************************************** 950 // Override default map settings functions 951 // *************************************** 952 953 /** 954 * Disables or enables the ability to focus on the map's internal buttons. 955 * @param {object} mapElem DOM map object 956 * @param {boolean} boolean Whether or not the controls should be visible 957 * Operating through map.getControls().foreach() would be better but not all controls are assigned as such :( 958 */ 959 function controlOverride(mapElem, boolean) { 960 const controlElements = []; 961 controlElements.push(mapElem.querySelector(".ol-zoom-in")); 962 controlElements.push(mapElem.querySelector(".ol-zoom-out")); 963 controlElements.push(...mapElem.querySelectorAll(".ol-jump-button")); 964 controlElements.push(mapElem.querySelector(".tablereader-button"));
965 controlElements.push(mapElem.querySelector(".screenshot-expand-button")); 966 mapElem.querySelector(".screenshot-expand-button").style.zIndex = 90; 967 controlElements.forEach((control) => { 968 control.tabIndex = boolean ? 0 : -1; 969 }); 970 } 971 972 /** 973 * Assigns CSS animations to keypresses. 974 * @param {object} mapElem the DOM map element 975 */ 976 function animateButtons(mapElem) { 977 const arrow = { 978 left: 37, 979 up: 38, 980 right: 39, 981 down: 40, 982 }; 983 const buttons = {}; 984 buttons.left = mapElem.querySelector("div#left button"); 985 buttons.up = mapElem.querySelector("div#up button"); 986 buttons.right = mapElem.querySelector("div#right button"); 987 buttons.down = mapElem.querySelector("div#down button"); 988 mapElem.addEventListener("keydown", function handleAnimateListener(e) { 989 // There's probably a better logic pattern but this works 990 if (Object.values(arrow).includes(e.which)) { 991 Object.values(buttons).forEach((button) => { 992 button.classList.remove("highlight-button"); 993 }); 994 switch (e.which) { 995 case arrow.left: 996 buttons.left.classList.add("highlight-button"); 997 break; 998 case arrow.up: 999 buttons.up.classList.add("highlight-button"); 1000 break; 1001 case arrow.right: 1002 buttons.right.classList.add("highlight-button"); 1003 break; 1004 case arrow.down: 1005 buttons.down.classList.add("highlight-button"); 1006 break; 1007 default: 1008 break; 1009 } 1010 } 1011 }); 1012 } 1013 1014 /** 1015 * Closes the Table Reader. 1016 * @param {HTMLButtonElement} target - The close button that triggers the function 1017 * @param {object} map - The OpenLayers map object 1018 */ 1019 function closeTableReader(target, map) { 1020 const wrapper = mapsUtil.findMap(target); 1021 const table = $(wrapper).find(".tablereader-container"); 1022 const overlay = $(wrapper).find(".tablereader-overlay"); 1023 1024 $(table).remove(); 1025 $(overlay).remove(); 1026 controlOverride(wrapper, true); 1027 map.getView().set("padding", [0, 0, 0, 0]); 1028 1029 $(wrapper).find(".tablereader-button").focus(); 1030 } 1031 1032 /** 1033 * export the functions to the window 1034 */ 1035 function exportConfig() { 1036 window.tableReader = { 1037 addTableReader, 1038 bindOverlay, 1039 initialize, 1040 }; 1041 } 1042})(window.jQuery, window.mapsUtil, window.ol, window.abbr);
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.