1class PsFormsConditionalLogic { 2 constructor(root = document) { 3 this.root = root; 4 this.conditionalFields = Array.from( 5 this.root.querySelectorAll("[data-psforms-conditional]") 6 ); 7 } 8 9 init() { 10 if (!this.conditionalFields.length) return; 11 this.storeInitialRequiredState(); 12 this.bindInputs(); 13 this.evaluateAll(); 14 } 15 16 storeInitialRequiredState() { 17 const inputs = this.root.querySelectorAll("input, select, textarea"); 18 19 inputs.forEach((input) => { 20 input.dataset.psformsOriginallyRequired = input.required ? "true" : "false"; 21 }); 22 } 23 24 bindInputs() { 25 const sourceFields = Array.from( 26 this.root.querySelectorAll("[data-condition-key]") 27 ); 28 29 sourceFields.forEach((fieldWrapper) => { 30 const inputs = fieldWrapper.querySelectorAll("input, select, textarea"); 31 32 inputs.forEach((input) => { 33 input.addEventListener("change", () => this.evaluateAll()); 34 35 if (this.shouldListenToInput(input)) { 36 input.addEventListener("input", () => this.evaluateAll()); 37 } 38 }); 39 }); 40 } 41 42 shouldListenToInput(input) { 43 const tag = input.tagName.toLowerCase(); 44 const type = (input.getAttribute("type") || "").toLowerCase(); 45 46 if (tag === "textarea") return true; 47 if (tag === "select") return false; 48 if (type === "radio" || type === "checkbox") return false; 49 50 return true; 51 } 52 53 evaluateAll() { 54 this.conditionalFields.forEach((targetField) => { 55 this.evaluateField(targetField); 56 }); 57 } 58 59 evaluateField(targetField) { 60 const rawConfig = targetField.dataset.psformsConditional; 61 if (!rawConfig) return; 62 63 let config; 64 65 try { 66 config = JSON.parse(rawConfig); 67 } catch { 68 return; 69 } 70 71 const results = (config.conditions || []).map((condition) => 72 this.evaluateCondition(condition) 73 ); 74 75 const matched = config.match === "any" 76 ? results.some(Boolean) 77 : results.every(Boolean); 78 79 const shouldShow = config.action === "show" ? matched : !matched; 80 81 this.applyVisibility(targetField, shouldShow); 82 } 83 84 evaluateCondition(condition) { 85 const sourceField = this.root.querySelector( 86 `[data-condition-key="${CSS.escape(condition.field)}"]` 87 ); 88 89 if (!sourceField) return false; 90 91 const currentValue = this.getFieldValue(sourceField); 92 const expectedValue = condition.value ?? ""; 93 const operator = condition.matchOperator ?? condition.operator ?? ""; 94 95 return this.compareValues(currentValue, operator, expectedValue, sourceField); 96 } 97 98 getFieldValue(fieldWrapper) { 99 const inputs = Array.from( 100 fieldWrapper.querySelectorAll("input, select, textarea") 101 ); 102 103 if (!inputs.length) return ""; 104 105 const radios = inputs.filter((input) => input.type === "radio"); 106 if (radios.length) { 107 const checked = radios.find((radio) => radio.checked); 108 return checked ? checked.value : ""; 109 } 110 111 const checkboxes = inputs.filter((input) => input.type === "checkbox"); 112 if (checkboxes.length) { 113 if (checkboxes.length === 1) { 114 return checkboxes[0].checked ? "true" : "false"; 115 } 116 117 return checkboxes 118 .filter((checkbox) => checkbox.checked) 119 .map((checkbox) => checkbox.value); 120 } 121 122 const select = inputs.find((input) => input.tagName.toLowerCase() === "select"); 123 if (select) { 124 return select.value ?? ""; 125 } 126 127 const textarea = inputs.find((input) => input.tagName.toLowerCase() === "textarea"); 128 if (textarea) { 129 return textarea.value ?? ""; 130 } 131 132 return inputs[0].value ?? ""; 133 } 134 135 compareValues(currentValue, operator, expectedValue, sourceField = null) { 136 if (Array.isArray(currentValue)) { 137 return this.compareArrayValues(currentValue, operator, expectedValue); 138 } 139 140 const actual = String(currentValue ?? ""); 141 const expected = String(expectedValue ?? ""); 142 143 switch (operator) { 144 case "eq": 145 return actual === expected; 146 case "neq": 147 return actual !== expected; 148 case "gt": { 149 if (this.isDateField(sourceField)) { 150 const actualDate = this.toDateValue(actual); 151 const expectedDate = this.toDateValue(expected); 152 153 if (actualDate === null || expectedDate === null) return false; 154 return actualDate > expectedDate; 155 } 156 157 const actualNumber = this.toNumber(actual); 158 const expectedNumber = this.toNumber(expected); 159 160 if (actualNumber === null || expectedNumber === null) return false; 161 return actualNumber > expectedNumber; 162 } 163 164 case "lt": { 165 if (this.isDateField(sourceField)) { 166 const actualDate = this.toDateValue(actual); 167 const expectedDate = this.toDateValue(expected); 168 169 if (actualDate === null || expectedDate === null) return false; 170 return actualDate < expectedDate; 171 } 172 173 const actualNumber = this.toNumber(actual); 174 const expectedNumber = this.toNumber(expected); 175 176 if (actualNumber === null || expectedNumber === null) return false; 177 return actualNumber < expectedNumber; 178 } 179 case "contains_cs": 180 return actual.includes(expected); 181 case "contains_ci": 182 return actual.toLowerCase().includes(expected.toLowerCase()); 183 case "starts_cs": 184 return actual.startsWith(expected); 185 case "starts_ci": 186 return actual.toLowerCase().startsWith(expected.toLowerCase()); 187 case "ends_cs": 188 return actual.endsWith(expected); 189 case "ends_ci": 190 return actual.toLowerCase().endsWith(expected.toLowerCase()); 191 case "not_contains_cs": 192 return !actual.includes(expected); 193 case "not_contains_ci": 194 return !actual.toLowerCase().includes(expected.toLowerCase()); 195 case "not_starts_cs": 196 return !actual.startsWith(expected); 197 case "not_starts_ci": 198 return !actual.toLowerCase().startsWith(expected.toLowerCase()); 199 case "not_ends_cs": 200 return !actual.endsWith(expected); 201 case "not_ends_ci": 202 return !actual.toLowerCase().endsWith(expected.toLowerCase()); 203 default: 204 return false; 205 } 206 } 207 208 compareArrayValues(currentValues, operator, expectedValue) { 209 switch (operator) { 210 case "contains_cs": 211 return currentValues.includes(expectedValue); 212 case "contains_ci": 213 return currentValues.some( 214 (value) => value.toLowerCase() === expectedValue.toLowerCase() 215 ); 216 case "not_contains_cs": 217 return !currentValues.includes(expectedValue); 218 case "not_contains_ci": 219 return !currentValues.some( 220 (value) => value.toLowerCase() === expectedValue.toLowerCase() 221 ); 222 case "eq": 223 return currentValues.length === 1 && currentValues[0] === expectedValue; 224 case "neq": 225 return !(currentValues.length === 1 && currentValues[0] === expectedValue); 226 default: 227 return false; 228 } 229 } 230 231 applyVisibility(field, shouldShow) { 232 field.hidden = !shouldShow; 233 field.classList.toggle("psforms-conditionally-hide", !shouldShow); 234 field.classList.toggle("psforms-conditionally-show", shouldShow); 235 236 const inputs = field.querySelectorAll("input, select, textarea"); 237
238 inputs.forEach((input) => { 239 if (shouldShow) { 240 input.disabled = false; 241 input.required = input.dataset.psformsOriginallyRequired === "true"; 242 } else { 243 input.required = false; 244 input.disabled = true; 245 } 246 }); 247 } 248 249 toNumber(value) { 250 if (value === null || value === undefined) return null; 251 252 const trimmed = String(value).trim(); 253 if (trimmed === "") return null; 254 255 const parsed = Number(trimmed); 256 return Number.isNaN(parsed) ? null : parsed; 257 } 258 259 isDateField(fieldWrapper) { 260 if (!fieldWrapper) return false; 261 262 const dateInput = fieldWrapper.querySelector('input[type="date"]'); 263 return !!dateInput; 264 } 265 266 toDateValue(value) { 267 if (value === null || value === undefined) return null; 268 269 const trimmed = String(value).trim(); 270 if (trimmed === "") return null; 271 272 const parts = trimmed.split("-"); 273 if (parts.length !== 3) return null; 274 275 const year = Number(parts[0]); 276 const month = Number(parts[1]); 277 const day = Number(parts[2]); 278 279 if (!year || !month || !day) return null; 280 281 const date = new Date(year, month - 1, day); 282 283 if ( 284 date.getFullYear() !== year || 285 date.getMonth() !== month - 1 || 286 date.getDate() !== day 287 ) { 288 return null; 289 } 290 291 return date.getTime(); 292 } 293} 294 295document.addEventListener("DOMContentLoaded", () => { 296 new PsFormsConditionalLogic().init(); 297});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.