.
* @param {string} id - Input field id.
* @param {Document} [doc=document] - Document object to query.
*/
utils.markFieldAsRequired = function markFieldAsRequired(id, doc = document) {
const label = utils.getLabelForField(id, doc);
if (!label) {
console.warn(`Cannot mark field as required: Label not found for ${utils.cleanId(id)}`);
return;
}
const infoDiv = label.closest('.info');
if (infoDiv) {
infoDiv.classList.add('required');
} else {
console.warn(`div.info not found for: ${utils.cleanId(id)} (label found)`);
}
};
/**
* Removes the 'required' classes from a field's
.
* @param {string} id - Input field id.
* @param {Document} [doc=document] - Document object to query.
*/
utils.unmarkFieldAsRequired = function unmarkFieldAsRequired(id, doc = document) {
const label = utils.getLabelForField(id, doc);
if (!label) {
console.warn(`Label not found for: ${utils.cleanId(id)}`);
return;
}
const infoDiv = label.closest('.info');
if (infoDiv) {
//infoDiv.classList.remove('force-asterisk');
infoDiv.classList.remove('required');
} else {
console.warn(`div.info not found for: ${cleanId} (label found)`);
}
};
/**
* Clears the values of multiple fields.
* Handles different field types: text inputs, checkboxes, radios, selects, etc.
* @param {Array|string} fields - Array of field IDs or a single field ID (e.g., ['field1', 'field2'] or 'field1').
* @param {Document} [doc=document] - Document object to query; defaults to the global document.
* @example
* utils.clearFieldValues(['textInput', 'selectDropdown']); // Clears specified fields
*/
utils.clearFieldValues = function (fields, doc = document) {
if (!Array.isArray(fields)) fields = [fields];
const processedGroups = new Set();
fields.forEach(id => {
const field = utils.getFieldById(id, doc);
if (!field) return;
if (utils.isDateField(id, doc)) {
// Clear both hidden and visible inputs for datepickers
field.value = ''; // Hidden input
const visibleId = utils.getDatepickerFullId(id);
const visibleField = doc.getElementById(visibleId);
if (visibleField) {
visibleField.value = ''; // Visible input
}
} else if (field.type === 'radio') {
const groupName = field.name;
if (!processedGroups.has(groupName)) {
const radios = doc.querySelectorAll(`input[type="radio"][name="${groupName}"]`);
radios.forEach(radio => radio.checked = false);
processedGroups.add(groupName);
}
} else if (field.type === 'checkbox') {
field.checked = false;
} else if (field.tagName === 'SELECT') {
if (field.multiple) {
Array.from(field.options).forEach(option => option.selected = false);
} else {
field.selectedIndex = 0;
}
} else {
field.value = '';
}
});
};
/**
* Hides fields, clears their values, and disables their validation.
* @param {Array|string} fields - Array of field IDs or a single field ID.
* @param {boolean} [hideLabel=true] - Whether to hide the field's label.
* @param {Document} [doc=document] - Document object to query.
*/
utils.hideClearDisableFields = function hideClearDisableFields(fields, hideLabel = true, clearValues = true, doc = document) {
if (!Array.isArray(fields)) fields = [fields];
if (clearValues) {
utils.clearFieldValues(fields, doc);
}
utils.hideFields(fields, hideLabel, doc);
utils.disableFieldValidation(fields);
console.log('Validators after disabling:', window.Page_Validators.map(v => v.controltovalidate));
};
/**
* Shows specified fields and enables their validation.
* @param {Array|string} fields - Array of field IDs or a single field ID.
* @param {boolean} [showLabel=true] - Whether to show the field's label.
* @param {Document} [doc=document] - Document object to query.
*/
utils.showEnableFields = function showEnableFields(fields, showLabel = true, doc = document) {
if (!Array.isArray(fields)) fields = [fields];
// Show the fields (and their labels if specified)
utils.showFields(fields, showLabel, doc);
// Re-enable validation for all fields
utils.enableFieldValidation(fields);
console.log('Validators after enabling:', window.Page_Validators.map(v => v.controltovalidate));
};
// Temporary storage for disabled validators
window.disabledValidators = window.disabledValidators || {};
// Disable validation for multiple fields
utils.disableFieldValidation = function disableFieldValidation(fields, doc = document) {
if (!Array.isArray(fields)) fields = [fields];
fields.forEach(id => {
let controlId = utils.isDateField(id, doc) ? utils.getDatepickerFullId(id) : id;
const validators = window.Page_Validators.filter(validator =>
validator.controltovalidate === controlId
);
validators.forEach(validator => {
validator.enabled = false;
validator.isvalid = true; // Ensures the validator doesn't mark the field as invalid
utils.log("log", `Disabled validator for: ${controlId}, Validator ID: ${validator.id}`);
});
});
};
// Re-enable validation for multiple fields
utils.enableFieldValidation = function enableFieldValidation(fields, doc = document) {
if (!Array.isArray(fields)) fields = [fields];
fields.forEach(id => {
let controlId = utils.isDateField(id, doc) ? utils.getDatepickerFullId(id) : id;
const validators = window.Page_Validators.filter(validator =>
validator.controltovalidate === controlId
);
validators.forEach(validator => {
validator.enabled = true;
utils.log("log", `Enabled validator for: ${controlId}, Validator ID: ${validator.id}`);
});
});
};
/**
* Binds a control’s state to dynamically manage field visibility, validation, and values.
*
* @param {Object} rulesObject - Configuration object with the following properties:
* - **controlId**: `string` - The ID of the primary control to bind (e.g., dropdown, checkbox, or radio group).
* - **rules**: `Object` - An object with control IDs as keys. Each key maps to an object with value-specific rules:
* - **fields**: `string[]` - Array of field IDs to display when the control’s value matches.
* - **retainValueFields**: `string[]` - Array of field IDs to display when the control’s value matches and also whose values are retained when hidden across all states.
* - **elements**: `HTMLElement[]` - Array of DOM elements to show when the control’s value matches.
* - **subControls**: `string[]` - Array of sub-control IDs to bind recursively when the value matches.
* @param {Document} [doc=document] - Optional document object to query. Defaults to the global `document`.
* @returns {undefined} - This function does not return an unbind function. The binding is persistent until the form or
* page is unloaded. If you need to stop the binding, simply avoid calling this function or manage it externally
* (e.g., by reloading or resetting the form). An unbind function is intentionally omitted to keep the implementation
* simple, as it’s not required for basic usage.
*
* @description
* The `bindControlState` function dynamically ties a control’s value (e.g., from a dropdown, checkbox, or radio group)
* to the visibility and behavior of fields, DOM elements, and sub-controls. It provides a flexible way to:
*
* - **Show/Hide Fields**: Fields listed in `fields` or 'retainValueFields' are shown when the control’s value matches; all others are hidden.
* - **Retain Values**: Fields listed in `retainValueFields` for any state (including sub-controls) automatically retain
* their values when hidden, without requiring additional configuration.
* - **Manage Sub-Controls**: Sub-controls listed in `subControls` are recursively bound with their own rules. Their
* states persist unless externally reset.
* - **Handle Events**: A `'change'` event listener updates the form state dynamically when the control’s value changes.
* @example
* // Example: A tax form where "incomeType" dropdown toggles field visibility
* const rulesObject = {
* controlId: "incomeType",
* rules: {
* "incomeType": {
* "Employment": {
* fields: ["employerName", "annualSalary"], // Show these fields
* retainValueFields: [],
* elements: [document.getElementById("employmentSection")],
* subControls: ["hasDependents"]
* },
* "Investment": {
* fields: [], // Show these fields
* retainValueFields: ["investmentType", "investmentAmount"], //show these fields and Retain these values when hidden
* elements: [document.getElementById("investmentSection")],
* subControls: []
* }
* },
* "hasDependents": {
* "true": {
* fields: ["dependentName", "dependentAge"], // Show these fields
* retainValueFields: [],
* elements: [document.getElementById("dependentsSection")],
* subControls: []
* },
* "false": {
* fields: [], // Show no fields
* subControls: []
* }
* }
* }
* };
*
* // Bind the control state to the form
* const unbind = bindControlState(rulesObject);
*/
utils.bindControlState = function bindControlState(rulesObject, doc = document) {
const { controlId, rules } = rulesObject;
const globalRetainFields = [];
if (!controlId || !rules[controlId]) {
console.error(`Invalid rulesObject: missing controlId or rules for ${controlId}`);
return;
}
const control = doc.getElementById(controlId);
if (!control) {
console.error(`Control with ID ${controlId} not found`);
return;
}
// Collect all fields from retainValueFields across all states and sub-controls
const newGlobalRetainFields = new Set(globalRetainFields);
Object.values(rules[controlId]).forEach(rule => {
if (rule.retainValueFields) {
rule.retainValueFields.forEach(field => newGlobalRetainFields.add(field));
}
if (rule.subControls) {
rule.subControls.forEach(subId => {
const subRules = rules[subId];
if (subRules) {
Object.values(subRules).forEach(subRule => {
if (subRule.retainValueFields) {
subRule.retainValueFields.forEach(field => newGlobalRetainFields.add(field));
}
});
}
});
}
});
// Collect all possible subControlIds
const allSubControlIds = new Set();
Object.values(rules[controlId]).forEach(rule => {
if (rule.subControls) {
rule.subControls.forEach(subId => allSubControlIds.add(subId));
}
});
let controlElements, getSelectedValue, addEventListeners;
// Control Type Detection and Setup
if (control.tagName === 'SELECT') {
controlElements = [control];
getSelectedValue = () => control.value;
addEventListeners = (handler) => {
if (!control._bindControlStateHandler) {
control.addEventListener('change', handler);
control._bindControlStateHandler = handler;
}
};
} else if (control.type === 'checkbox') {
controlElements = [control];
getSelectedValue = () => control.checked ? 'true' : 'false';
addEventListeners = (handler) => {
if (!control._bindControlStateHandler) {
control.addEventListener('change', handler);
control._bindControlStateHandler = handler;
}
};
} else if (control.querySelectorAll('input[type="radio"]').length > 0) {
// Radio group within a container
controlElements = control.querySelectorAll('input[type="radio"]');
getSelectedValue = () => {
const checkedRadio = Array.from(controlElements).find(radio => radio.checked);
return checkedRadio ? checkedRadio.value : null;
};
addEventListeners = (handler) => {
controlElements.forEach(radio => {
if (!radio._bindControlStateHandler) {
radio.addEventListener('change', handler);
radio._bindControlStateHandler = handler;
}
});
};
} else {
// Radio buttons by name attribute
controlElements = doc.querySelectorAll(`input[type="radio"][name="${controlId}"]`);
if (controlElements.length === 0) {
console.warn(`Unsupported control type or not found: ${controlId}`);
return;
}
getSelectedValue = () => {
const checkedRadio = Array.from(controlElements).find(radio => radio.checked);
return checkedRadio ? checkedRadio.value : null;
};
addEventListeners = (handler) => {
controlElements.forEach(radio => {
if (!radio._bindControlStateHandler) {
radio.addEventListener('change', handler);
radio._bindControlStateHandler = handler;
}
});
};
}
/**
* Updates field visibility, validation, and values based on the control’s current value.
* @param {boolean} clearValues - Whether to clear values of hidden fields not in globalRetainFields.
*/
function updateFields(clearValues = true) {
const selectedValue = getSelectedValue();
console.log(`Control ${controlId} selected value: ${selectedValue}`);
const allFields = new Set();
const allElements = new Set();
Object.values(rules[controlId]).forEach(rule => {
if (rule.fields) rule.fields.forEach(field => allFields.add(field));
if (rule.retainValueFields) rule.retainValueFields.forEach(field => allFields.add(field));
if (rule.elements) rule.elements.forEach(el => allElements.add(el));
});
const rule = rules[controlId][selectedValue] || {};
const fieldsToShow = [...new Set([...(rule.fields || []), ...(rule.retainValueFields || [])])];
const fieldsToHide = Array.from(allFields).filter(id => !fieldsToShow.includes(id));
const fieldsToHideAndClear = fieldsToHide.filter(id => !newGlobalRetainFields.has(id));
const fieldsToHideOnly = fieldsToHide.filter(id => newGlobalRetainFields.has(id));
if (fieldsToHideAndClear.length > 0) {
utils.hideClearDisableFields(fieldsToHideAndClear, true, clearValues, doc);
}
if (fieldsToHideOnly.length > 0) {
utils.hideClearDisableFields(fieldsToHideOnly, true, false, doc);
}
allElements.forEach(el => {
if (el && el.style) el.style.display = 'none';
});
if (fieldsToShow.length > 0) {
utils.showEnableFields(fieldsToShow, true, doc);
}
if (rule.elements && rule.elements.length > 0) {
rule.elements.forEach(el => {
if (el && el.style) el.style.display = '';
});
}
// Manage sub-controls
const currentSubControls = rule.subControls || [];
allSubControlIds.forEach(subControlId => {
const subControl = doc.getElementById(subControlId);
if (!subControl) return;
const label = utils.getLabelForField(subControlId, doc);
const infoDiv = label ? label.closest('.info') : null;
if (!currentSubControls.includes(subControlId)) {
// Hide both input and label
subControl.style.display = 'none';
if (infoDiv) infoDiv.style.display = 'none';
// Hide associated fields, respecting newGlobalRetainFields
const subRules = rules[subControlId];
if (subRules) {
const subAllFields = new Set();
Object.values(subRules).forEach(subRule => {
if (subRule.fields) subRule.fields.forEach(field => subAllFields.add(field));
if (subRule.retainValueFields) subRule.retainValueFields.forEach(field => subAllFields.add(field));
});
const subFieldsToHide = Array.from(subAllFields);
const subFieldsToHideAndClear = subFieldsToHide.filter(id => !newGlobalRetainFields.has(id));
const subFieldsToHideOnly = subFieldsToHide.filter(id => newGlobalRetainFields.has(id));
if (subFieldsToHideAndClear.length > 0) {
utils.hideClearDisableFields(subFieldsToHideAndClear, true, clearValues, doc);
}
if (subFieldsToHideOnly.length > 0) {
utils.hideClearDisableFields(subFieldsToHideOnly, true, false, doc);
}
}
} else {
// Show both input and label
subControl.style.display = '';
if (infoDiv) infoDiv.style.display = '';
// Bind the sub-control if not already bound
if (!subControl._bindControlStateHandler) {
const subRulesObject = { controlId: subControlId, rules: rulesObject.rules };
bindControlState(subRulesObject, doc);
}
}
});
}
// Event Listener Setup
const changeHandler = () => updateFields(true);
addEventListeners(changeHandler);
updateFields(false);
};
// ### Masking Functions ###
utils.jqueryMaskLoaded = false;
utils.jqueryMaskLoading = false;
/**
* Loads the jQuery Mask Plugin dynamically if not already available.
* @param {function} callback - Executes once the plugin is loaded.
* @returns {Promise|undefined} - Promise if loading, undefined if loaded.
*/
utils.loadJQueryMaskPlugin = function loadJQueryMaskPlugin(callback) {
// Check if jQuery Mask Plugin is already loaded
if (typeof $.mask !== 'undefined') {
utils.jqueryMaskLoaded = true;
return callback();
}
if (utils.jqueryMaskLoaded) return callback();
if (utils.jqueryMaskLoading) {
return new Promise((resolve) => {
document.addEventListener("jqueryMaskLoaded", () => {
callback();
resolve();
}, { once: true });
});
}
utils.jqueryMaskLoading = true;
const script = document.createElement("script");
script.src = "https://cdnjs.cloudflare.com/ajax/libs/jquery.mask/1.14.16/jquery.mask.min.js";
script.async = true;
script.onload = () => {
utils.jqueryMaskLoaded = true;
utils.jqueryMaskLoading = false;
document.dispatchEvent(new Event("jqueryMaskLoaded"));
utils.log("log", "jQuery Mask loaded from CDN");
callback();
};
script.onerror = () => {
utils.log("error", "Failed to load jQuery Mask from CDN");
utils.jqueryMaskLoading = false;
};
document.head.appendChild(script);
};
/**
* Applies an SSN mask (###-##-####) to an input field.
* @param {string} id - Input field id (e.g., 'ssnField').
* @param {Document} [doc=document] - Document object to query.
*/
utils.maskSSN = function maskSSN(id, doc = document) {
const field = utils.getFieldById(id, doc);
if (!field) return;
field.setAttribute("placeholder", "###-##-####");
field.setAttribute("autocomplete", "off");
utils.loadJQueryMaskPlugin(() => $(field).unmask().mask("000-00-0000"));
};
/**
* Applies a ZIP code mask (#####) to an input field.
* @param {string} id - Input field id (e.g., 'zipField').
* @param {Document} [doc=document] - Document object to query.
*/
utils.maskZip = function maskZip(id, doc = document) {
const field = utils.getFieldById(id, doc);
if (!field) return;
field.setAttribute("placeholder", "#####");
utils.loadJQueryMaskPlugin(() => $(field).unmask().mask("00000"));
};
/**
* Applies a phone number mask ((###) ###-####) to an input field.
* @param {string} id - Input field id (e.g., 'phoneField').
* @param {Document} [doc=document] - Document object to query.
*/
utils.maskPhone = function maskPhone(id, doc = document) {
const field = utils.getFieldById(id, doc);
if (!field) return;
field.setAttribute("placeholder", "(###) ###-####");
utils.loadJQueryMaskPlugin(() => $(field).unmask().mask("(000) 000-0000"));
};
/**
* Sets up an email field with a placeholder and real-time feedback.
* @param {string} id - Input field id (e.g., 'emailField').
* @param {Document} [doc=document] - Document object to query.
*/
utils.maskEmail = function maskEmail(id, doc = document) {
const field = utils.getFieldById(id, doc);
if (!field) return;
field.setAttribute("placeholder", "example@domain.com");
field.setAttribute("type", "email");
const inputHandler = debounce((event) => {
const value = event.target.value;
const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
const isValid = value === "" || emailPattern.test(value);
event.target.setAttribute("aria-invalid", !isValid);
}, config.debounceDelay);
field.addEventListener("input", inputHandler);
field._inputHandler = inputHandler;
};
/** Commented out because it's not used and it just causes issues.
* There's a built-in mask that this attempts to overwrite and it breaks things when doing that
* Applies a date mask (M/D/YYYY) to an input field.
* @param {string} id - Input field id (e.g., 'dateField').
* @param {Document} [doc=document] - Document object to query.
*/
// utils.maskDate = function maskDate(id, doc = document) {
// const field = utils.getFieldById(id, doc);
// if (!field) return;
// utils.loadJQueryMaskPlugin(() => $(field).unmask().mask("00/00/0000"));
// };
/** This is commented out because it's not working right. There seems to be no advantage to using it.
* Applies a money mask (#,##0.00) to an input field, right-to-left.
* @param {string} id - Input field id (e.g., 'moneyField').
* @param {Document} [doc=document] - Document object to query.
*/
// utils.maskMoney = function maskMoney(id, doc = document) {
// const field = utils.getFieldById(id, doc);
// if (!field) return;
// field.setAttribute("autocomplete", "off");
// utils.loadJQueryMaskPlugin(() => $(field).unmask().mask("#,##0.00", { reverse: true }));
// };
// ### Validation Functions ###
/**
* Adds SSN validation (9 digits) to an input field.
* @param {string} id - Input field id (e.g., 'ssnField').
* @param {boolean} [isRequired=true] - Whether the field is required.
* @param {Document} [doc=document] - Document object to query.
*/
utils.validateSSN = function validateSSN(id, isRequired = true, doc = document) {
const field = utils.getFieldById(id, doc);
if (!field) return;
const validatorId = "SSNValidator" + field.id;
const blurHandler = function () {
const digits = this.value.replace(/\D/g, "");
if (digits.length > 0 && digits.length < 9) this.dispatchEvent(new Event("change"));
};
field.removeEventListener("blur", field._blurHandler);
field.addEventListener("blur", blurHandler);
field._blurHandler = blurHandler;
utils.addValidator(
field.id,
validatorId,
"Please enter a complete 9-digit SSN.",
() => {
const value = field.value.replace(/\D/g, "");
const isValid = isRequired ? value.length === 9 : value.length === 0 || value.length === 9;
field.setAttribute("aria-invalid", !isValid);
return isValid;
},
null,
isRequired,
doc
);
};
/**
* Adds ZIP code validation (5 digits) to an input field.
* @param {string} id - Input field id (e.g., 'zipField').
* @param {boolean} [isRequired=true] - Whether the field is required.
* @param {Document} [doc=document] - Document object to query.
*/
utils.validateZip = function validateZip(id, isRequired = true, doc = document) {
const field = utils.getFieldById(id, doc);
if (!field) return;
const validatorId = "ZipValidator" + field.id;
const blurHandler = function () {
const digits = this.value.replace(/\D/g, "");
if (digits.length > 0 && digits.length < 5) this.dispatchEvent(new Event("change"));
};
field.removeEventListener("blur", field._blurHandler);
field.addEventListener("blur", blurHandler);
field._blurHandler = blurHandler;
utils.addValidator(
field.id,
validatorId,
"Please enter a complete 5-digit ZIP code.",
() => {
const value = field.value.replace(/\D/g, "");
const isValid = isRequired ? value.length === 5 : value.length === 0 || value.length === 5;
field.setAttribute("aria-invalid", !isValid);
return isValid;
},
null,
isRequired,
doc
);
};
/**
* Adds phone number validation (10 digits) to an input field.
* @param {string} id - Input field id (e.g., 'phoneField').
* @param {boolean} [isRequired=true] - Whether the field is required.
* @param {Document} [doc=document] - Document object to query.
*/
utils.validatePhone = function validatePhone(id, isRequired = true, doc = document) {
const field = utils.getFieldById(id, doc);
if (!field) return;
const validatorId = "PhoneValidator" + field.id;
const blurHandler = function () {
const digits = this.value.replace(/\D/g, "");
if (digits.length > 0 && digits.length < 10) this.dispatchEvent(new Event("change"));
};
field.removeEventListener("blur", field._blurHandler);
field.addEventListener("blur", blurHandler);
field._blurHandler = blurHandler;
utils.addValidator(
field.id,
validatorId,
"Please enter a complete 10-digit phone number.",
() => {
const value = field.value.replace(/\D/g, "");
const isValid = isRequired ? value.length === 10 : value.length === 0 || value.length === 10;
field.setAttribute("aria-invalid", !isValid);
return isValid;
},
null,
isRequired,
doc
);
};
/**
* Adds email validation to an input field.
* @param {string} id - Input field id (e.g., 'emailField').
* @param {boolean} [isRequired=true] - Whether the field is required.
* @param {Document} [doc=document] - Document object to query.
*/
utils.validateEmail = function validateEmail(id, isRequired = true, doc = document) {
const field = utils.getFieldById(id, doc);
if (!field) return;
const validatorId = "EmailValidator" + field.id;
utils.addValidator(
field.id,
validatorId,
"Please enter a valid email address.",
() => {
const value = field.value.trim();
const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
const isValid = isRequired ? emailPattern.test(value) : value === "" || emailPattern.test(value);
field.setAttribute("aria-invalid", !isValid);
return isValid;
},
null,
isRequired,
doc
);
};
/**
* Adds date validation (MM/DD/YYYY) to an input field.
* @param {string} id - Input field id (e.g., 'dateField').
* @param {boolean} [isRequired=true] - Whether the field is required.
* @param {Document} [doc=document] - Document object to query.
*/
utils.validateDate = function validateDate(id, isRequired = true, doc = document, maxRetries = 20, retryInterval = 300) {
const field = utils.getFieldById(id, doc); // Hidden input
if (!field) return;
const label = utils.getLabelForField(field.id, doc);
const labelText = label ? label.textContent.trim() : field.id;
const targetId = utils.getDatepickerFullId(id); // Visible datepicker input
let retries = 0;
const addValidatorWithRetry = () => {
const targetField = doc.getElementById(targetId);
if (targetField) {
const validatorId = "DateValidator" + targetId;
const dateRegex = /^(0?[1-9]|1[0-2])\/(0?[1-9]|[12]\d|3[01])\/\d{4}$/;
utils.addValidator(
targetId,
validatorId,
`${labelText} must be a valid date in M/D/YYYY format.`,
(validatedField) => {
const value = validatedField.value.trim();
const isValid = isRequired
? dateRegex.test(value) && isValidDate(value)
: value.length === 0 || (dateRegex.test(value) && isValidDate(value));
validatedField.setAttribute("aria-invalid", !isValid);
return isValid;
},
null,
isRequired,
doc
);
// Disable the built-in validator for the hidden field
const hiddenFieldId = utils.cleanId(id); // e.g., 'dhsbh_lastdateofemployment'
const builtInValidator = window.Page_Validators.find(v => v.controltovalidate === hiddenFieldId);
if (builtInValidator) {
builtInValidator.enabled = false;
builtInValidator.isvalid = true;
utils.log("log", `Disabled built-in validator for hidden field: ${hiddenFieldId}`);
}
utils.log("log", `Validator successfully added for ${targetId} after ${retries} retries`);
} else if (retries < maxRetries) {
retries++;
utils.log("warn", `Datepicker field not found: ${targetId}. Retry ${retries}/${maxRetries}`);
setTimeout(addValidatorWithRetry, retryInterval);
} else {
utils.log("error", `Failed to add validator for ${targetId} after ${maxRetries} retries`);
}
};
addValidatorWithRetry();
};
/**
* Adds money validation (positive number with up to 2 decimal places) to an input field.
* @param {string} id - Input field id (e.g., 'moneyField').
* @param {boolean} [isRequired=true] - Whether the field is required.
* @param {Document} [doc=document] - Document object to query.
*/
utils.validateMoney = function validateMoney(id, isRequired = true, doc = document) {
const field = utils.getFieldById(id, doc);
if (!field) return;
const label = utils.getLabelForField(field.id, doc);
const labelText = label ? label.textContent.trim() : field.id; // Fallback to ID if no label
const validatorId = "MoneyValidator" + field.id;
const errorMessage = `${labelText} must be a valid amount (e.g., 1111.11).`;
utils.addValidator(
field.id,
validatorId,
errorMessage,
() => {
const value = field.value.replace(/[^0-9.]/g, "");
const moneyPattern = /^\d+(\.\d{1,2})?$/;
const isValid = isRequired ? moneyPattern.test(value) && parseFloat(value) >= 0
: value === "" || (moneyPattern.test(value) && parseFloat(value) >= 0);
field.setAttribute("aria-invalid", !isValid);
return isValid;
},
null,
isRequired,
doc
);
};
// ### Mask and Validate Combinations ###
/**
* Generalized function to apply appropriate masking and validation based on field type.
* Uses class names and ID patterns to determine type; defaults to addRequiredValidator if no specific mask/validate applies.
* @param {string} id - Input field id (e.g., '#dhsbh_incomeverificationhouseholdsize' or 'dhsbh_incomeverificationhouseholdsize').
* @param {boolean} [isRequired=true] - Whether the field is required.
* @param {Document} [doc=document] - Document object to query.
* @param {Object} [options={}] - Additional options (e.g., { maxlength: 5 } for whole numbers).
*/
utils.maskAndValidate = function maskAndValidate(id, isRequired = true, doc = document, options = {}) {
const field = utils.getFieldById(id, doc);
if (!field) return;
const cleanId = utils.cleanId(id).toLowerCase();
const fieldType = field.type ? field.type.toLowerCase() : '';
const fieldTag = field.tagName ? field.tagName.toUpperCase() : '';
// Handle specific field types first
if (utils.isDateField(id, doc)) {
utils.maskAndValidateDate(id, isRequired, doc);
return;
}
if (fieldType === 'checkbox') {
utils.log('log', `Field: ${cleanId} - Using validator: addRequiredValidator (checkbox)`);
if (isRequired) {
utils.addRequiredValidator(id, null, doc); // Ensures the checkbox is checked
}
return;
}
if (fieldType === 'radio') {
utils.log('log', `Field: ${cleanId} - Using validator: addRequiredValidator (radio)`);
if (isRequired) {
utils.addRequiredValidator(id, null, doc); // Ensures a radio option is selected
}
return;
}
if (fieldTag === 'SELECT') {
utils.log('log', `Field: ${cleanId} - Using validator: addRequiredValidator (select)`);
if (isRequired) {
utils.addRequiredValidator(id, null, doc); // Ensures a dropdown option is selected
}
return;
}
// Handle additional field types
if (fieldTag === 'TEXTAREA') {
utils.log('log', `Field: ${cleanId} - Using validator: addRequiredValidator (textarea)`);
if (isRequired) {
utils.addRequiredValidator(id, null, doc); // Ensures textarea is not empty
}
return;
}
// Handle text-like fields with specific validations
if (fieldType === 'text' || fieldType === 'email' || fieldType === 'tel' || fieldType === 'number') {
if (cleanId.includes('email')) {
utils.log('log', `Field: ${cleanId} - Using validator: maskAndValidateEmail`);
utils.maskAndValidateEmail(id, isRequired, doc); // Email-specific validation
} else if (cleanId.includes('zip') || cleanId.includes('postal')) {
utils.log('log', `Field: ${cleanId} - Using validator: maskAndValidateZip`);
utils.maskAndValidateZip(id, isRequired, doc); // ZIP code validation
} else if (cleanId.includes('phone') || cleanId.includes('tel')) {
utils.log('log', `Field: ${cleanId} - Using validator: maskAndValidatePhone`);
utils.maskAndValidatePhone(id, isRequired, doc); // Phone number validation
} else if (field.classList.contains('money')) {
utils.log('log', `Field: ${cleanId} - Using validator: maskAndValidateMoney`);
utils.maskAndValidateMoney(id, isRequired, doc);
} else if (fieldType === 'number' || cleanId.includes('number') || field.classList.contains('integer')) {
utils.log('log', `Field: ${cleanId} - Using validator: maskAndValidateWholeNumber`);
utils.maskAndValidateWholeNumber(id, isRequired, options.maxlength || 4, doc); //Whole number validation
} else {
utils.log('log', `Field: ${cleanId} - Using validator: addRequiredValidator (default text)`);
if (isRequired) {
utils.addRequiredValidator(id, null, doc); // Default required validation for text
}
}
return;
}
// Fallback for unrecognized field types
utils.log('log', `Field: ${cleanId} - Using validator: addRequiredValidator (default)`);
if (isRequired) {
utils.addRequiredValidator(id, null, doc); // Default required validation
}
};
/**
* Applies Whole Number mask and validation.
* @param {string} id - Input field id.
* @param {boolean} [isRequired=true] - Whether the field is required.
* @param {number} [maxlength=4] - Maximum length of the input.
* @param {Document} [doc=document] - Document object to query.
*/
utils.maskAndValidateWholeNumber = function maskAndValidateWholeNumber(id, isRequired = true, maxlength = 4, doc = document) {
if (isRequired) {
utils.addRequiredFieldValidator(id, null, doc);
}
utils.restrictWholeNumberInput(id, maxlength, doc);
};
utils.maskAndValidateInteger = utils.maskAndValidateWholeNumber; //alias to try and prevent confusion.
/**
* Applies SSN mask and validation.
* @param {string} id - Input field id.
* @param {boolean} [isRequired=true] - Whether the field is required.
* @param {Document} [doc=document] - Document object to query.
*/
utils.maskAndValidateSSN = function maskAndValidateSSN(id, isRequired = true, doc = document) {
utils.maskSSN(id, doc);
utils.validateSSN(id, isRequired, doc);
};
/**
* Applies ZIP code mask and validation.
* @param {string} id - Input field id.
* @param {boolean} [isRequired=true] - Whether the field is required.
* @param {Document} [doc=document] - Document object to query.
*/
utils.maskAndValidateZip = function maskAndValidateZip(id, isRequired = true, doc = document) {
utils.maskZip(id, doc);
utils.validateZip(id, isRequired, doc);
};
/**
* Applies phone number mask and validation.
* @param {string} id - Input field id.
* @param {boolean} [isRequired=true] - Whether the field is required.
* @param {Document} [doc=document] - Document object to query.
*/
utils.maskAndValidatePhone = function maskAndValidatePhone(id, isRequired = true, doc = document) {
utils.maskPhone(id, doc);
utils.validatePhone(id, isRequired, doc);
};
/**
* Applies email mask and validation.
* @param {string} id - Input field id.
* @param {boolean} [isRequired=true] - Whether the field is required.
* @param {Document} [doc=document] - Document object to query.
*/
utils.maskAndValidateEmail = function maskAndValidateEmail(id, isRequired = true, doc = document) {
utils.maskEmail(id, doc);
utils.validateEmail(id, isRequired, doc);
};
/**
* Applies date mask and validation.
* @param {string} id - Input field id.
* @param {boolean} [isRequired=true] - Whether the field is required.
* @param {Document} [doc=document] - Document object to query.
*/
utils.maskAndValidateDate = function maskAndValidateDate(id, isRequired = true, doc = document) {
utils.restrictDateInput(id, doc);
utils.validateDate(id, isRequired, doc);
};
/**
* Applies money mask and validation.
* @param {string} id - Input field id.
* @param {boolean} [isRequired=true] - Whether the field is required.
* @param {Document} [doc=document] - Document object to query.
*/
utils.maskAndValidateMoney = function maskAndValidateMoney(id, isRequired = true, doc = document) {
utils.validateMoney(id, isRequired, doc);
utils.restrictMoneyInput(id, doc);
};
// ### Validator Management ###
/**
* Adds a custom validator to an input field without creating a validation span.
* @param {string} id - Input field id.
* @param {string} validatorId - Unique validator ID (e.g., 'ValidatorFieldId').
* @param {string} errorMessage - Error message for validation failure.
* @param {function} validationFunc - Returns true if valid, false otherwise.
* @param {string} [linkToLabelId] - Optional label ID to link error message - rarely used, only for when the label isn't a normal label
* @param {boolean} [isRequired=true] - Whether to add the 'required' class.
* @param {Document} [doc=document] - Document object to query.
*/
utils.addValidator = function addValidator(id, validatorId, errorMessage, validationFunc, linkToLabelId = null, isRequired = true, doc = document) {
const win = doc.defaultView || window;
const field = doc.getElementById(id);
if (!field) {
utils.log("error", `Field not found: ${id}`);
return;
}
// Check if validator already exists and remove it
if (utils.validatorExists(validatorId, doc)) {
utils.log("warn", `Validator already exists and will be replaced: ${validatorId}`);
utils.removeValidator(id, validatorId, doc);
}
field.classList.add('ndutils');
// Create the validator span
const validationSpan = doc.createElement("span");
validationSpan.id = validatorId;
validationSpan.classList.add('ndutils-validator');
validationSpan.style.display = "none";
validationSpan.controltovalidate = field.id;
validationSpan.evaluationfunction = function () {
const validatedField = doc.getElementById(this.controltovalidate);
if (!validatedField) {
utils.log('error', `Field not found for validation: ${this.controltovalidate}`);
return false;
}
const isValid = validationFunc(validatedField);
utils.log('log', `Validating ${this.controltovalidate}: value='${validatedField.value}', isValid=${isValid}`);
return isValid;
};
validationSpan.errormessage = errorMessage;
validationSpan.isvalid = true;
validationSpan.display = "None";
const label = utils.getLabelForField(field.id, doc);
const errorHtml = constructErrorMessage(field.id, label, errorMessage, linkToLabelId);
validationSpan.errormessage = errorHtml;
validationSpan.innerHTML = errorHtml;
field.parentNode.insertBefore(validationSpan, field.nextSibling);
// Add to Page_Validators
if (typeof win.Page_Validators === 'undefined') win.Page_Validators = [];
win.Page_Validators.push(validationSpan);
// Check if the field is hidden and disable the validator if so
const controlDiv = field.closest('.control');
if (controlDiv && (controlDiv.classList.contains('ndutils-hidden') || controlDiv.style.display === 'none')) {
validationSpan.enabled = false;
validationSpan.isvalid = true;
utils.log("log", `Disabled newly added validator for hidden field: ${id}, Validator ID: ${validatorId}`);
}
// Save validator parameters (existing code)
if (!window.validatorParams) window.validatorParams = {};
if (!window.validatorParams[id]) window.validatorParams[id] = [];
window.validatorParams[id].push({
validatorId: validatorId,
errorMessage: errorMessage,
validationFunc: validationFunc,
linkToLabelId: linkToLabelId,
isRequired: isRequired,
doc: doc
});
utils.log("log", `Added validator: ${validatorId} for field: ${id}`);
if (isRequired !== false) {
utils.markFieldAsRequired(field.id, doc);
} else {
utils.unmarkFieldAsRequired(field.id, doc);
}
}
/**
* Updates an existing validator’s message and function.
* @param {string} validatorId - Validator span ID.
* @param {string} errorMessage - New error message.
* @param {function} validationFunc - New validation function.
* @param {Document} [doc=document] - Document object to query.
*/
utils.updateValidator = function updateValidator(validatorId, errorMessage, validationFunc, doc = document) {
const validationSpan = doc.getElementById(validatorId);
if (!validationSpan) return utils.log("error", `Validator not found: ${validatorId}`);
validationSpan.errormessage = errorMessage;
validationSpan.evaluationfunction = validationFunc;
validationSpan.innerHTML = `
${errorMessage}`;
utils.log("log", `Updated validator: ${validatorId}`);
};
/**
* Removes a specific validator from a field and the DOM.
* @param {string} id - Input field id.
* @param {string} validatorId - Validator span ID.
* @param {Document} [doc=document] - Document object to query.
*/
utils.removeValidator = function removeValidator(id, validatorId, doc = document) {
const win = doc.defaultView || window;
if (!win.Page_Validators) return;
const validatorIndex = win.Page_Validators.findIndex(v => v.id === validatorId);
if (validatorIndex !== -1) {
win.Page_Validators.splice(validatorIndex, 1);
}
const validationSpan = doc.getElementById(validatorId);
if (validationSpan) {
validationSpan.remove(); // Remove from DOM entirely
utils.log("log", `Removed validator from DOM: ${validatorId}`);
}
};
/**
* Removes all validators for a field.
* @param {string} id - Input field id.
* @param {Document} [doc=document] - Document object to query.
*/
utils.removeAllValidatorsForField = function removeAllValidatorsForField(id, doc = document) {
const field = utils.getFieldById(id, doc);
if (!field) return;
const win = doc.defaultView || window;
if (!win.Page_Validators) return;
const validators = win.Page_Validators.filter((v) => v.controltovalidate === field.id);
win.Page_Validators = win.Page_Validators.filter((v) => v.controltovalidate !== field.id);
validators.forEach((v) => {
const span = doc.getElementById(v.id);
if (span) span.style.display = "none";
});
utils.log("log", `Hid all validators for: ${fieldId}`);
if (field && win.Page_Validators.every((v) => v.controltovalidate !== field.id)) {
field.classList.remove('ndutils');
}
};
/**
* Checks if a validator exists.
* @param {string} validatorId - Validator span ID.
* @param {Document} [doc=document] - Document object to query.
* @returns {boolean} - True if exists, false otherwise.
*/
utils.validatorExists = function validatorExists(validatorId, doc = document) {
const win = doc.defaultView || window;
return win.Page_Validators && win.Page_Validators.some((v) => v.id === validatorId);
};
// ### Required Field Validator ###
/**
* Simple way to make a field required for text fields, checkboxes, or radio button groups.
* @param {string} id - Input field id. (e.g., 'fieldId' for inputs, or container ID for radio groups).
* @param {string} [errorMessage] - Custom error message (optional).
* @param {Document} [doc=document] - Document object to query (optional, defaults to document).
*/
utils.addRequiredValidator = function addRequiredValidator(id, errorMessage, doc = document, hiddenFieldId = null) {
const field = utils.getFieldById(id, doc);
if (!field) return;
let controlToValidate = field.id;
let validationFunc;
// Handle date fields
if (utils.isDateField(field.id, doc)) {
const visibleId = utils.getDatepickerFullId(field.id);
const addValidatorForDateField = () => {
const visibleField = doc.getElementById(visibleId);
if (visibleField) {
const validatorId = `${visibleId}RequiredValidator`;
const validationFunc = () => visibleField.value.trim() !== '';
utils.addValidator(visibleId, validatorId, errorMessage, validationFunc, null, true, doc);
console.log(`[ndutils] Added validator: ${validatorId} for field: ${visibleId}`);
} else {
console.warn(`Visible date field not found for: ${visibleId}. Retrying...`);
setTimeout(addValidatorForDateField, 100); // Retry after 100ms
}
};
addValidatorForDateField();
return; // Exit function to prevent adding validator to hidden field
}
// Text input (non-lookup)
else if ((field.tagName === 'INPUT' && field.type === 'text' && !field.closest('.lookup-field')) || field.tagName === 'TEXTAREA') {
validationFunc = () => field.value.trim() !== '';
}
// Radio button group
else if (field.querySelectorAll('input[type="radio"]').length > 0) {
validationFunc = () => Array.from(field.querySelectorAll('input[type="radio"]')).some(radio => radio.checked);
}
// Checkbox
else if (field.tagName === 'INPUT' && field.type === 'checkbox') {
validationFunc = () => field.checked;
}
// Lookup field
else if (field.classList.contains('lookup')) {
const actualHiddenFieldId = hiddenFieldId || field.id.replace('_name', '');
const hiddenField = doc.getElementById(actualHiddenFieldId);
if (hiddenField) {
validationFunc = () => hiddenField.value.trim() !== '';
} else {
utils.log("error", `Hidden field not found for lookup: ${actualHiddenFieldId}`);
return;
}
}
// Single-select dropdown
else if (field.tagName === 'SELECT' && !field.hasAttribute('multiple')) {
validationFunc = () => field.value !== '';
}
// Multi-select field
else if (field.tagName === 'SELECT' && field.hasAttribute('multiple')) {
validationFunc = () => field.selectedOptions.length > 0;
}
else {
utils.log("error", `Unsupported field type for required validation: ${field.tagName}`);
return;
}
const validatorId = `${controlToValidate}RequiredValidator`;
utils.addValidator(controlToValidate, validatorId, errorMessage, validationFunc, null, true, doc);
};
//aliases, had a hard time deciding what to name this one
utils.makeRequired = utils.addRequiredValidator
utils.addRequiredValidator = utils.addRequiredValidator
utils.addRequired = utils.addRequiredValidator;
utils.addRequiredFieldValidator = utils.addRequiredValidator;
//used to contruct the error message with the scrollto link
function constructErrorMessage(id, label, customMsg, linkToLabelId) {
let errorText;
if (customMsg) {
errorText = customMsg;
} else if (label) {
const labelText = utils.cleanLabelText(label.textContent);
errorText = `${labelText} is a required field.`;
} else {
// Use the field's ID if no label is found
const cleanId = utils.cleanId(id);
errorText = `${cleanId} is a required field.`;
}
const targetLabelId = linkToLabelId || `${id}_label`;
return `
${errorText}`;
}
/**
* Cleans label text by limiting length and adding quotes around the label if necessary
* @param {string} text - The raw label text.
* @param {string} [suffix=" is a required field."] - The suffix that will be added to the error message.
* @param {number} [maxLength=60] - The maximum length of the entire error message.
* @returns {string} - The cleaned label text without the suffix.
*/
utils.cleanLabelText = function cleanLabelText(text, suffix = " is a required field.", maxLength = 60) {
// Return empty string if no text is provided
if (!text) return "No Label Found";
// Clean the text: replace non-breaking spaces with regular spaces and trim
let cleanedText = text.replace(/\u00A0/g, ' ').trim();
// Remove outer double quotes if present
if (cleanedText.startsWith('"') && cleanedText.endsWith('"') && cleanedText.length > 1) {
cleanedText = cleanedText.slice(1, -1).trim();
}
// Define suffixes to check for removal, including the provided suffix and common ones
const commonSuffixes = [" is a required field.", " is required."];
const suffixesToRemove = [suffix, ...commonSuffixes].filter((s, index, self) => self.indexOf(s) === index); // Remove duplicates
suffixesToRemove.sort((a, b) => b.length - a.length); // Sort by length descending to remove longest match first
// Remove the longest matching suffix if present
for (const s of suffixesToRemove) {
if (cleanedText.endsWith(s)) {
cleanedText = cleanedText.slice(0, -s.length).trim();
break;
}
}
// Calculate available length for the label based on the provided suffix
const availableLength = maxLength - suffix.length;
// Truncate the label if it exceeds the available length
let finalText = cleanedText;
let isTruncated = false;
if (finalText.length > availableLength) {
finalText = finalText.substring(0, availableLength - 3) + "...";
isTruncated = true;
}
// Define punctuation set for quoting conditions
const punctuation = '.,!?;:';
// Determine if the label should be wrapped in quotes
const startsWithNumber = /^\d/.test(finalText);
const startsWithPunctuation = finalText.length > 0 && punctuation.includes(finalText[0]);
const endsWithPunctuation = finalText.length > 0 && punctuation.includes(finalText[finalText.length - 1]);
// Wrap in quotes if it starts with a number, starts or ends with punctuation, or is truncated
if (startsWithNumber || startsWithPunctuation || endsWithPunctuation || isTruncated) {
finalText = `"${finalText}"`;
}
return finalText;
};
/**
* Scrolls to and focuses a field based on its label or input ID.
* @param {string} labelId - Label ID.
* @param {string} id - Input field id.
* @param {Document} [doc=document] - Document object to query.
*/
utils.scrollToAndFocus = function scrollToAndFocus(labelId, id, doc = document) {
const cleanId = utils.cleanId(id);
const cleanLabelId = utils.cleanId(labelId);
const element = doc.getElementById(cleanLabelId) || doc.getElementById(cleanId);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "center" });
const field = doc.getElementById(cleanId);
if (field) field.focus();
}
};
/**
* Adds an asterisk to a field’s label via the 'force-asterisk' class -should almost never be used or needed, just for edge cases, mostly checkboxes.
* @param {string} id - Input field id.
* @param {Document} [doc=document] - Document object to query.
*/
utils.addAsterisk = function addAsterisk(id, doc = document) {
const label = utils.getLabelForField(id, doc);
if (label) label.classList.add('force-asterisk');
else utils.log("warn", `Label not found for: ${utils.cleanId(id)}`);
};
/**
* Removes the 'force-asterisk' class from a field’s label - only use in conjuction with addAsterisk
* @param {string} id - Input field id.
* @param {Document} [doc=document] - Document object to query.
*/
utils.removeAsterisk = function removeAsterisk(id, doc = document) {
const label = utils.getLabelForField(id, doc);
if (label) label.classList.remove('force-asterisk');
else utils.log("warn", `Label not found for: ${utils.cleanId(id)}`);
};
/**
* Hides specified fields, their labels, and validation messages.
* @param {Array|string} fields - Array of field IDs or a single field ID.
* @param {boolean} [hideLabel=true] - Whether to hide the field's label.
* @param {Document} [doc=document] - Document object to query.
*/
utils.hideFields = function hideFields(fields, hideLabel = true, doc = document) {
if (!Array.isArray(fields)) fields = [fields];
fields.forEach(fieldId => {
const cleanedId = utils.cleanId(fieldId);
const isDate = utils.isDateField(cleanedId, doc);
const baseId = isDate ? utils.getDatepickerBaseId(cleanedId) : cleanedId;
let controlDiv;
if (isDate) {
// Handle date fields (unchanged behavior)
const baseField = doc.getElementById(baseId);
if (baseField) {
controlDiv = baseField.parentElement;
if (controlDiv && controlDiv.classList.contains('control')) {
controlDiv.style.display = 'none';
controlDiv.classList.add('ndutils-hidden');
}
}
} else {
// Handle non-date fields (e.g., money fields with addons)
const field = doc.getElementById(cleanedId);
if (field) {
controlDiv = field.closest('.control');
if (controlDiv) {
controlDiv.style.display = 'none';
controlDiv.classList.add('ndutils-hidden');
} else {
// Fallback: hide the field itself if no control div is found
field.style.display = 'none';
field.classList.add('ndutils-hidden');
}
}
}
if (hideLabel) {
const labelId = isDate ? baseId : cleanedId;
const label = utils.getLabelForField(labelId, doc);
if (label) {
const infoDiv = label.closest('.info');
if (infoDiv) {
infoDiv.style.display = 'none';
} else {
label.style.display = 'none';
}
}
}
// Dispatch 'fieldHidden' event with baseId
const event = new CustomEvent('fieldHidden', { detail: { fieldId: baseId } });
doc.dispatchEvent(event);
});
};
/**
* Shows specified form fields and their labels.
* @param {Array|string} fields - Array of field IDs or a single field ID.
* @param {boolean} [showLabel=true] - Whether to show the field's label.
* @param {Document} [doc=document] - Document object to query.
*/
utils.showFields = function showFields(fields, showLabel = true, doc = document) {
if (!Array.isArray(fields)) fields = [fields];
fields.forEach(fieldId => {
const cleanedId = utils.cleanId(fieldId);
const isDate = utils.isDateField(cleanedId, doc);
const baseId = isDate ? utils.getDatepickerBaseId(cleanedId) : cleanedId;
let controlDiv;
if (isDate) {
// Handle date fields
const baseField = doc.getElementById(baseId);
if (baseField) {
controlDiv = baseField.parentElement;
if (controlDiv && controlDiv.classList.contains('control')) {
controlDiv.style.display = '';
controlDiv.classList.remove('ndutils-hidden');
}
}
} else {
// Handle non-date fields (e.g., money fields with addons)
const field = doc.getElementById(cleanedId);
if (field) {
controlDiv = field.closest('.control');
if (controlDiv) {
controlDiv.style.display = '';
controlDiv.classList.remove('ndutils-hidden');
} else {
// Fallback: show the field itself if no control div is found
field.style.display = '';
field.classList.remove('ndutils-hidden');
}
}
}
if (showLabel) {
const labelId = isDate ? baseId : cleanedId;
const label = utils.getLabelForField(labelId, doc);
if (label) {
const infoDiv = label.closest('.info');
if (infoDiv) {
infoDiv.style.display = '';
} else {
label.style.display = '';
}
}
}
// Dispatch 'fieldShown' event with baseId
const event = new CustomEvent('fieldShown', { detail: { fieldId: baseId } });
doc.dispatchEvent(event);
});
};
/**
* Registers callbacks for field visibility changes.
* @param {Array|string} fieldIds - Single field ID or array of field IDs to monitor.
* @param {Function} onHidden - Callback when a field is hidden, receives fieldId as argument.
* @param {Function} onShown - Callback when a field is shown, receives fieldId as argument.
* @param {Document} [doc=document] - Document object to attach listeners to.
*/
utils.onFieldVisibilityChange = function (fieldIds, onHidden, onShown, doc = document) {
if (!Array.isArray(fieldIds)) fieldIds = [fieldIds];
fieldIds.forEach(fieldId => {
doc.addEventListener('fieldHidden', function (event) {
if (event.detail.fieldId === fieldId) {
onHidden(fieldId);
}
});
doc.addEventListener('fieldShown', function (event) {
if (event.detail.fieldId === fieldId) {
onShown(fieldId);
}
});
});
};
// // This code dynamically manages required validation for date fields based on their visibility in a Power Pages form.
// // It uses event listeners to detect when fields are hidden or shown, adjusting validation to ensure hidden fields
// // do not trigger errors and visible fields enforce data entry.
// // Event Listener for 'fieldHidden':
// // - Removes required validation from date fields when they are hidden to prevent unnecessary validation errors.
// document.addEventListener('fieldHidden', function(event) {
// const fieldId = event.detail.fieldId;
// if (utils.isDateField(fieldId, document)) {
// utils.removeRequiredValidator(fieldId);
// }
// });
// // Event Listener for 'fieldShown':
// // - Adds required validation to date fields when they are shown, ensuring a date is entered.
// // - The second argument provides a custom error message ('Please enter a date.').
// // If your implementation of utils.addRequiredValidator does not accept an error message and uses a default one instead,
// // simplify the call to utils.addRequiredValidator(fieldId). Adjust accordingly based on your utils library's documentation.
// document.addEventListener('fieldShown', function(event) {
// const fieldId = event.detail.fieldId;
// if (utils.isDateField(fieldId, document)) {
// utils.addRequiredValidator(fieldId, 'Please enter a date.');
// }
// });
/**
* Removes the required field validator added by utils.addRequiredValidator
* @param {string} id - Input field id.
* @param {Document} [doc=document] - Document object to query.
*/
utils.removeRequiredValidator = function removeRequiredValidator(id, doc = document) {
const cleanId = utils.cleanId(id);
let controlId;
if (utils.isDateField(cleanId, doc)) {
controlId = utils.getDatepickerFullId(cleanId);
} else {
controlId = cleanId;
}
const validatorId = `${controlId}RequiredValidator`;
utils.removeValidator(controlId, validatorId, doc);
const label = utils.getLabelForField(cleanId, doc); // Use original ID for label
if (label) {
const infoDiv = label.closest('.info');
if (infoDiv) {
infoDiv.classList.remove('required');
}
}
const field = utils.getFieldById(controlId, doc); // Use controlId to match validator attachment
const win = doc.defaultView || window;
if (field && win.Page_Validators.every((v) => v.controltovalidate !== controlId)) {
field.classList.remove('ndutils');
}
};
// aliases - I had a hard time naming these.
utils.removeRequiredFieldValidator = utils.removeRequiredValidator;
// ### Input Restriction Functions ###
/**
* Restricts date input field to only accept numbers and forward slashes.
* Retries finding the visible datepicker input until it’s available or max retries are reached.
* @param {string} id - Input field id (e.g., 'dhsbh_lastdateofemployment').
* @param {Document} [doc=document] - Document object to query.
* @param {number} [maxRetries=10] - Maximum number of retries.
* @param {number} [retryInterval=200] - Interval between retries in milliseconds.
*/
utils.restrictDateInput = function restrictDateInput(id, doc = document, maxRetries = 20, retryInterval = 300) {
const baseId = utils.getDatepickerBaseId(id);
const targetId = utils.getDatepickerFullId(baseId);
let retries = 0;
const applyRestrictions = () => {
const targetField = doc.getElementById(targetId);
if (targetField) {
// Remove any existing listeners to prevent duplicates
targetField.removeEventListener('keypress', restrictKeypress);
targetField.removeEventListener('paste', restrictPaste);
// Define restriction handlers
function restrictKeypress(event) {
if (!/[0-9/]/.test(event.key)) {
event.preventDefault();
}
}
function restrictPaste(event) {
const text = event.clipboardData?.getData('text') || '';
if (!/^[0-9/]*$/.test(text)) {
event.preventDefault();
}
}
// Apply restrictions
targetField.addEventListener('keypress', restrictKeypress);
targetField.addEventListener('paste', restrictPaste);
utils.log("log", `Restrictions applied to ${targetId} after ${retries} retries`);
} else if (retries < maxRetries) {
retries++;
utils.log("warn", `Visible datepicker field not found: ${targetId}. Retry ${retries}/${maxRetries}`);
setTimeout(applyRestrictions, retryInterval);
} else {
utils.log("error", `Failed to apply restrictions to ${targetId} after ${maxRetries} retries`);
}
};
applyRestrictions();
};
/**
* Restricts money input field to a valid decimal format with up to 2 decimal places.
* @param {string} id - Input field id.
* @param {Document} [doc=document] - Document object to query.
*/
utils.restrictMoneyInput = function restrictMoneyInput(id, doc = document) {
const field = utils.getFieldById(id, doc);
if (!field) return;
field.addEventListener('input', function () {
const value = this.value;
const validPattern = /^\d*\.?\d{0,2}$/;
if (!validPattern.test(value)) this.value = this.dataset.lastValid || '';
else this.dataset.lastValid = value;
});
};
/**
* Restricts input field to whole numbers only, with an optional maximum length.
* @param {string} id - Input field id.
* @param {number|null} [maxLength=null] - Maximum allowed length of the input, or null for no limit.
* @param {Document} [doc=document] - Document object to query.
*/
utils.restrictWholeNumberInput = function restrictWholeNumberInput(id, maxLength = null, doc = document) {
const field = utils.getFieldById(id, doc);
if (!field) return;
if (field._restrictWholeNumberInputHandler) {
field.removeEventListener('input', field._restrictWholeNumberInputHandler);
}
const handler = function () {
let value = this.value;
let cleanedValue = value.replace(/\D/g, '');
if (maxLength !== null && cleanedValue.length > maxLength) {
cleanedValue = cleanedValue.slice(0, maxLength);
}
if (value !== cleanedValue) this.value = cleanedValue;
};
field.addEventListener('input', handler);
field._restrictWholeNumberInputHandler = handler;
};
return utils;
})();