From 7d92949e46ad897dd46fce0d01929a99bce8eafd Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 18 Nov 2024 19:59:43 -0500 Subject: [PATCH] formatting --- src/registrar/assets/modules/combobox.js | 217 +++--- .../assets/modules/domain-validators.js | 6 - src/registrar/assets/modules/formset-forms.js | 2 +- ...et-csrf-token.js => helpers-csrf-token.js} | 0 src/registrar/assets/modules/helpers-uswds.js | 2 +- src/registrar/assets/modules/main.js | 8 +- .../modules/{helpers-radios.js => radios.js} | 0 .../assets/modules/requesting-entity.js | 20 +- src/registrar/assets/modules/table-base.js | 679 +++++++++-------- .../assets/modules/table-domain-requests.js | 708 +++++++++--------- src/registrar/assets/modules/table-domains.js | 281 ++++--- .../assets/modules/table-member-domains.js | 231 +++--- src/registrar/assets/modules/table-members.js | 687 +++++++++-------- src/registrar/assets/modules/user-profile.js | 323 ++++---- 14 files changed, 1554 insertions(+), 1610 deletions(-) rename src/registrar/assets/modules/{get-csrf-token.js => helpers-csrf-token.js} (100%) rename src/registrar/assets/modules/{helpers-radios.js => radios.js} (100%) diff --git a/src/registrar/assets/modules/combobox.js b/src/registrar/assets/modules/combobox.js index 5f51bc5ee..139106c59 100644 --- a/src/registrar/assets/modules/combobox.js +++ b/src/registrar/assets/modules/combobox.js @@ -1,122 +1,115 @@ -import { hideElement, showElement, scrollToElement, toggleCaret } from './helpers.js'; -import { initializeTooltips, initializeModals, unloadModals } from './helpers-uswds.js'; +import { hideElement, showElement } from './helpers.js'; -/** - * An IIFE that changes the default clear behavior on comboboxes to the input field. - * We want the search bar to act soley as a search bar. - */ export function loadInitialValuesForComboBoxes() { - var overrideDefaultClearButton = true; - var isTyping = false; - - document.addEventListener('DOMContentLoaded', (event) => { - handleAllComboBoxElements(); - }); - - function handleAllComboBoxElements() { - const comboBoxElements = document.querySelectorAll(".usa-combo-box"); - comboBoxElements.forEach(comboBox => { - const input = comboBox.querySelector("input"); - const select = comboBox.querySelector("select"); - if (!input || !select) { - console.warn("No combobox element found"); - return; - } - // Set the initial value of the combobox - let initialValue = select.getAttribute("data-default-value"); - let clearInputButton = comboBox.querySelector(".usa-combo-box__clear-input"); - if (!clearInputButton) { - console.warn("No clear element found"); - return; - } - - // Override the default clear button behavior such that it no longer clears the input, - // it just resets to the data-initial-value. - - // Due to the nature of how uswds works, this is slightly hacky. - - // Use a MutationObserver to watch for changes in the dropdown list - const dropdownList = comboBox.querySelector(`#${input.id}--list`); - const observer = new MutationObserver(function(mutations) { - mutations.forEach(function(mutation) { - if (mutation.type === "childList") { - addBlankOption(clearInputButton, dropdownList, initialValue); + var overrideDefaultClearButton = true; + var isTyping = false; + + document.addEventListener('DOMContentLoaded', (event) => { + handleAllComboBoxElements(); + }); + + function handleAllComboBoxElements() { + const comboBoxElements = document.querySelectorAll(".usa-combo-box"); + comboBoxElements.forEach(comboBox => { + const input = comboBox.querySelector("input"); + const select = comboBox.querySelector("select"); + if (!input || !select) { + console.warn("No combobox element found"); + return; + } + // Set the initial value of the combobox + let initialValue = select.getAttribute("data-default-value"); + let clearInputButton = comboBox.querySelector(".usa-combo-box__clear-input"); + if (!clearInputButton) { + console.warn("No clear element found"); + return; + } + + // Override the default clear button behavior such that it no longer clears the input, + // it just resets to the data-initial-value. + + // Due to the nature of how uswds works, this is slightly hacky. + + // Use a MutationObserver to watch for changes in the dropdown list + const dropdownList = comboBox.querySelector(`#${input.id}--list`); + const observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + if (mutation.type === "childList") { + addBlankOption(clearInputButton, dropdownList, initialValue); + } + }); + }); + + // Configure the observer to watch for changes in the dropdown list + const config = { childList: true, subtree: true }; + observer.observe(dropdownList, config); + + // Input event listener to detect typing + input.addEventListener("input", () => { + isTyping = true; + }); + + // Blur event listener to reset typing state + input.addEventListener("blur", () => { + isTyping = false; + }); + + // Hide the reset button when there is nothing to reset. + // Do this once on init, then everytime a change occurs. + updateClearButtonVisibility(select, initialValue, clearInputButton) + select.addEventListener("change", () => { + updateClearButtonVisibility(select, initialValue, clearInputButton) + }); + + // Change the default input behaviour - have it reset to the data default instead + clearInputButton.addEventListener("click", (e) => { + if (overrideDefaultClearButton && initialValue) { + e.preventDefault(); + e.stopPropagation(); + input.click(); + // Find the dropdown option with the desired value + const dropdownOptions = document.querySelectorAll(".usa-combo-box__list-option"); + if (dropdownOptions) { + dropdownOptions.forEach(option => { + if (option.getAttribute("data-value") === initialValue) { + // Simulate a click event on the dropdown option + option.click(); } }); - }); - - // Configure the observer to watch for changes in the dropdown list - const config = { childList: true, subtree: true }; - observer.observe(dropdownList, config); - - // Input event listener to detect typing - input.addEventListener("input", () => { - isTyping = true; - }); - - // Blur event listener to reset typing state - input.addEventListener("blur", () => { - isTyping = false; - }); - - // Hide the reset button when there is nothing to reset. - // Do this once on init, then everytime a change occurs. - updateClearButtonVisibility(select, initialValue, clearInputButton) - select.addEventListener("change", () => { - updateClearButtonVisibility(select, initialValue, clearInputButton) - }); - - // Change the default input behaviour - have it reset to the data default instead - clearInputButton.addEventListener("click", (e) => { - if (overrideDefaultClearButton && initialValue) { - e.preventDefault(); - e.stopPropagation(); - input.click(); - // Find the dropdown option with the desired value - const dropdownOptions = document.querySelectorAll(".usa-combo-box__list-option"); - if (dropdownOptions) { - dropdownOptions.forEach(option => { - if (option.getAttribute("data-value") === initialValue) { - // Simulate a click event on the dropdown option - option.click(); - } - }); - } } - }); + } }); - } - - function updateClearButtonVisibility(select, initialValue, clearInputButton) { - if (select.value === initialValue) { - hideElement(clearInputButton); - }else { - showElement(clearInputButton) - } - } - - function addBlankOption(clearInputButton, dropdownList, initialValue) { - if (dropdownList && !dropdownList.querySelector('[data-value=""]') && !isTyping) { - const blankOption = document.createElement("li"); - blankOption.setAttribute("role", "option"); - blankOption.setAttribute("data-value", ""); - blankOption.classList.add("usa-combo-box__list-option"); - if (!initialValue){ - blankOption.classList.add("usa-combo-box__list-option--selected") - } - blankOption.textContent = "⎯"; - - dropdownList.insertBefore(blankOption, dropdownList.firstChild); - blankOption.addEventListener("click", (e) => { - e.preventDefault(); - e.stopPropagation(); - overrideDefaultClearButton = false; - // Trigger the default clear behavior - clearInputButton.click(); - overrideDefaultClearButton = true; - }); - } + }); + } + + function updateClearButtonVisibility(select, initialValue, clearInputButton) { + if (select.value === initialValue) { + hideElement(clearInputButton); + }else { + showElement(clearInputButton) } } + function addBlankOption(clearInputButton, dropdownList, initialValue) { + if (dropdownList && !dropdownList.querySelector('[data-value=""]') && !isTyping) { + const blankOption = document.createElement("li"); + blankOption.setAttribute("role", "option"); + blankOption.setAttribute("data-value", ""); + blankOption.classList.add("usa-combo-box__list-option"); + if (!initialValue){ + blankOption.classList.add("usa-combo-box__list-option--selected") + } + blankOption.textContent = "⎯"; + dropdownList.insertBefore(blankOption, dropdownList.firstChild); + blankOption.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + overrideDefaultClearButton = false; + // Trigger the default clear behavior + clearInputButton.click(); + overrideDefaultClearButton = true; + }); + } + } +} diff --git a/src/registrar/assets/modules/domain-validators.js b/src/registrar/assets/modules/domain-validators.js index 5a09a06a2..368a1504c 100644 --- a/src/registrar/assets/modules/domain-validators.js +++ b/src/registrar/assets/modules/domain-validators.js @@ -1,10 +1,4 @@ -// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>> -// Event handlers. - var DEFAULT_ERROR = "Please check this field for errors."; - -var INFORMATIVE = "info"; -var WARNING = "warning"; var ERROR = "error"; var SUCCESS = "success"; diff --git a/src/registrar/assets/modules/formset-forms.js b/src/registrar/assets/modules/formset-forms.js index bbce0919a..6743df7f6 100644 --- a/src/registrar/assets/modules/formset-forms.js +++ b/src/registrar/assets/modules/formset-forms.js @@ -415,4 +415,4 @@ export function nameserversFormListener() { }); } } -} \ No newline at end of file +} diff --git a/src/registrar/assets/modules/get-csrf-token.js b/src/registrar/assets/modules/helpers-csrf-token.js similarity index 100% rename from src/registrar/assets/modules/get-csrf-token.js rename to src/registrar/assets/modules/helpers-csrf-token.js diff --git a/src/registrar/assets/modules/helpers-uswds.js b/src/registrar/assets/modules/helpers-uswds.js index 33dd2b076..e182d65c0 100644 --- a/src/registrar/assets/modules/helpers-uswds.js +++ b/src/registrar/assets/modules/helpers-uswds.js @@ -40,4 +40,4 @@ export function initializeModals() { */ export function unloadModals() { window.modal.off(); -} \ No newline at end of file +} diff --git a/src/registrar/assets/modules/main.js b/src/registrar/assets/modules/main.js index d4e171bf6..f99ef188c 100644 --- a/src/registrar/assets/modules/main.js +++ b/src/registrar/assets/modules/main.js @@ -1,7 +1,4 @@ -import { hideElement, showElement, scrollToElement, toggleCaret } from './helpers.js'; -import { initializeTooltips, initializeModals, unloadModals } from './helpers-uswds.js'; -import { hookupYesNoListener, hookupRadioTogglerListener } from './helpers-radios.js'; -import { getCsrfToken } from './get-csrf-token.js'; +import { hookupYesNoListener, hookupRadioTogglerListener } from './radios.js'; import { initDomainValidators } from './domain-validators.js'; import { initFormsetsForms, triggerModalOnDsDataForm, nameserversFormListener } from './formset-forms.js'; import { initializeUrbanizationToggle } from './urbanization.js'; @@ -43,6 +40,3 @@ initDomainsTable(); initDomainRequestsTable(); initMembersTable(); initMemberDomainsTable(); - - - diff --git a/src/registrar/assets/modules/helpers-radios.js b/src/registrar/assets/modules/radios.js similarity index 100% rename from src/registrar/assets/modules/helpers-radios.js rename to src/registrar/assets/modules/radios.js diff --git a/src/registrar/assets/modules/requesting-entity.js b/src/registrar/assets/modules/requesting-entity.js index e9be29ad4..1b54f2660 100644 --- a/src/registrar/assets/modules/requesting-entity.js +++ b/src/registrar/assets/modules/requesting-entity.js @@ -1,5 +1,5 @@ -import { hideElement, showElement, scrollToElement, toggleCaret } from './helpers.js'; -import { initializeTooltips, initializeModals, unloadModals } from './helpers-uswds.js'; +import { hideElement, showElement } from './helpers.js'; + /** An IIFE that intializes the requesting entity page. * This page has a radio button that dynamically toggles some fields * Within that, the dropdown also toggles some additional form elements. @@ -20,27 +20,27 @@ export function handleRequestingEntityFieldset() { var requestingNewSuborganization = document.getElementById(`id_${formPrefix}-is_requesting_new_suborganization`); function toggleSuborganization(radio=null) { - if (radio != null) requestingSuborganization = radio?.checked && radio.value === "True"; - requestingSuborganization ? showElement(suborgContainer) : hideElement(suborgContainer); - requestingNewSuborganization.value = requestingSuborganization && select.value === "other" ? "True" : "False"; - requestingNewSuborganization.value === "True" ? showElement(suborgDetailsContainer) : hideElement(suborgDetailsContainer); + if (radio != null) requestingSuborganization = radio?.checked && radio.value === "True"; + requestingSuborganization ? showElement(suborgContainer) : hideElement(suborgContainer); + requestingNewSuborganization.value = requestingSuborganization && select.value === "other" ? "True" : "False"; + requestingNewSuborganization.value === "True" ? showElement(suborgDetailsContainer) : hideElement(suborgDetailsContainer); } // Add fake "other" option to sub_organization select if (select && !Array.from(select.options).some(option => option.value === "other")) { - select.add(new Option("Other (enter your organization manually)", "other")); + select.add(new Option("Other (enter your organization manually)", "other")); } if (requestingNewSuborganization.value === "True") { - select.value = "other"; + select.value = "other"; } // Add event listener to is_suborganization radio buttons, and run for initial display toggleSuborganization(); radios.forEach(radio => { - radio.addEventListener("click", () => toggleSuborganization(radio)); + radio.addEventListener("click", () => toggleSuborganization(radio)); }); // Add event listener to the suborg dropdown to show/hide the suborg details section select.addEventListener("change", () => toggleSuborganization()); - } \ No newline at end of file +} diff --git a/src/registrar/assets/modules/table-base.js b/src/registrar/assets/modules/table-base.js index 74649b944..ed9cdc655 100644 --- a/src/registrar/assets/modules/table-base.js +++ b/src/registrar/assets/modules/table-base.js @@ -1,359 +1,358 @@ import { hideElement, showElement, toggleCaret } from './helpers.js'; -import { initializeTooltips, initializeModals, unloadModals } from './helpers-uswds.js'; export class LoadTableBase { - constructor(sectionSelector) { - this.tableWrapper = document.getElementById(`${sectionSelector}__table-wrapper`); - this.tableHeaders = document.querySelectorAll(`#${sectionSelector} th[data-sortable]`); - this.currentSortBy = 'id'; - this.currentOrder = 'asc'; - this.currentStatus = []; - this.currentSearchTerm = ''; - this.scrollToTable = false; - this.searchInput = document.getElementById(`${sectionSelector}__search-field`); - this.searchSubmit = document.getElementById(`${sectionSelector}__search-field-submit`); - this.tableAnnouncementRegion = document.getElementById(`${sectionSelector}__usa-table__announcement-region`); - this.resetSearchButton = document.getElementById(`${sectionSelector}__reset-search`); - this.resetFiltersButton = document.getElementById(`${sectionSelector}__reset-filters`); - this.statusCheckboxes = document.querySelectorAll(`.${sectionSelector} input[name="filter-status"]`); - this.statusIndicator = document.getElementById(`${sectionSelector}__filter-indicator`); - this.statusToggle = document.getElementById(`${sectionSelector}__usa-button--filter`); - this.noTableWrapper = document.getElementById(`${sectionSelector}__no-data`); - this.noSearchResultsWrapper = document.getElementById(`${sectionSelector}__no-search-results`); - this.portfolioElement = document.getElementById('portfolio-js-value'); - this.portfolioValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-portfolio') : null; - this.initializeTableHeaders(); - this.initializeSearchHandler(); - this.initializeStatusToggleHandler(); - this.initializeFilterCheckboxes(); - this.initializeResetSearchButton(); - this.initializeResetFiltersButton(); - this.initializeAccordionAccessibilityListeners(); + constructor(sectionSelector) { + this.tableWrapper = document.getElementById(`${sectionSelector}__table-wrapper`); + this.tableHeaders = document.querySelectorAll(`#${sectionSelector} th[data-sortable]`); + this.currentSortBy = 'id'; + this.currentOrder = 'asc'; + this.currentStatus = []; + this.currentSearchTerm = ''; + this.scrollToTable = false; + this.searchInput = document.getElementById(`${sectionSelector}__search-field`); + this.searchSubmit = document.getElementById(`${sectionSelector}__search-field-submit`); + this.tableAnnouncementRegion = document.getElementById(`${sectionSelector}__usa-table__announcement-region`); + this.resetSearchButton = document.getElementById(`${sectionSelector}__reset-search`); + this.resetFiltersButton = document.getElementById(`${sectionSelector}__reset-filters`); + this.statusCheckboxes = document.querySelectorAll(`.${sectionSelector} input[name="filter-status"]`); + this.statusIndicator = document.getElementById(`${sectionSelector}__filter-indicator`); + this.statusToggle = document.getElementById(`${sectionSelector}__usa-button--filter`); + this.noTableWrapper = document.getElementById(`${sectionSelector}__no-data`); + this.noSearchResultsWrapper = document.getElementById(`${sectionSelector}__no-search-results`); + this.portfolioElement = document.getElementById('portfolio-js-value'); + this.portfolioValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-portfolio') : null; + this.initializeTableHeaders(); + this.initializeSearchHandler(); + this.initializeStatusToggleHandler(); + this.initializeFilterCheckboxes(); + this.initializeResetSearchButton(); + this.initializeResetFiltersButton(); + this.initializeAccordionAccessibilityListeners(); + } + + /** + * Generalized function to update pagination for a list. + * @param {string} itemName - The name displayed in the counter + * @param {string} paginationSelector - CSS selector for the pagination container. + * @param {string} counterSelector - CSS selector for the pagination counter. + * @param {string} tableSelector - CSS selector for the header element to anchor the links to. + * @param {number} currentPage - The current page number (starting with 1). + * @param {number} numPages - The total number of pages. + * @param {boolean} hasPrevious - Whether there is a page before the current page. + * @param {boolean} hasNext - Whether there is a page after the current page. + * @param {number} total - The total number of items. + */ + updatePagination( + itemName, + paginationSelector, + counterSelector, + parentTableSelector, + currentPage, + numPages, + hasPrevious, + hasNext, + totalItems, + ) { + const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`); + const counterSelectorEl = document.querySelector(counterSelector); + const paginationSelectorEl = document.querySelector(paginationSelector); + counterSelectorEl.innerHTML = ''; + paginationButtons.innerHTML = ''; + + // Buttons should only be displayed if there are more than one pages of results + paginationButtons.classList.toggle('display-none', numPages <= 1); + + // Counter should only be displayed if there is more than 1 item + paginationSelectorEl.classList.toggle('display-none', totalItems < 1); + + counterSelectorEl.innerHTML = `${totalItems} ${itemName}${totalItems > 1 ? 's' : ''}${this.currentSearchTerm ? ' for ' + '"' + this.currentSearchTerm + '"' : ''}`; + + if (hasPrevious) { + const prevPageItem = document.createElement('li'); + prevPageItem.className = 'usa-pagination__item usa-pagination__arrow'; + prevPageItem.innerHTML = ` + + + Previous + + `; + prevPageItem.querySelector('a').addEventListener('click', (event) => { + event.preventDefault(); + this.loadTable(currentPage - 1); + }); + paginationButtons.appendChild(prevPageItem); } - - /** - * Generalized function to update pagination for a list. - * @param {string} itemName - The name displayed in the counter - * @param {string} paginationSelector - CSS selector for the pagination container. - * @param {string} counterSelector - CSS selector for the pagination counter. - * @param {string} tableSelector - CSS selector for the header element to anchor the links to. - * @param {number} currentPage - The current page number (starting with 1). - * @param {number} numPages - The total number of pages. - * @param {boolean} hasPrevious - Whether there is a page before the current page. - * @param {boolean} hasNext - Whether there is a page after the current page. - * @param {number} total - The total number of items. - */ - updatePagination( - itemName, - paginationSelector, - counterSelector, - parentTableSelector, - currentPage, - numPages, - hasPrevious, - hasNext, - totalItems, - ) { - const paginationButtons = document.querySelector(`${paginationSelector} .usa-pagination__list`); - const counterSelectorEl = document.querySelector(counterSelector); - const paginationSelectorEl = document.querySelector(paginationSelector); - counterSelectorEl.innerHTML = ''; - paginationButtons.innerHTML = ''; - - // Buttons should only be displayed if there are more than one pages of results - paginationButtons.classList.toggle('display-none', numPages <= 1); - - // Counter should only be displayed if there is more than 1 item - paginationSelectorEl.classList.toggle('display-none', totalItems < 1); - - counterSelectorEl.innerHTML = `${totalItems} ${itemName}${totalItems > 1 ? 's' : ''}${this.currentSearchTerm ? ' for ' + '"' + this.currentSearchTerm + '"' : ''}`; - - if (hasPrevious) { - const prevPageItem = document.createElement('li'); - prevPageItem.className = 'usa-pagination__item usa-pagination__arrow'; - prevPageItem.innerHTML = ` - - - Previous - - `; - prevPageItem.querySelector('a').addEventListener('click', (event) => { - event.preventDefault(); - this.loadTable(currentPage - 1); - }); - paginationButtons.appendChild(prevPageItem); - } - - // Add first page and ellipsis if necessary - if (currentPage > 2) { - paginationButtons.appendChild(this.createPageItem(1, parentTableSelector, currentPage)); - if (currentPage > 3) { - const ellipsis = document.createElement('li'); - ellipsis.className = 'usa-pagination__item usa-pagination__overflow'; - ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages'); - ellipsis.innerHTML = ''; - paginationButtons.appendChild(ellipsis); - } - } - - // Add pages around the current page - for (let i = Math.max(1, currentPage - 1); i <= Math.min(numPages, currentPage + 1); i++) { - paginationButtons.appendChild(this.createPageItem(i, parentTableSelector, currentPage)); - } - - // Add last page and ellipsis if necessary - if (currentPage < numPages - 1) { - if (currentPage < numPages - 2) { - const ellipsis = document.createElement('li'); - ellipsis.className = 'usa-pagination__item usa-pagination__overflow'; - ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages'); - ellipsis.innerHTML = ''; - paginationButtons.appendChild(ellipsis); - } - paginationButtons.appendChild(this.createPageItem(numPages, parentTableSelector, currentPage)); - } - - if (hasNext) { - const nextPageItem = document.createElement('li'); - nextPageItem.className = 'usa-pagination__item usa-pagination__arrow'; - nextPageItem.innerHTML = ` - - Next - - - `; - nextPageItem.querySelector('a').addEventListener('click', (event) => { - event.preventDefault(); - this.loadTable(currentPage + 1); - }); - paginationButtons.appendChild(nextPageItem); + + // Add first page and ellipsis if necessary + if (currentPage > 2) { + paginationButtons.appendChild(this.createPageItem(1, parentTableSelector, currentPage)); + if (currentPage > 3) { + const ellipsis = document.createElement('li'); + ellipsis.className = 'usa-pagination__item usa-pagination__overflow'; + ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages'); + ellipsis.innerHTML = ''; + paginationButtons.appendChild(ellipsis); } } - - /** - * A helper that toggles content/ no content/ no search results - * - */ - updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => { - const { unfiltered_total, total } = data; - if (unfiltered_total) { - if (total) { - showElement(dataWrapper); - hideElement(noSearchResultsWrapper); - hideElement(noDataWrapper); - } else { - hideElement(dataWrapper); - showElement(noSearchResultsWrapper); - hideElement(noDataWrapper); - } + + // Add pages around the current page + for (let i = Math.max(1, currentPage - 1); i <= Math.min(numPages, currentPage + 1); i++) { + paginationButtons.appendChild(this.createPageItem(i, parentTableSelector, currentPage)); + } + + // Add last page and ellipsis if necessary + if (currentPage < numPages - 1) { + if (currentPage < numPages - 2) { + const ellipsis = document.createElement('li'); + ellipsis.className = 'usa-pagination__item usa-pagination__overflow'; + ellipsis.setAttribute('aria-label', 'ellipsis indicating non-visible pages'); + ellipsis.innerHTML = ''; + paginationButtons.appendChild(ellipsis); + } + paginationButtons.appendChild(this.createPageItem(numPages, parentTableSelector, currentPage)); + } + + if (hasNext) { + const nextPageItem = document.createElement('li'); + nextPageItem.className = 'usa-pagination__item usa-pagination__arrow'; + nextPageItem.innerHTML = ` + + Next + + + `; + nextPageItem.querySelector('a').addEventListener('click', (event) => { + event.preventDefault(); + this.loadTable(currentPage + 1); + }); + paginationButtons.appendChild(nextPageItem); + } + } + + /** + * A helper that toggles content/ no content/ no search results + * + */ + updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => { + const { unfiltered_total, total } = data; + if (unfiltered_total) { + if (total) { + showElement(dataWrapper); + hideElement(noSearchResultsWrapper); + hideElement(noDataWrapper); } else { hideElement(dataWrapper); - hideElement(noSearchResultsWrapper); - showElement(noDataWrapper); + showElement(noSearchResultsWrapper); + hideElement(noDataWrapper); } - }; - - // Helper function to create a page item - createPageItem(page, parentTableSelector, currentPage) { - const pageItem = document.createElement('li'); - pageItem.className = 'usa-pagination__item usa-pagination__page-no'; - pageItem.innerHTML = ` - ${page} - `; - if (page === currentPage) { - pageItem.querySelector('a').classList.add('usa-current'); - pageItem.querySelector('a').setAttribute('aria-current', 'page'); - } - pageItem.querySelector('a').addEventListener('click', (event) => { - event.preventDefault(); - this.loadTable(page); - }); - return pageItem; + } else { + hideElement(dataWrapper); + hideElement(noSearchResultsWrapper); + showElement(noDataWrapper); } - - /** - * A helper that resets sortable table headers - * - */ - unsetHeader = (header) => { - header.removeAttribute('aria-sort'); - let headerName = header.innerText; - const headerLabel = `${headerName}, sortable column, currently unsorted"`; - const headerButtonLabel = `Click to sort by ascending order.`; - header.setAttribute("aria-label", headerLabel); - header.querySelector('.usa-table__header__button').setAttribute("title", headerButtonLabel); - }; - - // Abstract method (to be implemented in the child class) - loadTable(page, sortBy, order) { - throw new Error('loadData() must be implemented in a subclass'); + }; + + // Helper function to create a page item + createPageItem(page, parentTableSelector, currentPage) { + const pageItem = document.createElement('li'); + pageItem.className = 'usa-pagination__item usa-pagination__page-no'; + pageItem.innerHTML = ` + ${page} + `; + if (page === currentPage) { + pageItem.querySelector('a').classList.add('usa-current'); + pageItem.querySelector('a').setAttribute('aria-current', 'page'); } - - // Add event listeners to table headers for sorting - initializeTableHeaders() { - this.tableHeaders.forEach(header => { - header.addEventListener('click', () => { - const sortBy = header.getAttribute('data-sortable'); - let order = 'asc'; - // sort order will be ascending, unless the currently sorted column is ascending, and the user - // is selecting the same column to sort in descending order - if (sortBy === this.currentSortBy) { - order = this.currentOrder === 'asc' ? 'desc' : 'asc'; - } - // load the results with the updated sort - this.loadTable(1, sortBy, order); - }); - }); - } - - initializeSearchHandler() { - this.searchSubmit.addEventListener('click', (e) => { - e.preventDefault(); - this.currentSearchTerm = this.searchInput.value; - // If the search is blank, we match the resetSearch functionality - if (this.currentSearchTerm) { - showElement(this.resetSearchButton); - } else { - hideElement(this.resetSearchButton); + pageItem.querySelector('a').addEventListener('click', (event) => { + event.preventDefault(); + this.loadTable(page); + }); + return pageItem; + } + + /** + * A helper that resets sortable table headers + * + */ + unsetHeader = (header) => { + header.removeAttribute('aria-sort'); + let headerName = header.innerText; + const headerLabel = `${headerName}, sortable column, currently unsorted"`; + const headerButtonLabel = `Click to sort by ascending order.`; + header.setAttribute("aria-label", headerLabel); + header.querySelector('.usa-table__header__button').setAttribute("title", headerButtonLabel); + }; + + // Abstract method (to be implemented in the child class) + loadTable(page, sortBy, order) { + throw new Error('loadData() must be implemented in a subclass'); + } + + // Add event listeners to table headers for sorting + initializeTableHeaders() { + this.tableHeaders.forEach(header => { + header.addEventListener('click', () => { + const sortBy = header.getAttribute('data-sortable'); + let order = 'asc'; + // sort order will be ascending, unless the currently sorted column is ascending, and the user + // is selecting the same column to sort in descending order + if (sortBy === this.currentSortBy) { + order = this.currentOrder === 'asc' ? 'desc' : 'asc'; } - this.loadTable(1, 'id', 'asc'); - this.resetHeaders(); + // load the results with the updated sort + this.loadTable(1, sortBy, order); }); - } - - initializeStatusToggleHandler() { - if (this.statusToggle) { - this.statusToggle.addEventListener('click', () => { - toggleCaret(this.statusToggle); - }); + }); + } + + initializeSearchHandler() { + this.searchSubmit.addEventListener('click', (e) => { + e.preventDefault(); + this.currentSearchTerm = this.searchInput.value; + // If the search is blank, we match the resetSearch functionality + if (this.currentSearchTerm) { + showElement(this.resetSearchButton); + } else { + hideElement(this.resetSearchButton); } - } - - // Add event listeners to status filter checkboxes - initializeFilterCheckboxes() { - this.statusCheckboxes.forEach(checkbox => { - checkbox.addEventListener('change', () => { - const checkboxValue = checkbox.value; - - // Update currentStatus array based on checkbox state - if (checkbox.checked) { - this.currentStatus.push(checkboxValue); - } else { - const index = this.currentStatus.indexOf(checkboxValue); - if (index > -1) { - this.currentStatus.splice(index, 1); - } - } - - // Manage visibility of reset filters button - if (this.currentStatus.length == 0) { - hideElement(this.resetFiltersButton); - } else { - showElement(this.resetFiltersButton); - } - - // Disable the auto scroll - this.scrollToTable = false; - - // Call loadTable with updated status - this.loadTable(1, 'id', 'asc'); - this.resetHeaders(); - this.updateStatusIndicator(); - }); - }); - } - - // Reset UI and accessibility - resetHeaders() { - this.tableHeaders.forEach(header => { - // Unset sort UI in headers - this.unsetHeader(header); - }); - // Reset the announcement region - this.tableAnnouncementRegion.innerHTML = ''; - } - - resetSearch() { - this.searchInput.value = ''; - this.currentSearchTerm = ''; - hideElement(this.resetSearchButton); this.loadTable(1, 'id', 'asc'); this.resetHeaders(); - } - - initializeResetSearchButton() { - if (this.resetSearchButton) { - this.resetSearchButton.addEventListener('click', () => { - this.resetSearch(); - }); - } - } - - resetFilters() { - this.currentStatus = []; - this.statusCheckboxes.forEach(checkbox => { - checkbox.checked = false; - }); - hideElement(this.resetFiltersButton); - - // Disable the auto scroll - this.scrollToTable = false; - - this.loadTable(1, 'id', 'asc'); - this.resetHeaders(); - this.updateStatusIndicator(); - // No need to toggle close the filters. The focus shift will trigger that for us. - } - - initializeResetFiltersButton() { - if (this.resetFiltersButton) { - this.resetFiltersButton.addEventListener('click', () => { - this.resetFilters(); - }); - } - } - - updateStatusIndicator() { - this.statusIndicator.innerHTML = ''; - // Even if the element is empty, it'll mess up the flex layout unless we set display none - hideElement(this.statusIndicator); - if (this.currentStatus.length) - this.statusIndicator.innerHTML = '(' + this.currentStatus.length + ')'; - showElement(this.statusIndicator); - } - - closeFilters() { - if (this.statusToggle.getAttribute("aria-expanded") === "true") { - this.statusToggle.click(); - } - } - - initializeAccordionAccessibilityListeners() { - // Instead of managing the toggle/close on the filter buttons in all edge cases (user clicks on search, user clicks on ANOTHER filter, - // user clicks on main nav...) we add a listener and close the filters whenever the focus shifts out of the dropdown menu/filter button. - // NOTE: We may need to evolve this as we add more filters. - document.addEventListener('focusin', (event) => { - const accordion = document.querySelector('.usa-accordion--select'); - const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]'); - - if (accordionThatIsOpen && !accordion.contains(event.target)) { - this.closeFilters(); - } - }); - - // Close when user clicks outside - // NOTE: We may need to evolve this as we add more filters. - document.addEventListener('click', (event) => { - const accordion = document.querySelector('.usa-accordion--select'); - const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]'); - - if (accordionThatIsOpen && !accordion.contains(event.target)) { - this.closeFilters(); - } + }); + } + + initializeStatusToggleHandler() { + if (this.statusToggle) { + this.statusToggle.addEventListener('click', () => { + toggleCaret(this.statusToggle); }); } } + + // Add event listeners to status filter checkboxes + initializeFilterCheckboxes() { + this.statusCheckboxes.forEach(checkbox => { + checkbox.addEventListener('change', () => { + const checkboxValue = checkbox.value; + + // Update currentStatus array based on checkbox state + if (checkbox.checked) { + this.currentStatus.push(checkboxValue); + } else { + const index = this.currentStatus.indexOf(checkboxValue); + if (index > -1) { + this.currentStatus.splice(index, 1); + } + } + + // Manage visibility of reset filters button + if (this.currentStatus.length == 0) { + hideElement(this.resetFiltersButton); + } else { + showElement(this.resetFiltersButton); + } + + // Disable the auto scroll + this.scrollToTable = false; + + // Call loadTable with updated status + this.loadTable(1, 'id', 'asc'); + this.resetHeaders(); + this.updateStatusIndicator(); + }); + }); + } + + // Reset UI and accessibility + resetHeaders() { + this.tableHeaders.forEach(header => { + // Unset sort UI in headers + this.unsetHeader(header); + }); + // Reset the announcement region + this.tableAnnouncementRegion.innerHTML = ''; + } + + resetSearch() { + this.searchInput.value = ''; + this.currentSearchTerm = ''; + hideElement(this.resetSearchButton); + this.loadTable(1, 'id', 'asc'); + this.resetHeaders(); + } + + initializeResetSearchButton() { + if (this.resetSearchButton) { + this.resetSearchButton.addEventListener('click', () => { + this.resetSearch(); + }); + } + } + + resetFilters() { + this.currentStatus = []; + this.statusCheckboxes.forEach(checkbox => { + checkbox.checked = false; + }); + hideElement(this.resetFiltersButton); + + // Disable the auto scroll + this.scrollToTable = false; + + this.loadTable(1, 'id', 'asc'); + this.resetHeaders(); + this.updateStatusIndicator(); + // No need to toggle close the filters. The focus shift will trigger that for us. + } + + initializeResetFiltersButton() { + if (this.resetFiltersButton) { + this.resetFiltersButton.addEventListener('click', () => { + this.resetFilters(); + }); + } + } + + updateStatusIndicator() { + this.statusIndicator.innerHTML = ''; + // Even if the element is empty, it'll mess up the flex layout unless we set display none + hideElement(this.statusIndicator); + if (this.currentStatus.length) + this.statusIndicator.innerHTML = '(' + this.currentStatus.length + ')'; + showElement(this.statusIndicator); + } + + closeFilters() { + if (this.statusToggle.getAttribute("aria-expanded") === "true") { + this.statusToggle.click(); + } + } + + initializeAccordionAccessibilityListeners() { + // Instead of managing the toggle/close on the filter buttons in all edge cases (user clicks on search, user clicks on ANOTHER filter, + // user clicks on main nav...) we add a listener and close the filters whenever the focus shifts out of the dropdown menu/filter button. + // NOTE: We may need to evolve this as we add more filters. + document.addEventListener('focusin', (event) => { + const accordion = document.querySelector('.usa-accordion--select'); + const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]'); + + if (accordionThatIsOpen && !accordion.contains(event.target)) { + this.closeFilters(); + } + }); + + // Close when user clicks outside + // NOTE: We may need to evolve this as we add more filters. + document.addEventListener('click', (event) => { + const accordion = document.querySelector('.usa-accordion--select'); + const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]'); + + if (accordionThatIsOpen && !accordion.contains(event.target)) { + this.closeFilters(); + } + }); + } +} diff --git a/src/registrar/assets/modules/table-domain-requests.js b/src/registrar/assets/modules/table-domain-requests.js index 53af5d619..55b2c951c 100644 --- a/src/registrar/assets/modules/table-domain-requests.js +++ b/src/registrar/assets/modules/table-domain-requests.js @@ -1,6 +1,6 @@ -import { hideElement, showElement, scrollToElement, toggleCaret } from './helpers.js'; -import { initializeTooltips, initializeModals, unloadModals } from './helpers-uswds.js'; -import { getCsrfToken } from './get-csrf-token.js'; +import { hideElement, showElement, scrollToElement } from './helpers.js'; +import { initializeModals, unloadModals } from './helpers-uswds.js'; +import { getCsrfToken } from './helpers-csrf-token.js'; import { LoadTableBase } from './table-base.js'; @@ -22,381 +22,375 @@ const utcDateString = (dateString) => { export class DomainRequestsTable extends LoadTableBase { - constructor() { - super('domain-requests'); - } - - toggleExportButton(requests) { - const exportButton = document.getElementById('export-csv'); - if (exportButton) { - if (requests.length > 0) { - showElement(exportButton); - } else { - hideElement(exportButton); - } - } + constructor() { + super('domain-requests'); } - /** - * Loads rows in the domains list, as well as updates pagination around the domains list - * based on the supplied attributes. - * @param {*} page - the page number of the results (starts with 1) - * @param {*} sortBy - the sort column option - * @param {*} order - the sort order {asc, desc} - * @param {*} scroll - control for the scrollToElement functionality - * @param {*} status - control for the status filter - * @param {*} searchTerm - the search term - * @param {*} portfolio - the portfolio id - */ - loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm = this.currentSearchTerm, portfolio = this.portfolioValue) { - let baseUrl = document.getElementById("get_domain_requests_json_url"); - - if (!baseUrl) { - return; - } - - let baseUrlValue = baseUrl.innerHTML; - if (!baseUrlValue) { - return; - } - - // add searchParams - let searchParams = new URLSearchParams( - { - "page": page, - "sort_by": sortBy, - "order": order, - "status": status, - "search_term": searchTerm + toggleExportButton(requests) { + const exportButton = document.getElementById('export-csv'); + if (exportButton) { + if (requests.length > 0) { + showElement(exportButton); + } else { + hideElement(exportButton); } - ); - if (portfolio) - searchParams.append("portfolio", portfolio) - - let url = `${baseUrlValue}?${searchParams.toString()}` - fetch(url) - .then(response => response.json()) - .then(data => { - if (data.error) { - console.error('Error in AJAX call: ' + data.error); - return; + } +} + + /** + * Loads rows in the domains list, as well as updates pagination around the domains list + * based on the supplied attributes. + * @param {*} page - the page number of the results (starts with 1) + * @param {*} sortBy - the sort column option + * @param {*} order - the sort order {asc, desc} + * @param {*} scroll - control for the scrollToElement functionality + * @param {*} status - control for the status filter + * @param {*} searchTerm - the search term + * @param {*} portfolio - the portfolio id + */ + loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm = this.currentSearchTerm, portfolio = this.portfolioValue) { + let baseUrl = document.getElementById("get_domain_requests_json_url"); + + if (!baseUrl) { + return; + } + + let baseUrlValue = baseUrl.innerHTML; + if (!baseUrlValue) { + return; + } + + // add searchParams + let searchParams = new URLSearchParams( + { + "page": page, + "sort_by": sortBy, + "order": order, + "status": status, + "search_term": searchTerm + } + ); + if (portfolio) + searchParams.append("portfolio", portfolio) + + let url = `${baseUrlValue}?${searchParams.toString()}` + fetch(url) + .then(response => response.json()) + .then(data => { + if (data.error) { + console.error('Error in AJAX call: ' + data.error); + return; + } + + // Manage "export as CSV" visibility for domain requests + this.toggleExportButton(data.domain_requests); + + // handle the display of proper messaging in the event that no requests exist in the list or search returns no results + this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); + + // identify the DOM element where the domain request list will be inserted into the DOM + const tbody = document.querySelector('#domain-requests tbody'); + tbody.innerHTML = ''; + + // Unload modals will re-inject the DOM with the initial placeholders to allow for .on() in regular use cases + // We do NOT want that as it will cause multiple placeholders and therefore multiple inits on delete, + // which will cause bad delete requests to be sent. + const preExistingModalPlaceholders = document.querySelectorAll('[data-placeholder-for^="toggle-delete-domain-alert"]'); + preExistingModalPlaceholders.forEach(element => { + element.remove(); + }); + + // remove any existing modal elements from the DOM so they can be properly re-initialized + // after the DOM content changes and there are new delete modal buttons added + unloadModals(); + + let needsDeleteColumn = false; + + needsDeleteColumn = data.domain_requests.some(request => request.is_deletable); + + // Remove existing delete th and td if they exist + let existingDeleteTh = document.querySelector('.delete-header'); + if (!needsDeleteColumn) { + if (existingDeleteTh) + existingDeleteTh.remove(); + } else { + if (!existingDeleteTh) { + const delheader = document.createElement('th'); + delheader.setAttribute('scope', 'col'); + delheader.setAttribute('role', 'columnheader'); + delheader.setAttribute('class', 'delete-header'); + delheader.innerHTML = ` + Delete Action`; + let tableHeaderRow = document.querySelector('#domain-requests thead tr'); + tableHeaderRow.appendChild(delheader); } - - // Manage "export as CSV" visibility for domain requests - this.toggleExportButton(data.domain_requests); - - // handle the display of proper messaging in the event that no requests exist in the list or search returns no results - this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); - - // identify the DOM element where the domain request list will be inserted into the DOM - const tbody = document.querySelector('#domain-requests tbody'); - tbody.innerHTML = ''; - - // Unload modals will re-inject the DOM with the initial placeholders to allow for .on() in regular use cases - // We do NOT want that as it will cause multiple placeholders and therefore multiple inits on delete, - // which will cause bad delete requests to be sent. - const preExistingModalPlaceholders = document.querySelectorAll('[data-placeholder-for^="toggle-delete-domain-alert"]'); - preExistingModalPlaceholders.forEach(element => { - element.remove(); - }); - - // remove any existing modal elements from the DOM so they can be properly re-initialized - // after the DOM content changes and there are new delete modal buttons added - unloadModals(); - - let needsDeleteColumn = false; - - needsDeleteColumn = data.domain_requests.some(request => request.is_deletable); - - // Remove existing delete th and td if they exist - let existingDeleteTh = document.querySelector('.delete-header'); - if (!needsDeleteColumn) { - if (existingDeleteTh) - existingDeleteTh.remove(); - } else { - if (!existingDeleteTh) { - const delheader = document.createElement('th'); - delheader.setAttribute('scope', 'col'); - delheader.setAttribute('role', 'columnheader'); - delheader.setAttribute('class', 'delete-header'); - delheader.innerHTML = ` - Delete Action`; - let tableHeaderRow = document.querySelector('#domain-requests thead tr'); - tableHeaderRow.appendChild(delheader); - } + } + + data.domain_requests.forEach(request => { + const options = { year: 'numeric', month: 'short', day: 'numeric' }; + const domainName = request.requested_domain ? request.requested_domain : `New domain request
(${utcDateString(request.created_at)})`; + const actionUrl = request.action_url; + const actionLabel = request.action_label; + const submissionDate = request.last_submitted_date ? new Date(request.last_submitted_date).toLocaleDateString('en-US', options) : `Not submitted`; + + // The markup for the delete function either be a simple trigger or a 3 dots menu with a hidden trigger (in the case of portfolio requests page) + // If the request is not deletable, use the following (hidden) span for ANDI screenreaders to indicate this state to the end user + let modalTrigger = ` + Domain request cannot be deleted now. Edit the request for more information.`; + + let markupCreatorRow = ''; + + if (this.portfolioValue) { + markupCreatorRow = ` + + ${request.creator ? request.creator : ''} + + ` } - - data.domain_requests.forEach(request => { - const options = { year: 'numeric', month: 'short', day: 'numeric' }; - const domainName = request.requested_domain ? request.requested_domain : `New domain request
(${utcDateString(request.created_at)})`; - const actionUrl = request.action_url; - const actionLabel = request.action_label; - const submissionDate = request.last_submitted_date ? new Date(request.last_submitted_date).toLocaleDateString('en-US', options) : `Not submitted`; - - // The markup for the delete function either be a simple trigger or a 3 dots menu with a hidden trigger (in the case of portfolio requests page) - // If the request is not deletable, use the following (hidden) span for ANDI screenreaders to indicate this state to the end user - let modalTrigger = ` - Domain request cannot be deleted now. Edit the request for more information.`; - - let markupCreatorRow = ''; - - if (this.portfolioValue) { - markupCreatorRow = ` - - ${request.creator ? request.creator : ''} - - ` - } - - if (request.is_deletable) { - // If the request is deletable, create modal body and insert it. This is true for both requests and portfolio requests pages - let modalHeading = ''; - let modalDescription = ''; - - if (request.requested_domain) { - modalHeading = `Are you sure you want to delete ${request.requested_domain}?`; - modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.'; + + if (request.is_deletable) { + // If the request is deletable, create modal body and insert it. This is true for both requests and portfolio requests pages + let modalHeading = ''; + let modalDescription = ''; + + if (request.requested_domain) { + modalHeading = `Are you sure you want to delete ${request.requested_domain}?`; + modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.'; + } else { + if (request.created_at) { + modalHeading = 'Are you sure you want to delete this domain request?'; + modalDescription = `This will remove the domain request (created ${utcDateString(request.created_at)}) from the .gov registrar. This action cannot be undone`; } else { - if (request.created_at) { - modalHeading = 'Are you sure you want to delete this domain request?'; - modalDescription = `This will remove the domain request (created ${utcDateString(request.created_at)}) from the .gov registrar. This action cannot be undone`; - } else { - modalHeading = 'Are you sure you want to delete New domain request?'; - modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.'; - } + modalHeading = 'Are you sure you want to delete New domain request?'; + modalDescription = 'This will remove the domain request from the .gov registrar. This action cannot be undone.'; } - - modalTrigger = ` - - Delete ${domainName} - ` - - const modalSubmit = ` - - ` - - const modal = document.createElement('div'); - modal.setAttribute('class', 'usa-modal'); - modal.setAttribute('id', `toggle-delete-domain-alert-${request.id}`); - modal.setAttribute('aria-labelledby', 'Are you sure you want to continue?'); - modal.setAttribute('aria-describedby', 'Domain will be removed'); - modal.setAttribute('data-force-action', ''); - - modal.innerHTML = ` -
-
- -
- -
- + } + + modalTrigger = ` + + Delete ${domainName} + ` + + const modalSubmit = ` + + ` + + const modal = document.createElement('div'); + modal.setAttribute('class', 'usa-modal'); + modal.setAttribute('id', `toggle-delete-domain-alert-${request.id}`); + modal.setAttribute('aria-labelledby', 'Are you sure you want to continue?'); + modal.setAttribute('aria-describedby', 'Domain will be removed'); + modal.setAttribute('data-force-action', ''); + + modal.innerHTML = ` +
+
+ +
+
+ +
+ +
+ ` + + this.tableWrapper.appendChild(modal); + + // Request is deletable, modal and modalTrigger are built. Now check if we are on the portfolio requests page (by seeing if there is a portfolio value) and enhance the modalTrigger accordingly + if (this.portfolioValue) { + modalTrigger = ` + + Delete ${domainName} + + +
+
- ` - - this.tableWrapper.appendChild(modal); - - // Request is deletable, modal and modalTrigger are built. Now check if we are on the portfolio requests page (by seeing if there is a portfolio value) and enhance the modalTrigger accordingly - if (this.portfolioValue) { - modalTrigger = ` - - Delete ${domainName} - - -
-
- -
- + - ` - } +
+ ` } - - - const row = document.createElement('tr'); - row.innerHTML = ` - - ${domainName} - - - ${submissionDate} - - ${markupCreatorRow} - - ${request.status} - - - - - ${actionLabel} ${request.requested_domain ? request.requested_domain : 'New domain request'} - - - ${needsDeleteColumn ? ''+modalTrigger+'' : ''} - `; - tbody.appendChild(row); + } + + + const row = document.createElement('tr'); + row.innerHTML = ` + + ${domainName} + + + ${submissionDate} + + ${markupCreatorRow} + + ${request.status} + + + + + ${actionLabel} ${request.requested_domain ? request.requested_domain : 'New domain request'} + + + ${needsDeleteColumn ? ''+modalTrigger+'' : ''} + `; + tbody.appendChild(row); + }); + + // initialize modals immediately after the DOM content is updated + initializeModals(); + + // Now the DOM and modals are ready, add listeners to the submit buttons + const modals = document.querySelectorAll('.usa-modal__content'); + + modals.forEach(modal => { + const submitButton = modal.querySelector('.usa-modal__submit'); + const closeButton = modal.querySelector('.usa-modal__close'); + submitButton.addEventListener('click', () => { + let pk = submitButton.getAttribute('data-pk'); + // Close the modal to remove the USWDS UI local classes + closeButton.click(); + // If we're deleting the last item on a page that is not page 1, we'll need to refresh the display to the previous page + let pageToDisplay = data.page; + if (data.total == 1 && data.unfiltered_total > 1) { + pageToDisplay--; + } + this.deleteDomainRequest(pk, pageToDisplay); }); - - // initialize modals immediately after the DOM content is updated - initializeModals(); - - // Now the DOM and modals are ready, add listeners to the submit buttons - const modals = document.querySelectorAll('.usa-modal__content'); - - modals.forEach(modal => { - const submitButton = modal.querySelector('.usa-modal__submit'); - const closeButton = modal.querySelector('.usa-modal__close'); - submitButton.addEventListener('click', () => { - let pk = submitButton.getAttribute('data-pk'); - // Close the modal to remove the USWDS UI local classes - closeButton.click(); - // If we're deleting the last item on a page that is not page 1, we'll need to refresh the display to the previous page - let pageToDisplay = data.page; - if (data.total == 1 && data.unfiltered_total > 1) { - pageToDisplay--; - } - this.deleteDomainRequest(pk, pageToDisplay); - }); - }); - - // Do not scroll on first page load - if (scroll) - scrollToElement('class', 'domain-requests'); - this.scrollToTable = true; - - // update the pagination after the domain requests list is updated - this.updatePagination( - 'domain request', - '#domain-requests-pagination', - '#domain-requests-pagination .usa-pagination__counter', - '#domain-requests', - data.page, - data.num_pages, - data.has_previous, - data.has_next, - data.total, - ); - this.currentSortBy = sortBy; - this.currentOrder = order; - this.currentSearchTerm = searchTerm; - }) - .catch(error => console.error('Error fetching domain requests:', error)); - } - - /** - * Delete is actually a POST API that requires a csrf token. The token will be waiting for us in the template as a hidden input. - * @param {*} domainRequestPk - the identifier for the request that we're deleting - * @param {*} pageToDisplay - If we're deleting the last item on a page that is not page 1, we'll need to display the previous page - */ - deleteDomainRequest(domainRequestPk, pageToDisplay) { - // Use to debug uswds modal issues - //console.log('deleteDomainRequest') - - // Get csrf token - const csrfToken = getCsrfToken(); - // Create FormData object and append the CSRF token - const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}&delete-domain-request=`; - - fetch(`/domain-request/${domainRequestPk}/delete`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-CSRFToken': csrfToken, - }, - body: formData - }) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - // Update data and UI - this.loadTable(pageToDisplay, this.currentSortBy, this.currentOrder, this.scrollToTable, this.currentSearchTerm); + }); + + // Do not scroll on first page load + if (scroll) + scrollToElement('class', 'domain-requests'); + this.scrollToTable = true; + + // update the pagination after the domain requests list is updated + this.updatePagination( + 'domain request', + '#domain-requests-pagination', + '#domain-requests-pagination .usa-pagination__counter', + '#domain-requests', + data.page, + data.num_pages, + data.has_previous, + data.has_next, + data.total, + ); + this.currentSortBy = sortBy; + this.currentOrder = order; + this.currentSearchTerm = searchTerm; }) .catch(error => console.error('Error fetching domain requests:', error)); - } } - /** - * An IIFE that listens for DOM Content to be loaded, then executes. This function - * initializes the domain requests list and associated functionality. - * - */ + * Delete is actually a POST API that requires a csrf token. The token will be waiting for us in the template as a hidden input. + * @param {*} domainRequestPk - the identifier for the request that we're deleting + * @param {*} pageToDisplay - If we're deleting the last item on a page that is not page 1, we'll need to display the previous page + */ + deleteDomainRequest(domainRequestPk, pageToDisplay) { + // Use to debug uswds modal issues + //console.log('deleteDomainRequest') + + // Get csrf token + const csrfToken = getCsrfToken(); + // Create FormData object and append the CSRF token + const formData = `csrfmiddlewaretoken=${encodeURIComponent(csrfToken)}&delete-domain-request=`; + + fetch(`/domain-request/${domainRequestPk}/delete`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-CSRFToken': csrfToken, + }, + body: formData + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + // Update data and UI + this.loadTable(pageToDisplay, this.currentSortBy, this.currentOrder, this.scrollToTable, this.currentSearchTerm); + }) + .catch(error => console.error('Error fetching domain requests:', error)); + } +} + export function initDomainRequestsTable() { document.addEventListener('DOMContentLoaded', function() { const domainRequestsSectionWrapper = document.getElementById('domain-requests'); @@ -433,4 +427,4 @@ export function initDomainRequestsTable() { }); } }); -} \ No newline at end of file +} diff --git a/src/registrar/assets/modules/table-domains.js b/src/registrar/assets/modules/table-domains.js index 651e23e6b..30677a60d 100644 --- a/src/registrar/assets/modules/table-domains.js +++ b/src/registrar/assets/modules/table-domains.js @@ -1,152 +1,146 @@ -import { hideElement, showElement, scrollToElement, toggleCaret } from './helpers.js'; -import { initializeTooltips, initializeModals, unloadModals } from './helpers-uswds.js'; +import { scrollToElement } from './helpers.js'; +import { initializeTooltips } from './helpers-uswds.js'; import { LoadTableBase } from './table-base.js'; export class DomainsTable extends LoadTableBase { - constructor() { - super('domains'); - } - /** - * Loads rows in the domains list, as well as updates pagination around the domains list - * based on the supplied attributes. - * @param {*} page - the page number of the results (starts with 1) - * @param {*} sortBy - the sort column option - * @param {*} order - the sort order {asc, desc} - * @param {*} scroll - control for the scrollToElement functionality - * @param {*} status - control for the status filter - * @param {*} searchTerm - the search term - * @param {*} portfolio - the portfolio id - */ - loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) { - - // fetch json of page of domais, given params - let baseUrl = document.getElementById("get_domains_json_url"); - if (!baseUrl) { - return; - } - - let baseUrlValue = baseUrl.innerHTML; - if (!baseUrlValue) { - return; - } - - // fetch json of page of domains, given params - let searchParams = new URLSearchParams( - { - "page": page, - "sort_by": sortBy, - "order": order, - "status": status, - "search_term": searchTerm - } - ); - if (portfolio) - searchParams.append("portfolio", portfolio) - - let url = `${baseUrlValue}?${searchParams.toString()}` - fetch(url) - .then(response => response.json()) - .then(data => { - if (data.error) { - console.error('Error in AJAX call: ' + data.error); - return; - } - - // handle the display of proper messaging in the event that no domains exist in the list or search returns no results - this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); - - // identify the DOM element where the domain list will be inserted into the DOM - const domainList = document.querySelector('#domains tbody'); - domainList.innerHTML = ''; - - data.domains.forEach(domain => { - const options = { year: 'numeric', month: 'short', day: 'numeric' }; - const expirationDate = domain.expiration_date ? new Date(domain.expiration_date) : null; - const expirationDateFormatted = expirationDate ? expirationDate.toLocaleDateString('en-US', options) : ''; - const expirationDateSortValue = expirationDate ? expirationDate.getTime() : ''; - const actionUrl = domain.action_url; - const suborganization = domain.domain_info__sub_organization ? domain.domain_info__sub_organization : '⎯'; - - const row = document.createElement('tr'); - - let markupForSuborganizationRow = ''; - - if (this.portfolioValue) { - markupForSuborganizationRow = ` - - ${suborganization} - - ` - } - - row.innerHTML = ` - - ${domain.name} - - - ${expirationDateFormatted} - - - ${domain.state_display} - - - - - ${markupForSuborganizationRow} - - - - ${domain.action_label} ${domain.name} - - - `; - domainList.appendChild(row); - }); - // initialize tool tips immediately after the associated DOM elements are added - initializeTooltips(); - - // Do not scroll on first page load - if (scroll) - scrollToElement('class', 'domains'); - this.scrollToTable = true; - - // update pagination - this.updatePagination( - 'domain', - '#domains-pagination', - '#domains-pagination .usa-pagination__counter', - '#domains', - data.page, - data.num_pages, - data.has_previous, - data.has_next, - data.total, - ); - this.currentSortBy = sortBy; - this.currentOrder = order; - this.currentSearchTerm = searchTerm; - }) - .catch(error => console.error('Error fetching domains:', error)); - } + constructor() { + super('domains'); } - - /** - * An IIFE that listens for DOM Content to be loaded, then executes. This function - * initializes the domains list and associated functionality. - * - */ + * Loads rows in the domains list, as well as updates pagination around the domains list + * based on the supplied attributes. + * @param {*} page - the page number of the results (starts with 1) + * @param {*} sortBy - the sort column option + * @param {*} order - the sort order {asc, desc} + * @param {*} scroll - control for the scrollToElement functionality + * @param {*} status - control for the status filter + * @param {*} searchTerm - the search term + * @param {*} portfolio - the portfolio id + */ + loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) { + + // fetch json of page of domais, given params + let baseUrl = document.getElementById("get_domains_json_url"); + if (!baseUrl) { + return; + } + + let baseUrlValue = baseUrl.innerHTML; + if (!baseUrlValue) { + return; + } + + // fetch json of page of domains, given params + let searchParams = new URLSearchParams( + { + "page": page, + "sort_by": sortBy, + "order": order, + "status": status, + "search_term": searchTerm + } + ); + if (portfolio) + searchParams.append("portfolio", portfolio) + + let url = `${baseUrlValue}?${searchParams.toString()}` + fetch(url) + .then(response => response.json()) + .then(data => { + if (data.error) { + console.error('Error in AJAX call: ' + data.error); + return; + } + + // handle the display of proper messaging in the event that no domains exist in the list or search returns no results + this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); + + // identify the DOM element where the domain list will be inserted into the DOM + const domainList = document.querySelector('#domains tbody'); + domainList.innerHTML = ''; + + data.domains.forEach(domain => { + const options = { year: 'numeric', month: 'short', day: 'numeric' }; + const expirationDate = domain.expiration_date ? new Date(domain.expiration_date) : null; + const expirationDateFormatted = expirationDate ? expirationDate.toLocaleDateString('en-US', options) : ''; + const expirationDateSortValue = expirationDate ? expirationDate.getTime() : ''; + const actionUrl = domain.action_url; + const suborganization = domain.domain_info__sub_organization ? domain.domain_info__sub_organization : '⎯'; + + const row = document.createElement('tr'); + + let markupForSuborganizationRow = ''; + + if (this.portfolioValue) { + markupForSuborganizationRow = ` + + ${suborganization} + + ` + } + + row.innerHTML = ` + + ${domain.name} + + + ${expirationDateFormatted} + + + ${domain.state_display} + + + + + ${markupForSuborganizationRow} + + + + ${domain.action_label} ${domain.name} + + + `; + domainList.appendChild(row); + }); + // initialize tool tips immediately after the associated DOM elements are added + initializeTooltips(); + + // Do not scroll on first page load + if (scroll) + scrollToElement('class', 'domains'); + this.scrollToTable = true; + + // update pagination + this.updatePagination( + 'domain', + '#domains-pagination', + '#domains-pagination .usa-pagination__counter', + '#domains', + data.page, + data.num_pages, + data.has_previous, + data.has_next, + data.total, + ); + this.currentSortBy = sortBy; + this.currentOrder = order; + this.currentSearchTerm = searchTerm; + }) + .catch(error => console.error('Error fetching domains:', error)); + } +} + export function initDomainsTable() { document.addEventListener('DOMContentLoaded', function() { const isDomainsPage = document.getElementById("domains") @@ -158,7 +152,4 @@ export function initDomainsTable() { } } }); - } - - diff --git a/src/registrar/assets/modules/table-member-domains.js b/src/registrar/assets/modules/table-member-domains.js index afb61fce1..4bfd11d62 100644 --- a/src/registrar/assets/modules/table-member-domains.js +++ b/src/registrar/assets/modules/table-member-domains.js @@ -1,126 +1,125 @@ import { hideElement, showElement, scrollToElement, toggleCaret } from './helpers.js'; import { initializeTooltips, initializeModals, unloadModals } from './helpers-uswds.js'; -import { getCsrfToken } from './get-csrf-token.js'; +import { getCsrfToken } from './helpers-csrf-token.js'; import { LoadTableBase } from './table-base.js'; export class MemberDomainsTable extends LoadTableBase { - constructor() { - super('member-domains'); - this.currentSortBy = 'name'; - } - /** - * Loads rows in the members list, as well as updates pagination around the members list - * based on the supplied attributes. - * @param {*} page - the page number of the results (starts with 1) - * @param {*} sortBy - the sort column option - * @param {*} order - the sort order {asc, desc} - * @param {*} scroll - control for the scrollToElement functionality - * @param {*} searchTerm - the search term - * @param {*} portfolio - the portfolio id - */ - loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) { - - // --------- SEARCH - let searchParams = new URLSearchParams( - { - "page": page, - "sort_by": sortBy, - "order": order, - "search_term": searchTerm, - } - ); - - let emailValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-email') : null; - let memberIdValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-member-id') : null; - let memberOnly = this.portfolioElement ? this.portfolioElement.getAttribute('data-member-only') : null; - - if (portfolio) - searchParams.append("portfolio", portfolio) - if (emailValue) - searchParams.append("email", emailValue) - if (memberIdValue) - searchParams.append("member_id", memberIdValue) - if (memberOnly) - searchParams.append("member_only", memberOnly) - - - // --------- FETCH DATA - // fetch json of page of domais, given params - let baseUrl = document.getElementById("get_member_domains_json_url"); - if (!baseUrl) { - return; - } - - let baseUrlValue = baseUrl.innerHTML; - if (!baseUrlValue) { - return; - } - - let url = `${baseUrlValue}?${searchParams.toString()}` //TODO: uncomment for search function - fetch(url) - .then(response => response.json()) - .then(data => { - if (data.error) { - console.error('Error in AJAX call: ' + data.error); - return; - } - - // handle the display of proper messaging in the event that no members exist in the list or search returns no results - this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); - - // identify the DOM element where the domain list will be inserted into the DOM - const memberDomainsList = document.querySelector('#member-domains tbody'); - memberDomainsList.innerHTML = ''; - - - data.domains.forEach(domain => { - const row = document.createElement('tr'); - - row.innerHTML = ` - - ${domain.name} - - `; - memberDomainsList.appendChild(row); - }); - - // Do not scroll on first page load - if (scroll) - scrollToElement('class', 'member-domains'); - this.scrollToTable = true; - - // update pagination - this.updatePagination( - 'member domain', - '#member-domains-pagination', - '#member-domains-pagination .usa-pagination__counter', - '#member-domains', - data.page, - data.num_pages, - data.has_previous, - data.has_next, - data.total, - ); - this.currentSortBy = sortBy; - this.currentOrder = order; - this.currentSearchTerm = searchTerm; - }) - .catch(error => console.error('Error fetching domains:', error)); - } + constructor() { + super('member-domains'); + this.currentSortBy = 'name'; } - + /** + * Loads rows in the members list, as well as updates pagination around the members list + * based on the supplied attributes. + * @param {*} page - the page number of the results (starts with 1) + * @param {*} sortBy - the sort column option + * @param {*} order - the sort order {asc, desc} + * @param {*} scroll - control for the scrollToElement functionality + * @param {*} searchTerm - the search term + * @param {*} portfolio - the portfolio id + */ + loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) { - export function initMemberDomainsTable() { - document.addEventListener('DOMContentLoaded', function() { - const isMemberDomainsPage = document.getElementById("member-domains") - if (isMemberDomainsPage){ - const memberDomainsTable = new MemberDomainsTable(); - if (memberDomainsTable.tableWrapper) { - // Initial load - memberDomainsTable.loadTable(1); - } + // --------- SEARCH + let searchParams = new URLSearchParams( + { + "page": page, + "sort_by": sortBy, + "order": order, + "search_term": searchTerm, + } + ); + + let emailValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-email') : null; + let memberIdValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-member-id') : null; + let memberOnly = this.portfolioElement ? this.portfolioElement.getAttribute('data-member-only') : null; + + if (portfolio) + searchParams.append("portfolio", portfolio) + if (emailValue) + searchParams.append("email", emailValue) + if (memberIdValue) + searchParams.append("member_id", memberIdValue) + if (memberOnly) + searchParams.append("member_only", memberOnly) + + + // --------- FETCH DATA + // fetch json of page of domais, given params + let baseUrl = document.getElementById("get_member_domains_json_url"); + if (!baseUrl) { + return; + } + + let baseUrlValue = baseUrl.innerHTML; + if (!baseUrlValue) { + return; + } + + let url = `${baseUrlValue}?${searchParams.toString()}` //TODO: uncomment for search function + fetch(url) + .then(response => response.json()) + .then(data => { + if (data.error) { + console.error('Error in AJAX call: ' + data.error); + return; } - }); - } \ No newline at end of file + + // handle the display of proper messaging in the event that no members exist in the list or search returns no results + this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); + + // identify the DOM element where the domain list will be inserted into the DOM + const memberDomainsList = document.querySelector('#member-domains tbody'); + memberDomainsList.innerHTML = ''; + + + data.domains.forEach(domain => { + const row = document.createElement('tr'); + + row.innerHTML = ` + + ${domain.name} + + `; + memberDomainsList.appendChild(row); + }); + + // Do not scroll on first page load + if (scroll) + scrollToElement('class', 'member-domains'); + this.scrollToTable = true; + + // update pagination + this.updatePagination( + 'member domain', + '#member-domains-pagination', + '#member-domains-pagination .usa-pagination__counter', + '#member-domains', + data.page, + data.num_pages, + data.has_previous, + data.has_next, + data.total, + ); + this.currentSortBy = sortBy; + this.currentOrder = order; + this.currentSearchTerm = searchTerm; + }) + .catch(error => console.error('Error fetching domains:', error)); + } +} + +export function initMemberDomainsTable() { + document.addEventListener('DOMContentLoaded', function() { + const isMemberDomainsPage = document.getElementById("member-domains") + if (isMemberDomainsPage){ + const memberDomainsTable = new MemberDomainsTable(); + if (memberDomainsTable.tableWrapper) { + // Initial load + memberDomainsTable.loadTable(1); + } + } + }); +} diff --git a/src/registrar/assets/modules/table-members.js b/src/registrar/assets/modules/table-members.js index 1fd0168a6..f75b4c9e3 100644 --- a/src/registrar/assets/modules/table-members.js +++ b/src/registrar/assets/modules/table-members.js @@ -1,367 +1,358 @@ -import { hideElement, showElement, scrollToElement, toggleCaret } from './helpers.js'; -import { initializeTooltips, initializeModals, unloadModals } from './helpers-uswds.js'; -import { getCsrfToken } from './get-csrf-token.js'; +import { hideElement, showElement, scrollToElement } from './helpers.js'; import { LoadTableBase } from './table-base.js'; export class MembersTable extends LoadTableBase { - constructor() { - super('members'); - } - + constructor() { + super('members'); + } + + /** + * Initializes "Show More" buttons on the page, enabling toggle functionality to show or hide content. + * + * The function finds elements with "Show More" buttons and sets up a click event listener to toggle the visibility + * of a corresponding content div. When clicked, the button updates its visual state (e.g., text/icon change), + * and the associated content is shown or hidden based on its current visibility status. + * + * @function initShowMoreButtons + */ + initShowMoreButtons() { /** - * Initializes "Show More" buttons on the page, enabling toggle functionality to show or hide content. - * - * The function finds elements with "Show More" buttons and sets up a click event listener to toggle the visibility - * of a corresponding content div. When clicked, the button updates its visual state (e.g., text/icon change), - * and the associated content is shown or hidden based on its current visibility status. + * Toggles the visibility of a content section when the "Show More" button is clicked. + * Updates the button text/icon based on whether the content is shown or hidden. * - * @function initShowMoreButtons + * @param {HTMLElement} toggleButton - The button that toggles the content visibility. + * @param {HTMLElement} contentDiv - The content div whose visibility is toggled. + * @param {HTMLElement} buttonParentRow - The parent row element containing the button. */ - initShowMoreButtons() { - /** - * Toggles the visibility of a content section when the "Show More" button is clicked. - * Updates the button text/icon based on whether the content is shown or hidden. - * - * @param {HTMLElement} toggleButton - The button that toggles the content visibility. - * @param {HTMLElement} contentDiv - The content div whose visibility is toggled. - * @param {HTMLElement} buttonParentRow - The parent row element containing the button. - */ - function toggleShowMoreButton(toggleButton, contentDiv, buttonParentRow) { - const spanElement = toggleButton.querySelector('span'); - const useElement = toggleButton.querySelector('use'); - if (contentDiv.classList.contains('display-none')) { - showElement(contentDiv); - spanElement.textContent = 'Close'; - useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less'); - buttonParentRow.classList.add('hide-td-borders'); - toggleButton.setAttribute('aria-label', 'Close additional information'); - } else { - hideElement(contentDiv); - spanElement.textContent = 'Expand'; - useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more'); - buttonParentRow.classList.remove('hide-td-borders'); - toggleButton.setAttribute('aria-label', 'Expand for additional information'); - } + function toggleShowMoreButton(toggleButton, contentDiv, buttonParentRow) { + const spanElement = toggleButton.querySelector('span'); + const useElement = toggleButton.querySelector('use'); + if (contentDiv.classList.contains('display-none')) { + showElement(contentDiv); + spanElement.textContent = 'Close'; + useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less'); + buttonParentRow.classList.add('hide-td-borders'); + toggleButton.setAttribute('aria-label', 'Close additional information'); + } else { + hideElement(contentDiv); + spanElement.textContent = 'Expand'; + useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more'); + buttonParentRow.classList.remove('hide-td-borders'); + toggleButton.setAttribute('aria-label', 'Expand for additional information'); } - - let toggleButtons = document.querySelectorAll('.usa-button--show-more-button'); - toggleButtons.forEach((toggleButton) => { - - // get contentDiv for element specified in data-for attribute of toggleButton - let dataFor = toggleButton.dataset.for; - let contentDiv = document.getElementById(dataFor); - let buttonParentRow = toggleButton.parentElement.parentElement; - if (contentDiv && contentDiv.tagName.toLowerCase() === 'tr' && contentDiv.classList.contains('show-more-content') && buttonParentRow && buttonParentRow.tagName.toLowerCase() === 'tr') { - toggleButton.addEventListener('click', function() { - toggleShowMoreButton(toggleButton, contentDiv, buttonParentRow); - }); - } else { - console.warn('Found a toggle button with no associated toggleable content or parent row'); - } - - }); } - /** - * Converts a given `last_active` value into a display value and a numeric sort value. - * The input can be a UTC date, the strings "Invited", "Invalid date", or null/undefined. - * - * @param {string} last_active - UTC date string or special status like "Invited" or "Invalid date". - * @returns {Object} - An object containing `display_value` (formatted date or status string) - * and `sort_value` (numeric value for sorting). - */ - handleLastActive(last_active) { - const invited = 'Invited'; - const invalid_date = 'Invalid date'; - const options = { year: 'numeric', month: 'long', day: 'numeric' }; // Date display format - - let display_value = invalid_date; // Default display value for invalid or null dates - let sort_value = -1; // Default sort value for invalid or null dates - - if (last_active === invited) { - // Handle "Invited" status: special case with 0 sort value - display_value = invited; - sort_value = 0; - } else if (last_active && last_active !== invalid_date) { - // Parse and format valid UTC date strings - const parsedDate = new Date(last_active); - - if (!isNaN(parsedDate.getTime())) { - // Valid date - display_value = parsedDate.toLocaleDateString('en-US', options); - sort_value = parsedDate.getTime(); // Use timestamp for sorting - } else { - console.error(`Error: Invalid date string provided: ${last_active}`); - } - } - - return { display_value, sort_value }; - } - - /** - * Generates HTML for the list of domains assigned to a member. - * - * @param {number} num_domains - The number of domains the member is assigned to. - * @param {Array} domain_names - An array of domain names. - * @param {Array} domain_urls - An array of corresponding domain URLs. - * @returns {string} - A string of HTML displaying the domains assigned to the member. - */ - generateDomainsHTML(num_domains, domain_names, domain_urls, action_url) { - // Initialize an empty string for the HTML - let domainsHTML = ''; - - // Only generate HTML if the member has one or more assigned domains - if (num_domains > 0) { - domainsHTML += "
"; - domainsHTML += "

