PageSourceSearch

https://www.drought.gov/modules/custom/general_page_js_css/js/map-helpers/table-reader.js?tlrx84

js drought.gov collected 2026-09-24 08:36:11 UTC 34,800 bytes, 1,042 lines download raw bytes

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 = "&uparrow;";
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 = "&downarrow;";
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.