// ========================= // CustomBaseValidator Class // ========================= class CustomBaseValidator { static allCustomValidators = {}; // All custom validators constructor(targetErrorDisplayLocation, messageText, evaluationFunction, includeLabel = true) { if (!targetErrorDisplayLocation) throw new Error("targetErrorDisplayLocation is required"); // Flag for including field label in error this.includeLabel = includeLabel; // Resolve target element if (targetErrorDisplayLocation instanceof HTMLElement) { this.targetErrorDisplayEl = targetErrorDisplayLocation; } else if (typeof targetErrorDisplayLocation === "string") { this.targetErrorDisplayEl = document.getElementById(targetErrorDisplayLocation); } else { throw new Error("targetErrorDisplayLocation must be an HTMLElement or a string ID"); } if (!this.targetErrorDisplayEl) throw new Error("Element not found in DOM"); this.messageText = messageText || ""; // Use provided evaluation function. if (evaluationFunction && typeof evaluationFunction === "function") { this.evaluationFunction = evaluationFunction; } // Create error display element. this.element = document.createElement("span"); this.element.id = `${this.constructor.name}_${this.targetErrorDisplayEl.id}_validator`; this.element.style.display = "none"; this.element.errormessage = `${this.messageText}`; this.element.evaluationfunction = this.evaluationFunction.bind(this); // Store this instance. CustomBaseValidator.allCustomValidators[this.element.id] = this; // Register with the underlying system. if (typeof Page_Validators !== "undefined") { Page_Validators.push(this.element); Page_Validators.sort((a, b) => { if (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) return -1; if (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING) return 1; return 0; }); } } evaluationFunction() { throw new Error("evaluationFunction must be overridden"); } updateMessage(newMessage) { if (newMessage !== undefined) this.messageText = newMessage; const content = this.includeLabel && this.targetErrorDisplayEl.innerText ? `${this.targetErrorDisplayEl.innerText} ${this.messageText}` : this.messageText; this.element.errormessage = `${content}`; } } // ========================== // CustomFieldValidator Class // ========================== class CustomFieldValidator extends CustomBaseValidator { static childParentMapping = {}; constructor({ controlToValidate, messageText = "", id = "", evaluationFunction, targetErrorDisplayLocation, includeLabel = true } = {}) { if (!controlToValidate) throw new Error("controlToValidate is required."); // Resolve the control element. const control = controlToValidate instanceof HTMLElement ? controlToValidate : document.getElementById(controlToValidate); if (!control) throw new Error("Control not found in DOM."); const cell = control.closest('.cell'); const info = cell.querySelector('.info, .table-info'); // Get the control's label element. const fieldLabelEL = info.querySelector('label'); if (!fieldLabelEL) throw new Error("Label for field not found."); // Ensure consistent validation-field-label class fieldLabelEL.classList.add('validation-field-label'); // Get the validators container from the control. let validatorsContainer = info.querySelector('.validators'); if (!validatorsContainer) { validatorsContainer = document.createElement("div"); validatorsContainer.className = "validators"; fieldLabelEL.insertAdjacentElement('beforebegin', validatorsContainer); } // Determine display target. const displayTarget = targetErrorDisplayLocation || fieldLabelEL; // Initialize parent with includeLabel flag. super(displayTarget, "", evaluationFunction, includeLabel); if (Object.keys(CustomFieldValidator.childParentMapping).includes(controlToValidate.id)) { let parentValidation = CustomFieldValidator.childParentMapping[controlToValidate.id]; parentValidation.subValidations.push(this); } this.fieldLabel = fieldLabelEL; this.validatorsContainer = validatorsContainer; this.validatorsContainer.appendChild(this.element); this.subValidations = []; this.element.id = `${this.constructor.name}_${control.id}_validator`; this.control = control; if (messageText) this.customError = true; this.updateMessage(messageText); } updateMessage(newMessage) { if (newMessage !== undefined) this.messageText = newMessage; const labelPart = this.includeLabel ? `${this.getControlLabelText()} `: ''; const content = `${labelPart}${this.messageText}`; this.element.errormessage = `${content}`; } getChildrenValidationElements() {} static markControlForSubValidation(childControl, parentValidation) { CustomFieldValidator.childParentMapping[childControl.id] = parentValidation; } getControlLabelText() { const fallbackLabel = { 'en': 'This', 'fr': 'Ceci' } if (this.control?.label) { const firstChild = this.control.label.childNodes[0]; // Check if the first child is a text node (nodeType 3) and has a value if (firstChild && firstChild.nodeType === 3) { return firstChild.nodeValue.trim(); } } return fallbackLabel[LANG]; } } // =================================== // DynamicFieldRequiredValidator Class // =================================== class DynamicFieldRequiredValidator extends CustomFieldValidator { constructor({ controlToValidate, messageText = "", id = "", evaluationFunction, includeLabel = true } = {}) { const defaultMessage = { 'en': "is a required field", 'fr': "est obligatoire." } if (messageText === "") messageText = defaultMessage[LANG]; super({ controlToValidate, messageText, id, evaluationFunction }); // Optionally add a 'required' class. if (this.control?.label != null) { this.control.label.required = true; } } evaluationFunction() { return this.hasContent(); } hasContent() { if (this.control.hasOwnProperty('boolvalue')) { return this.control.boolvalue != null; } else if ('value' in this.control) { return this.control.value !== ""; } else if (this.control.classList.contains('subgrid')){ return true; } else { throw new Error("hasContent not detected"); return false; } } } class EmailMatchValidator extends CustomFieldValidator { constructor({ controlToValidate, controlToCompare, virtualFieldLocation, messageText, includeLabel= true }) { if (!controlToValidate) throw new Error("controlToValidate is required."); if ((controlToCompare && virtualFieldLocation) || (!controlToCompare && !virtualFieldLocation)) { throw new Error("Either controlToCompare or virtualFieldLocation must be set, but not both."); } const control = controlToValidate instanceof HTMLElement ? controlToValidate : document.getElementById(controlToValidate); if (!control) throw new Error("Control not found in DOM."); controlToCompare = controlToCompare instanceof HTMLElement ? controlToCompare : document.getElementById(controlToCompare); if (!messageText) { const defaultMessage = { 'en': "The email addresses entered do not match.", 'fr': "Les adresses de courrier électronique saisies ne correspondent pas." } messageText = defaultMessage[LANG]; } if (controlToCompare) { super({controlToValidate: controlToCompare, messageText: messageText, id: "verifyEmailValidator", includeLabel: includeLabel}); this.control = controlToValidate; this.compareControl = controlToCompare; } else { let {input: controlToCompare, validationLocation: cc_info} = EmailMatchValidator.generateVirtualizedField(control, virtualFieldLocation); super({ controlToValidate: controlToCompare, messageText: messageText, targetErrorDisplayLocation: cc_info, id: "verifyEmailValidator", includeLabel: includeLabel}); this.control = controlToValidate; this.compareControl = controlToCompare; } } evaluationFunction() { return this.control.value === this.compareControl.value; } static generateVirtualizedField(control, virtualFieldLocation) { let parentCell = control.closest(".cell"); let vf_isCell = virtualFieldLocation?.classList.contains('cell'); let newCell = (vf_isCell) ? virtualFieldLocation : document.createElement("td"); newCell.className = "clearfix cell text form-control-cell"; newCell.setAttribute("data-inner-field-id", "emailaddress_virtual"); newCell.setAttribute("maxchar", "100"); newCell.setAttribute("maxwords", "19"); newCell.innerHTML = `
Re-enter your email address.
0 / 100
`; if (!vf_isCell) { parentCell.insertAdjacentElement("afterend", newCell); } let input = newCell.querySelector("input"); initField(input) let validationLocation = newCell.querySelector("#verify_email_label"); return {input, validationLocation}; } } // =================================== // LinkedBooleanRequiredValidator Class (Parent) // =================================== class LinkedBooleanRequiredValidator extends DynamicFieldRequiredValidator { static isLinkedParent = true; constructor({ controlToValidate, linkedBooleanControl, validCondition = true, messageText = "", requiredOnlyIfParentRequired = true,} = {}) { super({ controlToValidate, messageText }); this.bool_control = linkedBooleanControl; this.valid_condition = validCondition; this.requiredOnlyIfParentRequired = requiredOnlyIfParentRequired; CustomFieldValidator.markControlForSubValidation(controlToValidate, this); // Attach listener to linkedBooleanControl this.bool_control.addEventListener('change', () => { this.syncRequiredLabel(); }); // Initial sync this.syncRequiredLabel(); } evaluationFunction() { const boolRequired = this.bool_control.info?.classList?.contains("required"); const boolNull = this.bool_control.boolvalue === null; const parentControlValid = (this.bool_control.boolvalue == this.valid_condition); if (boolNull || !parentControlValid) { this.subValidations.forEach((val) => { val.element.enabled = false }); } else { this.subValidations.forEach((val) => { val.element.enabled = true }); } if (boolNull || !boolRequired || !parentControlValid) return true; return this.hasContent(); } syncRequiredLabel() { const label = this.control?.label; if (!label) return; const parentBoolRequired = this.bool_control.label.required; const boolNull = this.bool_control.boolvalue === null; const parentControlValid = (this.bool_control.boolvalue == this.valid_condition); const shouldBeRequired = (!this.requiredOnlyIfParentRequired || parentBoolRequired) && parentControlValid && !boolNull; label.required = shouldBeRequired; } } // =================================== // LinkedSelectRequiredValidator Class (Parent) // =================================== class LinkedSelectRequiredValidator extends DynamicFieldRequiredValidator { static isLinkedParent = true; constructor({ controlToValidate, linkedSelectControl, validValues = [], // array of values that trigger requirement messageText = "", requiredOnlyIfParentRequired = true, invertLogic = false } = {}) { super({ controlToValidate, messageText }); this.select_control = linkedSelectControl; this.valid_values = validValues; this.requiredOnlyIfParentRequired = requiredOnlyIfParentRequired; this.invertLogic = invertLogic; CustomFieldValidator.markControlForSubValidation(controlToValidate, this); // Attach listener to linkedSelectControl this.select_control.addEventListener("change", () => { this.syncRequiredLabel(); }); // Initial sync this.syncRequiredLabel(); } evaluationFunction() { const selectRequired = this.select_control.info?.classList?.contains("required"); const selectValue = this.select_control.value; let parentControlValid = (this.invertLogic) ? !this.valid_values.includes(selectValue) : this.valid_values.includes(selectValue); if (!parentControlValid) { this.subValidations.forEach((val) => { val.element.enabled = false; }); } else { this.subValidations.forEach((val) => { val.element.enabled = true; }); } if (!selectRequired || !parentControlValid) return true; return this.hasContent(); } syncRequiredLabel() { const label = this.control?.label; if (!label) return; const parentSelectRequired = this.select_control.label.required; const selectValue = this.select_control.value; let parentControlValid = (this.invertLogic) ? !this.valid_values.includes(selectValue) : this.valid_values.includes(selectValue); const shouldBeRequired = (!this.requiredOnlyIfParentRequired || parentSelectRequired) && parentControlValid; label.required = shouldBeRequired; } } // =================================== // AtLeastOneValidator Class (Child) // =================================== class AtLeastOneValidator extends CustomFieldValidator { constructor({ controlToValidate, controlToCompare, messageText = "" } = {}) { super({ controlToValidate, messageText, includeLabel: false }); this.controlToCompare = controlToCompare instanceof HTMLElement ? controlToCompare : document.getElementById(controlToCompare); if (!this.controlToCompare) { // Throw the error *here* if the control is missing throw new Error(`Control to compare not found in DOM for ID: ${controlToCompare}`); } if (messageText === "") { // const label1 = this.getControlLabelText(this.control.id); // const label2 = this.getControlLabelText(controlToValidate); const label1 = this.getControlLabelText(this.controlToCompare.id); const label2 = this.getControlLabelText(this.control.id); const enMsg = `You must provide a value for either ${label1} or ${label2}.`; const frMsg = `Vous devez fournir une valeur soit pour ${label1} ou ${label2}.`; messageText = LANG === 'fr' ? frMsg : enMsg; this.updateMessage (messageText); } this.element.evaluationfunction = this.evaluationFunction.bind(this); } // Override the base class's evaluation function evaluationFunction() { // Get values of Field 1 and Field 2, trimming whitespace const value1 = this.controlToCompare.value.trim(); const value2 = this.control.value.trim(); // It fails if BOTH are empty. return value1 !== "" || value2 !== ""; } } // ========================= // SubgridValidator Class (Child) // ========================= class SubgridValidator extends CustomFieldValidator { constructor({ subgridElement, rowEvaluationFunction, minValidRows = 1, maxValidRows = Infinity, aggregator, messageText = "", markSectionInvalid = true //Marks a section invalid on table reload if evaluation function fails. } = {}) { if (!subgridElement) throw new Error("subgridElement is required."); const subgridEl = subgridElement instanceof HTMLElement ? subgridElement : document.getElementById(subgridElement); if (!subgridEl) throw new Error("Subgrid element not found in DOM."); // Generate markup. const { infoEl, labelEl, validatorsContainer } = SubgridValidator.generateMissingMarkup(subgridEl); // Default aggregator: count valid rows. if (typeof aggregator !== "function") { aggregator = (results, min, max) => { const count = results.filter(Boolean).length; return count >= min && count <= max; }; } // Default row evaluation. if (typeof rowEvaluationFunction !== "function") { rowEvaluationFunction = row => true; } super({ controlToValidate: subgridElement, messageText: messageText }); validatorsContainer.appendChild(this.element); this.element.id = `SubgridValidator_${subgridEl.id}_validator`; this.isFirstRun = true; // Instance-level flag for each subgrid this.rowEvaluationFunction = rowEvaluationFunction; this.minValidRows = minValidRows; this.maxValidRows = maxValidRows; this.aggregator = aggregator; if (minValidRows > 0) { infoEl.classList.add("required"); } this.element.evaluationfunction = this.evaluationFunction.bind(this); this.markSectionInvalid = markSectionInvalid; this.element.evaluationfunction = this.evaluationFunction.bind(this); // Setup the subgrid loading handler (runs every refresh of a subgrid); $(subgridEl.firstChild).on("loaded", ()=>{ SubgridValidator.subgridReloadHandler(this);}); } static async subgridReloadHandler(validatorInstance) { // Check if it's the first run (don't excute on page load, only after a change is made in the subgrid) if (validatorInstance.isFirstRun) { validatorInstance.isFirstRun = false; // Set the flag to false after the first run return; // Skip the validation for the first run } if (validatorInstance.markSectionInvalid && FormSteps.thisStepValid) { if (!validatorInstance.element.evaluationfunction()) { FormSteps.markStepIncompleteAsync(); } } } static generateMissingMarkup(subgridEl) { const CELL = subgridEl.closest(".subgrid-cell"); let infoEl = CELL.querySelector(".info, .table-info"); if (!infoEl) { infoEl = document.createElement("h3"); infoEl.className = "info form-subgrid-heading"; CELL.prepend(infoEl); } let labelEl = infoEl.querySelector("label"); if (!labelEl) { labelEl = document.createElement("label"); labelEl.className = "field-label"; labelEl.style.display = "block"; if (CELL.id) { labelEl.setAttribute("for", CELL.id); labelEl.id = `${CELL.id}_label`; } labelEl.innerText = CELL.getAttribute("data-label") || "Subgrid"; infoEl.appendChild(labelEl); } let validatorsContainer = infoEl.querySelector(".validators"); if (!validatorsContainer) { validatorsContainer = document.createElement("div"); validatorsContainer.className = "validators"; infoEl.appendChild(validatorsContainer); } return { infoEl, labelEl, validatorsContainer }; } evaluationFunction() { const rows = Array.from( this.control.querySelectorAll("table tbody > tr[data-entity]") ); const results = rows.map((row) => { const isValidRow = this.rowEvaluationFunction(row); row.dataset.valid = isValidRow; return isValidRow; }); const validCount = results.filter(Boolean).length; let failCondition = ""; if (this.minValidRows > 0 && rows.length === 0) { failCondition = "noRows"; } else if (this.minValidRows == 'all' && validCount != rows.length) { failCondition = "min"; //Note requires a rowEvaluationFunction } else if (validCount < this.minValidRows) { failCondition = "min"; } else if (validCount > this.maxValidRows) { failCondition = "max"; } if (!this.customError) { const errorMessage = this.generateMessage(failCondition, validCount, rows.length); this.updateMessage(errorMessage); } return failCondition === ""; } generateMessage(failCondition, validCount, totalRows) { let message = ""; if (LANG === 'fr') { switch (failCondition) { case "noRows": message = `Au moins ${this.minValidRows} enregistrement(s) requis, mais aucun trouvé.`; break; case "min": message = `${validCount} enregistrement(s) valides trouvés; le minimum requis est ${this.minValidRows}.`; break; case "max": message = `Trop d'enregistrements valides: ${validCount} trouvés; le maximum autorisé est ${this.maxValidRows}.`; break; } } else { switch (failCondition) { case "noRows": message = `At least ${this.minValidRows} record(s) required, but none were found.`; break; case "min": message = `${validCount} valid record(s) found; minimum required is ${this.minValidRows}.`; break; case "max": message = `Too many valid records: ${validCount} found; maximum allowed is ${this.maxValidRows}.`; break; } } return message; } getValidRows() { const rows = Array.from( this.subgridEl.querySelectorAll("table tbody > tr[data-entity]") ); return rows.filter(this.rowEvaluationFunction); } getInvalidRows() { const rows = Array.from( this.subgridEl.querySelectorAll("table tbody > tr[data-entity]") ); return rows.filter(row => !this.rowEvaluationFunction(row)); } } // =================================== // PostalZipValidator Class // =================================== class PostalZipValidator extends CustomFieldValidator { constructor({ controlToValidate, countryControl, id = "" } = {}) { const defaultMessage = { 'en': "Please enter a valid Postal or ZIP code format.", 'fr': "Veuillez saisir un code postal ou ZIP valide." } super({ controlToValidate, messageText: defaultMessage[LANG], id, includeLabel: false }); this.countryControl = countryControl; // Optionally add a 'required' class. this.control.label.required = true; } evaluationFunction() { function validateUSZip(zip) { const zipRegex = /^\d{5}(-\d{4})?$/; return zipRegex.test(zip); } function validateCanadianPostal(postal) { const postalRegex = /^[A-Za-z]\d[A-Za-z][ ]?\d[A-Za-z]\d$/; return postalRegex.test(postal); } if (this.countryControl.value == 'd800ecae-fd66-ef11-a670-000d3af45d64'){ return validateCanadianPostal(this.control.value); } if (this.countryControl.value == 'c200ecae-fd66-ef11-a670-000d3af45d64'){ return validateUSZip(this.control.value); } return true;//All other countries } } class DateAfterTodayValidator extends CustomFieldValidator { constructor({ controlToValidate, messageText = "" } = {}) { super({ controlToValidate, messageText: messageText || ( LANG === "fr" ? "ne peut pas être antérieure à aujourd’hui." : "cannot be before today." ) }); // wire up our check this.element.evaluationfunction = this.evaluationFunction.bind(this); } evaluationFunction() { const raw = this.control.value; if (!raw) return true; const picked = new Date(raw); const today = new Date(new Date().toDateString()); return !isNaN(picked) && picked >= today; } } class DateRangeFieldValidator extends CustomFieldValidator { constructor({ startDateControl, endDateControl, maxRange = Infinity, // Maximum duration after start minRange = new Duration({ days: 0 }), // Minimum duration after start minStart = null, // Earliest allowed start date (String of ISO date, or Date object or "today") maxEnd = null, // Latest allowed end date (String of ISO date, or Date object) maxFiscalYears = Infinity, // Max number of fiscal years the range can span messageText = "", id = "", lang = LANG, targetErrorDisplayLocation = null } = {}) { const displayTarget = targetErrorDisplayLocation || endDateControl; super({ controlToValidate: endDateControl, messageText: "", id, evaluationFunction: undefined, targetErrorDisplayLocation: displayTarget }); this.startDateControl = startDateControl; this.endDateControl = endDateControl; this.maxRange = maxRange; this.minRange = minRange; this.minStartRaw = minStart; // store raw input (e.g. "today") this.maxEndRaw = maxEnd; // store raw input in case we want same treatment this.minStartIsToday = (minStart === "today"); // keep this flag for messaging this.maxFiscalYears = maxFiscalYears; this.lang = lang; this.element.isvalid = true; this.updateMessage(this.generateMessage()); } normalizeDateInput(input) { if (input === "today") { return new Date(new Date().toDateString()); // today at midnight } if (input instanceof Date) return input; if (typeof input === "string" || typeof input === "number") return new Date(input); return null; } generateMessage(failCondition) { const locale = this.lang === "fr" ? "fr-CA" : "en-CA"; const shortLang = this.lang || "en"; const options = { year: "numeric", month: "short", day: "numeric" }; const messages = { recordCount: { en: { noRows: min => `At least ${min} record(s) required, but none were found.`, min: (valid, min) => `${valid} valid record(s) found; minimum required is ${min}.`, max: (valid, max) => `Too many valid records: ${valid} found; maximum allowed is ${max}.`, }, fr: { noRows: min => `Au moins ${min} enregistrement(s) requis, mais aucun trouvé.`, min: (valid, min) => `${valid} enregistrement(s) valides trouvés; le minimum requis est ${min}.`, max: (valid, max) => `Trop d'enregistrements valides: ${valid} trouvés; le maximum autorisé est ${max}.`, }, }, dateRange: { en: { endDate: "must be after the start date.", minRange: min => `Minimum range is ${min}.`, maxRange: max => `Maximum range is ${max === Infinity ? "∞" : max}.`, bothRange: (min, max) => `Date must be between ${min} and ${max}.`, minStartToday: "Start date cannot be before today.", minStart: date => `Start date cannot be before ${date}.`, maxEnd: date => `End date cannot be after ${date}.`, fiscal: (max, html) => ` spans more than ${max} fiscal years: ${html}`, }, fr: { endDate: "doit être postérieure à la date de début.", minRange: min => `La plage minimale est de ${min}.`, maxRange: max => `La plage maximale est de ${max === Infinity ? "∞" : max}.`, bothRange: (min, max) => `La date doit être comprise entre ${min} et ${max}.`, minStartToday: "La date de début ne peut pas être antérieure à aujourd’hui.", minStart: date => `La date de début ne peut pas être antérieure au ${date}.`, maxEnd: date => `La date de fin ne peut pas être postérieure au ${date}.`, fiscal: (max, html) => ` s'étend sur plus de ${max} années fiscales : ${html}`, }, }, }; const t = { count: messages.recordCount[shortLang], date: messages.dateRange[shortLang] }; switch (failCondition) { case "noRows": return t.count.noRows(this.minValidRows); case "min": return t.count.min(this.validCount, this.minValidRows); case "max": return t.count.max(this.validCount, this.maxValidRows); case "endDate": return t.date.endDate; case "minRange": return t.date.minRange(this.minRange.toString(locale)); case "maxRange": return t.date.maxRange(this.maxRange.toString(locale)); case "bothRange": return t.date.bothRange(this.minRange.toString(locale), this.maxRange.toString(locale)); case "minStartToday": return t.date.minStartToday; case "minStart": return t.date.minStart(this.normalizeDateInput(this.minStartRaw).toLocaleDateString(locale)); case "maxEnd": return t.date.maxEnd(this.maxEnd.toLocaleDateString(locale)); case "fiscal": { const start = new Date(this.startDateControl.value); const end = new Date(this.endDateControl.value); const fiscalYearFor = d => d.getMonth() < 3 ? d.getFullYear() - 1 : d.getFullYear(); const startFY = fiscalYearFor(start); const endFY = fiscalYearFor(end); const fiscalYearsSpanned = []; for (let year = startFY; year <= endFY; year++) { const from = new Date(year, 3, 1); const to = new Date(year + 1, 2, 31); const label = `${from.toLocaleDateString(locale, options)} – ${to.toLocaleDateString(locale, options)}`; fiscalYearsSpanned.push(`
  • ${label};
  • `); } const listHTML = ``; return t.date.fiscal(this.maxFiscalYears, listHTML); } default: return ""; } } countFiscalYearsSpanned(start, end) { const fiscalYearFor = (date) => date.getMonth() < 3 ? date.getFullYear() - 1 : date.getFullYear(); const startFY = fiscalYearFor(start); const endFY = fiscalYearFor(end); return endFY - startFY + 1; } evaluationFunction() { const startDate = new Date(this.startDateControl.value); const endDate = new Date(this.endDateControl.value); const durationMs = endDate - startDate; const minStart = this.normalizeDateInput(this.minStartRaw); const maxEnd = this.normalizeDateInput(this.maxEndRaw); let failCondition = ""; if (isNaN(startDate) || isNaN(endDate)) { this.updateMessage(""); return true; } if (startDate >= endDate) { failCondition = "endDate"; } else { const allowedMin = this.minRange instanceof Duration ? this.minRange.addTo(new Date(0)).getTime() - new Date(0).getTime() : 0; const allowedMax = this.maxRange === Infinity ? Infinity : this.maxRange instanceof Duration ? this.maxRange.addTo(new Date(0)).getTime() - new Date(0).getTime() : Infinity; if (allowedMin > 0 && allowedMax < Infinity) { if (durationMs < allowedMin || durationMs > allowedMax) { failCondition = "bothRange"; } } else if (allowedMin > 0 && allowedMax === Infinity) { if (durationMs < allowedMin) { failCondition = "minRange"; } } else if (allowedMax < Infinity && allowedMin === 0) { if (durationMs > allowedMax) { failCondition = "maxRange"; } } if (!failCondition && minStart && startDate < minStart) { failCondition = this.minStartIsToday ? "minStartToday" : "minStart"; } if (!failCondition && maxEnd && endDate > maxEnd) { failCondition = "maxEnd"; } if (!failCondition && this.maxFiscalYears !== Infinity) { const fiscalYears = this.countFiscalYearsSpanned(startDate, endDate); if (fiscalYears > this.maxFiscalYears) { failCondition = "fiscal"; } } } const errorMessage = this.generateMessage(failCondition); this.updateMessage(errorMessage); return errorMessage === ""; } } class AllStepsCompleteValidator extends CustomFieldValidator { constructor({ portalStepsControl, menuSelector = 'section.menu>ul>li:not(.hidden)', messageText = "", id = "" } = {}) { const portalEl = portalStepsControl instanceof HTMLElement ? portalStepsControl : document.getElementById("gac_stepscompleted_portal"); const activeAnchor = document.querySelector(`${menuSelector} a.active`); const targetEl = activeAnchor ? activeAnchor.parentElement : portalEl; super({ controlToValidate: portalEl, messageText, id, evaluationFunction: undefined, targetErrorDisplayLocation: targetEl }); this.menuSelector = menuSelector; this.portalStepsControl = portalEl; window.AllStepsValidator = this; } evaluationFunction() { let stepsJSON; try { stepsJSON = JSON.parse(this.portalStepsControl.value); } catch (e) { stepsJSON = null; } let valid = true; let missingSteps = ""; const stepArray = []; const stepMenuItem = {}; const stepUrl = {}; const stepInnerText = {}; document.querySelectorAll(this.menuSelector).forEach(li => { stepArray.push(li.id); const anchor = li.querySelector('a'); if (anchor) { stepMenuItem[li.id] = li; stepUrl[li.id] = anchor.href; stepInnerText[li.id] = anchor.innerText.trim(); } }); stepArray.forEach(step => { if (!stepsJSON || stepsJSON[step] !== 'true') { missingSteps += '
  • ' + stepInnerText[step] + '
  • '; stepMenuItem[step].classList.add('incomplete'); valid = false; } }); const overViewWarning = (LANG == 'en') ? 'These sections are not yet complete: ' : 'Ces sections ne sont pas encore terminées :'; const errorMessage = valid ? "" : `${overViewWarning}`; this.updateMessage(errorMessage); return valid; } // Override to prevent parent's label wrapping. updateMessage(newMessage) { this.element.errormessage = newMessage; } } // ========================= // Duration Class // ========================= /** * Duration * Represents a time duration. * @param {Object} params - Parameters object. * @param {number} [params.years=0] - Years component. * @param {number} [params.months=0] - Months component. * @param {number} [params.days=0] - Days component. */ class Duration { constructor({ years = 0, months = 0, days = 0 } = {}) { this.years = years; this.months = months; this.days = days; } /** * addTo * Adds this duration to a given date. * @param {Date} date - The date to add the duration to. * @returns {Date} New date with duration added. */ addTo(date) { const newDate = new Date(date); newDate.setFullYear(newDate.getFullYear() + this.years); newDate.setMonth(newDate.getMonth() + this.months); newDate.setDate(newDate.getDate() + this.days); return newDate; } /** * toString * Returns a string representation of the duration. * @param {string} [lang="en"] - Language code ("en" or "fr"). * @returns {string} Duration as a string. */ toString(lang = "en") { const parts = []; if (this.years) parts.push(lang === "fr" ? `${this.years} ${this.years > 1 ? "ans" : "an"}` : `${this.years} ${this.years > 1 ? "years" : "year"}`); if (this.months) parts.push(lang === "fr" ? `${this.months} mois` : `${this.months} ${this.months > 1 ? "months" : "month"}`); if (this.days) parts.push(lang === "fr" ? `${this.days} ${this.days > 1 ? "jours" : "jour"}` : `${this.days} ${this.days > 1 ? "days" : "day"}`); return parts.join(" ") || (lang === "fr" ? "0 jour" : "0 day"); } } document.addEventListener('DOMContentLoaded', () => { // 2) Inline‐error injector const summary = document.querySelector('div.validation-summary'); if (summary) { const updateCustomErrors = () => { try { // a) Remove any existing custom errors document.querySelectorAll('strong.customError').forEach(el => el.remove()); // b) Only proceed if the summary is visible if (summary.style.display !== '') return; const prefix = LANG === 'fr' ? 'Erreur : ' : 'Error: '; // Skip if AllStepsCompleteValidator is present /* if ( Object.values(CustomBaseValidator.allCustomValidators) .some(v => v instanceof AllStepsCompleteValidator) ) return; */ summary.querySelectorAll('li').forEach(li => { const link = li.querySelector('a[href], [referencecontrolid]'); if (!link || !link.textContent.trim()) return; // Get the field’s ID (strip leading “#” if present) const rawId = (link.getAttribute('href') || '').replace(/^#/, '') || link.getAttribute('referencecontrolid'); const fieldEl = document.getElementById(rawId); if (!fieldEl) return; // Use only the anchor’s innerHTML (so href isn’t copied) const errorHtml = link.innerHTML; fieldEl.insertAdjacentHTML( 'afterend', ` ${prefix} ${errorHtml} ` ); }); } catch (err) { console.error('Custom-error injection failed:', err); } }; // Single observer for style changes & new list items const observer = new MutationObserver(muts => { for (const m of muts) { if ( (m.type === 'attributes' && m.attributeName === 'style') || m.type === 'childList' ) { updateCustomErrors(); break; } } }); observer.observe(summary, { attributes: true, attributeFilter: ['style'], childList: true }); // Clean up on unload window.addEventListener('beforeunload', () => observer.disconnect()); } // 3) Remove inline errors as soon as the user edits const formContainer = document.querySelector('.crmEntityFormView'); if (formContainer) { const remover = e => e.target.closest('td')?.querySelector('strong.customError')?.remove(); formContainer .querySelectorAll( 'input[type="text"], input[type="url"], input[type="email"], textarea, select' ) .forEach(i => { i.addEventListener('input', remover); i.addEventListener('change', remover); }); formContainer .querySelectorAll('button.launchentitylookup') .forEach(btn => btn.addEventListener('click', remover)); } }); // AllStepsValidator Integration With the Submission Handler Workflow. // USAGE: Requires AllStepsValidator and the process workflow button on the same page // Notes: Makes use of a proxy button since the built in workflow event handlers are capturing meaning we can't inject code "before" them without signifcant effort. window.addEventListener('load', () => { function checkValidBeforeProceed() { return new Promise((resolve) => { FormSteps.validateWithServerCheck().then(resolve); }); } if (typeof AllStepsValidator !== 'undefined') { let SubmitHandlerWorkflowButton = document.querySelector('.workflow-link[data-workflowid="30b42c5d-2fe3-ef11-be21-6045bdf9bd66"]'); let BuiltInSubmit = document.querySelector('.submit-btn'); if (SubmitHandlerWorkflowButton) { if(BuiltInSubmit) { BuiltInSubmit.remove()} WebHelpFunc.replaceButtonVirtually(SubmitHandlerWorkflowButton, 'VirtualSubmitHandlerWorkflowButton', checkValidBeforeProceed ); } } });