Domains assigned

"; - domainsHTML += `

This member is assigned to ${num_domains} domains:

`; - domainsHTML += "
    "; - - // Display up to 6 domains with their URLs - for (let i = 0; i < num_domains && i < 6; i++) { - domainsHTML += `
  • ${domain_names[i]}
  • `; - } - - domainsHTML += "
"; - - // If there are more than 6 domains, display a "View assigned domains" link - if (num_domains >= 6) { - domainsHTML += `

View assigned domains

`; - } - - domainsHTML += "
"; - } - - return domainsHTML; - } - - /** - * Generates an HTML string summarizing a user's additional permissions within a portfolio, - * based on the user's permissions and predefined permission choices. - * - * @param {Array} member_permissions - An array of permission strings that the member has. - * @param {Object} UserPortfolioPermissionChoices - An object containing predefined permission choice constants. - * Expected keys include: - * - VIEW_ALL_DOMAINS - * - VIEW_MANAGED_DOMAINS - * - EDIT_REQUESTS - * - VIEW_ALL_REQUESTS - * - EDIT_MEMBERS - * - VIEW_MEMBERS - * - * @returns {string} - A string of HTML representing the user's additional permissions. - * If the user has no specific permissions, it returns a default message - * indicating no additional permissions. - * - * Behavior: - * - The function checks the user's permissions (`member_permissions`) and generates - * corresponding HTML sections based on the permission choices defined in `UserPortfolioPermissionChoices`. - * - Permissions are categorized into domains, requests, and members: - * - Domains: Determines whether the user can view or manage all or assigned domains. - * - Requests: Differentiates between users who can edit requests, view all requests, or have no request privileges. - * - Members: Distinguishes between members who can manage or only view other members. - * - If no relevant permissions are found, the function returns a message stating that the user has no additional permissions. - * - The resulting HTML always includes a header "Additional permissions for this member" and appends the relevant permission descriptions. - */ - generatePermissionsHTML(member_permissions, UserPortfolioPermissionChoices) { - let permissionsHTML = ''; - - // Check domain-related permissions - if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)) { - permissionsHTML += "

Domains: Can view all organization domains. Can manage domains they are assigned to and edit information about the domain (including DNS settings).

"; - } else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)) { - permissionsHTML += "

Domains: Can manage domains they are assigned to and edit information about the domain (including DNS settings).

"; - } - - // Check request-related permissions - if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_REQUESTS)) { - permissionsHTML += "

Domain requests: Can view all organization domain requests. Can create domain requests and modify their own requests.

"; - } else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)) { - permissionsHTML += "

Domain requests (view-only): Can view all organization domain requests. Can't create or modify any domain requests.

"; - } - - // Check member-related permissions - if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_MEMBERS)) { - permissionsHTML += "

Members: Can manage members including inviting new members, removing current members, and assigning domains to members.

"; - } else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MEMBERS)) { - permissionsHTML += "

Members (view-only): Can view all organizational members. Can't manage any members.

"; - } - - // If no specific permissions are assigned, display a message indicating no additional permissions - if (!permissionsHTML) { - permissionsHTML += "

No additional permissions: There are no additional permissions for this member.

"; - } - - // Add a permissions header and wrap the entire output in a container - permissionsHTML = "

Additional permissions for this member

" + permissionsHTML + "
"; + let toggleButtons = document.querySelectorAll('.usa-button--show-more-button'); + toggleButtons.forEach((toggleButton) => { - return permissionsHTML; - } - - /** - * Loads rows in the members list, as well as updates pagination around the members list - * based on the supplied attributes. - * @param {*} page - the page number of the results (starts with 1) - * @param {*} sortBy - the sort column option - * @param {*} order - the sort order {asc, desc} - * @param {*} scroll - control for the scrollToElement functionality - * @param {*} searchTerm - the search term - * @param {*} portfolio - the portfolio id - */ - loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) { - - // --------- SEARCH - let searchParams = new URLSearchParams( - { - "page": page, - "sort_by": sortBy, - "order": order, - "search_term": searchTerm - } - ); - if (portfolio) - searchParams.append("portfolio", portfolio) - - - // --------- FETCH DATA - // fetch json of page of domains, given params - let baseUrl = document.getElementById("get_members_json_url"); - if (!baseUrl) { - return; - } - - let baseUrlValue = baseUrl.innerHTML; - if (!baseUrlValue) { - return; - } - - let url = `${baseUrlValue}?${searchParams.toString()}` //TODO: uncomment for search function - fetch(url) - .then(response => response.json()) - .then(data => { - if (data.error) { - console.error('Error in AJAX call: ' + data.error); - return; - } - - // handle the display of proper messaging in the event that no members exist in the list or search returns no results - this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); - - // identify the DOM element where the domain list will be inserted into the DOM - const memberList = document.querySelector('#members tbody'); - memberList.innerHTML = ''; - - const UserPortfolioPermissionChoices = data.UserPortfolioPermissionChoices; - const invited = 'Invited'; - const invalid_date = 'Invalid date'; - - data.members.forEach(member => { - const member_id = member.source + member.id; - const member_name = member.name; - const member_display = member.member_display; - const member_permissions = member.permissions; - const domain_urls = member.domain_urls; - const domain_names = member.domain_names; - const num_domains = domain_urls.length; - - const last_active = this.handleLastActive(member.last_active); - - const action_url = member.action_url; - const action_label = member.action_label; - const svg_icon = member.svg_icon; - - const row = document.createElement('tr'); - - let admin_tagHTML = ``; - if (member.is_admin) - admin_tagHTML = `Admin` - - // generate html blocks for domains and permissions for the member - let domainsHTML = this.generateDomainsHTML(num_domains, domain_names, domain_urls, action_url); - let permissionsHTML = this.generatePermissionsHTML(member_permissions, UserPortfolioPermissionChoices); - - // domainsHTML block and permissionsHTML block need to be wrapped with hide/show toggle, Expand - let showMoreButton = ''; - const showMoreRow = document.createElement('tr'); - if (domainsHTML || permissionsHTML) { - showMoreButton = ` - - `; - - showMoreRow.innerHTML = `
${domainsHTML} ${permissionsHTML}
`; - showMoreRow.classList.add('show-more-content'); - showMoreRow.classList.add('display-none'); - showMoreRow.id = member_id; - } - - row.innerHTML = ` - - ${member_display} ${admin_tagHTML} ${showMoreButton} - - - ${last_active.display_value} - - - - - ${action_label} ${member_name} - - - `; - memberList.appendChild(row); - if (domainsHTML || permissionsHTML) { - memberList.appendChild(showMoreRow); - } - }); - - this.initShowMoreButtons(); - - // Do not scroll on first page load - if (scroll) - scrollToElement('class', 'members'); - this.scrollToTable = true; - - // update pagination - this.updatePagination( - 'member', - '#members-pagination', - '#members-pagination .usa-pagination__counter', - '#members', - data.page, - data.num_pages, - data.has_previous, - data.has_next, - data.total, - ); - this.currentSortBy = sortBy; - this.currentOrder = order; - this.currentSearchTerm = searchTerm; - }) - .catch(error => console.error('Error fetching members:', error)); - } + // get contentDiv for element specified in data-for attribute of toggleButton + let dataFor = toggleButton.dataset.for; + let contentDiv = document.getElementById(dataFor); + let buttonParentRow = toggleButton.parentElement.parentElement; + if (contentDiv && contentDiv.tagName.toLowerCase() === 'tr' && contentDiv.classList.contains('show-more-content') && buttonParentRow && buttonParentRow.tagName.toLowerCase() === 'tr') { + toggleButton.addEventListener('click', function() { + toggleShowMoreButton(toggleButton, contentDiv, buttonParentRow); + }); + } else { + console.warn('Found a toggle button with no associated toggleable content or parent row'); + } + + }); } + /** + * Converts a given `last_active` value into a display value and a numeric sort value. + * The input can be a UTC date, the strings "Invited", "Invalid date", or null/undefined. + * + * @param {string} last_active - UTC date string or special status like "Invited" or "Invalid date". + * @returns {Object} - An object containing `display_value` (formatted date or status string) + * and `sort_value` (numeric value for sorting). + */ + handleLastActive(last_active) { + const invited = 'Invited'; + const invalid_date = 'Invalid date'; + const options = { year: 'numeric', month: 'long', day: 'numeric' }; // Date display format + let display_value = invalid_date; // Default display value for invalid or null dates + let sort_value = -1; // Default sort value for invalid or null dates + + if (last_active === invited) { + // Handle "Invited" status: special case with 0 sort value + display_value = invited; + sort_value = 0; + } else if (last_active && last_active !== invalid_date) { + // Parse and format valid UTC date strings + const parsedDate = new Date(last_active); + + if (!isNaN(parsedDate.getTime())) { + // Valid date + display_value = parsedDate.toLocaleDateString('en-US', options); + sort_value = parsedDate.getTime(); // Use timestamp for sorting + } else { + console.error(`Error: Invalid date string provided: ${last_active}`); + } + } + + return { display_value, sort_value }; + } /** - * An IIFE that listens for DOM Content to be loaded, then executes. This function - * initializes the members list and associated functionality. - * - */ + * Generates HTML for the list of domains assigned to a member. + * + * @param {number} num_domains - The number of domains the member is assigned to. + * @param {Array} domain_names - An array of domain names. + * @param {Array} domain_urls - An array of corresponding domain URLs. + * @returns {string} - A string of HTML displaying the domains assigned to the member. + */ + generateDomainsHTML(num_domains, domain_names, domain_urls, action_url) { + // Initialize an empty string for the HTML + let domainsHTML = ''; + + // Only generate HTML if the member has one or more assigned domains + if (num_domains > 0) { + domainsHTML += "
"; + domainsHTML += "

Domains assigned

"; + domainsHTML += `

