import type { ConditionalRule, FormAnswers, Question } from './types' /** Returns true if the question should be shown given the current answers. */ export function isQuestionVisible( question: Question, answers: FormAnswers ): boolean { const rules = question.conditional_rules if (!rules || rules.length === 0) return true // All rules must pass (AND semantics) return rules.every((rule) => evaluateRule(rule, answers)) } /** Normalizes if_question to an array of codes regardless of rule shape. */ export function ruleQuestionCodes(rule: ConditionalRule): string[] { return Array.isArray(rule.if_question) ? rule.if_question : [rule.if_question] } function evaluateRule(rule: ConditionalRule, answers: FormAnswers): boolean { if (rule.op === 'eq') { return answers[rule.if_question as string] === rule.value } if (rule.op === 'eq_any') { return ruleQuestionCodes(rule).some((code) => answers[code] === rule.value) } return false } /** * Returns the codes of questions that will become hidden when `triggerCode` * changes to `newValue`. Used to clear those answers reactively. */ export function getHiddenCodes( triggerCode: string, newValue: string | string[] | number | null, questions: Question[], currentAnswers: FormAnswers ): string[] { const projected = { ...currentAnswers, [triggerCode]: newValue } return questions .filter((q) => { if (!q.conditional_rules?.length) return false const dependsOnTrigger = q.conditional_rules.some((r) => ruleQuestionCodes(r).includes(triggerCode) ) if (!dependsOnTrigger) return false return !isQuestionVisible(q, projected) }) .map((q) => q.code) } /** All trigger codes in the survey (questions that have dependents). */ export function getTriggerCodes(questions: Question[]): Set { const triggers = new Set() for (const q of questions) { for (const rule of q.conditional_rules ?? []) { for (const code of ruleQuestionCodes(rule)) triggers.add(code) } } return triggers }