diff --git a/core/src/main/java/ca/yukon/aem/core/models/AddressModel.java b/core/src/main/java/ca/yukon/aem/core/models/AddressModel.java index 9eb0bb5d..18adb02e 100644 --- a/core/src/main/java/ca/yukon/aem/core/models/AddressModel.java +++ b/core/src/main/java/ca/yukon/aem/core/models/AddressModel.java @@ -2,6 +2,7 @@ import ca.yukon.aem.core.forms.services.impl.CanadaPostApiService; import org.apache.sling.api.resource.Resource; +import org.apache.sling.models.annotations.Default; import org.apache.sling.models.annotations.Model; import org.apache.sling.models.annotations.Optional; import org.apache.sling.models.annotations.injectorspecific.ValueMapValue; @@ -28,6 +29,21 @@ protected void init() { } } + @ValueMapValue @Default(values = "addressLine1") + private String fieldLine1; + + @ValueMapValue @Default(values = "addressLine2") + private String fieldLine2; + + @ValueMapValue @Default(values = "city") + private String fieldCity; + + @ValueMapValue @Default(values = "province") + private String fieldProvince; + + @ValueMapValue @Default(values = "postalCode") + private String fieldPostalCode; + public String getHost() { return this.host; } @@ -39,4 +55,9 @@ public String getKey() { public int getLimit() { return this.limit; } + public String getFieldLine1() { return fieldLine1; } + public String getFieldLine2() { return fieldLine2; } + public String getFieldCity() { return fieldCity; } + public String getFieldProvince() { return fieldProvince; } + public String getFieldPostalCode() { return fieldPostalCode; } } diff --git a/core/src/main/java/ca/yukon/aem/core/models/ReviewSummaryModel.java b/core/src/main/java/ca/yukon/aem/core/models/ReviewSummaryModel.java new file mode 100644 index 00000000..bc31fe5b --- /dev/null +++ b/core/src/main/java/ca/yukon/aem/core/models/ReviewSummaryModel.java @@ -0,0 +1,65 @@ +package ca.yukon.aem.core.models; + +import com.adobe.cq.export.json.ExporterConstants; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.models.annotations.DefaultInjectionStrategy; +import org.apache.sling.models.annotations.Exporter; +import org.apache.sling.models.annotations.Model; +import org.apache.sling.models.annotations.injectorspecific.ValueMapValue; + +import javax.annotation.PostConstruct; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Sling Model for the Review Summary component. + * + * Reads authoring dialog properties and exposes them to the HTL template + * as data attributes consumed by review-summary.js at runtime. + * + * Resource type: yourproject/components/form/review-summary + */ +@Model( + adaptables = SlingHttpServletRequest.class, + defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL +) +@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, + extensions = ExporterConstants.SLING_MODEL_EXTENSION) +public class ReviewSummaryModel { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + /** Comma-separated field names (or Somerset names) to suppress from the review. */ + @ValueMapValue + private String[] excludedFields; + + /** Whether to render an "Edit" link next to each section heading. Default: true. */ + @ValueMapValue + private boolean showEditLinks = true; + + private String excludedFieldsJson; + + @PostConstruct + protected void init() { + List fields = (excludedFields != null) + ? Arrays.asList(excludedFields) + : Collections.emptyList(); + + try { + excludedFieldsJson = MAPPER.writeValueAsString(fields); + } catch (JsonProcessingException e) { + excludedFieldsJson = "[]"; + } + } + + public String getExcludedFieldsJson() { + return excludedFieldsJson; + } + + public boolean isShowEditLinks() { + return showEditLinks; + } +} diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/clientlibs/clientlib-utils/js.txt b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/clientlibs/clientlib-utils/js.txt index 8a758906..5709d299 100644 --- a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/clientlibs/clientlib-utils/js.txt +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/clientlibs/clientlib-utils/js.txt @@ -1,3 +1,5 @@ #base=js -repeatableHelpers.js \ No newline at end of file +repeatableHelpers.js +choiceFieldHelpers.js +dateFieldHelpers.js diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/clientlibs/clientlib-utils/js/choiceFieldHelpers.js b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/clientlibs/clientlib-utils/js/choiceFieldHelpers.js new file mode 100644 index 00000000..b8704284 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/clientlibs/clientlib-utils/js/choiceFieldHelpers.js @@ -0,0 +1,11 @@ +/** Returns an array with one option for choice fields (radio buttons or checkboxes). Used to set the options of such fields in AEM's rules. + * +@name createChoiceFieldOption Creates an array from a value and a display value +@param {string} value Value of the option (on Author, it's the value before the '=' sign) +@param {string} displayValue Display value of the option (on Author, it's the value after the '=' sign) +@return {string[]} An array with the new option + */ +function createChoiceFieldOption(value, displayValue) { + return [value + "=" + displayValue]; +} + diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/clientlibs/clientlib-utils/js/dateFieldHelpers.js b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/clientlibs/clientlib-utils/js/dateFieldHelpers.js new file mode 100644 index 00000000..51b8240d --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/clientlibs/clientlib-utils/js/dateFieldHelpers.js @@ -0,0 +1,15 @@ +/** Extracts the display value of a date field + * +@name getDateDisplayValue +@param {string} SOM expression of the date field to extract display value from +@return {string} The display value of the date field + */ +function getDateDisplayValue(dateFieldSOM) { + var dateField = window.guideBridge.resolveNode(dateFieldSOM); + if (!dateField) { + console.debug("Date field not found. Returning empty string as display value."); + return ""; + } + return dateField.formattedValue; +} + diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/clientlibs/clientlib-yukon/js/triggerPerPanelValidation.js b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/clientlibs/clientlib-yukon/js/triggerPerPanelValidation.js index a904d9b0..cb49bc18 100644 --- a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/clientlibs/clientlib-yukon/js/triggerPerPanelValidation.js +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/clientlibs/clientlib-yukon/js/triggerPerPanelValidation.js @@ -258,13 +258,13 @@ document.addEventListener('DOMContentLoaded', function() { }); /** - * Ensures that focus is set to the next item when the user uses the scribble signature field. + * Ensures that focus stays on the scribble signature field when the user uses it. */ document.addEventListener('DOMContentLoaded', function() { window.guideBridge.on("elementValueChanged", function(event, payload) { if (payload.target.className === "guideScribble") { - console.debug("Target was a scribble signature field. Setting focus to next item in the panel."); - window.guideBridge.setFocus(payload.target.parent.navigationContext.nextItem); + console.debug("Target was a scribble signature field. Setting focus to it again."); + window.guideBridge.setFocus(payload.target); } }); }); diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/clientlibs/clientlib-yukon/less/yukon-experience-fragments.less b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/clientlibs/clientlib-yukon/less/yukon-experience-fragments.less index ce351346..124d674f 100644 --- a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/clientlibs/clientlib-yukon/less/yukon-experience-fragments.less +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/clientlibs/clientlib-yukon/less/yukon-experience-fragments.less @@ -1,3 +1,8 @@ header.yukon-header { border-bottom: 4px solid #ffcd57; -} \ No newline at end of file +} + +footer.yukon-footer { + border-top: 4px solid #ffcd57; +} + diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/address/_cq_dialog/.content.xml b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/address/_cq_dialog/.content.xml index de0b31c0..bf50bf73 100644 --- a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/address/_cq_dialog/.content.xml +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/address/_cq_dialog/.content.xml @@ -28,15 +28,50 @@ jcr:title="Canada Post" sling:resourceType="granite/ui/components/coral/foundation/container"> - + + + + + diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/address/_cq_template/.content.xml b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/address/_cq_template/.content.xml index 69acb276..d7d882b0 100644 --- a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/address/_cq_template/.content.xml +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/address/_cq_template/.content.xml @@ -1,5 +1,5 @@ diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/address/clientlibs/.content.xml b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/address/clientlibs/.content.xml index 6507b512..0770b032 100644 --- a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/address/clientlibs/.content.xml +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/address/clientlibs/.content.xml @@ -2,4 +2,5 @@ + categories="[yukon-forms.components]" + dependencies="[yukon-forms.typeahead]"/> diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/address/clientlibs/js/address.js b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/address/clientlibs/js/address.js index d329c189..4311c8cb 100644 --- a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/address/clientlibs/js/address.js +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/address/clientlibs/js/address.js @@ -1,47 +1,48 @@ (function() { "use strict"; - class AddressInput { - static bemBlock = 'cmp-adaptiveform-addressinput'; - static selectors = { - widget: `.${AddressInput.bemBlock}__widget` - }; + const INIT_KEY = 'addressInputInitialized'; + + function initAll(root) { + (root || document).querySelectorAll( + '[data-cmp-is="adaptiveFormAddressInput"] .cmp-adaptiveform-addressinput__widget' + ).forEach(function(input) { + new AddressInput(input); + }); + } + + class AddressInput { // initializing input field with canada post api - constructor(params) { - this.element = params.element; - this.container = params.formContainer; - this.host = $(this.element).data("host"); - this.key = $(this.element).data("key"); - this.fixedLimit = $(this.element).data("limit") || 7; - this.limit = this.fixedLimit; - this.language = $(this.element).data("language") || "en"; + constructor(input) { + if ($(input).data(INIT_KEY)) return; + $(input).data(INIT_KEY, true); + + this.element = $(input).closest('.address_container'); + this.container = this.element.closest('.panel'); + if (this.container.length === 0) { + this.container = this.element.closest('.rootPanel'); + } + this.host = this.element.data("host"); + this.key = this.element.data("key"); + this.fixedLimit = this.element.data("limit") || 7; + this.language = this.element.data("language") || "en"; this.isFrench = this.language.includes("fr"); this.language = this.isFrench ? "fr" : "en"; - this.usingRefinedSuggestions = false; + this.fieldLine1 = this.element.data('field-line1') || 'addressLine1'; + this.fieldLine2 = this.element.data('field-line2') || 'addressLine2'; + this.fieldCity = this.element.data('field-city') || 'city'; + this.fieldProvince = this.element.data('field-province') || 'province'; + this.fieldPostalCode = this.element.data('field-postal-code') || 'postalCode'; if (!this.host) { console.error("Invalid Canada Post HOST...") return; } - // setting initial typeahead objects - this.addressEngine = new Bloodhound({ - remote: { - url: this.getUrl(), - wildcard: '%QUERY' - }, - datumTokenizer: Bloodhound.tokenizers.whitespace, - queryTokenizer: Bloodhound.tokenizers.whitespace - }); - this.widget = this.getWidget(); - this.initTypeahead(this.addressEngine); + this.widget = $(input); + this.initTypeahead(this._buildSource()); this.addEventListeners(); } - // returns input field - getWidget() { - return $(this.element).find(AddressInput.selectors.widget).get(0); - } - // returns API url getUrl(params) { let text = params && params.text; @@ -49,25 +50,25 @@ let limit = params && params.limit; let url = this.host + "/addresscomplete/interactive/find/v2.10/json3.ws?"; - url += "Key=" + this.key; - url += "&SearchTerm=" + (text || '%QUERY'); - url += "&LastId=" + (id || ''); - url += "&SearchFor="; - url += "&Country=CAN"; - url += "&LanguagePreference=" + (this.language || 'en'); - url += "&MaxSuggestions=" + (limit || ''); - url += "&MaxResults="; - url += "&Origin="; - url += "&Bias="; - url += "&Filter="; - url += "&GeoFence="; + url += "Key=" + this.key; + url += "&SearchTerm=" + (text || '%QUERY'); + url += "&LastId=" + (id || ''); + url += "&SearchFor="; + url += "&Country=CAN"; + url += "&LanguagePreference=" + (this.language || 'en'); + url += "&MaxSuggestions=" + (limit || ''); + url += "&MaxResults="; + url += "&Origin="; + url += "&Bias="; + url += "&Filter="; + url += "&GeoFence="; return url; } // instantiating typeahead library with configuration initTypeahead(sourceEngine) { - $(this.widget).typeahead('destroy'); - $(this.widget).typeahead( + this.widget.typeahead('destroy'); + this.widget.typeahead( { hint: false, highlight: true, @@ -76,7 +77,7 @@ { name: 'address-suggestions', display: 'Text', - limit: this.limit, + limit: 100, source: sourceEngine, templates: { suggestion: function(data) { @@ -96,7 +97,7 @@ addEventListeners() { const self = this; - $(this.widget).on('typeahead:select', function(e, suggestion) { + this.widget.on('typeahead:select', function(e, suggestion) { e.stopPropagation(); e.stopImmediatePropagation(); @@ -104,29 +105,7 @@ return; } - // when user selects on dropdown with multiple address - if (suggestion.Next === "Find") { - let url = self.getUrl({Id: suggestion.Id, text: suggestion.Text, limit: 200}) - $.ajax({ - url: url, - dataType: 'json', - success: function(newResponse) { - const newSuggestions = newResponse.Items || []; - self.limit = newSuggestions.length; - self.initTypeahead(function(query, syncResults, asyncResults) { - syncResults(newSuggestions); - }); - $(self.widget).typeahead('val', suggestion.Text); - $(self.widget).focus(); - self.usingRefinedSuggestions = true; - }, - error: function() { - console.log('Unable to find following address...'); - } - }); - } else { - // for single address selection - $.ajax({ + $.ajax({ url: self.host + '/addresscomplete/interactive/retrieve/v2.11/json3.ws', data: { Key: self.key, @@ -144,37 +123,79 @@ if (!address && items.length > 0) { address = items[0]; } - self.limit = self.fixedLimit; - $(self.container).find('[data-name="address2"]').val(address.Line2).blur(); - $(self.container).find('[data-name="city"]').val(address.City).blur(); - $(self.container).find('[data-name="province"]').val(address.ProvinceCode).blur(); - $(self.container).find('[data-name="postalCode"]').val(address.PostalCode).blur(); - $(self.widget).val(address.Line1); - $(self.widget).blur(); + self.container.find('.' + CSS.escape(self.fieldLine1)).find('input').val(address.Line1).blur(); + self.container.find('.' + CSS.escape(self.fieldLine2)).find('input').val(address.Line2).blur(); + self.container.find('.' + CSS.escape(self.fieldCity)).find('input').val(address.City).blur(); + self.container.find('.' + CSS.escape(self.fieldProvince)).find('input').val(address.ProvinceName).blur(); + self.container.find('.' + CSS.escape(self.fieldPostalCode)).find('input').val(address.PostalCode).blur(); + self.widget.val(address.Line1); + self.widget.blur(); }, error: function() { console.log('Unable to find selected address...'); } }); - } }); + } - // resets input fields when interaction is completed - $(this.widget).on('blur', function() { - if (self.usingRefinedSuggestions) { - self.usingRefinedSuggestions = false; - self.limit = self.fixedLimit; - self.initTypeahead(self.addressEngine); - } - }); + _buildSource() { + const self = this; + return function(query, syncResults, asyncResults) { + $.ajax({ + url: self.getUrl({ text: query, limit: self.fixedLimit }), + dataType: 'json', + success: function(response) { + const items = response.Items || []; + const findItems = items.filter(function(i) { return i.Next === 'Find'; }); + const directItems = items.filter(function(i) { return i.Next !== 'Find'; }); + + if (findItems.length === 0) { + asyncResults(directItems); + return; + } + + let pending = findItems.length; + let allItems = directItems.slice(); + + findItems.forEach(function(item) { + $.ajax({ + url: self.getUrl({ Id: item.Id, text: item.Text, limit: 200 }), + dataType: 'json', + success: function(sub) { + allItems = allItems.concat(sub.Items || []); + if (--pending === 0) asyncResults(allItems); + }, + error: function() { + if (--pending === 0) asyncResults(allItems); + } + }); + }); + }, + error: function() { + asyncResults([]); + } + }); + }; } } - $(document).ready(function() { - $("[data-cmp-is='adaptiveFormAddressInput']").each(function() { - let $element = $(this); - let formContainer = $element.closest('.panel').get(0) || $element.closest('.rootPanel').get(0) || null; - new AddressInput({ element: $element.get(0), formContainer: formContainer }); + window.AddressInput = AddressInput; + + // Initialize inputs already in the DOM. + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { initAll(); }); + } else { + initAll(); + } + + // Re-initialize whenever a new panel instance is added to a repeatable panel. + new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + mutation.addedNodes.forEach(function(node) { + if (node.nodeType === Node.ELEMENT_NODE) { + initAll(node); + } + }); }); - }); + }).observe(document.body, { childList: true, subtree: true }); })(); diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/address/widget.html b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/address/widget.html index fda61e6d..f0ed8e16 100644 --- a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/address/widget.html +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/address/widget.html @@ -10,6 +10,11 @@ data-host="${canadaPost.host}" data-limit="${canadaPost.limit}" data-language="${currentPage.language}" + data-field-line1="${canadaPost.fieldLine1}" + data-field-line2="${canadaPost.fieldLine2}" + data-field-city="${canadaPost.fieldCity}" + data-field-province="${canadaPost.fieldProvince}" + data-field-postal-code="${canadaPost.fieldPostalCode}" style="${guideField.styles @ context='scriptString'}"> diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/panelcontainer/clientlibs/js/panel.js b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/panelcontainer/clientlibs/js/panel.js index 27405add..7af09015 100644 --- a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/panelcontainer/clientlibs/js/panel.js +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/panelcontainer/clientlibs/js/panel.js @@ -25,53 +25,56 @@ function setAccordion(accordionEl, expand) { }); } -document.addEventListener('click', function(e) { - var expandBtn = e.target.closest('.expandAllPanelsButton'); - var collapseBtn = e.target.closest('.collapseAllPanelsButton'); +function _removeAccordionElement(e) { + var tab = e.target.closest('.accordion-navigators > div'); + if (!tab) return; - var clicked = expandBtn || collapseBtn; - if (!clicked) return; + // Check for a control button, in which case, use that instead of the default remove button + var removeControl = tab.querySelector('.removeAccordionControl button'); + if (!removeControl) { + return; + } + e.stopPropagation(); + e.preventDefault(); + removeControl.click(); +} - var expand = !!expandBtn; +function _openCloseAccordionElement(e) { -  // Walk up to the outermost guide-item wrapper - var buttonWrapper = clicked.closest('[data-guide-parent-id]'); - if (!buttonWrapper) return; + var toggle = e.target.closest('[data-guide-toggle="accordion-tab"]'); + if (!toggle) return; + + var panel = toggle.closest('[data-guide-parent-id]'); + if (!panel) return; -  // The accordion is in a sibling div, find the next sibling that contains .accordion-navigators - var sibling = buttonWrapper.nextElementSibling; - while (sibling) { - var accordion = sibling.querySelector('.accordion-navigators'); - if (accordion) { - setAccordion(accordion, expand); - return; + var btn = panel.querySelector('[aria-expanded]'); + var content = panel.querySelector('.afAccordionPanel'); + var isExpanded = btn && btn.getAttribute('aria-expanded') === 'true'; + + if (isExpanded) { + panel.classList.remove('active'); + if (btn) { + btn.setAttribute('aria-expanded', 'false'); + btn.setAttribute('aria-pressed', 'false'); } - sibling = sibling.nextElementSibling; + if (content) content.style.display = 'none'; + } else { + panel.classList.add('active'); + if (btn) { + btn.setAttribute('aria-expanded', 'true'); + btn.setAttribute('aria-pressed', 'true'); + } + if (content) content.style.display = ''; } -}); -// Replace AEM's built-in panel header functionality to work with our expand/collapse all -document.addEventListener('click', function(e) { +} +function _handleAccordionInteraction(e) { var toggle = e.target.closest('[data-guide-toggle="accordion-tab"]'); if (!toggle) return; var tab = e.target.closest('.accordion-navigators > div'); if (!tab) return; - - // Allow the remove button to work normally - if (e.target.closest('[data-guide-addremove="remove"]')) { - // Check for a control button, in which case, use that instead of the default remove button - var removeControl = tab.querySelector('.removeAccordionControl button'); - if (!removeControl) { - return; - } - e.stopPropagation(); - e.preventDefault(); - removeControl.click(); - return; - } - // Find the .accordion-navigators this toggle belongs to var accordionNav = tab.closest('.accordion-navigators'); if (!accordionNav) return; @@ -92,27 +95,56 @@ document.addEventListener('click', function(e) { // Otherwise, stop AEM's listener from firing e.stopPropagation(); e.preventDefault(); + + _openCloseAccordionElement(e); +} - var panel = toggle.closest('[data-guide-parent-id]'); - if (!panel) return; +document.addEventListener('click', function(e) { + var expandBtn = e.target.closest('.expandAllPanelsButton'); + var collapseBtn = e.target.closest('.collapseAllPanelsButton'); + + var clicked = expandBtn || collapseBtn; + if (!clicked) return; - var btn = panel.querySelector('[aria-expanded]'); - var content = panel.querySelector('.afAccordionPanel'); - var isExpanded = btn && btn.getAttribute('aria-expanded') === 'true'; + var expand = !!expandBtn; - if (isExpanded) { - panel.classList.remove('active'); - if (btn) { - btn.setAttribute('aria-expanded', 'false'); - btn.setAttribute('aria-pressed', 'false'); +  // Walk up to the outermost guide-item wrapper + var buttonWrapper = clicked.closest('[data-guide-parent-id]'); + if (!buttonWrapper) return; + +  // The accordion is in a sibling div, find the next sibling that contains .accordion-navigators + var sibling = buttonWrapper.nextElementSibling; + while (sibling) { + var accordion = sibling.querySelector('.accordion-navigators'); + if (accordion) { + setAccordion(accordion, expand); + return; } - if (content) content.style.display = 'none'; - } else { - panel.classList.add('active'); - if (btn) { - btn.setAttribute('aria-expanded', 'true'); - btn.setAttribute('aria-pressed', 'true'); + sibling = sibling.nextElementSibling; + } +}); + +// Replace AEM's built-in panel header functionality to work with our expand/collapse all +document.addEventListener('click', function(e) { + + // Allow the remove button to work normally + if (e.target.closest('[data-guide-addremove="remove"]')) { + _removeAccordionElement(e); + return; + } + + _handleAccordionInteraction(e); +}, true); + +document.addEventListener('keydown', function(e) { + if (e.key === "Enter") { + // Allow the remove button to work normally + if (e.target.closest('[data-guide-addremove="remove"]')) { + _removeAccordionElement(e); + return; } - if (content) content.style.display = ''; + } + if (e.code === "Space") { + _handleAccordionInteraction(e); } }, true); diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/.content.xml b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/.content.xml new file mode 100644 index 00000000..b2982603 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/.content.xml @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/_cq_dialog/.content.xml b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/_cq_dialog/.content.xml new file mode 100644 index 00000000..fb52096f --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/_cq_dialog/.content.xml @@ -0,0 +1,49 @@ + + + + + + +
+ + + + + + + + + +
+
+
+
+
+
\ No newline at end of file diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/.content.xml b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/.content.xml new file mode 100644 index 00000000..f9046901 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/.content.xml @@ -0,0 +1,5 @@ + + diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/css.txt b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/css.txt new file mode 100644 index 00000000..86358c30 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/css.txt @@ -0,0 +1,3 @@ +#base=css + +review-summary.css diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/css/review-summary.css b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/css/review-summary.css new file mode 100644 index 00000000..a94dad0a --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/css/review-summary.css @@ -0,0 +1,100 @@ +/* + * review-summary.css + * + * Styles for the flat-list Review Summary component. + */ + +/* ── Design tokens ── */ +.review-summary { + --rs-label-color: #6b7280; + --rs-value-color: #555; + --rs-panel-color: #1f2937; + --rs-divider-color: #e5e7eb; + --rs-font-size-sm: 0.875rem; + --rs-font-size-base: 1rem; + --rs-font-size-panel: 1rem; +} + +/* ── Loading state ── */ +.review-summary__loading { + color: var(--rs-label-color); + font-size: var(--rs-font-size-sm); + padding: 1rem 0; +} + +/* ── Empty state ── */ +.review-summary__empty { + color: var(--rs-label-color); + font-style: italic; +} + +/* ── Sections ── */ +.rs-panel-block + .rs-panel-block, +.rs-panel-block .rs-panel-block { + margin-top: 1rem; +} + +.rs-section-header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.75rem; + padding-bottom: 0.25rem; +} + +.rs-section-title { + margin: 0; + color: var(--rs-panel-color); + font-weight: 600; + font-size: var(--rs-font-size-panel); +} + +.rs-page-header { + font-size: 1.25rem; + padding: 10px 10px 10px 0; + font-weight: bold; + color: #000; + border-width: 0 0 4px; + border-style: solid; + border-color: #f1ab00; +} + +.rs-page-header .rs-section-title { + font-size: inherit; +} + +.rs-panel-block button.rs-page-goto-btn { + display: block; + margin-top: 0.75rem; + font-size: var(--rs-font-size-sm); + text-align: left; + cursor: pointer; + border-color: #00616d; + border-style: solid; + border-radius: 8px; + background: #ffffff; + color: #00616d; +} + +.rs-field { + display: block; + padding: 0.375rem 0; +} + +.rs-label { + font-size: var(--rs-font-size-sm); + font-weight: bold; + width: 100%; + color: black; +} + +.rs-value { + color: var(--rs-value-color); + font-size: var(--rs-font-size-sm); + word-break: break-word; + width: 100%; + margin-top: .25rem; + padding-right: .75rem; + padding-top: .5rem; + margin-bottom: .25rem; +} diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/js.txt b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/js.txt new file mode 100644 index 00000000..c5541b63 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/js.txt @@ -0,0 +1,3 @@ +#base=js + +review-summary.js diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/js/review-summary.js b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/js/review-summary.js new file mode 100644 index 00000000..813b8402 --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/clientlibs/js/review-summary.js @@ -0,0 +1,254 @@ +(function (window, document, $) { + "use strict"; + + // ─── Constants ──────────────────────────────────────────────────────────── + + var BRIDGE_READY_EVENT = "bridgeInitializeStart"; + + var INPUT_CLASSES = [ + "guideTextBox", + "guideTermsAndConditions", + "guideTelephone", + "guideSwitch", + "guideRadioButton", + "guidePasswordBox", + "guideNumericBox", + "guideCheckBox", + "guideFileUpload", + "guideDropDownList", + "guideDatePicker" + ]; + + // ─── Model helpers ──────────────────────────────────────────────────────── + + function allParentsVisible(node) { + var parent = node.parent; + while (parent) { + if (parent.visible === false) return false; + parent = parent.parent; + } + return true; + } + + function getDisplayValue(field) { + var raw = field.value; + var display = field.displayValue; + if (raw === null || raw === undefined || raw === "") return null; + if (field.className === "guideCheckBox" && field.jsonModel && field.jsonModel.options) { + var value = null; + field.jsonModel.options.forEach(item => { + var nameValues = item.split('='); + if (nameValues.length === 2) { + if (raw === nameValues[0]) { + value = nameValues[1]; + } + } + }); + if (value) { + return value; + } + } + if (Array.isArray(raw) && raw.length === 0) return null; + if (Array.isArray(display)) return display.join(", ") || null; + if (display !== null && display !== undefined && display !== "") return String(display); + if (Array.isArray(raw)) return raw.join(", ") || null; + return String(raw); + } + + function maybeMask(field, value) { + if (field.jsonModel && field.jsonModel.sensitive === true) return "••••••••"; + return value; + } + + // ─── Flat list rendering ────────────────────────────────────────────────── + + function buildSection(container, excludedFields, showEditLinks, isTopLevelContainer) { + var section = document.createElement("div"); + section.className = "rs-section"; + + container.items.forEach(function (item) { + if (item.type === "panel") { + var panel = item.node.panel; + if (excludedFields.indexOf(panel.name) >= 0) return; + + var panelName = panel.name; + var panelSom = panel.somExpression || panelName; + var title = panel.title || panelName; + + var header = null; + if (panel.title) { + header = document.createElement("div"); + header.className = "rs-section-header" + (isTopLevelContainer ? " rs-page-header" : ""); + + var heading = document.createElement("h3"); + heading.className = "rs-section-title"; + heading.innerHTML = title; + header.appendChild(heading); + } + + var childSection = buildSection(item.node, excludedFields, showEditLinks, false); + + var panelBlock = document.createElement("div"); + panelBlock.className = "rs-panel-block"; + if (header) panelBlock.appendChild(header); + panelBlock.appendChild(childSection); + + if (isTopLevelContainer && showEditLinks) { + var rootItems = panel.parent && panel.parent.items; + var pageNumber = rootItems ? rootItems.indexOf(panel) : null; + + var goBackBtn = document.createElement("button"); + goBackBtn.type = "button"; + goBackBtn.className = "rs-page-goto-btn"; + goBackBtn.textContent = "Go back to page " + pageNumber + " to edit your " + title; + goBackBtn.dataset.rsPanel = panelName; + goBackBtn.dataset.rsPanelSom = panelSom; + panelBlock.appendChild(goBackBtn); + } + + section.appendChild(panelBlock); + } else { + var row = document.createElement("div"); + row.className = "rs-field"; + + if (item.label) { + var label = document.createElement("div"); + label.className = "rs-label"; + label.innerHTML = item.label; + row.appendChild(label); + } + var value = document.createElement("div"); + value.className = "rs-value"; + value.innerHTML = item.value; + row.appendChild(value); + + section.appendChild(row); + } + }); + + return section; + } + + // ─── Core renderer ──────────────────────────────────────────────────────── + + function render(root, guideBridge) { + var excludedFields = []; + var showEditLinks = root.dataset.showEditLinks !== "false"; + + try { + excludedFields = JSON.parse(root.dataset.excludedFields || "[]"); + } catch (e) { /* ignore */ } + + // ── Build tree data structure ───────────────────────────────────────── + // children[] uses panel object identity as the key so repeatable panel + // instances (same name, different object) each get their own tree node. + var treeRoot = { items: [], children: [] }; + + function findOrCreateChild(container, panel) { + for (var i = 0; i < container.children.length; i++) { + if (container.children[i].panel === panel) return container.children[i].node; + } + var treeNode = { panel: panel, items: [], children: [] }; + container.children.push({ panel: panel, node: treeNode }); + container.items.push({ type: "panel", node: treeNode }); + return treeNode; + } + + guideBridge.visit(function (node) { + if (INPUT_CLASSES.indexOf(node.className) < 0) return; + if (!node.visible) return; + if (!allParentsVisible(node)) return; + if (!node.parent) return; + if (excludedFields.indexOf(node.name) >= 0) return; + + var value = getDisplayValue(node); + if (value === null) return; + value = maybeMask(node, value); + + // Panel chain from top-level panel down to node.parent, + // stopping before the guideContainer root (no parent of its own). + var chain = []; + var p = node.parent; + while (p) { + if (!p.parent) break; // p is guideContainer — stop + if (!p.parent.parent) break; // p is root panel — stop + chain.unshift(p); + p = p.parent; + } + + var current = treeRoot; + chain.forEach(function (panel) { + current = findOrCreateChild(current, panel); + }); + + current.items.push({ + type: "field", + label: node.jsonModel && node.jsonModel.hideTitle === 'true' ? '' : (node.title || node.name), + value: value + }); + }); + + // ── Render flat sections ────────────────────────────────────────────── + var $root = $(root); + $root.off(".rs"); + root.innerHTML = ""; + + if (!treeRoot.items.length) { + var empty = document.createElement("p"); + empty.className = "review-summary__empty"; + empty.textContent = "No responses to display."; + root.appendChild(empty); + return; + } + + root.appendChild(buildSection(treeRoot, excludedFields, showEditLinks, true)); + + if (showEditLinks) { + // Delegated handler on $root catches clicks regardless of DOM re-renders + $root.on("click.rs", ".rs-page-goto-btn", function (e) { + e.preventDefault(); + e.stopPropagation(); + var panelName = this.dataset.rsPanel; + var panelSom = this.dataset.rsPanelSom || panelName; + guideBridge.setFocus(panelSom); + setTimeout(function () { + var el = document.getElementById(panelName); + if (el) el.scrollIntoView({ behavior: "smooth", block: "start" }); + }, 300); + }); + } + } + + // ─── Initialisation ─────────────────────────────────────────────────────── + + function init() { + var roots = document.querySelectorAll(".review-summary[id='review-summary-root']"); + if (!roots.length) return; + + function attachBridge(guideBridge) { + guideBridge.connect(function () { + roots.forEach(function (root) { + guideBridge.on("elementFocusChanged", function () { + if (root.offsetParent !== null) render(root, guideBridge); + }); + if (root.offsetParent !== null) render(root, guideBridge); + }); + }); + } + + if (window.guideBridge && window.guideBridge.isConnected()) { + attachBridge(window.guideBridge); + } else { + window.addEventListener(BRIDGE_READY_EVENT, function (e) { + attachBridge(e.detail.guideBridge || window.guideBridge); + }); + } + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } + +}(window, document, window.jQuery)); diff --git a/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/review-summary.html b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/review-summary.html new file mode 100644 index 00000000..02e243fb --- /dev/null +++ b/ui.apps/src/main/content/jcr_root/apps/yukon-forms/components/adaptiveForm/review-summary/review-summary.html @@ -0,0 +1,25 @@ + +
+ + +
+ Loading your responses… +
+