conditinal-fields.js
214 lines
| 1 | document.addEventListener('readystatechange', event => { |
| 2 | if (event.target.readyState !== 'complete') { |
| 3 | return null; |
| 4 | } |
| 5 | |
| 6 | const state = {}; |
| 7 | |
| 8 | /** |
| 9 | * @since 2.15.0 |
| 10 | * |
| 11 | * @param {HTMLElement} inputField |
| 12 | * @return {string} |
| 13 | */ |
| 14 | function getFieldSelector(inputField) { |
| 15 | const container = inputField.closest('.form-row'); |
| 16 | let fieldSelector = ''; |
| 17 | |
| 18 | if (inputField.name) { |
| 19 | fieldSelector = inputField.name; |
| 20 | } else if ('html' === container.getAttribute('data-field-type')) { |
| 21 | fieldSelector = `[data-field-name="${container.getAttribute('data-field-name')}"]`; |
| 22 | } else { |
| 23 | fieldSelector = `[data-field-name="${container.getAttribute('data-field-name')}"] ${inputField.nodeName.toLowerCase()}`; |
| 24 | } |
| 25 | |
| 26 | return fieldSelector; |
| 27 | } |
| 28 | |
| 29 | /** |
| 30 | * Get list of watched fields. |
| 31 | * @since 2.15.0 |
| 32 | * |
| 33 | * @return object |
| 34 | */ |
| 35 | function getWatchedElementNames(donationForm) { |
| 36 | const fields = {}; |
| 37 | |
| 38 | donationForm.querySelectorAll('[data-field-visibility-conditions]').forEach(function (inputField) { |
| 39 | const visibilityConditions = JSON.parse(inputField.getAttribute('data-field-visibility-conditions')); |
| 40 | const visibilityCondition = visibilityConditions[0]; // Currently we support only one visibility condition. |
| 41 | let fieldSelector = getFieldSelector(inputField); |
| 42 | let {field} = visibilityCondition; |
| 43 | |
| 44 | // Get field. It will tell use real name of field. |
| 45 | field = document.querySelector(`[name="${field}"], [name="${field}[]"]`); |
| 46 | |
| 47 | if (field) { |
| 48 | fields[field.name] = { |
| 49 | ...fields[field], |
| 50 | [fieldSelector]: visibilityConditions |
| 51 | } |
| 52 | } |
| 53 | }); |
| 54 | |
| 55 | return fields; |
| 56 | } |
| 57 | |
| 58 | /** |
| 59 | * @since 2.15.0 |
| 60 | * |
| 61 | * @param operator |
| 62 | * @param firstData |
| 63 | * @param secondData |
| 64 | * |
| 65 | * @return boolean |
| 66 | */ |
| 67 | function compareWithOperator( operator, firstData, secondData ){ |
| 68 | return { |
| 69 | '=': firstData === secondData, |
| 70 | '!=': firstData != secondData, |
| 71 | '>': firstData > secondData, |
| 72 | '>=': firstData >= secondData, |
| 73 | '<': firstData < secondData, |
| 74 | '<=': firstData <=secondData |
| 75 | }[operator] |
| 76 | } |
| 77 | |
| 78 | /** |
| 79 | * Handle fields visibility. |
| 80 | * @since 2.15.0 |
| 81 | */ |
| 82 | function handleVisibility(donationForm, watchedFieldName, visibilityConditionsForWatchedField) { |
| 83 | for (const [inputFieldName, visibilityConditions] of Object.entries(visibilityConditionsForWatchedField)) { |
| 84 | const inputField = -1 === inputFieldName.indexOf('data-field-name') ? |
| 85 | donationForm.querySelector(`[name="${inputFieldName}"]`) : |
| 86 | donationForm.querySelector(inputFieldName); |
| 87 | const fieldWrapperWithoutInputField = inputField.classList.contains('.form-row'); |
| 88 | const fieldWrapper = fieldWrapperWithoutInputField ? inputField : inputField.closest('.form-row'); |
| 89 | const visibilityCondition = visibilityConditions[0]; // Currently we support only one visibility condition. |
| 90 | let visible = false; |
| 91 | const {operator, value} = visibilityCondition; |
| 92 | |
| 93 | const inputs = donationForm.querySelectorAll(`[name="${watchedFieldName}"]`); |
| 94 | let hasFieldController = !!inputs.length; |
| 95 | |
| 96 | if (hasFieldController) { |
| 97 | inputs.forEach((input) => { |
| 98 | const fieldType = input.getAttribute('type'); |
| 99 | const comparisonResult = compareWithOperator(operator, input.value, value); |
| 100 | |
| 101 | if (fieldType && (fieldType === 'radio' || fieldType === 'checkbox')) { |
| 102 | if (input.checked && comparisonResult) { |
| 103 | visible = true; |
| 104 | } |
| 105 | } else if (comparisonResult) { |
| 106 | visible = true; |
| 107 | } |
| 108 | }); |
| 109 | |
| 110 | // Show or Hide field wrapper. |
| 111 | visible ? |
| 112 | fieldWrapper.classList.remove('give-hidden') : |
| 113 | fieldWrapper.classList.add('give-hidden'); |
| 114 | } |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | /** |
| 119 | * Setup state for condition visibility settings. |
| 120 | * state contains list of watched elements per donation form. |
| 121 | * |
| 122 | * @since 2.15.0 |
| 123 | */ |
| 124 | function addVisibilityConditionsToStateForDonationForm(donationForm) { |
| 125 | const uniqueDonationFormId = donationForm.getAttribute('data-id'); |
| 126 | const watchedFields = getWatchedElementNames(donationForm); |
| 127 | |
| 128 | // Add donation form to state only if visibility conditions exiting for at least form field. |
| 129 | if (uniqueDonationFormId && Object.keys(watchedFields).length) { |
| 130 | state[uniqueDonationFormId] = watchedFields; |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | /** |
| 135 | * @since 2.15.0 |
| 136 | * @param donationForm |
| 137 | */ |
| 138 | function applyVisibilityConditionsToDonationForm(donationForm) { |
| 139 | const uniqueDonationFormId = donationForm.getAttribute('data-id'); |
| 140 | |
| 141 | if (uniqueDonationFormId && (uniqueDonationFormId in state)) { |
| 142 | const formState = state[uniqueDonationFormId]; |
| 143 | |
| 144 | for (const [watchedFieldName, visibilityConditions] of Object.entries(formState)) { |
| 145 | handleVisibility( |
| 146 | document.querySelector(`form[data-id="${uniqueDonationFormId}"]`) |
| 147 | .closest('.give-form'), |
| 148 | watchedFieldName, |
| 149 | visibilityConditions |
| 150 | ); |
| 151 | } |
| 152 | } |
| 153 | } |
| 154 | |
| 155 | /** |
| 156 | * @since 2.15.0 |
| 157 | */ |
| 158 | function addChangeEventToWatchedElementsForDonationForm(donationFormUniqueId) { |
| 159 | const donationForm = document |
| 160 | .querySelector(`form.give-form[data-id="${donationFormUniqueId}"`) |
| 161 | .closest('form.give-form'); |
| 162 | |
| 163 | if (!donationForm || !state.hasOwnProperty(donationFormUniqueId)) { |
| 164 | return; |
| 165 | } |
| 166 | |
| 167 | for (const [watchedElementName, VisibilityConditions] of Object.entries(state[donationFormUniqueId])) { |
| 168 | document.querySelectorAll(`[name = "${watchedElementName}"]`) |
| 169 | .forEach(field => { |
| 170 | field.addEventListener( |
| 171 | 'change', |
| 172 | () => handleVisibility(donationForm, watchedElementName, VisibilityConditions) |
| 173 | ); |
| 174 | }); |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | /** |
| 179 | * @since 2.15.0 |
| 180 | */ |
| 181 | function bootVisibilityConditionsFormAllDonationForm() { |
| 182 | document.querySelectorAll('form.give-form').forEach(addVisibilityConditionsToStateForDonationForm); |
| 183 | |
| 184 | // Apply visibility conditions. |
| 185 | // Add change event to watched field. |
| 186 | for (const [donationFormUniqueId, donationFormState] of Object.entries(state)) { |
| 187 | for (const [watchedFieldName, visibilityConditions] of Object.entries(donationFormState)) { |
| 188 | handleVisibility( |
| 189 | document.querySelector(`form[data-id="${donationFormUniqueId}"]`) |
| 190 | .closest('.give-form'), |
| 191 | watchedFieldName, |
| 192 | visibilityConditions |
| 193 | ); |
| 194 | } |
| 195 | |
| 196 | addChangeEventToWatchedElementsForDonationForm(donationFormUniqueId); |
| 197 | } |
| 198 | } |
| 199 | |
| 200 | bootVisibilityConditionsFormAllDonationForm(); |
| 201 | |
| 202 | // Apply visibility conditions to donation form when donor switch gateway. |
| 203 | document.addEventListener( |
| 204 | 'give_gateway_loaded', |
| 205 | event => { |
| 206 | const donationForm = document.getElementById(event.detail.formIdAttribute); |
| 207 | const uniqueDonationFormId = donationForm.getAttribute('data-id'); |
| 208 | addVisibilityConditionsToStateForDonationForm(donationForm); |
| 209 | applyVisibilityConditionsToDonationForm(donationForm); |
| 210 | addChangeEventToWatchedElementsForDonationForm(uniqueDonationFormId) |
| 211 | } |
| 212 | ); |
| 213 | }); |
| 214 |