From a92acc5edbbf83da704e596523183bb5444e6c1a Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 26 Apr 2024 12:05:44 -0400 Subject: [PATCH 1/3] override h2s with h3s for multiselects --- src/registrar/assets/js/get-gov-admin.js | 231 +++---------------- src/registrar/assets/sass/_theme/_admin.scss | 22 ++ 2 files changed, 57 insertions(+), 196 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 126ab0a2a..17662a367 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -233,10 +233,8 @@ function openInNewTab(el, removeAttribute = false){ // Initialize custom filter_horizontal widgets; each widget has a "from" select list // and a "to" select list; initialization is based off of the presence of the // "to" select list - checkToListThenInitWidget('id_other_contacts_to', 0); - checkToListThenInitWidget('id_domain_info-0-other_contacts_to', 0); - checkToListThenInitWidget('id_current_websites_to', 0); - checkToListThenInitWidget('id_alternative_domains_to', 0); + checkToListThenInitWidget('id_groups_to', 0); + // checkToListThenInitWidget('id_user_permissions_to', 0); })(); // Function to check for the existence of the "to" select list element in the DOM, and if and when found, @@ -245,215 +243,56 @@ function checkToListThenInitWidget(toListId, attempts) { let toList = document.getElementById(toListId); attempts++; - if (attempts < 6) { - if ((toList !== null)) { + if (attempts < 12) { + if ((toList)) { // toList found, handle it - // Add an event listener on the element - // Add disabled buttons on the element's great-grandparent - initializeWidgetOnToList(toList, toListId); + // Then get fromList and handle it + initializeWidgetOnList(toList, ".selector-chosen"); + let fromList = toList.closest('.selector').querySelector(".selector-available select"); + initializeWidgetOnList(fromList, ".selector-available"); } else { // Element not found, check again after a delay - setTimeout(() => checkToListThenInitWidget(toListId, attempts), 1000); // Check every 1000 milliseconds (1 second) + setTimeout(() => checkToListThenInitWidget(toListId, attempts), 300); // Check every 1000 milliseconds (1 second) } } } // Initialize the widget: -// add related buttons to the widget for edit, delete and view -// add event listeners on the from list, the to list, and selector buttons which either enable or disable the related buttons -function initializeWidgetOnToList(toList, toListId) { - // create the change button - let changeLink = createAndCustomizeLink( - toList, - toListId, - 'related-widget-wrapper-link change-related', - 'Change', - '/public/admin/img/icon-changelink.svg', - { - 'contacts': '/admin/registrar/contact/__fk__/change/?_to_field=id&_popup=1', - 'websites': '/admin/registrar/website/__fk__/change/?_to_field=id&_popup=1', - 'alternative_domains': '/admin/registrar/website/__fk__/change/?_to_field=id&_popup=1', - }, - true, - true - ); +// Replace h2 with more semantic h3 +function initializeWidgetOnList(list, parentId) { + if (list) { + // Get h2 and its container + const parentElement = list.closest(parentId); + const h2Element = parentElement.querySelector('h2'); - let hasDeletePermission = hasDeletePermissionOnPage(); + // One last check + if (parentElement && h2Element) { + // Create a new

element + const h3Element = document.createElement('h3'); - let deleteLink = null; - if (hasDeletePermission) { - // create the delete button if user has permission to delete - deleteLink = createAndCustomizeLink( - toList, - toListId, - 'related-widget-wrapper-link delete-related', - 'Delete', - '/public/admin/img/icon-deletelink.svg', - { - 'contacts': '/admin/registrar/contact/__fk__/delete/?_to_field=id&_popup=1', - 'websites': '/admin/registrar/website/__fk__/delete/?_to_field=id&_popup=1', - 'alternative_domains': '/admin/registrar/website/__fk__/delete/?_to_field=id&_popup=1', - }, - true, - false - ); - } + // Copy the text content from the

element to the

element + h3Element.textContent = h2Element.textContent; - // create the view button - let viewLink = createAndCustomizeLink( - toList, - toListId, - 'related-widget-wrapper-link view-related', - 'View', - '/public/admin/img/icon-viewlink.svg', - { - 'contacts': '/admin/registrar/contact/__fk__/change/?_to_field=id', - 'websites': '/admin/registrar/website/__fk__/change/?_to_field=id', - 'alternative_domains': '/admin/registrar/website/__fk__/change/?_to_field=id', - }, - // NOTE: If we open view in the same window then use the back button - // to go back, the 'chosen' list will fail to initialize correctly in - // sandbozes (but will work fine on local). This is related to how the - // Django JS runs (SelectBox.js) and is probably due to a race condition. - true, - false - ); + // Find the nested element inside the

+ const nestedSpan = h2Element.querySelector('span[class][title]'); - // identify the fromList element in the DOM - let fromList = toList.closest('.selector').querySelector(".selector-available select"); + // If the nested element exists + if (nestedSpan) { + // Create a new element + const newSpan = document.createElement('span'); - fromList.addEventListener('click', function(event) { - handleSelectClick(fromList, changeLink, deleteLink, viewLink); - }); - - toList.addEventListener('click', function(event) { - handleSelectClick(toList, changeLink, deleteLink, viewLink); - }); - - // Disable buttons when the selectors are interacted with (items are moved from one column to the other) - let selectorButtons = []; - selectorButtons.push(toList.closest(".selector").querySelector(".selector-chooseall")); - selectorButtons.push(toList.closest(".selector").querySelector(".selector-add")); - selectorButtons.push(toList.closest(".selector").querySelector(".selector-remove")); + // Copy the class and title attributes from the nested element + newSpan.className = nestedSpan.className; + newSpan.title = nestedSpan.title; - selectorButtons.forEach((selector) => { - selector.addEventListener("click", ()=>{disableRelatedWidgetButtons(changeLink, deleteLink, viewLink)}); - }); -} - -// create and customize the button, then add to the DOM, relative to the toList -// toList - the element in the DOM for the toList -// toListId - the ID of the element in the DOM -// className - className to add to the created link -// action - the action to perform on the item {change, delete, view} -// imgSrc - the img.src for the created link -// dataMappings - dictionary which relates toListId to href for the created link -// dataPopup - boolean for whether the link should produce a popup window -// firstPosition - boolean indicating if link should be first position in list of links, otherwise, should be last link -function createAndCustomizeLink(toList, toListId, className, action, imgSrc, dataMappings, dataPopup, firstPosition) { - // Create a link element - var link = document.createElement('a'); - - // Set class attribute for the link - link.className = className; - - // Set id - // Determine function {change, link, view} from the className - // Add {function}_ to the beginning of the string - let modifiedLinkString = className.split('-')[0] + '_' + toListId; - // Remove '_to' from the end of the string - modifiedLinkString = modifiedLinkString.replace('_to', ''); - link.id = modifiedLinkString; - - // Set data-href-template - for (const [idPattern, template] of Object.entries(dataMappings)) { - if (toListId.includes(idPattern)) { - link.setAttribute('data-href-template', template); - break; // Stop checking once a match is found - } - } - - if (dataPopup) - link.setAttribute('data-popup', 'yes'); - - link.setAttribute('title-template', action + " selected item") - link.title = link.getAttribute('title-template'); - - // Create an 'img' element - var img = document.createElement('img'); - - // Set attributes for the new image - img.src = imgSrc; - img.alt = action; - - // Append the image to the link - link.appendChild(img); - - let relatedWidgetWrapper = toList.closest('.related-widget-wrapper'); - // If firstPosition is true, insert link as the first child element - if (firstPosition) { - relatedWidgetWrapper.insertBefore(link, relatedWidgetWrapper.children[0]); - } else { - // otherwise, insert the link prior to the last child (which is a div) - // and also prior to any text elements immediately preceding the last - // child node - var lastChild = relatedWidgetWrapper.lastChild; - - // Check if lastChild is an element node (not a text node, comment, etc.) - if (lastChild.nodeType === 1) { - var previousSibling = lastChild.previousSibling; - // need to work around some white space which has been inserted into the dom - while (previousSibling.nodeType !== 1) { - previousSibling = previousSibling.previousSibling; + // Append the new element to the

element + h3Element.appendChild(newSpan); } - relatedWidgetWrapper.insertBefore(link, previousSibling.nextSibling); + + // Replace the

element with the new

element + parentElement.replaceChild(h3Element, h2Element); } } - - // Return the link, which we'll use in the disable and enable functions - return link; -} - -// Either enable or disable widget buttons when select is clicked. Action (enable or disable) taken depends on the count -// of selected items in selectElement. If exactly one item is selected, buttons are enabled, and urls for the buttons are -// associated with the selected item -function handleSelectClick(selectElement, changeLink, deleteLink, viewLink) { - - // If one item is selected (across selectElement and relatedSelectElement), enable buttons; otherwise, disable them - if (selectElement.selectedOptions.length === 1) { - // enable buttons for selected item in selectElement - enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, selectElement.selectedOptions[0].value, selectElement.selectedOptions[0].text); - } else { - disableRelatedWidgetButtons(changeLink, deleteLink, viewLink); - } -} - -// return true if there exist elements on the page with classname of delete-related. -// presence of one or more of these elements indicates user has permission to delete -function hasDeletePermissionOnPage() { - return document.querySelector('.delete-related') != null -} - -function disableRelatedWidgetButtons(changeLink, deleteLink, viewLink) { - changeLink.removeAttribute('href'); - changeLink.setAttribute('title', changeLink.getAttribute('title-template')); - if (deleteLink) { - deleteLink.removeAttribute('href'); - deleteLink.setAttribute('title', deleteLink.getAttribute('title-template')); - } - viewLink.removeAttribute('href'); - viewLink.setAttribute('title', viewLink.getAttribute('title-template')); -} - -function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk, elementText) { - changeLink.setAttribute('href', changeLink.getAttribute('data-href-template').replace('__fk__', elementPk)); - changeLink.setAttribute('title', changeLink.getAttribute('title-template').replace('selected item', elementText)); - if (deleteLink) { - deleteLink.setAttribute('href', deleteLink.getAttribute('data-href-template').replace('__fk__', elementPk)); - deleteLink.setAttribute('title', deleteLink.getAttribute('title-template').replace('selected item', elementText)); - } - viewLink.setAttribute('href', viewLink.getAttribute('data-href-template').replace('__fk__', elementPk)); - viewLink.setAttribute('title', viewLink.getAttribute('title-template').replace('selected item', elementText)); } /** An IIFE for admin in DjangoAdmin to listen to changes on the domain request diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 2dcbfbb06..a9a71a616 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -630,3 +630,25 @@ address.dja-address-contact-list { .usa-button__small-text { font-size: small; } + +// We override the DJA header on multi list selects from h2 to h3 +// The following block of code styles our generated h3s to match the old h2s +.selector .selector-available h3 { + background: var(--darkened-bg); + color: var(--body-quiet-color); +} +.selector-available h3, .selector-chosen h3 { + border: 1px solid var(--border-color); + border-radius: 4px 4px 0 0; + margin: 0; + padding: 8px; + font-size: 0.8125rem; + text-align: left; + margin: 0; + padding: 8px; + line-height: 1.3; +} +.selector .selector-chosen h3 { + background: var(--primary); + color: var(--header-link-color); +} From 15a74243a93d354f271f42e4639c20a894c1c053 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 26 Apr 2024 12:20:00 -0400 Subject: [PATCH 2/3] cleanup --- src/registrar/assets/js/get-gov-admin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 17662a367..5a24469bb 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -234,7 +234,7 @@ function openInNewTab(el, removeAttribute = false){ // and a "to" select list; initialization is based off of the presence of the // "to" select list checkToListThenInitWidget('id_groups_to', 0); - // checkToListThenInitWidget('id_user_permissions_to', 0); + checkToListThenInitWidget('id_user_permissions_to', 0); })(); // Function to check for the existence of the "to" select list element in the DOM, and if and when found, @@ -244,7 +244,7 @@ function checkToListThenInitWidget(toListId, attempts) { attempts++; if (attempts < 12) { - if ((toList)) { + if ((toList && toList !== undefined)) { // toList found, handle it // Then get fromList and handle it initializeWidgetOnList(toList, ".selector-chosen"); From 8d98434352ebd1a271f511d5234f15d617919f75 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Tue, 30 Apr 2024 20:56:24 -0400 Subject: [PATCH 3/3] cleanup --- src/registrar/assets/js/get-gov-admin.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 5a24469bb..f38afd252 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -244,7 +244,7 @@ function checkToListThenInitWidget(toListId, attempts) { attempts++; if (attempts < 12) { - if ((toList && toList !== undefined)) { + if (toList) { // toList found, handle it // Then get fromList and handle it initializeWidgetOnList(toList, ".selector-chosen"); @@ -252,7 +252,7 @@ function checkToListThenInitWidget(toListId, attempts) { initializeWidgetOnList(fromList, ".selector-available"); } else { // Element not found, check again after a delay - setTimeout(() => checkToListThenInitWidget(toListId, attempts), 300); // Check every 1000 milliseconds (1 second) + setTimeout(() => checkToListThenInitWidget(toListId, attempts), 300); // Check every 300 milliseconds } } }