From 0cd76eb3c5aaeaab0e3add2519448f8b7921266c Mon Sep 17 00:00:00 2001 From: latin-panda <66472237+latin-panda@users.noreply.github.com> Date: Mon, 25 Aug 2025 22:29:53 +0700 Subject: [PATCH 01/40] engine support for metadata in itemset nodes --- packages/xforms-engine/src/client/RankNode.ts | 7 +--- .../xforms-engine/src/client/SelectNode.ts | 8 ++-- .../lib/reactivity/createItemCollection.ts | 40 +++++++++++++++---- .../parse/body/control/ItemsetDefinition.ts | 19 ++++++++- .../expression/ItemsetGeometryExpression.ts | 8 ++++ .../expression/ItemsetMetadataExpression.ts | 14 +++++++ 6 files changed, 79 insertions(+), 17 deletions(-) create mode 100644 packages/xforms-engine/src/parse/expression/ItemsetGeometryExpression.ts create mode 100644 packages/xforms-engine/src/parse/expression/ItemsetMetadataExpression.ts diff --git a/packages/xforms-engine/src/client/RankNode.ts b/packages/xforms-engine/src/client/RankNode.ts index 94510fa9c..78927b174 100644 --- a/packages/xforms-engine/src/client/RankNode.ts +++ b/packages/xforms-engine/src/client/RankNode.ts @@ -1,3 +1,4 @@ +import type { BaseItem } from '../lib/reactivity/createItemCollection.ts'; import type { RankControlDefinition } from '../parse/body/control/RankControlDefinition.ts'; import type { LeafNodeDefinition } from '../parse/model/LeafNodeDefinition.ts'; import type { BaseValueNode, BaseValueNodeState } from './BaseValueNode.ts'; @@ -8,11 +9,7 @@ import type { LeafNodeValidationState } from './validation.ts'; import type { UnknownAppearanceDefinition } from '../parse/body/appearance/unknownAppearanceParser.ts'; import type { ValueType } from './ValueType.ts'; -export interface RankItem { - get label(): TextRange<'item-label'>; - get value(): string; -} - +export type RankItem = BaseItem; export type RankValueOptions = readonly RankItem[]; export interface RankNodeState extends BaseValueNodeState { diff --git a/packages/xforms-engine/src/client/SelectNode.ts b/packages/xforms-engine/src/client/SelectNode.ts index f9844a734..35d6a9964 100644 --- a/packages/xforms-engine/src/client/SelectNode.ts +++ b/packages/xforms-engine/src/client/SelectNode.ts @@ -1,3 +1,4 @@ +import type { BaseItem } from '../lib/reactivity/createItemCollection.ts'; import type { AnySelectControlDefinition, SelectType, @@ -6,14 +7,13 @@ import type { LeafNodeDefinition } from '../parse/model/LeafNodeDefinition.ts'; import type { BaseValueNode, BaseValueNodeState } from './BaseValueNode.ts'; import type { NodeAppearances } from './NodeAppearances.ts'; import type { RootNode } from './RootNode.ts'; -import type { TextRange } from './TextRange.ts'; import type { ValueType } from './ValueType.ts'; import type { GeneralParentNode } from './hierarchy.ts'; import type { LeafNodeValidationState } from './validation.ts'; -export interface SelectItem { - get label(): TextRange<'item-label'>; - get value(): string; +export interface SelectItem extends BaseItem { + geometry?(): string; + metadata?: Array<{ label: string; value(): string }>; } export type SelectValueOptions = readonly SelectItem[]; diff --git a/packages/xforms-engine/src/lib/reactivity/createItemCollection.ts b/packages/xforms-engine/src/lib/reactivity/createItemCollection.ts index f7a7f75e6..405184592 100644 --- a/packages/xforms-engine/src/lib/reactivity/createItemCollection.ts +++ b/packages/xforms-engine/src/lib/reactivity/createItemCollection.ts @@ -2,8 +2,6 @@ import { UpsertableMap } from '@getodk/common/lib/collections/UpsertableMap.ts'; import type { Accessor } from 'solid-js'; import { createMemo } from 'solid-js'; import type { ActiveLanguage } from '../../client/FormLanguage.ts'; -import type { SelectItem } from '../../client/SelectNode.ts'; -import type { RankItem } from '../../client/RankNode.ts'; import type { TextRange as ClientTextRange } from '../../client/TextRange.ts'; import type { EvaluationContext } from '../../instance/internal-api/EvaluationContext.ts'; import type { TranslationContext } from '../../instance/internal-api/TranslationContext.ts'; @@ -20,8 +18,11 @@ import type { ReactiveScope } from './scope.ts'; import { createTextRange } from './text/createTextRange.ts'; export type ItemCollectionControl = RankControl | SelectControl; -type Item = RankItem | SelectItem; type DerivedItemLabel = ClientTextRange<'item-label', 'form-derived'>; +export interface BaseItem { + get label(): ClientTextRange<'item-label'>; + get value(): string; +} const derivedItemLabel = (context: TranslationContext, value: string): DerivedItemLabel => { const chunk = new TextChunk(context, 'literal', value); @@ -45,7 +46,7 @@ const createItemLabel = ( const createTranslatedStaticItems = ( control: ItemCollectionControl, items: readonly ItemDefinition[] -): Accessor => { +): Accessor => { return control.scope.runTask(() => { const labeledItems = items.map((item) => { const { value } = item; @@ -101,6 +102,8 @@ const createItemsetItemLabel = ( interface ItemsetItem { label(): ClientTextRange<'item-label'>; value(): string; + geometry(): string; + metadata: Array<{ label: string; value(): string }>; } const createItemsetItems = ( @@ -120,11 +123,30 @@ const createItemsetItems = ( const value = createComputedExpression(context, itemset.value, { defaultValue: '', }); + const label = createItemsetItemLabel(context, itemset, value); + const geometry = createComputedExpression(context, itemset.geometry, { + defaultValue: '', + }); + + const nodeElements = itemNode + .getXPathChildNodes() + .filter((node) => node.nodeType === 'static-element'); + const metadata = itemset.getMetadataExpressions(nodeElements).map((meta) => { + return { + label: meta.elementName, + value: createComputedExpression(context, meta, { + defaultValue: '', + }), + }; + }); + return { label, value, + geometry, + metadata, }; }); }); @@ -135,7 +157,7 @@ const createItemsetItems = ( const createItemset = ( control: ItemCollectionControl, itemset: ItemsetDefinition -): Accessor => { +): Accessor => { return control.scope.runTask(() => { const itemsetItems = createItemsetItems(control, itemset); @@ -144,6 +166,8 @@ const createItemset = ( return { label: item.label(), value: item.value(), + geometry: item.geometry(), + metadata: item.metadata.map((meta) => ({ label: meta.label, value: meta.value() })), }; }); }); @@ -152,7 +176,7 @@ const createItemset = ( /** * Creates a reactive computation of a {@link ItemCollectionControl}'s - * {@link Item}s, in support of the field's `valueOptions`. + * {@link BaseItem}s, in support of the field's `valueOptions`. * * - The control defined with static ``s will compute to an corresponding * static list of items. @@ -162,7 +186,9 @@ const createItemset = ( * their appropriate dependencies (whether relative to the itemset item node, * referencing a form's `itext` translations, etc). */ -export const createItemCollection = (control: ItemCollectionControl): Accessor => { +export const createItemCollection = ( + control: ItemCollectionControl +): Accessor => { const { items, itemset } = control.definition.bodyElement; if (itemset != null) { diff --git a/packages/xforms-engine/src/parse/body/control/ItemsetDefinition.ts b/packages/xforms-engine/src/parse/body/control/ItemsetDefinition.ts index a936b3cc4..4821b7b38 100644 --- a/packages/xforms-engine/src/parse/body/control/ItemsetDefinition.ts +++ b/packages/xforms-engine/src/parse/body/control/ItemsetDefinition.ts @@ -1,5 +1,8 @@ +import { StaticElement } from '../../../integration/xpath/static-dom/StaticElement.ts'; import type { ItemsetElement } from '../../../lib/dom/query.ts'; import { getValueElement } from '../../../lib/dom/query.ts'; +import { ItemsetGeometryExpression } from '../../expression/ItemsetGeometryExpression.ts'; +import { ItemsetMetadataExpression } from '../../expression/ItemsetMetadataExpression.ts'; import { ItemsetNodesetExpression } from '../../expression/ItemsetNodesetExpression.ts'; import { ItemsetValueExpression } from '../../expression/ItemsetValueExpression.ts'; import { ItemsetLabelDefinition } from '../../text/ItemsetLabelDefinition.ts'; @@ -18,6 +21,8 @@ export class ItemsetDefinition extends BodyElementDefinition<'itemset'> { readonly nodes: ItemsetNodesetExpression; readonly value: ItemsetValueExpression; + readonly geometry: ItemsetGeometryExpression; + readonly metadataExclusions: string[]; constructor( form: XFormDefinition, @@ -49,7 +54,19 @@ export class ItemsetDefinition extends BodyElementDefinition<'itemset'> { throw new Error(` has no `); } - this.value = new ItemsetValueExpression(this, valueExpression); this.label = ItemsetLabelDefinition.from(form, this); + this.value = new ItemsetValueExpression(this, valueExpression); + this.geometry = new ItemsetGeometryExpression(this); + + this.metadataExclusions = ['itextId', this.value.expression, this.geometry.expression]; + } + + getMetadataExpressions(metadataNodes: StaticElement[]): ItemsetMetadataExpression[] { + return metadataNodes + .filter((node) => { + const { localName } = node.qualifiedName; + return localName.length && !this.metadataExclusions.includes(localName); + }) + .map((node) => new ItemsetMetadataExpression(this, node.qualifiedName.localName)); } } diff --git a/packages/xforms-engine/src/parse/expression/ItemsetGeometryExpression.ts b/packages/xforms-engine/src/parse/expression/ItemsetGeometryExpression.ts new file mode 100644 index 000000000..6d8702f08 --- /dev/null +++ b/packages/xforms-engine/src/parse/expression/ItemsetGeometryExpression.ts @@ -0,0 +1,8 @@ +import type { ItemsetDefinition } from '../body/control/ItemsetDefinition.ts'; +import { DependentExpression } from './abstract/DependentExpression.ts'; + +export class ItemsetGeometryExpression extends DependentExpression<'string'> { + constructor(readonly itemset: ItemsetDefinition) { + super(itemset, 'string', 'geometry'); + } +} diff --git a/packages/xforms-engine/src/parse/expression/ItemsetMetadataExpression.ts b/packages/xforms-engine/src/parse/expression/ItemsetMetadataExpression.ts new file mode 100644 index 000000000..c249f3435 --- /dev/null +++ b/packages/xforms-engine/src/parse/expression/ItemsetMetadataExpression.ts @@ -0,0 +1,14 @@ +import type { ItemsetDefinition } from '../body/control/ItemsetDefinition.ts'; +import { DependentExpression } from './abstract/DependentExpression.ts'; + +export class ItemsetMetadataExpression extends DependentExpression<'string'> { + readonly elementName: string; + + constructor( + readonly itemset: ItemsetDefinition, + elementName: string + ) { + super(itemset, 'string', elementName); + this.elementName = elementName; + } +} From 16f3cdeb9da1f2cd37ec1596c33599efd67605be Mon Sep 17 00:00:00 2001 From: latin-panda <66472237+latin-panda@users.noreply.github.com> Date: Thu, 28 Aug 2025 23:09:06 +0700 Subject: [PATCH 02/40] Async map bundle loading --- packages/web-forms/package.json | 5 +- .../src/components/common/map/AsyncMap.vue | 106 ++++++++++++++++++ .../src/components/common/map/MapBlock.vue | 50 +++++++++ .../form-elements/select/Select1Control.vue | 5 +- yarn.lock | 106 ++++++++++++++++++ 5 files changed, 269 insertions(+), 3 deletions(-) create mode 100644 packages/web-forms/src/components/common/map/AsyncMap.vue create mode 100644 packages/web-forms/src/components/common/map/MapBlock.vue diff --git a/packages/web-forms/package.json b/packages/web-forms/package.json index bb8d28a3e..71a397c04 100644 --- a/packages/web-forms/package.json +++ b/packages/web-forms/package.json @@ -78,8 +78,9 @@ "vue": "^3.5.18" }, "dependencies": { - "vue-draggable-plus": "^0.6.0", - "@mdi/js": "^7.4.47" + "@mdi/js": "^7.4.47", + "ol": "^10.6.1", + "vue-draggable-plus": "^0.6.0" }, "publishConfig": { "access": "public" diff --git a/packages/web-forms/src/components/common/map/AsyncMap.vue b/packages/web-forms/src/components/common/map/AsyncMap.vue new file mode 100644 index 000000000..2b395ea09 --- /dev/null +++ b/packages/web-forms/src/components/common/map/AsyncMap.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/packages/web-forms/src/components/common/map/MapBlock.vue b/packages/web-forms/src/components/common/map/MapBlock.vue new file mode 100644 index 000000000..17b54b702 --- /dev/null +++ b/packages/web-forms/src/components/common/map/MapBlock.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/packages/web-forms/src/components/form-elements/select/Select1Control.vue b/packages/web-forms/src/components/form-elements/select/Select1Control.vue index 9ee74d14d..686353f07 100644 --- a/packages/web-forms/src/components/form-elements/select/Select1Control.vue +++ b/packages/web-forms/src/components/form-elements/select/Select1Control.vue @@ -2,6 +2,7 @@ import ColumnarAppearance from '@/components/appearances/ColumnarAppearance.vue'; import FieldListTable from '@/components/appearances/FieldListTable.vue'; import UnsupportedAppearance from '@/components/appearances/UnsupportedAppearance.vue'; +import AsyncMap from '@/components/common/map/AsyncMap.vue'; import ControlText from '@/components/form-elements/ControlText.vue'; import ValidationMessage from '@/components/common/ValidationMessage.vue'; import LikertWidget from '@/components/common/LikertWidget.vue'; @@ -47,6 +48,8 @@ watchEffect(() => { :question="question" /> + +