This member is assigned to ${num_domains} domains:

`; + domainsHTML += "
    "; + + // Display up to 6 domains with their URLs + for (let i = 0; i < num_domains && i < 6; i++) { + domainsHTML += `
  • ${domain_names[i]}
  • `; + } + + domainsHTML += "
"; + + // If there are more than 6 domains, display a "View assigned domains" link + if (num_domains >= 6) { + domainsHTML += `

View assigned domains

`; + } + + domainsHTML += "
"; + } + + return domainsHTML; + } + + /** + * Generates an HTML string summarizing a user's additional permissions within a portfolio, + * based on the user's permissions and predefined permission choices. + * + * @param {Array} member_permissions - An array of permission strings that the member has. + * @param {Object} UserPortfolioPermissionChoices - An object containing predefined permission choice constants. + * Expected keys include: + * - VIEW_ALL_DOMAINS + * - VIEW_MANAGED_DOMAINS + * - EDIT_REQUESTS + * - VIEW_ALL_REQUESTS + * - EDIT_MEMBERS + * - VIEW_MEMBERS + * + * @returns {string} - A string of HTML representing the user's additional permissions. + * If the user has no specific permissions, it returns a default message + * indicating no additional permissions. + * + * Behavior: + * - The function checks the user's permissions (`member_permissions`) and generates + * corresponding HTML sections based on the permission choices defined in `UserPortfolioPermissionChoices`. + * - Permissions are categorized into domains, requests, and members: + * - Domains: Determines whether the user can view or manage all or assigned domains. + * - Requests: Differentiates between users who can edit requests, view all requests, or have no request privileges. + * - Members: Distinguishes between members who can manage or only view other members. + * - If no relevant permissions are found, the function returns a message stating that the user has no additional permissions. + * - The resulting HTML always includes a header "Additional permissions for this member" and appends the relevant permission descriptions. + */ + generatePermissionsHTML(member_permissions, UserPortfolioPermissionChoices) { + let permissionsHTML = ''; + + // Check domain-related permissions + if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)) { + permissionsHTML += "

Domains: Can view all organization domains. Can manage domains they are assigned to and edit information about the domain (including DNS settings).

"; + } else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)) { + permissionsHTML += "

Domains: Can manage domains they are assigned to and edit information about the domain (including DNS settings).

"; + } + + // Check request-related permissions + if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_REQUESTS)) { + permissionsHTML += "

Domain requests: Can view all organization domain requests. Can create domain requests and modify their own requests.

"; + } else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)) { + permissionsHTML += "

Domain requests (view-only): Can view all organization domain requests. Can't create or modify any domain requests.

"; + } + + // Check member-related permissions + if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_MEMBERS)) { + permissionsHTML += "

Members: Can manage members including inviting new members, removing current members, and assigning domains to members.

"; + } else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MEMBERS)) { + permissionsHTML += "

Members (view-only): Can view all organizational members. Can't manage any members.

"; + } + + // If no specific permissions are assigned, display a message indicating no additional permissions + if (!permissionsHTML) { + permissionsHTML += "

No additional permissions: There are no additional permissions for this member.

"; + } + + // Add a permissions header and wrap the entire output in a container + permissionsHTML = "

Additional permissions for this member

" + permissionsHTML + "
"; + + return permissionsHTML; + } + + /** + * Loads rows in the members list, as well as updates pagination around the members list + * based on the supplied attributes. + * @param {*} page - the page number of the results (starts with 1) + * @param {*} sortBy - the sort column option + * @param {*} order - the sort order {asc, desc} + * @param {*} scroll - control for the scrollToElement functionality + * @param {*} searchTerm - the search term + * @param {*} portfolio - the portfolio id + */ + loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) { + + // --------- SEARCH + let searchParams = new URLSearchParams( + { + "page": page, + "sort_by": sortBy, + "order": order, + "search_term": searchTerm + } + ); + if (portfolio) + searchParams.append("portfolio", portfolio) + + + // --------- FETCH DATA + // fetch json of page of domains, given params + let baseUrl = document.getElementById("get_members_json_url"); + if (!baseUrl) { + return; + } + + let baseUrlValue = baseUrl.innerHTML; + if (!baseUrlValue) { + return; + } + + let url = `${baseUrlValue}?${searchParams.toString()}` //TODO: uncomment for search function + fetch(url) + .then(response => response.json()) + .then(data => { + if (data.error) { + console.error('Error in AJAX call: ' + data.error); + return; + } + + // handle the display of proper messaging in the event that no members exist in the list or search returns no results + this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); + + // identify the DOM element where the domain list will be inserted into the DOM + const memberList = document.querySelector('#members tbody'); + memberList.innerHTML = ''; + + const UserPortfolioPermissionChoices = data.UserPortfolioPermissionChoices; + const invited = 'Invited'; + const invalid_date = 'Invalid date'; + + data.members.forEach(member => { + const member_id = member.source + member.id; + const member_name = member.name; + const member_display = member.member_display; + const member_permissions = member.permissions; + const domain_urls = member.domain_urls; + const domain_names = member.domain_names; + const num_domains = domain_urls.length; + + const last_active = this.handleLastActive(member.last_active); + + const action_url = member.action_url; + const action_label = member.action_label; + const svg_icon = member.svg_icon; + + const row = document.createElement('tr'); + + let admin_tagHTML = ``; + if (member.is_admin) + admin_tagHTML = `Admin` + + // generate html blocks for domains and permissions for the member + let domainsHTML = this.generateDomainsHTML(num_domains, domain_names, domain_urls, action_url); + let permissionsHTML = this.generatePermissionsHTML(member_permissions, UserPortfolioPermissionChoices); + + // domainsHTML block and permissionsHTML block need to be wrapped with hide/show toggle, Expand + let showMoreButton = ''; + const showMoreRow = document.createElement('tr'); + if (domainsHTML || permissionsHTML) { + showMoreButton = ` + + `; + + showMoreRow.innerHTML = `
${domainsHTML} ${permissionsHTML}
`; + showMoreRow.classList.add('show-more-content'); + showMoreRow.classList.add('display-none'); + showMoreRow.id = member_id; + } + + row.innerHTML = ` + + ${member_display} ${admin_tagHTML} ${showMoreButton} + + + ${last_active.display_value} + + + + + ${action_label} ${member_name} + + + `; + memberList.appendChild(row); + if (domainsHTML || permissionsHTML) { + memberList.appendChild(showMoreRow); + } + }); + + this.initShowMoreButtons(); + + // Do not scroll on first page load + if (scroll) + scrollToElement('class', 'members'); + this.scrollToTable = true; + + // update pagination + this.updatePagination( + 'member', + '#members-pagination', + '#members-pagination .usa-pagination__counter', + '#members', + data.page, + data.num_pages, + data.has_previous, + data.has_next, + data.total, + ); + this.currentSortBy = sortBy; + this.currentOrder = order; + this.currentSearchTerm = searchTerm; + }) + .catch(error => console.error('Error fetching members:', error)); + } +} + export function initMembersTable() { document.addEventListener('DOMContentLoaded', function() { const isMembersPage = document.getElementById("members") @@ -373,4 +364,4 @@ export function initMembersTable() { } } }); -} \ No newline at end of file +} diff --git a/src/registrar/assets/modules/user-profile.js b/src/registrar/assets/modules/user-profile.js index c4fd254e7..5c3cf30f1 100644 --- a/src/registrar/assets/modules/user-profile.js +++ b/src/registrar/assets/modules/user-profile.js @@ -1,186 +1,175 @@ -import { hideElement, showElement, scrollToElement, toggleCaret } from './helpers.js'; -import { initializeTooltips, initializeModals, unloadModals } from './helpers-uswds.js'; -import { getCsrfToken } from './get-csrf-token.js'; - -/** - * An IIFE that displays confirmation modal on the user profile page - */ export function userProfileListener() { - - const showConfirmationModalTrigger = document.querySelector('.show-confirmation-modal'); - if (showConfirmationModalTrigger) { - showConfirmationModalTrigger.click(); - } + const showConfirmationModalTrigger = document.querySelector('.show-confirmation-modal'); + if (showConfirmationModalTrigger) { + showConfirmationModalTrigger.click(); } - - /** - * An IIFE that hooks up the edit buttons on the finish-user-setup page - */ +} + export function finishUserSetupListener() { - function getInputField(fieldName){ - return document.querySelector(`#id_${fieldName}`) + function getInputField(fieldName){ + return document.querySelector(`#id_${fieldName}`) + } + + // Shows the hidden input field and hides the readonly one + function showInputFieldHideReadonlyField(fieldName, button) { + let inputField = getInputField(fieldName) + let readonlyField = document.querySelector(`#${fieldName}__edit-button-readonly`) + + readonlyField.classList.toggle('display-none'); + inputField.classList.toggle('display-none'); + + // Toggle the bold style on the grid row + let gridRow = button.closest(".grid-col-2").closest(".grid-row") + if (gridRow){ + gridRow.classList.toggle("bold-usa-label") } - - // Shows the hidden input field and hides the readonly one - function showInputFieldHideReadonlyField(fieldName, button) { - let inputField = getInputField(fieldName) - let readonlyField = document.querySelector(`#${fieldName}__edit-button-readonly`) - - readonlyField.classList.toggle('display-none'); - inputField.classList.toggle('display-none'); - - // Toggle the bold style on the grid row - let gridRow = button.closest(".grid-col-2").closest(".grid-row") - if (gridRow){ - gridRow.classList.toggle("bold-usa-label") + } + + function handleFullNameField(fieldName = "full_name") { + // Remove the display-none class from the nearest parent div + let nameFieldset = document.querySelector("#profile-name-group"); + if (nameFieldset){ + nameFieldset.classList.remove("display-none"); + } + + // Hide the "full_name" field + let inputField = getInputField(fieldName); + if (inputField) { + inputFieldParentDiv = inputField.closest("div"); + if (inputFieldParentDiv) { + inputFieldParentDiv.classList.add("display-none"); } } - - function handleFullNameField(fieldName = "full_name") { - // Remove the display-none class from the nearest parent div - let nameFieldset = document.querySelector("#profile-name-group"); - if (nameFieldset){ - nameFieldset.classList.remove("display-none"); + } + + function handleEditButtonClick(fieldName, button){ + button.addEventListener('click', function() { + // Lock the edit button while this operation occurs + button.disabled = true + + if (fieldName == "full_name"){ + handleFullNameField(); + }else { + showInputFieldHideReadonlyField(fieldName, button); } - - // Hide the "full_name" field - let inputField = getInputField(fieldName); - if (inputField) { - inputFieldParentDiv = inputField.closest("div"); - if (inputFieldParentDiv) { - inputFieldParentDiv.classList.add("display-none"); - } - } - } - - function handleEditButtonClick(fieldName, button){ - button.addEventListener('click', function() { - // Lock the edit button while this operation occurs - button.disabled = true - - if (fieldName == "full_name"){ - handleFullNameField(); - }else { - showInputFieldHideReadonlyField(fieldName, button); - } - - // Hide the button itself - button.classList.add("display-none"); - - // Unlock after it completes - button.disabled = false - }); - } - - function setupListener(){ - - - document.querySelectorAll('[id$="__edit-button"]').forEach(function(button) { - // Get the "{field_name}" and "edit-button" - let fieldIdParts = button.id.split("__") - if (fieldIdParts && fieldIdParts.length > 0){ - let fieldName = fieldIdParts[0] - - // When the edit button is clicked, show the input field under it - handleEditButtonClick(fieldName, button); - - let editableFormGroup = button.parentElement.parentElement.parentElement; - if (editableFormGroup){ - let readonlyField = editableFormGroup.querySelector(".toggleable_input__readonly-field") - let inputField = document.getElementById(`id_${fieldName}`); - if (!inputField || !readonlyField) { - return; - } - - let inputFieldValue = inputField.value - if (inputFieldValue || fieldName == "full_name"){ - if (fieldName == "full_name"){ - let firstName = document.querySelector("#id_first_name"); - let middleName = document.querySelector("#id_middle_name"); - let lastName = document.querySelector("#id_last_name"); - if (firstName && lastName && firstName.value && lastName.value) { - let values = [firstName.value, middleName.value, lastName.value] - readonlyField.innerHTML = values.join(" "); - }else { - let fullNameField = document.querySelector('#full_name__edit-button-readonly'); - let svg = fullNameField.querySelector("svg use") - if (svg) { - const currentHref = svg.getAttribute('xlink:href'); - if (currentHref) { - const parts = currentHref.split('#'); - if (parts.length === 2) { - // Keep the path before '#' and replace the part after '#' with 'invalid' - const newHref = parts[0] + '#error'; - svg.setAttribute('xlink:href', newHref); - fullNameField.classList.add("toggleable_input__error") - label = fullNameField.querySelector(".toggleable_input__readonly-field") - label.innerHTML = "Unknown"; - } + // Hide the button itself + button.classList.add("display-none"); + + // Unlock after it completes + button.disabled = false + }); + } + + function setupListener(){ + + + + document.querySelectorAll('[id$="__edit-button"]').forEach(function(button) { + // Get the "{field_name}" and "edit-button" + let fieldIdParts = button.id.split("__") + if (fieldIdParts && fieldIdParts.length > 0){ + let fieldName = fieldIdParts[0] + + // When the edit button is clicked, show the input field under it + handleEditButtonClick(fieldName, button); + + let editableFormGroup = button.parentElement.parentElement.parentElement; + if (editableFormGroup){ + let readonlyField = editableFormGroup.querySelector(".toggleable_input__readonly-field") + let inputField = document.getElementById(`id_${fieldName}`); + if (!inputField || !readonlyField) { + return; + } + + let inputFieldValue = inputField.value + if (inputFieldValue || fieldName == "full_name"){ + if (fieldName == "full_name"){ + let firstName = document.querySelector("#id_first_name"); + let middleName = document.querySelector("#id_middle_name"); + let lastName = document.querySelector("#id_last_name"); + if (firstName && lastName && firstName.value && lastName.value) { + let values = [firstName.value, middleName.value, lastName.value] + readonlyField.innerHTML = values.join(" "); + }else { + let fullNameField = document.querySelector('#full_name__edit-button-readonly'); + let svg = fullNameField.querySelector("svg use") + if (svg) { + const currentHref = svg.getAttribute('xlink:href'); + if (currentHref) { + const parts = currentHref.split('#'); + if (parts.length === 2) { + // Keep the path before '#' and replace the part after '#' with 'invalid' + const newHref = parts[0] + '#error'; + svg.setAttribute('xlink:href', newHref); + fullNameField.classList.add("toggleable_input__error") + label = fullNameField.querySelector(".toggleable_input__readonly-field") + label.innerHTML = "Unknown"; } } } - - // Technically, the full_name field is optional, but we want to display it as required. - // This style is applied to readonly fields (gray text). This just removes it, as - // this is difficult to achieve otherwise by modifying the .readonly property. - if (readonlyField.classList.contains("text-base")) { - readonlyField.classList.remove("text-base") - } - }else { - readonlyField.innerHTML = inputFieldValue } + + // Technically, the full_name field is optional, but we want to display it as required. + // This style is applied to readonly fields (gray text). This just removes it, as + // this is difficult to achieve otherwise by modifying the .readonly property. + if (readonlyField.classList.contains("text-base")) { + readonlyField.classList.remove("text-base") + } + }else { + readonlyField.innerHTML = inputFieldValue } } } - }); - } - - function showInputOnErrorFields(){ - document.addEventListener('DOMContentLoaded', function() { - - // Get all input elements within the form - let form = document.querySelector("#finish-profile-setup-form"); - let inputs = form ? form.querySelectorAll("input") : null; - if (!inputs) { + } + }); + } + + function showInputOnErrorFields(){ + document.addEventListener('DOMContentLoaded', function() { + + // Get all input elements within the form + let form = document.querySelector("#finish-profile-setup-form"); + let inputs = form ? form.querySelectorAll("input") : null; + if (!inputs) { + return null; + } + + let fullNameButtonClicked = false + inputs.forEach(function(input) { + let fieldName = input.name; + let errorMessage = document.querySelector(`#id_${fieldName}__error-message`); + + // If no error message is found, do nothing + if (!fieldName || !errorMessage) { return null; } - - let fullNameButtonClicked = false - inputs.forEach(function(input) { - let fieldName = input.name; - let errorMessage = document.querySelector(`#id_${fieldName}__error-message`); - - // If no error message is found, do nothing - if (!fieldName || !errorMessage) { - return null; + + let editButton = document.querySelector(`#${fieldName}__edit-button`); + if (editButton){ + // Show the input field of the field that errored out + editButton.click(); + } + + // If either the full_name field errors out, + // or if any of its associated fields do - show all name related fields. + let nameFields = ["first_name", "middle_name", "last_name"]; + if (nameFields.includes(fieldName) && !fullNameButtonClicked){ + // Click the full name button if any of its related fields error out + fullNameButton = document.querySelector("#full_name__edit-button"); + if (fullNameButton) { + fullNameButton.click(); + fullNameButtonClicked = true; } - - let editButton = document.querySelector(`#${fieldName}__edit-button`); - if (editButton){ - // Show the input field of the field that errored out - editButton.click(); - } - - // If either the full_name field errors out, - // or if any of its associated fields do - show all name related fields. - let nameFields = ["first_name", "middle_name", "last_name"]; - if (nameFields.includes(fieldName) && !fullNameButtonClicked){ - // Click the full name button if any of its related fields error out - fullNameButton = document.querySelector("#full_name__edit-button"); - if (fullNameButton) { - fullNameButton.click(); - fullNameButtonClicked = true; - } - } - }); - }); - }; - - setupListener(); - - // Show the input fields if an error exists - showInputOnErrorFields(); - - } \ No newline at end of file + } + }); + }); + }; + + setupListener(); + + // Show the input fields if an error exists + showInputOnErrorFields(); + +}