/* * Copyright 2025 Commonwealth Scientific and Industrial Research * Organisation (CSIRO) ABN 41 687 119 230. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import type { BackboneElement, Extension, Questionnaire, QuestionnaireItem } from 'fhir/r4'; import { getChoiceControlType } from './choice'; import { ChoiceItemControl, OpenChoiceItemControl } from '../interfaces/choice.enum'; import { getOpenChoiceControlType } from './openChoice'; import type { EnableWhenExpressions, EnableWhenItems } from '../interfaces/enableWhen.interface'; import { structuredDataCapture } from 'fhir-sdc-helpers'; interface isHiddenByEnableWhensParams { linkId: string; enableWhenIsActivated: boolean; enableWhenItems: EnableWhenItems; enableWhenExpressions: EnableWhenExpressions; parentRepeatGroupIndex?: number; } export function isHiddenByEnableWhen(params: isHiddenByEnableWhensParams): boolean { const { linkId, enableWhenIsActivated, enableWhenItems, enableWhenExpressions, parentRepeatGroupIndex } = params; // If enableWhen is not activated, items are not hidden by enableWhen if (!enableWhenIsActivated) { return false; } // Check enableWhen items const { singleItems, repeatItems } = enableWhenItems; if (singleItems[linkId]) { return !singleItems[linkId].isEnabled; } if (repeatItems[linkId] && parentRepeatGroupIndex !== undefined) { return !repeatItems[linkId].enabledIndexes[parentRepeatGroupIndex]; } // Check enableWhenExpressions const { singleExpressions, repeatExpressions } = enableWhenExpressions; if (repeatExpressions[linkId] && parentRepeatGroupIndex !== undefined) { return !repeatExpressions[linkId].enabledIndexes[parentRepeatGroupIndex]; } if (singleExpressions[linkId]) { return !enableWhenExpressions.singleExpressions[linkId].isEnabled; } return false; } /** * Check if qItem is a repeat item AND if it isn't a checkbox item * Note: repeat checkbox items are rendered as multi-select checkbox instead of being rendered as a traditional repeat item * * @author Sean Fong */ export function isRepeatItemAndNotCheckbox(qItem: QuestionnaireItem): boolean { // Prevents form from crashing due to mismatched Q and QR // In reality this should never happen if (!qItem) { return false; } const isCheckbox = getChoiceControlType(qItem) === ChoiceItemControl.Checkbox || getOpenChoiceControlType(qItem) === OpenChoiceItemControl.Checkbox; return !!qItem['repeats'] && !isCheckbox; } /** * Check if qItem is a checkbox item */ export function isCheckbox(qItem: QuestionnaireItem): boolean { // In reality this should never happen if (!qItem) { return false; } return getChoiceControlType(qItem) === ChoiceItemControl.Checkbox; } export function getXHtmlStringFromQuestionnaire(questionnaire: Questionnaire): string | null { const itemControl = questionnaire._title?.extension?.find( (extension: Extension) => extension.url === 'http://hl7.org/fhir/StructureDefinition/rendering-xhtml' ); if (itemControl) { if (itemControl.valueString) { return itemControl.valueString; } } return null; } /** * Recursively traverses all QuestionnaireItems in a questionnaire and produces a map of (with `item` omitted). */ export function getLinkIdPartialItemMap( questionnaire: Questionnaire ): Record> { if (!questionnaire.item || questionnaire.item.length === 0) { return {}; } const tuples: [string, Omit][] = []; for (const qItem of questionnaire.item) { tuples.push(...getLinkIdPartialTuplesFromItemRecursive(qItem)); } return Object.fromEntries(tuples); } /** * Recursively traverse a QuestionnaireItem and all its children, collecting a tuple [linkId, QItem] (with `item` omitted). * The resulting array can be used to construct a flat map of linkId to node metadata (excluding their child items, for memory efficiency). */ export function getLinkIdPartialTuplesFromItemRecursive( qItem: QuestionnaireItem ): [string, Omit][] { const tuples: [string, Omit][] = []; const { item, ...partial } = qItem; if (qItem.linkId) { tuples.push([qItem.linkId, partial]); } if (item) { for (const child of item) { tuples.push(...getLinkIdPartialTuplesFromItemRecursive(child)); } } return tuples; } function getPreferredTerminologyServer(element: BackboneElement): string | null { const preferredTerminologyServerExtension = element.extension?.find( (ext) => ext.url === 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-preferredTerminologyServer' ); if (preferredTerminologyServerExtension) { return ( preferredTerminologyServerExtension.valueUrl ?? preferredTerminologyServerExtension.valueUri ?? preferredTerminologyServerExtension.valueString ?? null ); } return null; } export function getLinkIdPreferredTerminologyServerTuples( questionnaire: Questionnaire ): [string, string][] { if (!questionnaire.item || questionnaire.item.length === 0) { return []; } const preferredTerminologyServer = getPreferredTerminologyServer(questionnaire); const linkIds: [string, string][] = []; for (const topLevelItem of questionnaire.item) { linkIds.push( ...getLinkIdPreferredTerminologyServerTuplesRecursive( topLevelItem, preferredTerminologyServer ) ); } return linkIds; } function getLinkIdPreferredTerminologyServerTuplesRecursive( qItem: QuestionnaireItem, parentPreferredTerminologyServer: string | null ): [string, string][] { const linkIds: [string, string][] = []; let preferredTerminologyServer = null; if (qItem.linkId) { preferredTerminologyServer = getPreferredTerminologyServer(qItem) ?? parentPreferredTerminologyServer ?? null; if (preferredTerminologyServer) { linkIds.push([qItem.linkId, preferredTerminologyServer]); } } if (qItem.item) { for (const childItem of qItem.item) { linkIds.push( ...getLinkIdPreferredTerminologyServerTuplesRecursive(childItem, preferredTerminologyServer) ); } } return linkIds; } export type CollapsibleType = 'default-open' | 'default-closed'; function valueCodeIsCollapsibleType(valueCode: string): valueCode is CollapsibleType { return valueCode === 'default-open' || valueCode === 'default-closed'; } export function getGroupCollapsible(qItem: QuestionnaireItem): CollapsibleType | null { const collapsibleExtension = qItem.extension?.find( (ext) => ext.url === 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-collapsible' ); if (collapsibleExtension) { if ( collapsibleExtension.valueCode && valueCodeIsCollapsibleType(collapsibleExtension.valueCode) ) { return collapsibleExtension.valueCode; } } return null; } /** * Plain function to determine if a QuestionnaireItem is hidden via item.hidden, enableWhens, enableWhenExpressions. * When checking for repeating group enableWhen items, the parentRepeatGroupIndex should be provided. * Recommended to use the `useHidden` hook instead for React components, where applicable. * * @author Sean Fong */ export function isItemHidden( qItem: QuestionnaireItem, enableWhenIsActivated: boolean, enableWhenItems: EnableWhenItems, enableWhenExpressions: EnableWhenExpressions, enableWhenAsReadOnly: boolean | Set, parentRepeatGroupIndex?: number ): boolean { if (structuredDataCapture.getHidden(qItem)) { return true; } // If enableWhenAsReadOnly is true, then items hidden by enableWhen should be displayed, but set as readOnly // If enableWhenAsReadOnly is a Set, all item types in the set should be displayed, but set as readOnly if ( enableWhenAsReadOnly === true || (enableWhenAsReadOnly instanceof Set && enableWhenAsReadOnly.has(qItem.type)) ) { return false; } return isHiddenByEnableWhen({ linkId: qItem.linkId, enableWhenIsActivated, enableWhenItems, enableWhenExpressions, parentRepeatGroupIndex }); }