@conduction/nextcloud-vue 2.141.0 → 2.143.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +27 -0
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
- package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue2.js.map +1 -1
- package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue3.js +2 -1
- package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue3.js.map +1 -1
- package/dist/esm/components/CnAiCompanion/CnAiInput.vue2.js +9 -0
- package/dist/esm/components/CnAiCompanion/CnAiInput.vue2.js.map +1 -1
- package/dist/esm/components/CnAiCompanion/CnAiInput.vue3.js +21 -22
- package/dist/esm/components/CnAiCompanion/CnAiInput.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +29 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js +28 -13
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +2 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +13 -0
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +3 -3
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +1 -1
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +21 -0
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +34 -3
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +70 -5
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +5 -3
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js +17 -4
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +21 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +36 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +14 -0
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +3 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +2 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js +6 -1
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js.map +1 -1
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js.map +1 -1
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js +1 -1
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js +31 -1
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js.map +1 -1
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +12 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +2 -2
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js +1 -1
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js +4 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js.map +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js +18 -8
- package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue.js +1 -1
- package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue2.js +52 -1
- package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue3.js +119 -65
- package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue.js +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js +86 -4
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js +67 -5
- package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +10 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +12 -3
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +976 -592
- package/dist/esm/utils/buildViewTree.js +23 -8
- package/dist/esm/utils/buildViewTree.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +1363 -734
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +976 -592
- package/l10n/en.json +10 -1
- package/l10n/nl.json +12 -3
- package/package.json +1 -1
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +28 -1
- package/src/components/CnAiCompanion/CnAiChatPanel.vue +12 -0
- package/src/components/CnAiCompanion/CnAiInput.vue +32 -1
- package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +34 -4
- package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +1 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +15 -1
- package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +8 -0
- package/src/components/CnChoiceCards/CnChoiceCards.vue +1 -0
- package/src/components/CnDataTable/CnDataTable.md +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +22 -1
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +1 -0
- package/src/components/CnFactsWidget/CnFactsWidget.vue +34 -3
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +72 -7
- package/src/components/CnFilesBrowser/filesBrowser.js +17 -4
- package/src/components/CnFilesBrowser/filesBrowserColumns.js +20 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +37 -2
- package/src/components/CnIndexPage/useSelfFetchList.js +14 -0
- package/src/components/CnObjectSidebar/CnFilesTab.vue +4 -1
- package/src/components/CnPageHeader/CnHeaderTrail.vue +6 -1
- package/src/components/CnPartyPicker/CnPartyPicker.vue +31 -1
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +21 -1
- package/src/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue +6 -0
- package/src/components/CnTimelineWidget/CnTimelineManage.vue +10 -1
- package/src/css/dialog.css +18 -0
- package/src/css/look-board-detail.css +126 -0
- package/src/css/look-board-index.css +95 -1
- package/src/css/look-board.css +90 -1
- package/src/integrations/builtin/contacts/CnContactsTab.vue +101 -26
- package/src/integrations/builtin/contacts/__tests__/CnContactsTabBoardRows.spec.js +67 -0
- package/src/integrations/builtin/deck/CnDeckCard.vue +121 -2
- package/src/integrations/builtin/deck/__tests__/CnDeckCardHeaderActions.spec.js +68 -0
- package/src/utils/buildViewTree.js +23 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnChecklistRunWidget.vue3.js","sources":["../../../../src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue?vue&type=template&id=3d97a9f0&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div class=\"cn-checklist-run\" data-testid=\"cn-checklist-run\">\n\t\t<p v-if=\"error\"\n\t\t\tclass=\"cn-checklist-run__error\"\n\t\t\trole=\"alert\"\n\t\t\tdata-testid=\"cn-checklist-run-error\">\n\t\t\t{{ error }}\n\t\t</p>\n\t\t<div v-else-if=\"loading\" class=\"cn-checklist-run__loading\">\n\t\t\t<NcLoadingIcon :size=\"20\" :name=\"loadingLabel\" />\n\t\t</div>\n\t\t<p v-else-if=\"questions.length === 0\" class=\"cn-checklist-run__empty\" data-testid=\"cn-checklist-run-empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\t\t<form v-else\n\t\t\tclass=\"cn-checklist-run__form\"\n\t\t\tnovalidate\n\t\t\t@submit.prevent=\"next\">\n\t\t\t<p\n\t\t\t\tref=\"position\"\n\t\t\t\tclass=\"cn-checklist-run__position\"\n\t\t\t\ttabindex=\"-1\"\n\t\t\t\tdata-testid=\"cn-checklist-run-position\">\n\t\t\t\t{{ positionText }}\n\t\t\t</p>\n\t\t\t<span\n\t\t\t\tclass=\"cn-checklist-run__progress\"\n\t\t\t\trole=\"progressbar\"\n\t\t\t\t:aria-label=\"progressLabel\"\n\t\t\t\taria-valuemin=\"0\"\n\t\t\t\t:aria-valuemax=\"questions.length\"\n\t\t\t\t:aria-valuenow=\"answeredCount\"\n\t\t\t\tdata-testid=\"cn-checklist-run-progress\">\n\t\t\t\t<span class=\"cn-checklist-run__progress-fill\" :style=\"{ width: `${progressShare}%` }\" />\n\t\t\t</span>\n\t\t\t<span class=\"cn-checklist-run__live\" aria-live=\"polite\">{{ liveText }}</span>\n\n\t\t\t<p v-if=\"pageGroup\" class=\"cn-checklist-run__group\" data-testid=\"cn-checklist-run-group\">\n\t\t\t\t{{ pageGroup }}\n\t\t\t</p>\n\t\t\t<fieldset\n\t\t\t\tv-for=\"question in pageQuestions\"\n\t\t\t\t:key=\"question.id\"\n\t\t\t\tclass=\"cn-checklist-run__question\"\n\t\t\t\t:aria-describedby=\"question.help ? `${uid}-help-${question.id}` : null\"\n\t\t\t\tdata-testid=\"cn-checklist-run-question\">\n\t\t\t\t<legend class=\"cn-checklist-run__legend\">\n\t\t\t\t\t{{ question.text }}<span v-if=\"question.required\" class=\"cn-checklist-run__required\">\n\t\t\t\t\t\t<span aria-hidden=\"true\"> *</span><span class=\"cn-checklist-run__sr\">{{ requiredLabel }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t</legend>\n\t\t\t\t<p v-if=\"question.help\" :id=\"`${uid}-help-${question.id}`\" class=\"cn-checklist-run__help\">\n\t\t\t\t\t{{ question.help }}\n\t\t\t\t</p>\n\t\t\t\t<template v-if=\"question.choices.length\">\n\t\t\t\t\t<label\n\t\t\t\t\t\tv-for=\"choice in question.choices\"\n\t\t\t\t\t\t:key=\"choice.value\"\n\t\t\t\t\t\tclass=\"cn-checklist-run__choice\">\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tv-model=\"draft[question.id].value\"\n\t\t\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t\t\t:name=\"`${uid}-${question.id}`\"\n\t\t\t\t\t\t\t:value=\"choice.value\"\n\t\t\t\t\t\t\t:required=\"question.required\"\n\t\t\t\t\t\t\tdata-testid=\"cn-checklist-run-choice\"\n\t\t\t\t\t\t\t@change=\"markDirty(question.id)\">\n\t\t\t\t\t\t{{ choice.label }}\n\t\t\t\t\t</label>\n\t\t\t\t</template>\n\t\t\t\t<input\n\t\t\t\t\tv-else-if=\"question.kind === 'number'\"\n\t\t\t\t\tv-model=\"draft[question.id].value\"\n\t\t\t\t\tclass=\"cn-checklist-run__number\"\n\t\t\t\t\ttype=\"number\"\n\t\t\t\t\tinputmode=\"decimal\"\n\t\t\t\t\t:aria-label=\"question.text\"\n\t\t\t\t\t:required=\"question.required\"\n\t\t\t\t\tdata-testid=\"cn-checklist-run-number\"\n\t\t\t\t\t@input=\"markDirty(question.id)\">\n\t\t\t\t<p v-else-if=\"question.kind === 'photo'\" class=\"cn-checklist-run__help\">\n\t\t\t\t\t{{ photoOnlyLabel }}\n\t\t\t\t</p>\n\t\t\t\t<textarea\n\t\t\t\t\tv-else\n\t\t\t\t\tv-model=\"draft[question.id].value\"\n\t\t\t\t\tclass=\"cn-checklist-run__text\"\n\t\t\t\t\trows=\"3\"\n\t\t\t\t\t:aria-label=\"question.text\"\n\t\t\t\t\t:required=\"question.required\"\n\t\t\t\t\tdata-testid=\"cn-checklist-run-text\"\n\t\t\t\t\t@input=\"markDirty(question.id)\" />\n\t\t\t\t<label v-if=\"allowNote\" class=\"cn-checklist-run__note-label\">\n\t\t\t\t\t{{ noteLabel }}\n\t\t\t\t\t<textarea\n\t\t\t\t\t\tv-model=\"draft[question.id].note\"\n\t\t\t\t\t\tclass=\"cn-checklist-run__text\"\n\t\t\t\t\t\trows=\"2\"\n\t\t\t\t\t\tdata-testid=\"cn-checklist-run-note\"\n\t\t\t\t\t\t@input=\"markDirty(question.id)\" />\n\t\t\t\t</label>\n\t\t\t\t<p\n\t\t\t\t\tv-if=\"missing.includes(question.id)\"\n\t\t\t\t\tclass=\"cn-checklist-run__error\"\n\t\t\t\t\trole=\"alert\"\n\t\t\t\t\tdata-testid=\"cn-checklist-run-missing\">\n\t\t\t\t\t{{ missingLabel }}\n\t\t\t\t</p>\n\t\t\t</fieldset>\n\n\t\t\t<div\n\t\t\t\tv-for=\"question in photoQuestions\"\n\t\t\t\t:key=\"`photos-${question.id}`\"\n\t\t\t\tclass=\"cn-checklist-run__photos\"\n\t\t\t\tdata-testid=\"cn-checklist-run-photos\">\n\t\t\t\t<strong class=\"cn-checklist-run__photos-title\">{{ photoTitle(question) }}</strong>\n\t\t\t\t<ul v-if=\"photoNamesFor(question).length\" class=\"cn-checklist-run__photo-list\">\n\t\t\t\t\t<li v-for=\"name in photoNamesFor(question)\" :key=\"name\" data-testid=\"cn-checklist-run-photo\">\n\t\t\t\t\t\t{{ name }}\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t\t<label class=\"cn-checklist-run__photo-button\">\n\t\t\t\t\t<input\n\t\t\t\t\t\tclass=\"cn-checklist-run__photo-input\"\n\t\t\t\t\t\ttype=\"file\"\n\t\t\t\t\t\taccept=\"image/*\"\n\t\t\t\t\t\tcapture=\"environment\"\n\t\t\t\t\t\t:aria-describedby=\"missingPhoto.includes(question.id) ? `${uid}-photo-${question.id}` : null\"\n\t\t\t\t\t\tdata-testid=\"cn-checklist-run-photo-input\"\n\t\t\t\t\t\t@change=\"addPhotos($event, question.id)\">\n\t\t\t\t\t{{ photoNamesFor(question).length || (storedPhotos[question.id] || []).length ? morePhotoLabel : addPhotoLabel }}\n\t\t\t\t</label>\n\t\t\t\t<p\n\t\t\t\t\tv-if=\"missingPhoto.includes(question.id)\"\n\t\t\t\t\t:id=\"`${uid}-photo-${question.id}`\"\n\t\t\t\t\tclass=\"cn-checklist-run__error\"\n\t\t\t\t\trole=\"alert\"\n\t\t\t\t\tdata-testid=\"cn-checklist-run-missing-photo\">\n\t\t\t\t\t{{ missingPhotoLabel }}\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div class=\"cn-checklist-run__nav\">\n\t\t\t\t<NcButton\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t:disabled=\"page === 0 || saving\"\n\t\t\t\t\tdata-testid=\"cn-checklist-run-previous\"\n\t\t\t\t\t@click=\"previous\">\n\t\t\t\t\t{{ previousLabel }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton\n\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t:disabled=\"saving\"\n\t\t\t\t\tdata-testid=\"cn-checklist-run-next\">\n\t\t\t\t\t{{ isLastPage ? finishLabel : nextLabel }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t\t<p v-if=\"saveError\"\n\t\t\t\tclass=\"cn-checklist-run__error\"\n\t\t\t\trole=\"alert\"\n\t\t\t\tdata-testid=\"cn-checklist-run-save-error\">\n\t\t\t\t{{ saveError }}\n\t\t\t</p>\n\t\t</form>\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton, NcLoadingIcon } from '@nextcloud/vue'\nimport { uploadHeldFiles } from '../../composables/useHeldFileUpload.js'\nimport { dispatchAction } from '../../utils/actionsDispatcher.js'\nimport { fetchReferenceObjects } from '../../utils/boardReferenceColumns.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { hasUnresolvedTokens, resolveDeepTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'\n\nconst OBJECT_API = '/apps/openregister/api/objects'\n\nlet instanceCounter = 0\n\n/**\n * The choices a question type offers, before translation.\n *\n * @type {Record<string, Array<{value: string, label: string}>>}\n */\nconst TYPE_CHOICES = Object.freeze({\n\tyes_no: [{ key: 'yes', label: 'Yes' }, { key: 'no', label: 'No' }],\n\tyes_no_na: [{ key: 'yes', label: 'Yes' }, { key: 'no', label: 'No' }, { key: 'na', label: 'Not applicable' }],\n})\n\n/** The kinds of question the widget draws. */\nconst KINDS = Object.freeze(['yes_no', 'yes_no_na', 'choice', 'text', 'number', 'photo'])\n\n/**\n * Sort by a numeric field, lowest first, keeping the given order for ties and\n * for entries without the field.\n *\n * @param {Array<object>} list The entries.\n * @param {string} field The field.\n * @return {Array<object>} A sorted copy.\n */\nfunction byOrder(list, field) {\n\tif (!field) {\n\t\treturn [...list]\n\t}\n\tconst rank = (entry) => {\n\t\tconst n = Number(readPath(entry, field))\n\t\treturn Number.isFinite(n) ? n : Infinity\n\t}\n\treturn list.map((entry, index) => ({ entry, index })).sort((a, b) => (rank(a.entry) - rank(b.entry)) || (a.index - b.index)).map(({ entry }) => entry)\n}\n\n/**\n * A register or schema as the object API path takes it.\n *\n * @param {string|object} value A slug, or a register/schema object.\n * @return {string} The slug or id.\n */\nfunction slugOf(value) {\n\tif (value && typeof value === 'object') {\n\t\treturn String(value.slug ?? value.id ?? '')\n\t}\n\treturn value ? String(value) : ''\n}\n\n/**\n * The id of an object, wherever OpenRegister carries it.\n *\n * @param {object|null} object The object.\n * @return {string} The id, or ''.\n */\nfunction idOf(object) {\n\treturn String(object?.id ?? object?.['@self']?.id ?? object?.uuid ?? '')\n}\n\n/**\n * CnChecklistRunWidget — the `checklist-run` widget: a checklist answered a\n * page of questions at a time (board DqInspectie, the middle phone frame).\n *\n * The questions are records of a register, listed with a token-resolved\n * filter. Each page shows `pageSize` questions (one by default) as radio\n * choices or a text field, an optional note, and photos for the first\n * question of the page, then Previous and Next. The answers of a page are\n * saved when the user moves on.\n *\n * Answers are stored one record per question through the OpenRegister object\n * API, under the reader's own rights: an existing answer is updated, a new\n * one created with the `answers.create` fields. Photos go up through the\n * object's own file upload (`uploadHeldFiles`) and are attached to the\n * answer's `photosField`. An app that stores answers its own way names a\n * `saveAction` instead; it is dispatched per answer with the answer as its\n * last argument, and photos are not uploaded then.\n *\n * ```json\n * {\n * \"type\": \"checklist-run\",\n * \"content\": {\n * \"questions\": { \"register\": \"app\", \"schema\": \"question\", \"filter\": { \"checklist\": \"@object.checklist\" }, \"orderField\": \"position\" },\n * \"fields\": { \"text\": \"text\", \"type\": \"type\", \"options\": \"options\", \"required\": \"required\", \"help\": \"help\" },\n * \"answers\": {\n * \"register\": \"app\", \"schema\": \"answer\", \"filter\": { \"run\": \"@objectId\" },\n * \"questionField\": \"question\", \"valueField\": \"answer\", \"noteField\": \"note\", \"photosField\": \"photos\",\n * \"create\": { \"run\": \"@objectId\" }\n * },\n * \"pageSize\": 1,\n * \"caption\": \"{title} version {version}\"\n * }\n * }\n * ```\n *\n * The questions and answers can also live inside the open record:\n * `questionsPath` (with `itemsKey` for groups, both sorted by `orderField`)\n * reads the questions off it, and `answersPath` writes the answers into an\n * array on it, one entry per question keyed by `answerKey`, by updating the\n * record through the object API. Photos then go to the record's own files\n * (`recordPhotosField`) and their references into the answer.\n *\n * A question can require a photo: `fields.photoRequired` names its rule,\n * `photoRequiredValues` maps the stored values onto `never`, `always` or\n * `{ whenAnswer }`, and the page holds until the photo is there. On a page\n * with no bound record, `record: { register, schema, filter, sort }` picks\n * the open record: the first match, under the reader's rights.\n *\n * `startAt: \"firstOpen\"` opens a run that was started earlier at the page of\n * its first unanswered question (the last page when all are answered), once\n * per record; without it the run opens at the first page.\n *\n * Question kinds: `yes_no`, `yes_no_na`, `choice` (the question's `options`,\n * a list of strings or `{ value, label }`), `number`, `photo` and `text`.\n * `typeMap` maps the app's type values onto them, `choiceValues` the stored\n * values of yes / no / not applicable.\n * Keyboard: the choices are native radio buttons in a fieldset whose legend is\n * the question; on a new page focus moves to the position line and a polite\n * live region reads it.\n */\nexport default {\n\tname: 'CnChecklistRunWidget',\n\n\tcomponents: { NcButton, NcLoadingIcon },\n\n\tinject: {\n\t\t/** The detail page's object context (`{ objectId, object }`), for tokens. */\n\t\tcnObjectContext: { default: null },\n\t\t/** The page's workspace values, for `@workspace.<key>` tokens. */\n\t\tcnWorkspaceContext: { default: null },\n\t\t/** The host's translate, for manifest texts and question texts. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** The page's pre-bound action dispatcher, for `saveAction` / `finishAction`. */\n\t\tcnDispatchAction: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget config: `{ questions | questionsPath, itemsKey?,\n\t\t * groupLabelField?, orderField?, fields?, typeMap?, choiceValues?,\n\t\t * answers | answersPath, answerKey?, answerFields?, recordPhotosField?,\n\t\t * record?, saveAction?, finishAction?, pageSize?, allowNote?,\n\t\t * allowPhotos?, caption?, emptyText? }`, as in the component\n\t\t * description and docs/components/cn-checklist-run-widget.md.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The bound record's id, for `@objectId`. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The bound record, for `@object.<field>`, the caption and `questionsPath` / `answersPath`. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The bound record's register, for writing `answersPath` (`content.record` wins). */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The bound record's schema, for writing `answersPath` (`content.record` wins). */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['finished'],\n\n\tdata() {\n\t\tinstanceCounter += 1\n\t\treturn {\n\t\t\tuid: `cn-checklist-run-${instanceCounter}`,\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t\tsaving: false,\n\t\t\tsaveError: '',\n\t\t\tquestions: [],\n\t\t\tanswerIds: {},\n\t\t\t// Photos an answer already holds, kept when new ones are attached.\n\t\t\tstoredPhotos: {},\n\t\t\t// `answersPath` mode: the answer entries as last read or written.\n\t\t\trecordAnswers: [],\n\t\t\tdraft: {},\n\t\t\tdirty: {},\n\t\t\tphotos: {},\n\t\t\tpage: 0,\n\t\t\t// The source the page was last positioned for; `startAt` applies once per source.\n\t\t\tpositionedFor: '',\n\t\t\tmissing: [],\n\t\t\t// Questions on this page that still need their photo.\n\t\t\tmissingPhoto: [],\n\t\t\t// `content.record` with a filter: the record it picked, or null.\n\t\t\tpickedRecord: null,\n\t\t\tliveText: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {object} What the tokens resolve against. */\n\t\ttokenCtx() {\n\t\t\tconst unwrap = (c) => (c && typeof c === 'object' && 'value' in c ? c.value : c)\n\t\t\tconst injected = unwrap(this.cnObjectContext) || {}\n\t\t\treturn {\n\t\t\t\t...injected,\n\t\t\t\tobjectId: (this.pickedRecord && idOf(this.pickedRecord)) || this.objectId || injected.objectId || this.objectData?.id || null,\n\t\t\t\tobject: this.pickedRecord || this.objectData || injected.object || null,\n\t\t\t\tworkspace: unwrap(this.cnWorkspaceContext) || {},\n\t\t\t}\n\t\t},\n\n\t\t/** @return {object} The question field paths, with their defaults. */\n\t\tfields() {\n\t\t\treturn { id: 'id', text: 'text', type: 'type', options: 'options', required: 'required', help: 'help', ...(this.content?.fields || {}) }\n\t\t},\n\n\t\t/** @return {number} Questions per page, at least 1. */\n\t\tpageSize() {\n\t\t\tconst n = Math.floor(Number(this.content?.pageSize))\n\t\t\treturn Number.isFinite(n) && n >= 1 ? n : 1\n\t\t},\n\n\t\t/** @return {number} The number of pages. */\n\t\tpageCount() {\n\t\t\treturn Math.max(1, Math.ceil(this.questions.length / this.pageSize))\n\t\t},\n\n\t\t/** @return {boolean} Whether this is the last page. */\n\t\tisLastPage() {\n\t\t\treturn this.page >= this.pageCount - 1\n\t\t},\n\n\t\t/** @return {object[]} The questions on this page. */\n\t\tpageQuestions() {\n\t\t\treturn this.questions.slice(this.page * this.pageSize, (this.page + 1) * this.pageSize)\n\t\t},\n\n\t\t/** @return {object|null} The first question on this page, the one photos belong to. */\n\t\tcurrentQuestion() {\n\t\t\treturn this.pageQuestions[0] || null\n\t\t},\n\n\t\t/** @return {number} Questions with an answer. */\n\t\tansweredCount() {\n\t\t\treturn this.questions.filter((q) => this.isAnswered(q)).length\n\t\t},\n\n\t\t/** @return {number} The share answered, 0 to 100. */\n\t\tprogressShare() {\n\t\t\treturn this.questions.length ? Math.round((this.answeredCount / this.questions.length) * 100) : 0\n\t\t},\n\n\t\t/** @return {string} \"Question 7 of 24\", with the caption after it. */\n\t\tpositionText() {\n\t\t\tconst first = this.page * this.pageSize + 1\n\t\t\tconst position = t('nextcloud-vue', 'Question {n} of {total}', { n: first, total: this.questions.length })\n\t\t\tconst caption = this.caption\n\t\t\treturn caption ? `${position} · ${caption}` : position\n\t\t},\n\n\t\t/** @return {string} `content.caption` with `{field}` filled from the record. */\n\t\tcaption() {\n\t\t\tconst template = this.content?.caption\n\t\t\tif (typeof template !== 'string' || template === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst object = this.tokenCtx.object || {}\n\t\t\treturn this.translate(template).replace(/\\{([A-Za-z0-9_.]+)\\}/g, (_m, path) => {\n\t\t\t\tconst value = readPath(object, path)\n\t\t\t\treturn value === null || value === undefined ? '' : String(value)\n\t\t\t}).trim()\n\t\t},\n\n\t\t/** @return {boolean} Whether a note field shows (default true). */\n\t\tallowNote() {\n\t\t\treturn this.content?.allowNote !== false\n\t\t},\n\n\t\t/** @return {boolean} Whether the answers are written into an array on the open record. */\n\t\tinRecord() {\n\t\t\treturn typeof this.content?.answersPath === 'string' && this.content.answersPath !== ''\n\t\t},\n\n\t\t/** @return {object} `answersPath` field names, with their defaults. */\n\t\tanswerFields() {\n\t\t\treturn { value: 'value', number: 'value', choice: 'value', note: 'note', photos: 'photos', ...(this.content?.answerFields || {}) }\n\t\t},\n\n\t\t/** @return {{register: string, schema: string}} Where the open record lives. */\n\t\trecordRef() {\n\t\t\tconst own = this.content?.record || {}\n\t\t\treturn { register: slugOf(own.register || this.register), schema: slugOf(own.schema || this.schema) }\n\t\t},\n\n\t\t/**\n\t\t * Whether photos can be added: allowed, and somewhere to put them.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/checklist-run-widget/specs/checklist-run-widget/spec.md#requirement-questions-and-answers-can-live-inside-the-open-record\n\t\t */\n\t\tphotosEnabled() {\n\t\t\tif (this.content?.allowPhotos === false || this.content?.saveAction) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (this.inRecord) {\n\t\t\t\treturn Boolean(this.content?.recordPhotosField && this.answerFields.photos)\n\t\t\t}\n\t\t\treturn Boolean(this.content?.answers?.photosField)\n\t\t},\n\n\t\t/** @return {string} The group (section) of this page's first question, or ''. */\n\t\tpageGroup() {\n\t\t\treturn this.currentQuestion?.group || ''\n\t\t},\n\n\t\t/**\n\t\t * The questions of this page that get a photo control: with a\n\t\t * `photoRequired` field, those whose rule is not never (and photo\n\t\t * questions); without one, the first question of the page.\n\t\t *\n\t\t * @return {object[]} The questions.\n\t\t * @spec openspec/changes/checklist-run-photo-rule/specs/checklist-run-photo-rule/spec.md#requirement-a-question-can-require-a-photo\n\t\t */\n\t\tphotoQuestions() {\n\t\t\tif (!this.photosEnabled) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tif (this.fields.photoRequired) {\n\t\t\t\treturn this.pageQuestions.filter((q) => q.kind === 'photo' || q.photoRule !== 'never')\n\t\t\t}\n\t\t\tconst first = this.currentQuestion\n\t\t\tconst extra = this.pageQuestions.filter((q) => q.kind === 'photo' && q !== first)\n\t\t\treturn first ? [first, ...extra] : extra\n\t\t},\n\n\t\t/**\n\t\t * `content.record` with a `filter`: the list to pick the open record\n\t\t * from (first match of `sort`), `'blocked'` while a token is\n\t\t * unresolved, or null without a filter.\n\t\t *\n\t\t * @return {object|string|null}\n\t\t * @spec openspec/changes/checklist-run-photo-rule/specs/checklist-run-photo-rule/spec.md#requirement-a-checklist-run-can-pick-its-record-by-filter\n\t\t */\n\t\trecordPicker() {\n\t\t\tconst own = this.content?.record\n\t\t\tif (!own || typeof own !== 'object' || !own.filter || typeof own.filter !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst unwrap = (c) => (c && typeof c === 'object' && 'value' in c ? c.value : c)\n\t\t\tconst filter = resolveFilterTokens(own.filter, { objectId: this.objectId || null, object: this.objectData, workspace: unwrap(this.cnWorkspaceContext) || {} })\n\t\t\tif (hasUnresolvedTokens(filter)) {\n\t\t\t\treturn 'blocked'\n\t\t\t}\n\t\t\tconst sort = own.sort && typeof own.sort === 'object' && own.sort.field ? { [`_order[${own.sort.field}]`]: own.sort.dir === 'asc' ? 'asc' : 'desc' } : {}\n\t\t\treturn { register: slugOf(own.register), schema: slugOf(own.schema), filter: { ...filter, ...sort } }\n\t\t},\n\n\t\t/** @return {object|null} The record the run lives in: the picked one, else the bound one. */\n\t\topenRecord() {\n\t\t\treturn this.recordPicker ? this.pickedRecord : this.tokenCtx.object\n\t\t},\n\n\t\t/** @return {string} The key of the source lists, to read them again when they change. */\n\t\tsourceKey() {\n\t\t\tconst picker = this.recordPicker\n\t\t\tif (picker === 'blocked') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (picker && typeof this.content?.questionsPath === 'string' && this.content.questionsPath !== '') {\n\t\t\t\treturn JSON.stringify({ pick: picker, path: this.content.questionsPath })\n\t\t\t}\n\t\t\tif (typeof this.content?.questionsPath === 'string' && this.content.questionsPath !== '') {\n\t\t\t\tconst object = this.tokenCtx.object\n\t\t\t\treturn object ? JSON.stringify({ path: this.content.questionsPath, id: idOf(object), at: object['@self']?.updated ?? null }) : ''\n\t\t\t}\n\t\t\tconst q = this.content?.questions\n\t\t\tif (!q || !q.register || !q.schema) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst qFilter = resolveFilterTokens(q.filter || {}, this.tokenCtx)\n\t\t\tconst a = this.content?.answers\n\t\t\tconst aFilter = a && a.register && a.schema ? resolveFilterTokens(a.filter || {}, this.tokenCtx) : null\n\t\t\tif (hasUnresolvedTokens(qFilter) || (aFilter && hasUnresolvedTokens(aFilter))) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn JSON.stringify({ q: [q.register, q.schema, qFilter], a: aFilter ? [a.register, a.schema, aFilter] : null })\n\t\t},\n\n\t\tloadingLabel: () => t('nextcloud-vue', 'Loading …'),\n\t\tprogressLabel: () => t('nextcloud-vue', 'Questions answered'),\n\t\trequiredLabel: () => ` ${t('nextcloud-vue', '(required)')}`,\n\t\tmissingLabel: () => t('nextcloud-vue', 'Answer this question to go on.'),\n\t\tpreviousLabel: () => t('nextcloud-vue', 'Previous'),\n\t\tnextLabel: () => t('nextcloud-vue', 'Next'),\n\t\tfinishLabel: () => t('nextcloud-vue', 'Finish'),\n\t\taddPhotoLabel: () => t('nextcloud-vue', 'Take or add a photo'),\n\t\tmissingPhotoLabel: () => t('nextcloud-vue', 'This answer needs a photo. Add one to go on.'),\n\t\tphotoOnlyLabel: () => t('nextcloud-vue', 'Answer with a photo.'),\n\t\tmorePhotoLabel: () => t('nextcloud-vue', 'Another photo'),\n\n\t\t/** @return {string} The note field's label. */\n\t\tnoteLabel() {\n\t\t\treturn this.content?.noteLabel ? this.translate(this.content.noteLabel) : t('nextcloud-vue', 'Note')\n\t\t},\n\n\t\t/** @return {string} The empty-state text. */\n\t\temptyLabel() {\n\t\t\treturn this.content?.emptyText ? this.translate(this.content.emptyText) : t('nextcloud-vue', 'This checklist has no questions.')\n\t\t},\n\t},\n\n\twatch: {\n\t\tsourceKey: {\n\t\t\thandler() {\n\t\t\t\tthis.load()\n\t\t\t},\n\n\t\t\timmediate: true,\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * The host's translate, or the text unchanged.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string} The translation.\n\t\t */\n\t\ttranslate(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * Whether a value counts as an answer.\n\t\t *\n\t\t * @param {unknown} value The value.\n\t\t * @return {boolean} True for a non-empty value.\n\t\t */\n\t\thasValue(value) {\n\t\t\treturn value !== null && value !== undefined && String(value).trim() !== ''\n\t\t},\n\n\t\t/**\n\t\t * Whether a question has an answer: a value, or for a photo question\n\t\t * a photo stored or held.\n\t\t *\n\t\t * @param {object} question The question.\n\t\t * @return {boolean} True when answered.\n\t\t */\n\t\tisAnswered(question) {\n\t\t\tif (question.kind === 'photo') {\n\t\t\t\treturn this.hasPhoto(question)\n\t\t\t}\n\t\t\treturn this.hasValue(this.draft[question.id]?.value)\n\t\t},\n\n\t\t/**\n\t\t * A question record as the widget draws it.\n\t\t *\n\t\t * @param {object} record The question record.\n\t\t * @param {string} [group] The group (section) it belongs to.\n\t\t * @return {{id: string, text: string, kind: string, required: boolean, help: string, choices: Array<{value: string, label: string}>, group: string}} The question.\n\t\t */\n\t\ttoQuestion(record, group = '') {\n\t\t\tconst f = this.fields\n\t\t\tconst raw = String(readPath(record, f.type) ?? '')\n\t\t\tconst mapped = this.content?.typeMap && typeof this.content.typeMap === 'object' ? this.content.typeMap[raw] : undefined\n\t\t\tconst kind = KINDS.includes(mapped) ? mapped : (KINDS.includes(raw) ? raw : 'text')\n\t\t\tconst stored = { yes: 'yes', no: 'no', na: 'na', ...(this.content?.choiceValues || {}) }\n\t\t\tlet choices = (TYPE_CHOICES[kind] || []).map((c) => ({ value: String(stored[c.key]), label: t('nextcloud-vue', c.label) }))\n\t\t\tif (kind === 'choice') {\n\t\t\t\tconst options = readPath(record, f.options)\n\t\t\t\tchoices = (Array.isArray(options) ? options : []).map((o) => (o && typeof o === 'object'\n\t\t\t\t\t? { value: String(o.value ?? o.label ?? ''), label: this.translate(String(o.label ?? o.value ?? '')) }\n\t\t\t\t\t: { value: String(o), label: this.translate(String(o)) })).filter((c) => c.value !== '')\n\t\t\t}\n\t\t\tconst help = readPath(record, f.help)\n\t\t\treturn {\n\t\t\t\tid: String(readPath(record, f.id) ?? idOf(record)),\n\t\t\t\ttext: this.translate(String(readPath(record, f.text) ?? '')),\n\t\t\t\tkind,\n\t\t\t\trequired: Boolean(readPath(record, f.required)),\n\t\t\t\thelp: typeof help === 'string' && help !== '' ? this.translate(help) : '',\n\t\t\t\tchoices,\n\t\t\t\tgroup,\n\t\t\t\tphotoRule: this.photoRuleOf(record),\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A question's photo rule, from its `photoRequired` field mapped\n\t\t * through `photoRequiredValues`: `'never'`, `'always'` or\n\t\t * `{ whenAnswer }` (a photo is needed when the answer is that value).\n\t\t *\n\t\t * @param {object} record The question record.\n\t\t * @return {('never'|'always'|{whenAnswer: string})} The rule.\n\t\t * @spec openspec/changes/checklist-run-photo-rule/specs/checklist-run-photo-rule/spec.md#requirement-a-question-can-require-a-photo\n\t\t */\n\t\tphotoRuleOf(record) {\n\t\t\tconst path = this.fields.photoRequired\n\t\t\tif (!path) {\n\t\t\t\treturn 'never'\n\t\t\t}\n\t\t\tconst raw = readPath(record, path)\n\t\t\tconst map = { true: 'always', false: 'never', always: 'always', never: 'never', ...(this.content?.photoRequiredValues || {}) }\n\t\t\tconst rule = raw === null || raw === undefined ? 'never' : map[String(raw)]\n\t\t\tif (rule && typeof rule === 'object' && rule.whenAnswer !== undefined) {\n\t\t\t\treturn { whenAnswer: String(rule.whenAnswer) }\n\t\t\t}\n\t\t\treturn rule === 'always' ? 'always' : 'never'\n\t\t},\n\n\t\t/**\n\t\t * Whether a question needs a photo with its current answer.\n\t\t *\n\t\t * @param {object} question The question.\n\t\t * @return {boolean} True when a photo is required now.\n\t\t */\n\t\tneedsPhoto(question) {\n\t\t\tconst rule = question.photoRule\n\t\t\tif (rule === 'always') {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn Boolean(rule && typeof rule === 'object' && String(this.draft[question.id]?.value ?? '') === rule.whenAnswer)\n\t\t},\n\n\t\t/**\n\t\t * Whether a question has a photo, stored or held.\n\t\t *\n\t\t * @param {object} question The question.\n\t\t * @return {boolean} True with at least one photo.\n\t\t */\n\t\thasPhoto(question) {\n\t\t\treturn (this.storedPhotos[question.id] || []).length > 0 || (this.photos[question.id] || []).length > 0\n\t\t},\n\n\t\t/**\n\t\t * The photo card title for a question: its position in the run.\n\t\t *\n\t\t * @param {object} question The question.\n\t\t * @return {string} The title.\n\t\t */\n\t\tphotoTitle(question) {\n\t\t\tconst n = this.questions.indexOf(question) + 1\n\t\t\treturn t('nextcloud-vue', 'Photo for question {n}', { n })\n\t\t},\n\n\t\t/**\n\t\t * The names of the photos held for a question.\n\t\t *\n\t\t * @param {object} question The question.\n\t\t * @return {string[]} The file names.\n\t\t */\n\t\tphotoNamesFor(question) {\n\t\t\treturn (this.photos[question.id] || []).map((file) => file.name)\n\t\t},\n\n\t\t/**\n\t\t * The questions on the open record (`questionsPath`): a list of\n\t\t * questions, or of groups whose `itemsKey` holds them, both levels\n\t\t * sorted by `orderField`.\n\t\t *\n\t\t * @return {object[]} The questions.\n\t\t * @spec openspec/changes/checklist-run-widget/specs/checklist-run-widget/spec.md#requirement-questions-and-answers-can-live-inside-the-open-record\n\t\t */\n\t\tquestionsFromRecord() {\n\t\t\tconst c = this.content\n\t\t\tconst list = readPath(this.openRecord, c.questionsPath)\n\t\t\tconst orderField = c.orderField === undefined ? 'order' : c.orderField\n\t\t\tconst top = byOrder(Array.isArray(list) ? list : [], orderField)\n\t\t\tif (!c.itemsKey) {\n\t\t\t\treturn top.map((record) => this.toQuestion(record))\n\t\t\t}\n\t\t\tconst labelField = c.groupLabelField || 'name'\n\t\t\treturn top.flatMap((group) => {\n\t\t\t\tconst items = readPath(group, c.itemsKey)\n\t\t\t\tconst label = readPath(group, labelField)\n\t\t\t\treturn byOrder(Array.isArray(items) ? items : [], orderField).map((record) => this.toQuestion(record, typeof label === 'string' ? this.translate(label) : ''))\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The draft of one answer entry from the open record's `answersPath`.\n\t\t *\n\t\t * @param {object} question The question.\n\t\t * @param {object} entry The stored entry.\n\t\t * @return {{value: string, note: string}} The draft.\n\t\t */\n\t\tdraftFromEntry(question, entry) {\n\t\t\tconst af = this.answerFields\n\t\t\tconst field = question.kind === 'number' ? af.number : (question.kind === 'choice' ? af.choice : af.value)\n\t\t\tconst value = readPath(entry, field)\n\t\t\tconst note = readPath(entry, af.note)\n\t\t\treturn { value: value === null || value === undefined ? '' : String(value), note: note === null || note === undefined ? '' : String(note) }\n\t\t},\n\n\t\t/**\n\t\t * Read the questions and the answers given so far.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/checklist-run-widget/specs/checklist-run-widget/spec.md#requirement-a-checklist-run-shows-a-page-of-questions-at-a-time\n\t\t */\n\t\tasync load() {\n\t\t\tconst key = this.sourceKey\n\t\t\tif (key === '') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst picker = this.recordPicker\n\t\t\t\tif (picker) {\n\t\t\t\t\tconst found = await fetchReferenceObjects({ register: picker.register, schema: picker.schema, filter: picker.filter, limit: 1 })\n\t\t\t\t\tif (key !== this.sourceKey) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tthis.pickedRecord = found[0] || null\n\t\t\t\t}\n\t\t\t\tlet questions\n\t\t\t\tif (picker && !this.pickedRecord) {\n\t\t\t\t\tquestions = []\n\t\t\t\t} else if (this.content.questionsPath) {\n\t\t\t\t\tquestions = this.questionsFromRecord()\n\t\t\t\t} else {\n\t\t\t\t\tconst q = this.content.questions\n\t\t\t\t\tconst records = await fetchReferenceObjects({ register: q.register, schema: q.schema, filter: resolveFilterTokens(q.filter || {}, this.tokenCtx), limit: q.limit || 500 })\n\t\t\t\t\tquestions = byOrder(records, q.orderField).map((r) => this.toQuestion(r))\n\t\t\t\t}\n\t\t\t\tquestions = questions.filter((x) => x.id !== '' && x.text !== '')\n\t\t\t\tconst a = this.inRecord ? null : this.content.answers\n\t\t\t\tconst answers = a && a.register && a.schema\n\t\t\t\t\t? await fetchReferenceObjects({ register: a.register, schema: a.schema, filter: resolveFilterTokens(a.filter || {}, this.tokenCtx), limit: a.limit || 500 })\n\t\t\t\t\t: []\n\t\t\t\tif (key !== this.sourceKey) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst draft = {}\n\t\t\t\tconst answerIds = {}\n\t\t\t\tconst storedPhotos = {}\n\t\t\t\tfor (const question of questions) {\n\t\t\t\t\tdraft[question.id] = { value: '', note: '' }\n\t\t\t\t}\n\t\t\t\tif (this.inRecord) {\n\t\t\t\t\tconst entries = readPath(this.openRecord, this.content.answersPath)\n\t\t\t\t\tthis.recordAnswers = Array.isArray(entries) ? entries : []\n\t\t\t\t\tconst key = this.content.answerKey || 'questionId'\n\t\t\t\t\tfor (const entry of this.recordAnswers) {\n\t\t\t\t\t\tconst qid = String(readPath(entry, key) ?? '')\n\t\t\t\t\t\tconst question = questions.find((x) => x.id === qid)\n\t\t\t\t\t\tif (question && !answerIds[qid]) {\n\t\t\t\t\t\t\tdraft[qid] = this.draftFromEntry(question, entry)\n\t\t\t\t\t\t\tanswerIds[qid] = qid\n\t\t\t\t\t\t\tconst photos = readPath(entry, this.answerFields.photos)\n\t\t\t\t\t\t\tstoredPhotos[qid] = Array.isArray(photos) ? photos : []\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfor (const answer of answers) {\n\t\t\t\t\tconst qid = String(readPath(answer, a.questionField || 'question') ?? '')\n\t\t\t\t\tif (draft[qid] && !answerIds[qid]) {\n\t\t\t\t\t\tdraft[qid] = {\n\t\t\t\t\t\t\tvalue: String(readPath(answer, a.valueField || 'answer') ?? ''),\n\t\t\t\t\t\t\tnote: String(readPath(answer, a.noteField || 'note') ?? ''),\n\t\t\t\t\t\t}\n\t\t\t\t\t\tanswerIds[qid] = idOf(answer)\n\t\t\t\t\t\tconst stored = a.photosField ? readPath(answer, a.photosField) : null\n\t\t\t\t\t\tstoredPhotos[qid] = Array.isArray(stored) ? stored : []\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.questions = questions\n\t\t\t\tthis.draft = draft\n\t\t\t\tthis.answerIds = answerIds\n\t\t\t\tthis.storedPhotos = storedPhotos\n\t\t\t\tthis.dirty = {}\n\t\t\t\tconst lastPage = Math.max(0, Math.ceil(questions.length / this.pageSize) - 1)\n\t\t\t\tif (this.positionedFor !== key && this.content?.startAt === 'firstOpen') {\n\t\t\t\t\tconst open = questions.findIndex((question) => !this.isAnswered(question))\n\t\t\t\t\tthis.page = open === -1 ? lastPage : Math.floor(open / this.pageSize)\n\t\t\t\t}\n\t\t\t\tthis.positionedFor = key\n\t\t\t\tthis.page = Math.min(this.page, lastPage)\n\t\t\t} catch {\n\t\t\t\tthis.error = t('nextcloud-vue', 'The checklist could not be loaded.')\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Note that a question's answer changed, so the page saves it.\n\t\t *\n\t\t * @param {string} id The question id.\n\t\t * @return {void}\n\t\t */\n\t\tmarkDirty(id) {\n\t\t\tthis.dirty = { ...this.dirty, [id]: true }\n\t\t\tthis.missing = this.missing.filter((m) => m !== id)\n\t\t},\n\n\t\t/**\n\t\t * Hold the photos picked for a question until the page saves.\n\t\t *\n\t\t * @param {Event} event The file input's change event.\n\t\t * @param {string} [questionId] The question, else the first of the page.\n\t\t * @return {void}\n\t\t */\n\t\taddPhotos(event, questionId) {\n\t\t\tconst id = questionId || this.currentQuestion?.id\n\t\t\tconst files = Array.from(event?.target?.files || [])\n\t\t\tif (!id || files.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.photos = { ...this.photos, [id]: [...(this.photos[id] || []), ...files] }\n\t\t\tthis.dirty = { ...this.dirty, [id]: true }\n\t\t\tthis.missingPhoto = this.missingPhoto.filter((m) => m !== id)\n\t\t\tif (event.target) {\n\t\t\t\tevent.target.value = ''\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Save the changed answers of this page: through `saveAction` when\n\t\t * set, else one answer record per question through the object API,\n\t\t * then its held photos.\n\t\t *\n\t\t * @return {Promise<boolean>} True when everything saved.\n\t\t * @spec openspec/changes/checklist-run-widget/specs/checklist-run-widget/spec.md#requirement-answers-are-saved-under-the-readers-own-rights\n\t\t */\n\t\tasync savePage() {\n\t\t\tconst changed = this.pageQuestions.filter((q) => this.dirty[q.id])\n\t\t\tif (changed.length === 0) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tthis.saving = true\n\t\t\tthis.saveError = ''\n\t\t\ttry {\n\t\t\t\tif (this.inRecord && !this.content?.saveAction) {\n\t\t\t\t\tawait this.saveIntoRecord(changed)\n\t\t\t\t\tthis.dirty = Object.fromEntries(Object.entries(this.dirty).filter(([id]) => !changed.some((q) => q.id === id)))\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\tfor (const question of changed) {\n\t\t\t\t\tawait this.saveAnswer(question)\n\t\t\t\t\tconst { [question.id]: _done, ...rest } = this.dirty\n\t\t\t\t\tthis.dirty = rest\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t} catch {\n\t\t\t\tthis.saveError = t('nextcloud-vue', 'The answer could not be saved. Try again.')\n\t\t\t\treturn false\n\t\t\t} finally {\n\t\t\t\tthis.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Write the changed answers of this page into the open record's\n\t\t * `answersPath` array (one entry per question, keyed by `answerKey`)\n\t\t * with one update of the record through the object API. Held photos\n\t\t * go first to the record's own files (`recordPhotosField`), and their\n\t\t * references (`photoRefField`, default `id`) into the answer.\n\t\t *\n\t\t * @param {object[]} changed The changed questions.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/checklist-run-widget/specs/checklist-run-widget/spec.md#requirement-questions-and-answers-can-live-inside-the-open-record\n\t\t */\n\t\tasync saveIntoRecord(changed) {\n\t\t\tconst { register, schema } = this.recordRef\n\t\t\tconst objectId = idOf(this.openRecord) || String(this.openRecordId || '')\n\t\t\tif (!register || !schema || !objectId) {\n\t\t\t\tthrow new Error('no record to write to')\n\t\t\t}\n\t\t\tconst c = this.content\n\t\t\tconst af = this.answerFields\n\t\t\tconst key = c.answerKey || 'questionId'\n\t\t\tconst refField = c.photoRefField || 'id'\n\t\t\tlet recordPhotos = Array.isArray(readPath(this.openRecord, c.recordPhotosField)) ? readPath(this.openRecord, c.recordPhotosField) : []\n\t\t\tconst entries = [...this.recordAnswers]\n\t\t\tfor (const question of changed) {\n\t\t\t\tconst draft = this.draft[question.id]\n\t\t\t\tconst index = entries.findIndex((entry) => String(readPath(entry, key) ?? '') === question.id)\n\t\t\t\tconst entry = { ...(index >= 0 ? entries[index] : { [key]: question.id }) }\n\t\t\t\tif (question.kind === 'number') {\n\t\t\t\t\tentry[af.number] = draft.value === '' ? null : Number(draft.value)\n\t\t\t\t} else if (question.kind === 'choice') {\n\t\t\t\t\tentry[af.choice] = draft.value\n\t\t\t\t} else if (question.kind !== 'photo') {\n\t\t\t\t\tentry[af.value] = draft.value\n\t\t\t\t}\n\t\t\t\tif (this.allowNote) {\n\t\t\t\t\tentry[af.note] = draft.note\n\t\t\t\t}\n\t\t\t\tif (af.at) {\n\t\t\t\t\tentry[af.at] = new Date().toISOString()\n\t\t\t\t}\n\t\t\t\tconst held = this.photos[question.id] || []\n\t\t\t\tif (this.photosEnabled && held.length) {\n\t\t\t\t\tconst field = c.recordPhotosField\n\t\t\t\t\tconst { failed, attached } = await uploadHeldFiles({ register, schema, objectId, held: { [field]: held }, kept: { [field]: recordPhotos } })\n\t\t\t\t\tif (failed.length) {\n\t\t\t\t\t\tthrow new Error('photo upload failed')\n\t\t\t\t\t}\n\t\t\t\t\tconst now = Array.isArray(attached[field]) ? attached[field] : recordPhotos\n\t\t\t\t\tconst added = now.slice(recordPhotos.length).map((ref) => (ref && typeof ref === 'object' ? ref[refField] ?? ref : ref))\n\t\t\t\t\trecordPhotos = now\n\t\t\t\t\tconst existing = Array.isArray(entry[af.photos]) ? entry[af.photos] : []\n\t\t\t\t\tentry[af.photos] = [...existing, ...added]\n\t\t\t\t\tthis.storedPhotos = { ...this.storedPhotos, [question.id]: entry[af.photos] }\n\t\t\t\t\tconst { [question.id]: _sent, ...rest } = this.photos\n\t\t\t\t\tthis.photos = rest\n\t\t\t\t}\n\t\t\t\tif (index >= 0) {\n\t\t\t\t\tentries[index] = entry\n\t\t\t\t} else {\n\t\t\t\t\tentries.push(entry)\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst url = generateUrl(`${OBJECT_API}/${encodeURIComponent(register)}/${encodeURIComponent(schema)}/${encodeURIComponent(objectId)}`)\n\t\t\tawait axios.patch(url, { [c.answersPath]: entries })\n\t\t\tthis.recordAnswers = entries\n\t\t},\n\n\t\t/**\n\t\t * Save one answer.\n\t\t *\n\t\t * @param {object} question The question.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync saveAnswer(question) {\n\t\t\tconst entry = this.draft[question.id]\n\t\t\tconst answer = { questionId: question.id, value: entry.value, note: entry.note }\n\t\t\tconst action = this.content?.saveAction\n\t\t\tif (action && typeof action === 'object') {\n\t\t\t\tconst resolved = resolveDeepTokens(action, this.tokenCtx)\n\t\t\t\tconst wrapped = { ...resolved, args: [...(Array.isArray(resolved.args) ? resolved.args : []), answer] }\n\t\t\t\tconst run = typeof this.cnDispatchAction === 'function' ? this.cnDispatchAction(wrapped) : dispatchAction(wrapped, { router: this.$router || null, tokenCtx: this.tokenCtx })\n\t\t\t\tawait run\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst a = this.content?.answers\n\t\t\tif (!a || !a.register || !a.schema) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst body = {\n\t\t\t\t[a.questionField || 'question']: question.id,\n\t\t\t\t[a.valueField || 'answer']: entry.value,\n\t\t\t}\n\t\t\tif (this.allowNote) {\n\t\t\t\tbody[a.noteField || 'note'] = entry.note\n\t\t\t}\n\t\t\tconst base = generateUrl(`${OBJECT_API}/${encodeURIComponent(a.register)}/${encodeURIComponent(a.schema)}`)\n\t\t\tlet id = this.answerIds[question.id]\n\t\t\tif (id) {\n\t\t\t\tawait axios.patch(`${base}/${encodeURIComponent(id)}`, body)\n\t\t\t} else {\n\t\t\t\tconst create = resolveDeepTokens(a.create && typeof a.create === 'object' ? a.create : {}, this.tokenCtx)\n\t\t\t\tconst response = await axios.post(base, { ...create, ...body })\n\t\t\t\tid = idOf(response?.data)\n\t\t\t\tthis.answerIds = { ...this.answerIds, [question.id]: id }\n\t\t\t}\n\t\t\tconst held = this.photos[question.id] || []\n\t\t\tif (this.photosEnabled && held.length && id) {\n\t\t\t\tconst { failed, attached } = await uploadHeldFiles({ register: a.register, schema: a.schema, objectId: id, held: { [a.photosField]: held }, kept: { [a.photosField]: this.storedPhotos[question.id] || [] } })\n\t\t\t\tif (failed.length) {\n\t\t\t\t\tthrow new Error('photo upload failed')\n\t\t\t\t}\n\t\t\t\tif (Array.isArray(attached[a.photosField])) {\n\t\t\t\t\tthis.storedPhotos = { ...this.storedPhotos, [question.id]: attached[a.photosField] }\n\t\t\t\t}\n\t\t\t\tconst { [question.id]: _sent, ...rest } = this.photos\n\t\t\t\tthis.photos = rest\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The required questions on this page without an answer.\n\t\t *\n\t\t * @return {string[]} Their ids.\n\t\t */\n\t\tunansweredRequired() {\n\t\t\treturn this.pageQuestions.filter((q) => q.required && !this.isAnswered(q)).map((q) => q.id)\n\t\t},\n\n\t\t/**\n\t\t * Save this page and go to the next one; on the last page, finish.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/checklist-run-widget/specs/checklist-run-widget/spec.md#requirement-a-checklist-run-shows-a-page-of-questions-at-a-time\n\t\t */\n\t\tasync next() {\n\t\t\tthis.missing = this.unansweredRequired()\n\t\t\tthis.missingPhoto = this.photosEnabled ? this.pageQuestions.filter((q) => this.needsPhoto(q) && !this.hasPhoto(q)).map((q) => q.id) : []\n\t\t\tif (this.missing.length || this.missingPhoto.length) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!(await this.savePage())) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.isLastPage) {\n\t\t\t\tawait this.finish()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.goTo(this.page + 1)\n\t\t},\n\n\t\t/**\n\t\t * Save this page and go back one.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync previous() {\n\t\t\tif (this.page === 0 || !(await this.savePage())) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.missing = []\n\t\t\tthis.missingPhoto = []\n\t\t\tthis.goTo(this.page - 1)\n\t\t},\n\n\t\t/**\n\t\t * Show a page, move focus to its position line and announce it.\n\t\t *\n\t\t * @param {number} page The page.\n\t\t * @return {void}\n\t\t */\n\t\tgoTo(page) {\n\t\t\tthis.page = page\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tthis.liveText = this.positionText\n\t\t\t\tthis.$refs.position?.focus?.()\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The run is done: dispatch `finishAction` when set, and emit `finished`.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync finish() {\n\t\t\tconst action = this.content?.finishAction\n\t\t\tif (action && typeof action === 'object') {\n\t\t\t\tconst resolved = resolveDeepTokens(action, this.tokenCtx)\n\t\t\t\tconst run = typeof this.cnDispatchAction === 'function' ? this.cnDispatchAction(resolved) : dispatchAction(resolved, { router: this.$router || null, tokenCtx: this.tokenCtx })\n\t\t\t\tawait run\n\t\t\t}\n\t\t\tthis.liveText = t('nextcloud-vue', 'Checklist finished.')\n\t\t\t/**\n\t\t\t * @event finished The last page was saved.\n\t\t\t * @type {{answered: number, total: number}}\n\t\t\t */\n\t\t\tthis.$emit('finished', { answered: this.answeredCount, total: this.questions.length })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-checklist-run__form {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-checklist-run__position {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0;\n}\n\n.cn-checklist-run__progress {\n\tbackground: var(--color-border);\n\tborder-radius: 3px;\n\tdisplay: block;\n\theight: 6px;\n\toverflow: hidden;\n}\n\n.cn-checklist-run__progress-fill {\n\tbackground: var(--color-primary-element);\n\tdisplay: block;\n\theight: 100%;\n}\n\n.cn-checklist-run__live,\n.cn-checklist-run__sr {\n\tclip-path: inset(50%);\n\theight: 1px;\n\toverflow: hidden;\n\tposition: absolute;\n\twhite-space: nowrap;\n\twidth: 1px;\n}\n\n.cn-checklist-run__question,\n.cn-checklist-run__photos {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 12px);\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmargin: 0;\n\tpadding: 12px 14px;\n}\n\n.cn-checklist-run__legend {\n\tfont-weight: 600;\n\tpadding: 0 4px;\n}\n\n.cn-checklist-run__help {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0;\n}\n\n.cn-checklist-run__choice {\n\talign-items: center;\n\tdisplay: flex;\n\tgap: 10px;\n\tmin-height: 34px;\n}\n\n.cn-checklist-run__choice input {\n\taccent-color: var(--color-primary-element);\n\theight: 20px;\n\tmargin: 0;\n\twidth: 20px;\n}\n\n.cn-checklist-run__text,\n.cn-checklist-run__number {\n\tbox-sizing: border-box;\n\twidth: 100%;\n}\n\n.cn-checklist-run__group {\n\tfont-weight: 600;\n\tmargin: 0;\n}\n\n.cn-checklist-run__note-label {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-checklist-run__photo-list {\n\tmargin: 0;\n\tpadding-inline-start: 18px;\n}\n\n.cn-checklist-run__photo-button {\n\talign-items: center;\n\talign-self: flex-start;\n\tborder: 1px solid var(--color-border-dark);\n\tborder-radius: var(--border-radius-element, 8px);\n\tcursor: pointer;\n\tdisplay: inline-flex;\n\tfont-weight: 600;\n\tmin-height: 34px;\n\tpadding: 0 14px;\n}\n\n.cn-checklist-run__photo-button:focus-within {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-checklist-run__photo-input {\n\tclip-path: inset(50%);\n\theight: 1px;\n\toverflow: hidden;\n\tposition: absolute;\n\twidth: 1px;\n}\n\n.cn-checklist-run__nav {\n\tdisplay: flex;\n\tjustify-content: space-between;\n}\n\n.cn-checklist-run__error {\n\tcolor: var(--color-error-text, var(--color-error));\n\tmargin: 0;\n}\n\n.cn-checklist-run__empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_toDisplayString","_createVNode","_createElementVNode","_Fragment","_renderList","_createTextVNode","_vModelRadio","_vModelText"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnChecklistRunWidget.vue3.js","sources":["../../../../src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue?vue&type=template&id=dd87442e&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div class=\"cn-checklist-run\" data-testid=\"cn-checklist-run\">\n\t\t<p v-if=\"error\"\n\t\t\tclass=\"cn-checklist-run__error\"\n\t\t\trole=\"alert\"\n\t\t\tdata-testid=\"cn-checklist-run-error\">\n\t\t\t{{ error }}\n\t\t</p>\n\t\t<div v-else-if=\"loading\" class=\"cn-checklist-run__loading\">\n\t\t\t<NcLoadingIcon :size=\"20\" :name=\"loadingLabel\" />\n\t\t</div>\n\t\t<p v-else-if=\"questions.length === 0\" class=\"cn-checklist-run__empty\" data-testid=\"cn-checklist-run-empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\t\t<form v-else\n\t\t\tclass=\"cn-checklist-run__form\"\n\t\t\tnovalidate\n\t\t\t@submit.prevent=\"next\">\n\t\t\t<p\n\t\t\t\tref=\"position\"\n\t\t\t\tclass=\"cn-checklist-run__position\"\n\t\t\t\ttabindex=\"-1\"\n\t\t\t\tdata-testid=\"cn-checklist-run-position\">\n\t\t\t\t{{ positionText }}\n\t\t\t</p>\n\t\t\t<span\n\t\t\t\tclass=\"cn-checklist-run__progress\"\n\t\t\t\trole=\"progressbar\"\n\t\t\t\t:aria-label=\"progressLabel\"\n\t\t\t\taria-valuemin=\"0\"\n\t\t\t\t:aria-valuemax=\"questions.length\"\n\t\t\t\t:aria-valuenow=\"answeredCount\"\n\t\t\t\tdata-testid=\"cn-checklist-run-progress\">\n\t\t\t\t<span class=\"cn-checklist-run__progress-fill\" :style=\"{ width: `${progressShare}%` }\" />\n\t\t\t</span>\n\t\t\t<span class=\"cn-checklist-run__live\" aria-live=\"polite\">{{ liveText }}</span>\n\n\t\t\t<p v-if=\"pageGroup\" class=\"cn-checklist-run__group\" data-testid=\"cn-checklist-run-group\">\n\t\t\t\t{{ pageGroup }}\n\t\t\t</p>\n\t\t\t<fieldset\n\t\t\t\tv-for=\"question in pageQuestions\"\n\t\t\t\t:key=\"question.id\"\n\t\t\t\tclass=\"cn-checklist-run__question\"\n\t\t\t\t:aria-describedby=\"question.help ? `${uid}-help-${question.id}` : null\"\n\t\t\t\tdata-testid=\"cn-checklist-run-question\">\n\t\t\t\t<legend class=\"cn-checklist-run__legend\">\n\t\t\t\t\t{{ question.text }}<span v-if=\"question.required\" class=\"cn-checklist-run__required\">\n\t\t\t\t\t\t<span aria-hidden=\"true\"> *</span><span class=\"cn-checklist-run__sr\">{{ requiredLabel }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t</legend>\n\t\t\t\t<p v-if=\"question.help\" :id=\"`${uid}-help-${question.id}`\" class=\"cn-checklist-run__help\">\n\t\t\t\t\t{{ question.help }}\n\t\t\t\t</p>\n\t\t\t\t<template v-if=\"question.choices.length\">\n\t\t\t\t\t<label\n\t\t\t\t\t\tv-for=\"choice in question.choices\"\n\t\t\t\t\t\t:key=\"choice.value\"\n\t\t\t\t\t\tclass=\"cn-checklist-run__choice\">\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\tv-model=\"draft[question.id].value\"\n\t\t\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t\t\t:name=\"`${uid}-${question.id}`\"\n\t\t\t\t\t\t\t:value=\"choice.value\"\n\t\t\t\t\t\t\t:required=\"question.required\"\n\t\t\t\t\t\t\tdata-testid=\"cn-checklist-run-choice\"\n\t\t\t\t\t\t\t@change=\"markDirty(question.id)\">\n\t\t\t\t\t\t{{ choice.label }}\n\t\t\t\t\t</label>\n\t\t\t\t</template>\n\t\t\t\t<input\n\t\t\t\t\tv-else-if=\"question.kind === 'number'\"\n\t\t\t\t\tv-model=\"draft[question.id].value\"\n\t\t\t\t\tclass=\"cn-checklist-run__number\"\n\t\t\t\t\ttype=\"number\"\n\t\t\t\t\tinputmode=\"decimal\"\n\t\t\t\t\t:aria-label=\"question.text\"\n\t\t\t\t\t:required=\"question.required\"\n\t\t\t\t\tdata-testid=\"cn-checklist-run-number\"\n\t\t\t\t\t@input=\"markDirty(question.id)\">\n\t\t\t\t<p v-else-if=\"question.kind === 'photo'\" class=\"cn-checklist-run__help\">\n\t\t\t\t\t{{ photoOnlyLabel }}\n\t\t\t\t</p>\n\t\t\t\t<textarea\n\t\t\t\t\tv-else\n\t\t\t\t\tv-model=\"draft[question.id].value\"\n\t\t\t\t\tclass=\"cn-checklist-run__text\"\n\t\t\t\t\trows=\"3\"\n\t\t\t\t\t:aria-label=\"question.text\"\n\t\t\t\t\t:required=\"question.required\"\n\t\t\t\t\tdata-testid=\"cn-checklist-run-text\"\n\t\t\t\t\t@input=\"markDirty(question.id)\" />\n\t\t\t\t<label v-if=\"allowNote\" class=\"cn-checklist-run__note-label\">\n\t\t\t\t\t{{ noteLabel }}\n\t\t\t\t\t<textarea\n\t\t\t\t\t\tv-model=\"draft[question.id].note\"\n\t\t\t\t\t\tclass=\"cn-checklist-run__text\"\n\t\t\t\t\t\trows=\"2\"\n\t\t\t\t\t\tdata-testid=\"cn-checklist-run-note\"\n\t\t\t\t\t\t@input=\"markDirty(question.id)\" />\n\t\t\t\t</label>\n\t\t\t\t<p\n\t\t\t\t\tv-if=\"missing.includes(question.id)\"\n\t\t\t\t\tclass=\"cn-checklist-run__error\"\n\t\t\t\t\trole=\"alert\"\n\t\t\t\t\tdata-testid=\"cn-checklist-run-missing\">\n\t\t\t\t\t{{ missingLabel }}\n\t\t\t\t</p>\n\t\t\t</fieldset>\n\n\t\t\t<div\n\t\t\t\tv-for=\"question in photoQuestions\"\n\t\t\t\t:key=\"`photos-${question.id}`\"\n\t\t\t\tclass=\"cn-checklist-run__photos\"\n\t\t\t\tdata-testid=\"cn-checklist-run-photos\">\n\t\t\t\t<strong class=\"cn-checklist-run__photos-title\">{{ photoTitle(question) }}</strong>\n\t\t\t\t<ul v-if=\"photoNamesFor(question).length\" class=\"cn-checklist-run__photo-list\">\n\t\t\t\t\t<li v-for=\"name in photoNamesFor(question)\" :key=\"name\" data-testid=\"cn-checklist-run-photo\">\n\t\t\t\t\t\t{{ name }}\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t\t<label class=\"cn-checklist-run__photo-button\">\n\t\t\t\t\t<input\n\t\t\t\t\t\tclass=\"cn-checklist-run__photo-input\"\n\t\t\t\t\t\ttype=\"file\"\n\t\t\t\t\t\taccept=\"image/*\"\n\t\t\t\t\t\tcapture=\"environment\"\n\t\t\t\t\t\t:aria-describedby=\"missingPhoto.includes(question.id) ? `${uid}-photo-${question.id}` : null\"\n\t\t\t\t\t\tdata-testid=\"cn-checklist-run-photo-input\"\n\t\t\t\t\t\t@change=\"addPhotos($event, question.id)\">\n\t\t\t\t\t{{ photoNamesFor(question).length || (storedPhotos[question.id] || []).length ? morePhotoLabel : addPhotoLabel }}\n\t\t\t\t</label>\n\t\t\t\t<p\n\t\t\t\t\tv-if=\"missingPhoto.includes(question.id)\"\n\t\t\t\t\t:id=\"`${uid}-photo-${question.id}`\"\n\t\t\t\t\tclass=\"cn-checklist-run__error\"\n\t\t\t\t\trole=\"alert\"\n\t\t\t\t\tdata-testid=\"cn-checklist-run-missing-photo\">\n\t\t\t\t\t{{ missingPhotoLabel }}\n\t\t\t\t</p>\n\t\t\t</div>\n\n\t\t\t<div class=\"cn-checklist-run__nav\">\n\t\t\t\t<NcButton\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t:disabled=\"page === 0 || saving\"\n\t\t\t\t\tdata-testid=\"cn-checklist-run-previous\"\n\t\t\t\t\t@click=\"previous\">\n\t\t\t\t\t{{ previousLabel }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton\n\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t:disabled=\"saving\"\n\t\t\t\t\tdata-testid=\"cn-checklist-run-next\">\n\t\t\t\t\t{{ isLastPage ? finishLabel : nextLabel }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t\t<p v-if=\"saveError\"\n\t\t\t\tclass=\"cn-checklist-run__error\"\n\t\t\t\trole=\"alert\"\n\t\t\t\tdata-testid=\"cn-checklist-run-save-error\">\n\t\t\t\t{{ saveError }}\n\t\t\t</p>\n\t\t</form>\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton, NcLoadingIcon } from '@nextcloud/vue'\nimport { uploadHeldFiles } from '../../composables/useHeldFileUpload.js'\nimport { dispatchAction } from '../../utils/actionsDispatcher.js'\nimport { fetchReferenceObjects } from '../../utils/boardReferenceColumns.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { hasUnresolvedTokens, resolveDeepTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'\n\nconst OBJECT_API = '/apps/openregister/api/objects'\n\nlet instanceCounter = 0\n\n/**\n * The choices a question type offers, before translation.\n *\n * @type {Record<string, Array<{value: string, label: string}>>}\n */\nconst TYPE_CHOICES = Object.freeze({\n\tyes_no: [{ key: 'yes', label: 'Yes' }, { key: 'no', label: 'No' }],\n\tyes_no_na: [{ key: 'yes', label: 'Yes' }, { key: 'no', label: 'No' }, { key: 'na', label: 'Not applicable' }],\n})\n\n/** The kinds of question the widget draws. */\nconst KINDS = Object.freeze(['yes_no', 'yes_no_na', 'choice', 'text', 'number', 'photo'])\n\n/**\n * Sort by a numeric field, lowest first, keeping the given order for ties and\n * for entries without the field.\n *\n * @param {Array<object>} list The entries.\n * @param {string} field The field.\n * @return {Array<object>} A sorted copy.\n */\nfunction byOrder(list, field) {\n\tif (!field) {\n\t\treturn [...list]\n\t}\n\tconst rank = (entry) => {\n\t\tconst n = Number(readPath(entry, field))\n\t\treturn Number.isFinite(n) ? n : Infinity\n\t}\n\treturn list.map((entry, index) => ({ entry, index })).sort((a, b) => (rank(a.entry) - rank(b.entry)) || (a.index - b.index)).map(({ entry }) => entry)\n}\n\n/**\n * A register or schema as the object API path takes it.\n *\n * @param {string|object} value A slug, or a register/schema object.\n * @return {string} The slug or id.\n */\nfunction slugOf(value) {\n\tif (value && typeof value === 'object') {\n\t\treturn String(value.slug ?? value.id ?? '')\n\t}\n\treturn value ? String(value) : ''\n}\n\n/**\n * The id of an object, wherever OpenRegister carries it.\n *\n * @param {object|null} object The object.\n * @return {string} The id, or ''.\n */\nfunction idOf(object) {\n\treturn String(object?.id ?? object?.['@self']?.id ?? object?.uuid ?? '')\n}\n\n/**\n * CnChecklistRunWidget — the `checklist-run` widget: a checklist answered a\n * page of questions at a time (board DqInspectie, the middle phone frame).\n *\n * The questions are records of a register, listed with a token-resolved\n * filter. Each page shows `pageSize` questions (one by default) as radio\n * choices or a text field, an optional note, and photos for the first\n * question of the page, then Previous and Next. The answers of a page are\n * saved when the user moves on.\n *\n * Answers are stored one record per question through the OpenRegister object\n * API, under the reader's own rights: an existing answer is updated, a new\n * one created with the `answers.create` fields. Photos go up through the\n * object's own file upload (`uploadHeldFiles`) and are attached to the\n * answer's `photosField`. An app that stores answers its own way names a\n * `saveAction` instead; it is dispatched per answer with the answer as its\n * last argument, and photos are not uploaded then.\n *\n * ```json\n * {\n * \"type\": \"checklist-run\",\n * \"content\": {\n * \"questions\": { \"register\": \"app\", \"schema\": \"question\", \"filter\": { \"checklist\": \"@object.checklist\" }, \"orderField\": \"position\" },\n * \"fields\": { \"text\": \"text\", \"type\": \"type\", \"options\": \"options\", \"required\": \"required\", \"help\": \"help\" },\n * \"answers\": {\n * \"register\": \"app\", \"schema\": \"answer\", \"filter\": { \"run\": \"@objectId\" },\n * \"questionField\": \"question\", \"valueField\": \"answer\", \"noteField\": \"note\", \"photosField\": \"photos\",\n * \"create\": { \"run\": \"@objectId\" }\n * },\n * \"pageSize\": 1,\n * \"caption\": \"{title} version {version}\"\n * }\n * }\n * ```\n *\n * The questions and answers can also live inside the open record:\n * `questionsPath` (with `itemsKey` for groups, both sorted by `orderField`)\n * reads the questions off it, and `answersPath` writes the answers into an\n * array on it, one entry per question keyed by `answerKey`, by updating the\n * record through the object API. Photos then go to the record's own files\n * (`recordPhotosField`) and their references into the answer.\n *\n * A question can require a photo: `fields.photoRequired` names its rule,\n * `photoRequiredValues` maps the stored values onto `never`, `always` or\n * `{ whenAnswer }`, and the page holds until the photo is there. On a page\n * with no bound record, `record: { register, schema, filter, sort }` picks\n * the open record: the first match, under the reader's rights.\n *\n * `startAt: \"firstOpen\"` opens a run that was started earlier at the page of\n * its first unanswered question (the last page when all are answered), once\n * per record; without it the run opens at the first page.\n *\n * Question kinds: `yes_no`, `yes_no_na`, `choice` (the question's `options`,\n * a list of strings or `{ value, label }`), `number`, `photo` and `text`.\n * `typeMap` maps the app's type values onto them, `choiceValues` the stored\n * values of yes / no / not applicable.\n * Keyboard: the choices are native radio buttons in a fieldset whose legend is\n * the question; on a new page focus moves to the position line and a polite\n * live region reads it.\n */\nexport default {\n\tname: 'CnChecklistRunWidget',\n\n\tcomponents: { NcButton, NcLoadingIcon },\n\n\tinject: {\n\t\t/** The detail page's object context (`{ objectId, object }`), for tokens. */\n\t\tcnObjectContext: { default: null },\n\t\t/** The page's workspace values, for `@workspace.<key>` tokens. */\n\t\tcnWorkspaceContext: { default: null },\n\t\t/** The host's translate, for manifest texts and question texts. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** The page's pre-bound action dispatcher, for `saveAction` / `finishAction`. */\n\t\tcnDispatchAction: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget config: `{ questions | questionsPath, itemsKey?,\n\t\t * groupLabelField?, orderField?, fields?, typeMap?, choiceValues?,\n\t\t * answers | answersPath, answerKey?, answerFields?, recordPhotosField?,\n\t\t * record?, saveAction?, finishAction?, pageSize?, allowNote?,\n\t\t * allowPhotos?, caption?, emptyText? }`, as in the component\n\t\t * description and docs/components/cn-checklist-run-widget.md.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The bound record's id, for `@objectId`. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The bound record, for `@object.<field>`, the caption and `questionsPath` / `answersPath`. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The bound record's register, for writing `answersPath` (`content.record` wins). */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The bound record's schema, for writing `answersPath` (`content.record` wins). */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['finished'],\n\n\tdata() {\n\t\tinstanceCounter += 1\n\t\treturn {\n\t\t\tuid: `cn-checklist-run-${instanceCounter}`,\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t\tsaving: false,\n\t\t\tsaveError: '',\n\t\t\tquestions: [],\n\t\t\tanswerIds: {},\n\t\t\t// Photos an answer already holds, kept when new ones are attached.\n\t\t\tstoredPhotos: {},\n\t\t\t// `answersPath` mode: the answer entries as last read or written.\n\t\t\trecordAnswers: [],\n\t\t\tdraft: {},\n\t\t\tdirty: {},\n\t\t\tphotos: {},\n\t\t\tpage: 0,\n\t\t\t// The source the page was last positioned for; `startAt` applies once per source.\n\t\t\tpositionedFor: '',\n\t\t\tmissing: [],\n\t\t\t// Questions on this page that still need their photo.\n\t\t\tmissingPhoto: [],\n\t\t\t// `content.record` with a filter: the record it picked, or null.\n\t\t\tpickedRecord: null,\n\t\t\tliveText: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {object} What the tokens resolve against. */\n\t\ttokenCtx() {\n\t\t\tconst unwrap = (c) => (c && typeof c === 'object' && 'value' in c ? c.value : c)\n\t\t\tconst injected = unwrap(this.cnObjectContext) || {}\n\t\t\treturn {\n\t\t\t\t...injected,\n\t\t\t\tobjectId: (this.pickedRecord && idOf(this.pickedRecord)) || this.objectId || injected.objectId || this.objectData?.id || null,\n\t\t\t\tobject: this.pickedRecord || this.objectData || injected.object || null,\n\t\t\t\tworkspace: unwrap(this.cnWorkspaceContext) || {},\n\t\t\t}\n\t\t},\n\n\t\t/** @return {object} The question field paths, with their defaults. */\n\t\tfields() {\n\t\t\treturn { id: 'id', text: 'text', type: 'type', options: 'options', required: 'required', help: 'help', ...(this.content?.fields || {}) }\n\t\t},\n\n\t\t/** @return {number} Questions per page, at least 1. */\n\t\tpageSize() {\n\t\t\tconst n = Math.floor(Number(this.content?.pageSize))\n\t\t\treturn Number.isFinite(n) && n >= 1 ? n : 1\n\t\t},\n\n\t\t/** @return {number} The number of pages. */\n\t\tpageCount() {\n\t\t\treturn Math.max(1, Math.ceil(this.questions.length / this.pageSize))\n\t\t},\n\n\t\t/** @return {boolean} Whether this is the last page. */\n\t\tisLastPage() {\n\t\t\treturn this.page >= this.pageCount - 1\n\t\t},\n\n\t\t/** @return {object[]} The questions on this page. */\n\t\tpageQuestions() {\n\t\t\treturn this.questions.slice(this.page * this.pageSize, (this.page + 1) * this.pageSize)\n\t\t},\n\n\t\t/** @return {object|null} The first question on this page, the one photos belong to. */\n\t\tcurrentQuestion() {\n\t\t\treturn this.pageQuestions[0] || null\n\t\t},\n\n\t\t/** @return {number} Questions with an answer. */\n\t\tansweredCount() {\n\t\t\treturn this.questions.filter((q) => this.isAnswered(q)).length\n\t\t},\n\n\t\t/** @return {number} The share answered, 0 to 100. */\n\t\tprogressShare() {\n\t\t\treturn this.questions.length ? Math.round((this.answeredCount / this.questions.length) * 100) : 0\n\t\t},\n\n\t\t/** @return {string} \"Question 7 of 24\", with the caption after it. */\n\t\tpositionText() {\n\t\t\tconst first = this.page * this.pageSize + 1\n\t\t\tconst position = t('nextcloud-vue', 'Question {n} of {total}', { n: first, total: this.questions.length })\n\t\t\tconst caption = this.caption\n\t\t\treturn caption ? `${position} · ${caption}` : position\n\t\t},\n\n\t\t/** @return {string} `content.caption` with `{field}` filled from the record. */\n\t\tcaption() {\n\t\t\tconst template = this.content?.caption\n\t\t\tif (typeof template !== 'string' || template === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst object = this.tokenCtx.object || {}\n\t\t\treturn this.translate(template).replace(/\\{([A-Za-z0-9_.]+)\\}/g, (_m, path) => {\n\t\t\t\tconst value = readPath(object, path)\n\t\t\t\treturn value === null || value === undefined ? '' : String(value)\n\t\t\t}).trim()\n\t\t},\n\n\t\t/** @return {boolean} Whether a note field shows (default true). */\n\t\tallowNote() {\n\t\t\treturn this.content?.allowNote !== false\n\t\t},\n\n\t\t/** @return {boolean} Whether the answers are written into an array on the open record. */\n\t\tinRecord() {\n\t\t\treturn typeof this.content?.answersPath === 'string' && this.content.answersPath !== ''\n\t\t},\n\n\t\t/** @return {object} `answersPath` field names, with their defaults. */\n\t\tanswerFields() {\n\t\t\treturn { value: 'value', number: 'value', choice: 'value', note: 'note', photos: 'photos', ...(this.content?.answerFields || {}) }\n\t\t},\n\n\t\t/** @return {{register: string, schema: string}} Where the open record lives. */\n\t\trecordRef() {\n\t\t\tconst own = this.content?.record || {}\n\t\t\treturn { register: slugOf(own.register || this.register), schema: slugOf(own.schema || this.schema) }\n\t\t},\n\n\t\t/**\n\t\t * Whether photos can be added: allowed, and somewhere to put them.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/checklist-run-widget/specs/checklist-run-widget/spec.md#requirement-questions-and-answers-can-live-inside-the-open-record\n\t\t */\n\t\tphotosEnabled() {\n\t\t\tif (this.content?.allowPhotos === false || this.content?.saveAction) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (this.inRecord) {\n\t\t\t\treturn Boolean(this.content?.recordPhotosField && this.answerFields.photos)\n\t\t\t}\n\t\t\treturn Boolean(this.content?.answers?.photosField)\n\t\t},\n\n\t\t/** @return {string} The group (section) of this page's first question, or ''. */\n\t\tpageGroup() {\n\t\t\treturn this.currentQuestion?.group || ''\n\t\t},\n\n\t\t/**\n\t\t * The questions of this page that get a photo control: with a\n\t\t * `photoRequired` field, those whose rule is not never (and photo\n\t\t * questions); without one, the first question of the page.\n\t\t *\n\t\t * @return {object[]} The questions.\n\t\t * @spec openspec/changes/checklist-run-photo-rule/specs/checklist-run-photo-rule/spec.md#requirement-a-question-can-require-a-photo\n\t\t */\n\t\tphotoQuestions() {\n\t\t\tif (!this.photosEnabled) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tif (this.fields.photoRequired) {\n\t\t\t\treturn this.pageQuestions.filter((q) => q.kind === 'photo' || q.photoRule !== 'never')\n\t\t\t}\n\t\t\tconst first = this.currentQuestion\n\t\t\tconst extra = this.pageQuestions.filter((q) => q.kind === 'photo' && q !== first)\n\t\t\treturn first ? [first, ...extra] : extra\n\t\t},\n\n\t\t/**\n\t\t * `content.record` with a `filter`: the list to pick the open record\n\t\t * from (first match of `sort`), `'blocked'` while a token is\n\t\t * unresolved, or null without a filter.\n\t\t *\n\t\t * @return {object|string|null}\n\t\t * @spec openspec/changes/checklist-run-photo-rule/specs/checklist-run-photo-rule/spec.md#requirement-a-checklist-run-can-pick-its-record-by-filter\n\t\t */\n\t\trecordPicker() {\n\t\t\tconst own = this.content?.record\n\t\t\tif (!own || typeof own !== 'object' || !own.filter || typeof own.filter !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst unwrap = (c) => (c && typeof c === 'object' && 'value' in c ? c.value : c)\n\t\t\tconst filter = resolveFilterTokens(own.filter, { objectId: this.objectId || null, object: this.objectData, workspace: unwrap(this.cnWorkspaceContext) || {} })\n\t\t\tif (hasUnresolvedTokens(filter)) {\n\t\t\t\treturn 'blocked'\n\t\t\t}\n\t\t\tconst sort = own.sort && typeof own.sort === 'object' && own.sort.field ? { [`_order[${own.sort.field}]`]: own.sort.dir === 'asc' ? 'asc' : 'desc' } : {}\n\t\t\treturn { register: slugOf(own.register), schema: slugOf(own.schema), filter: { ...filter, ...sort } }\n\t\t},\n\n\t\t/** @return {object|null} The record the run lives in: the picked one, else the bound one. */\n\t\topenRecord() {\n\t\t\treturn this.recordPicker ? this.pickedRecord : this.tokenCtx.object\n\t\t},\n\n\t\t/** @return {string} The key of the source lists, to read them again when they change. */\n\t\tsourceKey() {\n\t\t\tconst picker = this.recordPicker\n\t\t\tif (picker === 'blocked') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (picker && typeof this.content?.questionsPath === 'string' && this.content.questionsPath !== '') {\n\t\t\t\treturn JSON.stringify({ pick: picker, path: this.content.questionsPath })\n\t\t\t}\n\t\t\tif (typeof this.content?.questionsPath === 'string' && this.content.questionsPath !== '') {\n\t\t\t\tconst object = this.tokenCtx.object\n\t\t\t\treturn object ? JSON.stringify({ path: this.content.questionsPath, id: idOf(object), at: object['@self']?.updated ?? null }) : ''\n\t\t\t}\n\t\t\tconst q = this.content?.questions\n\t\t\tif (!q || !q.register || !q.schema) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst qFilter = resolveFilterTokens(q.filter || {}, this.tokenCtx)\n\t\t\tconst a = this.content?.answers\n\t\t\tconst aFilter = a && a.register && a.schema ? resolveFilterTokens(a.filter || {}, this.tokenCtx) : null\n\t\t\tif (hasUnresolvedTokens(qFilter) || (aFilter && hasUnresolvedTokens(aFilter))) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn JSON.stringify({ q: [q.register, q.schema, qFilter], a: aFilter ? [a.register, a.schema, aFilter] : null })\n\t\t},\n\n\t\tloadingLabel: () => t('nextcloud-vue', 'Loading …'),\n\t\tprogressLabel: () => t('nextcloud-vue', 'Questions answered'),\n\t\trequiredLabel: () => ` ${t('nextcloud-vue', '(required)')}`,\n\t\tmissingLabel: () => t('nextcloud-vue', 'Answer this question to go on.'),\n\t\tpreviousLabel: () => t('nextcloud-vue', 'Previous'),\n\t\tnextLabel: () => t('nextcloud-vue', 'Next'),\n\t\tfinishLabel: () => t('nextcloud-vue', 'Finish'),\n\t\taddPhotoLabel: () => t('nextcloud-vue', 'Take or add a photo'),\n\t\tmissingPhotoLabel: () => t('nextcloud-vue', 'This answer needs a photo. Add one to go on.'),\n\t\tphotoOnlyLabel: () => t('nextcloud-vue', 'Answer with a photo.'),\n\t\tmorePhotoLabel: () => t('nextcloud-vue', 'Another photo'),\n\n\t\t/** @return {string} The note field's label. */\n\t\tnoteLabel() {\n\t\t\treturn this.content?.noteLabel ? this.translate(this.content.noteLabel) : t('nextcloud-vue', 'Note')\n\t\t},\n\n\t\t/** @return {string} The empty-state text. */\n\t\temptyLabel() {\n\t\t\treturn this.content?.emptyText ? this.translate(this.content.emptyText) : t('nextcloud-vue', 'This checklist has no questions.')\n\t\t},\n\t},\n\n\twatch: {\n\t\tsourceKey: {\n\t\t\thandler() {\n\t\t\t\tthis.load()\n\t\t\t},\n\n\t\t\timmediate: true,\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * The host's translate, or the text unchanged.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string} The translation.\n\t\t */\n\t\ttranslate(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * Whether a value counts as an answer.\n\t\t *\n\t\t * @param {unknown} value The value.\n\t\t * @return {boolean} True for a non-empty value.\n\t\t */\n\t\thasValue(value) {\n\t\t\treturn value !== null && value !== undefined && String(value).trim() !== ''\n\t\t},\n\n\t\t/**\n\t\t * Whether a question has an answer: a value, or for a photo question\n\t\t * a photo stored or held.\n\t\t *\n\t\t * @param {object} question The question.\n\t\t * @return {boolean} True when answered.\n\t\t */\n\t\tisAnswered(question) {\n\t\t\tif (question.kind === 'photo') {\n\t\t\t\treturn this.hasPhoto(question)\n\t\t\t}\n\t\t\treturn this.hasValue(this.draft[question.id]?.value)\n\t\t},\n\n\t\t/**\n\t\t * A question record as the widget draws it.\n\t\t *\n\t\t * @param {object} record The question record.\n\t\t * @param {string} [group] The group (section) it belongs to.\n\t\t * @return {{id: string, text: string, kind: string, required: boolean, help: string, choices: Array<{value: string, label: string}>, group: string}} The question.\n\t\t */\n\t\ttoQuestion(record, group = '') {\n\t\t\tconst f = this.fields\n\t\t\tconst raw = String(readPath(record, f.type) ?? '')\n\t\t\tconst mapped = this.content?.typeMap && typeof this.content.typeMap === 'object' ? this.content.typeMap[raw] : undefined\n\t\t\tconst kind = KINDS.includes(mapped) ? mapped : (KINDS.includes(raw) ? raw : 'text')\n\t\t\tconst stored = { yes: 'yes', no: 'no', na: 'na', ...(this.content?.choiceValues || {}) }\n\t\t\tlet choices = (TYPE_CHOICES[kind] || []).map((c) => ({ value: String(stored[c.key]), label: t('nextcloud-vue', c.label) }))\n\t\t\tif (kind === 'choice') {\n\t\t\t\tconst options = readPath(record, f.options)\n\t\t\t\tchoices = (Array.isArray(options) ? options : []).map((o) => (o && typeof o === 'object'\n\t\t\t\t\t? { value: String(o.value ?? o.label ?? ''), label: this.translate(String(o.label ?? o.value ?? '')) }\n\t\t\t\t\t: { value: String(o), label: this.translate(String(o)) })).filter((c) => c.value !== '')\n\t\t\t}\n\t\t\tconst help = readPath(record, f.help)\n\t\t\treturn {\n\t\t\t\tid: String(readPath(record, f.id) ?? idOf(record)),\n\t\t\t\ttext: this.translate(String(readPath(record, f.text) ?? '')),\n\t\t\t\tkind,\n\t\t\t\trequired: Boolean(readPath(record, f.required)),\n\t\t\t\thelp: typeof help === 'string' && help !== '' ? this.translate(help) : '',\n\t\t\t\tchoices,\n\t\t\t\tgroup,\n\t\t\t\tphotoRule: this.photoRuleOf(record),\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A question's photo rule, from its `photoRequired` field mapped\n\t\t * through `photoRequiredValues`: `'never'`, `'always'` or\n\t\t * `{ whenAnswer }` (a photo is needed when the answer is that value).\n\t\t *\n\t\t * @param {object} record The question record.\n\t\t * @return {('never'|'always'|{whenAnswer: string})} The rule.\n\t\t * @spec openspec/changes/checklist-run-photo-rule/specs/checklist-run-photo-rule/spec.md#requirement-a-question-can-require-a-photo\n\t\t */\n\t\tphotoRuleOf(record) {\n\t\t\tconst path = this.fields.photoRequired\n\t\t\tif (!path) {\n\t\t\t\treturn 'never'\n\t\t\t}\n\t\t\tconst raw = readPath(record, path)\n\t\t\tconst map = { true: 'always', false: 'never', always: 'always', never: 'never', ...(this.content?.photoRequiredValues || {}) }\n\t\t\tconst rule = raw === null || raw === undefined ? 'never' : map[String(raw)]\n\t\t\tif (rule && typeof rule === 'object' && rule.whenAnswer !== undefined) {\n\t\t\t\treturn { whenAnswer: String(rule.whenAnswer) }\n\t\t\t}\n\t\t\treturn rule === 'always' ? 'always' : 'never'\n\t\t},\n\n\t\t/**\n\t\t * Whether a question needs a photo with its current answer.\n\t\t *\n\t\t * @param {object} question The question.\n\t\t * @return {boolean} True when a photo is required now.\n\t\t */\n\t\tneedsPhoto(question) {\n\t\t\tconst rule = question.photoRule\n\t\t\tif (rule === 'always') {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn Boolean(rule && typeof rule === 'object' && String(this.draft[question.id]?.value ?? '') === rule.whenAnswer)\n\t\t},\n\n\t\t/**\n\t\t * Whether a question has a photo, stored or held.\n\t\t *\n\t\t * @param {object} question The question.\n\t\t * @return {boolean} True with at least one photo.\n\t\t */\n\t\thasPhoto(question) {\n\t\t\treturn (this.storedPhotos[question.id] || []).length > 0 || (this.photos[question.id] || []).length > 0\n\t\t},\n\n\t\t/**\n\t\t * The photo card title for a question: its position in the run.\n\t\t *\n\t\t * @param {object} question The question.\n\t\t * @return {string} The title.\n\t\t */\n\t\tphotoTitle(question) {\n\t\t\tconst n = this.questions.indexOf(question) + 1\n\t\t\treturn t('nextcloud-vue', 'Photo for question {n}', { n })\n\t\t},\n\n\t\t/**\n\t\t * The names of the photos held for a question.\n\t\t *\n\t\t * @param {object} question The question.\n\t\t * @return {string[]} The file names.\n\t\t */\n\t\tphotoNamesFor(question) {\n\t\t\treturn (this.photos[question.id] || []).map((file) => file.name)\n\t\t},\n\n\t\t/**\n\t\t * The questions on the open record (`questionsPath`): a list of\n\t\t * questions, or of groups whose `itemsKey` holds them, both levels\n\t\t * sorted by `orderField`.\n\t\t *\n\t\t * @return {object[]} The questions.\n\t\t * @spec openspec/changes/checklist-run-widget/specs/checklist-run-widget/spec.md#requirement-questions-and-answers-can-live-inside-the-open-record\n\t\t */\n\t\tquestionsFromRecord() {\n\t\t\tconst c = this.content\n\t\t\tconst list = readPath(this.openRecord, c.questionsPath)\n\t\t\tconst orderField = c.orderField === undefined ? 'order' : c.orderField\n\t\t\tconst top = byOrder(Array.isArray(list) ? list : [], orderField)\n\t\t\tif (!c.itemsKey) {\n\t\t\t\treturn top.map((record) => this.toQuestion(record))\n\t\t\t}\n\t\t\tconst labelField = c.groupLabelField || 'name'\n\t\t\treturn top.flatMap((group) => {\n\t\t\t\tconst items = readPath(group, c.itemsKey)\n\t\t\t\tconst label = readPath(group, labelField)\n\t\t\t\treturn byOrder(Array.isArray(items) ? items : [], orderField).map((record) => this.toQuestion(record, typeof label === 'string' ? this.translate(label) : ''))\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The draft of one answer entry from the open record's `answersPath`.\n\t\t *\n\t\t * @param {object} question The question.\n\t\t * @param {object} entry The stored entry.\n\t\t * @return {{value: string, note: string}} The draft.\n\t\t */\n\t\tdraftFromEntry(question, entry) {\n\t\t\tconst af = this.answerFields\n\t\t\tconst field = question.kind === 'number' ? af.number : (question.kind === 'choice' ? af.choice : af.value)\n\t\t\tconst value = readPath(entry, field)\n\t\t\tconst note = readPath(entry, af.note)\n\t\t\treturn { value: value === null || value === undefined ? '' : String(value), note: note === null || note === undefined ? '' : String(note) }\n\t\t},\n\n\t\t/**\n\t\t * Read the questions and the answers given so far.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/checklist-run-widget/specs/checklist-run-widget/spec.md#requirement-a-checklist-run-shows-a-page-of-questions-at-a-time\n\t\t */\n\t\tasync load() {\n\t\t\tconst key = this.sourceKey\n\t\t\tif (key === '') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst picker = this.recordPicker\n\t\t\t\tif (picker) {\n\t\t\t\t\tconst found = await fetchReferenceObjects({ register: picker.register, schema: picker.schema, filter: picker.filter, limit: 1 })\n\t\t\t\t\tif (key !== this.sourceKey) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tthis.pickedRecord = found[0] || null\n\t\t\t\t}\n\t\t\t\tlet questions\n\t\t\t\tif (picker && !this.pickedRecord) {\n\t\t\t\t\tquestions = []\n\t\t\t\t} else if (this.content.questionsPath) {\n\t\t\t\t\tquestions = this.questionsFromRecord()\n\t\t\t\t} else {\n\t\t\t\t\tconst q = this.content.questions\n\t\t\t\t\tconst records = await fetchReferenceObjects({ register: q.register, schema: q.schema, filter: resolveFilterTokens(q.filter || {}, this.tokenCtx), limit: q.limit || 500 })\n\t\t\t\t\tquestions = byOrder(records, q.orderField).map((r) => this.toQuestion(r))\n\t\t\t\t}\n\t\t\t\tquestions = questions.filter((x) => x.id !== '' && x.text !== '')\n\t\t\t\tconst a = this.inRecord ? null : this.content.answers\n\t\t\t\tconst answers = a && a.register && a.schema\n\t\t\t\t\t? await fetchReferenceObjects({ register: a.register, schema: a.schema, filter: resolveFilterTokens(a.filter || {}, this.tokenCtx), limit: a.limit || 500 })\n\t\t\t\t\t: []\n\t\t\t\tif (key !== this.sourceKey) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst draft = {}\n\t\t\t\tconst answerIds = {}\n\t\t\t\tconst storedPhotos = {}\n\t\t\t\tfor (const question of questions) {\n\t\t\t\t\tdraft[question.id] = { value: '', note: '' }\n\t\t\t\t}\n\t\t\t\tif (this.inRecord) {\n\t\t\t\t\tconst entries = readPath(this.openRecord, this.content.answersPath)\n\t\t\t\t\tthis.recordAnswers = Array.isArray(entries) ? entries : []\n\t\t\t\t\tconst key = this.content.answerKey || 'questionId'\n\t\t\t\t\tfor (const entry of this.recordAnswers) {\n\t\t\t\t\t\tconst qid = String(readPath(entry, key) ?? '')\n\t\t\t\t\t\tconst question = questions.find((x) => x.id === qid)\n\t\t\t\t\t\tif (question && !answerIds[qid]) {\n\t\t\t\t\t\t\tdraft[qid] = this.draftFromEntry(question, entry)\n\t\t\t\t\t\t\tanswerIds[qid] = qid\n\t\t\t\t\t\t\tconst photos = readPath(entry, this.answerFields.photos)\n\t\t\t\t\t\t\tstoredPhotos[qid] = Array.isArray(photos) ? photos : []\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfor (const answer of answers) {\n\t\t\t\t\tconst qid = String(readPath(answer, a.questionField || 'question') ?? '')\n\t\t\t\t\tif (draft[qid] && !answerIds[qid]) {\n\t\t\t\t\t\tdraft[qid] = {\n\t\t\t\t\t\t\tvalue: String(readPath(answer, a.valueField || 'answer') ?? ''),\n\t\t\t\t\t\t\tnote: String(readPath(answer, a.noteField || 'note') ?? ''),\n\t\t\t\t\t\t}\n\t\t\t\t\t\tanswerIds[qid] = idOf(answer)\n\t\t\t\t\t\tconst stored = a.photosField ? readPath(answer, a.photosField) : null\n\t\t\t\t\t\tstoredPhotos[qid] = Array.isArray(stored) ? stored : []\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.questions = questions\n\t\t\t\tthis.draft = draft\n\t\t\t\tthis.answerIds = answerIds\n\t\t\t\tthis.storedPhotos = storedPhotos\n\t\t\t\tthis.dirty = {}\n\t\t\t\tconst lastPage = Math.max(0, Math.ceil(questions.length / this.pageSize) - 1)\n\t\t\t\tif (this.positionedFor !== key && this.content?.startAt === 'firstOpen') {\n\t\t\t\t\tconst open = questions.findIndex((question) => !this.isAnswered(question))\n\t\t\t\t\tthis.page = open === -1 ? lastPage : Math.floor(open / this.pageSize)\n\t\t\t\t}\n\t\t\t\tthis.positionedFor = key\n\t\t\t\tthis.page = Math.min(this.page, lastPage)\n\t\t\t} catch {\n\t\t\t\tthis.error = t('nextcloud-vue', 'The checklist could not be loaded.')\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Note that a question's answer changed, so the page saves it.\n\t\t *\n\t\t * @param {string} id The question id.\n\t\t * @return {void}\n\t\t */\n\t\tmarkDirty(id) {\n\t\t\tthis.dirty = { ...this.dirty, [id]: true }\n\t\t\tthis.missing = this.missing.filter((m) => m !== id)\n\t\t},\n\n\t\t/**\n\t\t * Hold the photos picked for a question until the page saves.\n\t\t *\n\t\t * @param {Event} event The file input's change event.\n\t\t * @param {string} [questionId] The question, else the first of the page.\n\t\t * @return {void}\n\t\t */\n\t\taddPhotos(event, questionId) {\n\t\t\tconst id = questionId || this.currentQuestion?.id\n\t\t\tconst files = Array.from(event?.target?.files || [])\n\t\t\tif (!id || files.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.photos = { ...this.photos, [id]: [...(this.photos[id] || []), ...files] }\n\t\t\tthis.dirty = { ...this.dirty, [id]: true }\n\t\t\tthis.missingPhoto = this.missingPhoto.filter((m) => m !== id)\n\t\t\tif (event.target) {\n\t\t\t\tevent.target.value = ''\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Save the changed answers of this page: through `saveAction` when\n\t\t * set, else one answer record per question through the object API,\n\t\t * then its held photos.\n\t\t *\n\t\t * @return {Promise<boolean>} True when everything saved.\n\t\t * @spec openspec/changes/checklist-run-widget/specs/checklist-run-widget/spec.md#requirement-answers-are-saved-under-the-readers-own-rights\n\t\t */\n\t\tasync savePage() {\n\t\t\tconst changed = this.pageQuestions.filter((q) => this.dirty[q.id])\n\t\t\tif (changed.length === 0) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tthis.saving = true\n\t\t\tthis.saveError = ''\n\t\t\ttry {\n\t\t\t\tif (this.inRecord && !this.content?.saveAction) {\n\t\t\t\t\tawait this.saveIntoRecord(changed)\n\t\t\t\t\tthis.dirty = Object.fromEntries(Object.entries(this.dirty).filter(([id]) => !changed.some((q) => q.id === id)))\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\tfor (const question of changed) {\n\t\t\t\t\tawait this.saveAnswer(question)\n\t\t\t\t\tconst { [question.id]: _done, ...rest } = this.dirty\n\t\t\t\t\tthis.dirty = rest\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t} catch {\n\t\t\t\tthis.saveError = t('nextcloud-vue', 'The answer could not be saved. Try again.')\n\t\t\t\treturn false\n\t\t\t} finally {\n\t\t\t\tthis.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Write the changed answers of this page into the open record's\n\t\t * `answersPath` array (one entry per question, keyed by `answerKey`)\n\t\t * with one update of the record through the object API. Held photos\n\t\t * go first to the record's own files (`recordPhotosField`), and their\n\t\t * references (`photoRefField`, default `id`) into the answer.\n\t\t *\n\t\t * @param {object[]} changed The changed questions.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/checklist-run-widget/specs/checklist-run-widget/spec.md#requirement-questions-and-answers-can-live-inside-the-open-record\n\t\t */\n\t\tasync saveIntoRecord(changed) {\n\t\t\tconst { register, schema } = this.recordRef\n\t\t\tconst objectId = idOf(this.openRecord) || String(this.openRecordId || '')\n\t\t\tif (!register || !schema || !objectId) {\n\t\t\t\tthrow new Error('no record to write to')\n\t\t\t}\n\t\t\tconst c = this.content\n\t\t\tconst af = this.answerFields\n\t\t\tconst key = c.answerKey || 'questionId'\n\t\t\tconst refField = c.photoRefField || 'id'\n\t\t\tlet recordPhotos = Array.isArray(readPath(this.openRecord, c.recordPhotosField)) ? readPath(this.openRecord, c.recordPhotosField) : []\n\t\t\tconst entries = [...this.recordAnswers]\n\t\t\tfor (const question of changed) {\n\t\t\t\tconst draft = this.draft[question.id]\n\t\t\t\tconst index = entries.findIndex((entry) => String(readPath(entry, key) ?? '') === question.id)\n\t\t\t\tconst entry = { ...(index >= 0 ? entries[index] : { [key]: question.id }) }\n\t\t\t\tif (question.kind === 'number') {\n\t\t\t\t\tentry[af.number] = draft.value === '' ? null : Number(draft.value)\n\t\t\t\t} else if (question.kind === 'choice') {\n\t\t\t\t\tentry[af.choice] = draft.value\n\t\t\t\t} else if (question.kind !== 'photo') {\n\t\t\t\t\tentry[af.value] = draft.value\n\t\t\t\t}\n\t\t\t\tif (this.allowNote) {\n\t\t\t\t\tentry[af.note] = draft.note\n\t\t\t\t}\n\t\t\t\tif (af.at) {\n\t\t\t\t\tentry[af.at] = new Date().toISOString()\n\t\t\t\t}\n\t\t\t\tconst held = this.photos[question.id] || []\n\t\t\t\tif (this.photosEnabled && held.length) {\n\t\t\t\t\tconst field = c.recordPhotosField\n\t\t\t\t\tconst { failed, attached } = await uploadHeldFiles({ register, schema, objectId, held: { [field]: held }, kept: { [field]: recordPhotos } })\n\t\t\t\t\tif (failed.length) {\n\t\t\t\t\t\tthrow new Error('photo upload failed')\n\t\t\t\t\t}\n\t\t\t\t\tconst now = Array.isArray(attached[field]) ? attached[field] : recordPhotos\n\t\t\t\t\tconst added = now.slice(recordPhotos.length).map((ref) => (ref && typeof ref === 'object' ? ref[refField] ?? ref : ref))\n\t\t\t\t\trecordPhotos = now\n\t\t\t\t\tconst existing = Array.isArray(entry[af.photos]) ? entry[af.photos] : []\n\t\t\t\t\tentry[af.photos] = [...existing, ...added]\n\t\t\t\t\tthis.storedPhotos = { ...this.storedPhotos, [question.id]: entry[af.photos] }\n\t\t\t\t\tconst { [question.id]: _sent, ...rest } = this.photos\n\t\t\t\t\tthis.photos = rest\n\t\t\t\t}\n\t\t\t\tif (index >= 0) {\n\t\t\t\t\tentries[index] = entry\n\t\t\t\t} else {\n\t\t\t\t\tentries.push(entry)\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst url = generateUrl(`${OBJECT_API}/${encodeURIComponent(register)}/${encodeURIComponent(schema)}/${encodeURIComponent(objectId)}`)\n\t\t\tawait axios.patch(url, { [c.answersPath]: entries })\n\t\t\tthis.recordAnswers = entries\n\t\t},\n\n\t\t/**\n\t\t * Save one answer.\n\t\t *\n\t\t * @param {object} question The question.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync saveAnswer(question) {\n\t\t\tconst entry = this.draft[question.id]\n\t\t\tconst answer = { questionId: question.id, value: entry.value, note: entry.note }\n\t\t\tconst action = this.content?.saveAction\n\t\t\tif (action && typeof action === 'object') {\n\t\t\t\tconst resolved = resolveDeepTokens(action, this.tokenCtx)\n\t\t\t\tconst wrapped = { ...resolved, args: [...(Array.isArray(resolved.args) ? resolved.args : []), answer] }\n\t\t\t\tconst run = typeof this.cnDispatchAction === 'function' ? this.cnDispatchAction(wrapped) : dispatchAction(wrapped, { router: this.$router || null, tokenCtx: this.tokenCtx })\n\t\t\t\tawait run\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst a = this.content?.answers\n\t\t\tif (!a || !a.register || !a.schema) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst body = {\n\t\t\t\t[a.questionField || 'question']: question.id,\n\t\t\t\t[a.valueField || 'answer']: entry.value,\n\t\t\t}\n\t\t\tif (this.allowNote) {\n\t\t\t\tbody[a.noteField || 'note'] = entry.note\n\t\t\t}\n\t\t\tconst base = generateUrl(`${OBJECT_API}/${encodeURIComponent(a.register)}/${encodeURIComponent(a.schema)}`)\n\t\t\tlet id = this.answerIds[question.id]\n\t\t\tif (id) {\n\t\t\t\tawait axios.patch(`${base}/${encodeURIComponent(id)}`, body)\n\t\t\t} else {\n\t\t\t\tconst create = resolveDeepTokens(a.create && typeof a.create === 'object' ? a.create : {}, this.tokenCtx)\n\t\t\t\tconst response = await axios.post(base, { ...create, ...body })\n\t\t\t\tid = idOf(response?.data)\n\t\t\t\tthis.answerIds = { ...this.answerIds, [question.id]: id }\n\t\t\t}\n\t\t\tconst held = this.photos[question.id] || []\n\t\t\tif (this.photosEnabled && held.length && id) {\n\t\t\t\tconst { failed, attached } = await uploadHeldFiles({ register: a.register, schema: a.schema, objectId: id, held: { [a.photosField]: held }, kept: { [a.photosField]: this.storedPhotos[question.id] || [] } })\n\t\t\t\tif (failed.length) {\n\t\t\t\t\tthrow new Error('photo upload failed')\n\t\t\t\t}\n\t\t\t\tif (Array.isArray(attached[a.photosField])) {\n\t\t\t\t\tthis.storedPhotos = { ...this.storedPhotos, [question.id]: attached[a.photosField] }\n\t\t\t\t}\n\t\t\t\tconst { [question.id]: _sent, ...rest } = this.photos\n\t\t\t\tthis.photos = rest\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The required questions on this page without an answer.\n\t\t *\n\t\t * @return {string[]} Their ids.\n\t\t */\n\t\tunansweredRequired() {\n\t\t\treturn this.pageQuestions.filter((q) => q.required && !this.isAnswered(q)).map((q) => q.id)\n\t\t},\n\n\t\t/**\n\t\t * Save this page and go to the next one; on the last page, finish.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/checklist-run-widget/specs/checklist-run-widget/spec.md#requirement-a-checklist-run-shows-a-page-of-questions-at-a-time\n\t\t */\n\t\tasync next() {\n\t\t\tthis.missing = this.unansweredRequired()\n\t\t\tthis.missingPhoto = this.photosEnabled ? this.pageQuestions.filter((q) => this.needsPhoto(q) && !this.hasPhoto(q)).map((q) => q.id) : []\n\t\t\tif (this.missing.length || this.missingPhoto.length) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!(await this.savePage())) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.isLastPage) {\n\t\t\t\tawait this.finish()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.goTo(this.page + 1)\n\t\t},\n\n\t\t/**\n\t\t * Save this page and go back one.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync previous() {\n\t\t\tif (this.page === 0 || !(await this.savePage())) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.missing = []\n\t\t\tthis.missingPhoto = []\n\t\t\tthis.goTo(this.page - 1)\n\t\t},\n\n\t\t/**\n\t\t * Show a page, move focus to its position line and announce it.\n\t\t *\n\t\t * @param {number} page The page.\n\t\t * @return {void}\n\t\t */\n\t\tgoTo(page) {\n\t\t\tthis.page = page\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tthis.liveText = this.positionText\n\t\t\t\tthis.$refs.position?.focus?.()\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The run is done: dispatch `finishAction` when set, and emit `finished`.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync finish() {\n\t\t\tconst action = this.content?.finishAction\n\t\t\tif (action && typeof action === 'object') {\n\t\t\t\tconst resolved = resolveDeepTokens(action, this.tokenCtx)\n\t\t\t\tconst run = typeof this.cnDispatchAction === 'function' ? this.cnDispatchAction(resolved) : dispatchAction(resolved, { router: this.$router || null, tokenCtx: this.tokenCtx })\n\t\t\t\tawait run\n\t\t\t}\n\t\t\tthis.liveText = t('nextcloud-vue', 'Checklist finished.')\n\t\t\t/**\n\t\t\t * @event finished The last page was saved.\n\t\t\t * @type {{answered: number, total: number}}\n\t\t\t */\n\t\t\tthis.$emit('finished', { answered: this.answeredCount, total: this.questions.length })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-checklist-run__form {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-checklist-run__position {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0;\n}\n\n.cn-checklist-run__progress {\n\tbackground: var(--color-border);\n\tborder-radius: 3px;\n\tdisplay: block;\n\theight: 6px;\n\toverflow: hidden;\n}\n\n.cn-checklist-run__progress-fill {\n\tbackground: var(--color-primary-element);\n\tdisplay: block;\n\theight: 100%;\n}\n\n.cn-checklist-run__live,\n.cn-checklist-run__sr {\n\tclip-path: inset(50%);\n\theight: 1px;\n\toverflow: hidden;\n\tposition: absolute;\n\twhite-space: nowrap;\n\twidth: 1px;\n}\n\n.cn-checklist-run__question,\n.cn-checklist-run__photos {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 12px);\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmargin: 0;\n\tpadding: 12px 14px;\n}\n\n.cn-checklist-run__legend {\n\tfont-size: 15px;\n\tfont-weight: 600;\n\tline-height: 1.2;\n\tpadding: 0 4px;\n}\n\n.cn-checklist-run__help {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 14px;\n\tmargin: 0;\n}\n\n/* An option reads as text, not as a field label (a theme makes every label\n bold); 15px as the board draws it (DqInspectie). */\n.cn-checklist-run__choice {\n\talign-items: center;\n\tdisplay: flex;\n\tfont-size: 15px;\n\tfont-weight: 400;\n\tgap: 10px;\n\tmin-height: 34px;\n}\n\n.cn-checklist-run__choice input {\n\taccent-color: var(--color-primary-element);\n\theight: 20px;\n\tmargin: 0;\n\twidth: 20px;\n}\n\n.cn-checklist-run__text,\n.cn-checklist-run__number {\n\tbox-sizing: border-box;\n\twidth: 100%;\n}\n\n.cn-checklist-run__group {\n\tfont-size: 14px;\n\tfont-weight: 600;\n\tmargin: 0;\n}\n\n.cn-checklist-run__note-label {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-checklist-run__photo-list {\n\tmargin: 0;\n\tpadding-inline-start: 18px;\n}\n\n.cn-checklist-run__photo-button {\n\talign-items: center;\n\talign-self: flex-start;\n\tborder: 1px solid var(--color-border-dark);\n\tborder-radius: var(--border-radius-element, 8px);\n\tcursor: pointer;\n\tdisplay: inline-flex;\n\tfont-weight: 600;\n\tmin-height: 34px;\n\tpadding: 0 14px;\n}\n\n.cn-checklist-run__photo-button:focus-within {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-checklist-run__photo-input {\n\tclip-path: inset(50%);\n\theight: 1px;\n\toverflow: hidden;\n\tposition: absolute;\n\twidth: 1px;\n}\n\n.cn-checklist-run__nav {\n\tdisplay: flex;\n\tjustify-content: space-between;\n}\n\n.cn-checklist-run__error {\n\tcolor: var(--color-error-text, var(--color-error));\n\tmargin: 0;\n}\n\n.cn-checklist-run__empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_toDisplayString","_createVNode","_createElementVNode","_Fragment","_renderList","_createTextVNode","_vModelRadio","_vModelText"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import script from './CnChoiceCards.vue2.js';
|
|
|
2
2
|
import { render } from './CnChoiceCards.vue3.js';
|
|
3
3
|
|
|
4
4
|
script.render = render;
|
|
5
|
-
script.__scopeId = "data-v-
|
|
5
|
+
script.__scopeId = "data-v-51150930";
|
|
6
6
|
const __sfc_main = script;
|
|
7
7
|
|
|
8
8
|
export { __sfc_main as default };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnChoiceCards.vue2.js","sources":["../../../../src/components/CnChoiceCards/CnChoiceCards.vue"],"sourcesContent":["<!-- SPDX-License-Identifier: EUPL-1.2 -->\n<!-- SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl> -->\n<template>\n\t<fieldset class=\"cn-choice-cards\" :aria-busy=\"loading ? 'true' : 'false'\">\n\t\t<legend v-if=\"label\" class=\"cn-choice-cards__legend\" :class=\"{ 'hidden-visually': hideLabel }\">\n\t\t\t{{ label }}\n\t\t</legend>\n\n\t\t<div v-if=\"loading\" class=\"cn-choice-cards__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t</div>\n\n\t\t<NcNoteCard v-else-if=\"normalizedOptions.length === 0\" type=\"warning\">\n\t\t\t{{ emptyText }}\n\t\t</NcNoteCard>\n\n\t\t<!-- Rows: one full-width option per row, the input on the left and the\n\t\t title over a muted description, no card chrome. -->\n\t\t<div v-else-if=\"layout === 'rows'\"\n\t\t\tclass=\"cn-choice-cards__rows\"\n\t\t\t:style=\"rowsStyle\"\n\t\t\tdata-testid=\"cn-choice-cards-rows\">\n\t\t\t<div\n\t\t\t\tv-for=\"option in normalizedOptions\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-choice-cards__cell\"\n\t\t\t\t:class=\"{ 'cn-choice-cards__cell--with-details': hasDetails(option) }\">\n\t\t\t\t<label\n\t\t\t\t\tclass=\"cn-choice-cards__row\"\n\t\t\t\t\t:class=\"{ 'cn-choice-cards__row--selected': isSelected(option), 'cn-choice-cards__row--disabled': isDisabled(option), 'cn-choice-cards__row--readonly': readonly }\">\n\t\t\t\t\t<input\n\t\t\t\t\t\tclass=\"cn-choice-cards__row-input\"\n\t\t\t\t\t\t:type=\"multiple ? 'checkbox' : 'radio'\"\n\t\t\t\t\t\t:name=\"groupName\"\n\t\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t\t:checked=\"isSelected(option)\"\n\t\t\t\t\t\t:disabled=\"isDisabled(option) || readonly\"\n\t\t\t\t\t\t:aria-describedby=\"option.disabled && option.disabledReason ? reasonId(option) : null\"\n\t\t\t\t\t\t@change=\"onToggle(option)\">\n\t\t\t\t\t<CnIcon v-if=\"option.icon\"\n\t\t\t\t\t\t:name=\"option.icon\"\n\t\t\t\t\t\t:size=\"20\"\n\t\t\t\t\t\tclass=\"cn-choice-cards__row-icon\" />\n\t\t\t\t\t<span class=\"cn-choice-cards__row-text\">\n\t\t\t\t\t\t<span class=\"cn-choice-cards__row-title\">{{ option.label }}</span>\n\t\t\t\t\t\t<span v-if=\"option.description\" class=\"cn-choice-cards__row-description\">{{ option.description }}</span>\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tv-if=\"option.disabled && option.disabledReason\"\n\t\t\t\t\t\t\t:id=\"reasonId(option)\"\n\t\t\t\t\t\t\tclass=\"cn-choice-cards__reason\"\n\t\t\t\t\t\t\tdata-testid=\"cn-choice-cards-reason\">{{ option.disabledReason }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<div v-if=\"hasDetails(option)\"\n\t\t\t\t\tclass=\"cn-choice-cards__details\"\n\t\t\t\t\t:data-testid=\"`cn-choice-cards-details-${option.value}`\">\n\t\t\t\t\t<!-- @slot option-details The chosen option's own fields, drawn\n\t\t\t\t\t inside the selected row's frame under its text (DqVolgendeStap:\n\t\t\t\t\t Opschorten's reason, days and return date). Rendered only for a\n\t\t\t\t\t selected option, outside the label so a field is not part of\n\t\t\t\t\t the radio's label. Scope: `{ option }`. -->\n\t\t\t\t\t<slot name=\"option-details\" :option=\"option\" />\n\t\t\t\t</div>\n\t\t\t\t<div v-if=\"$slots['option-actions']\" class=\"cn-choice-cards__actions\">\n\t\t\t\t\t<slot name=\"option-actions\" :option=\"option\" :selected=\"isSelected(option)\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div v-else class=\"cn-choice-cards__grid\" :style=\"gridStyle\">\n\t\t\t<!-- One cell per option: the card's label, and below it whatever the\n\t\t\t `#option-actions` slot adds. The slot sits OUTSIDE the label on\n\t\t\t purpose: a button inside a label is interactive content inside\n\t\t\t interactive content, and a click on it would toggle the radio. -->\n\t\t\t<div\n\t\t\t\tv-for=\"option in normalizedOptions\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-choice-cards__cell\">\n\t\t\t\t<label\n\t\t\t\t\tclass=\"cn-choice-cards__option\"\n\t\t\t\t\t:class=\"{ 'cn-choice-cards__option--selected': isSelected(option), 'cn-choice-cards__option--disabled': isDisabled(option) }\">\n\t\t\t\t\t<!-- A REAL, VISIBLE input, not a styled div with a click handler.\n\t\t\t\t\t It carries the checked state to assistive technology and to\n\t\t\t\t\t Windows high-contrast mode for free, and it keeps the\n\t\t\t\t\t selection perceivable without colour (WCAG 1.4.1) — the\n\t\t\t\t\t card's active border alone would not be. -->\n\t\t\t\t\t<input\n\t\t\t\t\t\tclass=\"cn-choice-cards__input\"\n\t\t\t\t\t\t:type=\"multiple ? 'checkbox' : 'radio'\"\n\t\t\t\t\t\t:name=\"groupName\"\n\t\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t\t:checked=\"isSelected(option)\"\n\t\t\t\t\t\t:disabled=\"isDisabled(option) || readonly\"\n\t\t\t\t\t\t:aria-describedby=\"option.disabled && option.disabledReason ? reasonId(option) : null\"\n\t\t\t\t\t\t@change=\"onToggle(option)\">\n\t\t\t\t\t<CnCard\n\t\t\t\t\t\tclass=\"cn-choice-cards__card\"\n\t\t\t\t\t\t:title=\"option.label\"\n\t\t\t\t\t\ttitleTag=\"span\"\n\t\t\t\t\t\t:description=\"option.description\"\n\t\t\t\t\t\t:stats=\"option.stats || []\"\n\t\t\t\t\t\t:tags=\"option.tags || []\"\n\t\t\t\t\t\t:active=\"isSelected(option)\"\n\t\t\t\t\t\tactiveVariant=\"primary\"\n\t\t\t\t\t\t:descriptionLines=\"descriptionLines\">\n\t\t\t\t\t\t<template v-if=\"option.icon\" #icon>\n\t\t\t\t\t\t\t<CnIcon :name=\"option.icon\" :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</CnCard>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"option.disabled && option.disabledReason\"\n\t\t\t\t\t\t:id=\"reasonId(option)\"\n\t\t\t\t\t\tclass=\"cn-choice-cards__reason cn-choice-cards__reason--card\"\n\t\t\t\t\t\tdata-testid=\"cn-choice-cards-reason\">{{ option.disabledReason }}</span>\n\t\t\t\t</label>\n\t\t\t\t<div v-if=\"$slots['option-actions']\" class=\"cn-choice-cards__actions\">\n\t\t\t\t\t<!-- @slot option-actions Content below one card, outside its label\n\t\t\t\t\t (e.g. a per-option Load button). Scope: `{ option, selected }`. -->\n\t\t\t\t\t<slot name=\"option-actions\" :option=\"option\" :selected=\"isSelected(option)\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</fieldset>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport CnCard from '../CnCard/CnCard.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\n\nlet groupSeq = 0\n\n/**\n * CnChoiceCards — pick one option, or several, from a grid of cards.\n *\n * A card carries what a dropdown entry cannot: a description of what the\n * option actually is, and a few numbers about it. Use it where the choice\n * deserves explaining — the setup wizard's example-data step, an onboarding\n * template picker — and a plain `NcSelect` where a label already says enough.\n *\n * Each card is a `<label>` around a real radio (or checkbox when `multiple`),\n * so keyboard navigation, the checked state and high-contrast rendering are\n * the browser's, not a reimplementation.\n *\n * ```vue\n * <CnChoiceCards\n * v-model=\"picked\"\n * label=\"Which kind of organisation is this for?\"\n * :options=\"[\n * { value: 'municipality', label: 'Municipality',\n * description: 'A city council with committees and a decision list.',\n * stats: [{ label: 'Objects', value: 170 }] },\n * { value: 'none', label: 'None, I will set this up myself' },\n * ]\" />\n * ```\n */\nexport default {\n\tname: 'CnChoiceCards',\n\n\tcomponents: {\n\t\tCnCard,\n\t\tCnIcon,\n\t\tNcLoadingIcon,\n\t\tNcNoteCard,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The options to offer. Each: `{ value, label, description?, stats?,\n\t\t * tags?, icon?, disabled?, disabledReason? }` — `icon` is a PascalCase\n\t\t * MDI name resolved by CnIcon; a `disabled` option is greyed out and\n\t\t * cannot be picked, with `disabledReason` shown under it.\n\t\t */\n\t\toptions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The selected value: a scalar, or an array when `multiple`.\n\t\t *\n\t\t * @type {string|number|boolean|Array|null}\n\t\t */\n\t\tmodelValue: {\n\t\t\t// eslint-disable-next-line vue/prefer-prop-type-boolean-first -- an option value of '' must stay a string\n\t\t\ttype: [String, Number, Boolean, Array, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Allow selecting several cards; the model becomes an array. */\n\t\tmultiple: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Group label, rendered as the fieldset's legend. */\n\t\tlabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Keep the legend for assistive tech only, where the dialog's title\n\t\t * already says what is chosen (DqZaakSamenwerken).\n\t\t */\n\t\thideLabel: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Disable every card (e.g. a dependent choice with no parent value yet). */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Show the choice without letting it change: every option at full\n\t\t * contrast, the selected one marked, nothing pickable (DqBesluit's\n\t\t * recorded outcome). `disabled` greys the options out instead.\n\t\t */\n\t\treadonly: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Show a spinner instead of the grid while options are being fetched. */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Message shown when there is nothing to choose from. */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Nothing to choose from here.'),\n\t\t},\n\n\t\t/**\n\t\t * `cards`: a grid of cards with stats and tags. `rows`: one option per\n\t\t * row, full width, the input on the left and the title over a muted\n\t\t * description; with `multiple` and one option it is a checkbox card.\n\t\t */\n\t\tlayout: {\n\t\t\ttype: String,\n\t\t\tdefault: 'cards',\n\t\t\tvalidator: (value) => ['cards', 'rows'].includes(value),\n\t\t},\n\n\t\t/**\n\t\t * Columns of the card grid; 0 lets the grid fit as many as the width\n\t\t * allows. In `rows`, more than 1 sets the rows side by side\n\t\t * (DqZaakSamenwerken: Share | Hand over); 0 or 1 stacks them.\n\t\t */\n\t\tcolumns: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\n\t\t},\n\n\t\t/** Lines of description shown before clamping. */\n\t\tdescriptionLines: {\n\t\t\ttype: Number,\n\t\t\tdefault: 4,\n\t\t},\n\t},\n\n\temits: ['update:modelValue'],\n\n\tdata() {\n\t\tgroupSeq += 1\n\t\treturn {\n\t\t\t// Radios only group by `name`, so two card groups on one page would\n\t\t\t// otherwise share a selection.\n\t\t\tgroupName: 'cn-choice-cards-' + groupSeq,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Options with a usable `value` and `label`, tolerating the shapes a\n\t\t * server list arrives in: `id` instead of `value`, `name` instead of\n\t\t * `label`, and a bare string for a value that is its own label.\n\t\t *\n\t\t * @return {Array<object>} The normalised options.\n\t\t */\n\t\tnormalizedOptions() {\n\t\t\treturn (this.options || [])\n\t\t\t\t.filter((option) => option !== null && option !== undefined)\n\t\t\t\t.map((option) => {\n\t\t\t\t\tif (typeof option !== 'object') {\n\t\t\t\t\t\treturn { value: option, label: String(option) }\n\t\t\t\t\t}\n\t\t\t\t\tconst value = option.value !== undefined ? option.value : option.id\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...option,\n\t\t\t\t\t\tvalue,\n\t\t\t\t\t\tlabel: option.label || option.name || String(value),\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.filter((option) => option.value !== undefined)\n\t\t},\n\n\t\t/**\n\t\t * The current selection as an array of values, whatever the model shape.\n\t\t *\n\t\t * @return {Array} The selected values.\n\t\t */\n\t\tselectedValues() {\n\t\t\tif (this.multiple) {\n\t\t\t\treturn Array.isArray(this.modelValue) ? this.modelValue : []\n\t\t\t}\n\t\t\treturn (this.modelValue === null || this.modelValue === undefined || this.modelValue === '')\n\t\t\t\t? []\n\t\t\t\t: [this.modelValue]\n\t\t},\n\n\t\t/**\n\t\t * Inline override for the rows' columns: side by side as a grid when\n\t\t * `columns` is above 1, else the stylesheet's single column (`{}`).\n\t\t *\n\t\t * @return {object} A grid override, or `{}`.\n\t\t */\n\t\trowsStyle() {\n\t\t\tif (this.columns > 1) {\n\t\t\t\treturn { display: 'grid', gridTemplateColumns: `repeat(${this.columns}, minmax(0, 1fr))` }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\n\t\t/**\n\t\t * Inline override for the grid's column count. `auto-fit` with a\n\t\t * 260px minimum wraps a small option count onto its own row well\n\t\t * before the container is actually too narrow for them side by\n\t\t * side — two cards would stack at ~550px wide even though halving\n\t\t * the available width still leaves each one legible. A binary (or\n\t\t * near-binary) choice should always sit in one row and fill the\n\t\t * available width; a larger option count keeps the responsive\n\t\t * `auto-fit` wrapping from the stylesheet (`{}` leaves it alone).\n\t\t *\n\t\t * @return {object} A `gridTemplateColumns` override, or `{}`.\n\t\t */\n\t\tgridStyle() {\n\t\t\tif (this.columns > 0) {\n\t\t\t\treturn { gridTemplateColumns: `repeat(${this.columns}, minmax(0, 1fr))` }\n\t\t\t}\n\t\t\tconst count = this.normalizedOptions.length\n\t\t\tif (count > 0 && count <= 2) {\n\t\t\t\treturn { gridTemplateColumns: `repeat(${count}, minmax(0, 1fr))` }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Whether an option is currently selected.\n\t\t *\n\t\t * Compares as strings: a value that made the round trip through the\n\t\t * server as `\"1\"` must still match the `1` it was sent as.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {boolean} True when selected.\n\t\t */\n\t\t/**\n\t\t * Whether a rows option draws the `option-details` slot in its frame:\n\t\t * the slot is given and the option is the chosen one.\n\t\t *\n\t\t * @param {object} option The option.\n\t\t * @return {boolean}\n\t\t */\n\t\thasDetails(option) {\n\t\t\treturn Boolean(this.$slots['option-details']) && this.isSelected(option)\n\t\t},\n\n\t\t/**\n\t\t * Whether an option cannot be picked: the whole group is disabled, or the option is.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {boolean} True when disabled.\n\t\t */\n\t\tisDisabled(option) {\n\t\t\treturn this.disabled || option.disabled === true\n\t\t},\n\n\t\t/**\n\t\t * Id of an option's disabled reason, for `aria-describedby`.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {string} The id.\n\t\t */\n\t\treasonId(option) {\n\t\t\treturn `${this.groupName}-reason-${String(option.value).replace(/[^A-Za-z0-9_-]/g, '-')}`\n\t\t},\n\n\t\tisSelected(option) {\n\t\t\treturn this.selectedValues.some((v) => String(v) === String(option.value))\n\t\t},\n\n\t\t/**\n\t\t * Select (single) or toggle (multiple) an option and emit the new model.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {void}\n\t\t */\n\t\tonToggle(option) {\n\t\t\tif (this.readonly || this.isDisabled(option)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.multiple) {\n\t\t\t\t/**\n\t\t\t\t * @event update:modelValue The selected value, or the array of values when `multiple`.\n\t\t\t\t * @type {string|number|boolean|Array}\n\t\t\t\t */\n\t\t\t\t// Never the option OBJECT: a parent has to be able to hand back\n\t\t\t\t// what it receives as `modelValue`.\n\t\t\t\tthis.$emit('update:modelValue', option.value)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = this.selectedValues.filter((v) => String(v) !== String(option.value))\n\t\t\tif (next.length === this.selectedValues.length) {\n\t\t\t\tnext.push(option.value)\n\t\t\t}\n\t\t\tthis.$emit('update:modelValue', next)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped lang=\"scss\">\n.cn-choice-cards {\n\tborder: none;\n\tpadding: 0;\n\tmargin: 0;\n\tmin-inline-size: 0;\n}\n\n.cn-choice-cards__legend {\n\tpadding: 0;\n\tmargin-block-end: 8px;\n\tfont-weight: bold;\n}\n\n.cn-choice-cards__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 24px 0;\n}\n\n.cn-choice-cards__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fit, minmax(260px, 1fr));\n\tgap: 12px;\n\t/* Room for the card shadow. `src/css/card.css` is loaded globally by every\n\t consumer and gives EVERY `.cn-card` a `box-shadow` — including these — so\n\t a grid flush against its container gets the shadow shaved off wherever\n\t that container clips, which the setup wizard's dialog body does on the\n\t sides and the bottom. 12px clears the widest of those shadows. */\n\tpadding: 12px;\n}\n\n.cn-choice-cards__cell {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmin-inline-size: 0;\n}\n\n.cn-choice-cards__actions {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-choice-cards__option {\n\tposition: relative;\n\tdisplay: block;\n\tcursor: pointer;\n}\n\n.cn-choice-cards__input {\n\tposition: absolute;\n\tinset-block-start: 16px;\n\tinset-inline-end: 14px;\n\tz-index: 1;\n\tmargin: 0;\n\tcursor: pointer;\n}\n\n/* Room for the input, so a long title never runs underneath it. */\n.cn-choice-cards__option :deep(.cn-card) {\n\tpadding-inline-end: 44px;\n\ttransition: box-shadow 0.2s ease, border-color 0.2s ease;\n}\n\n/* The same global `card.css` also scales `.cn-card` up on hover. A transform\n counts towards the scrollable overflow of the nearest scroll container, so in\n a dialog body sized to its content a scrollbar appears the moment the pointer\n touches a card, and the grid twitches. The border colour below already\n carries hover, so the scale is dropped: at (0,3,0) this beats the global\n `.cn-card:hover` at (0,2,0). */\n.cn-choice-cards__option:hover :deep(.cn-card) {\n\tborder-color: var(--color-primary-element);\n\ttransform: none;\n}\n\n/* The focus ring belongs on the card, not on the small input inside it. */\n.cn-choice-cards__input:focus-visible + :deep(.cn-card) {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-choice-cards__input:disabled {\n\tcursor: default;\n}\n\n.cn-choice-cards__option--disabled .cn-choice-cards__input + :deep(.cn-card) {\n\topacity: 0.6;\n}\n\n.cn-choice-cards__rows {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n}\n\n/* 12px 14px, as the boards draw an option row (DqVolgendeStap, DqBesluit). */\n.cn-choice-cards__row {\n\tdisplay: flex;\n\talign-items: flex-start;\n\tgap: 12px;\n\tpadding: 12px 14px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n\tbackground: var(--color-main-background);\n\tcursor: pointer;\n}\n\n.cn-choice-cards__row:hover {\n\tborder-color: var(--color-border-dark);\n}\n\n.cn-choice-cards__row--selected,\n.cn-choice-cards__row--selected:hover {\n\tborder-color: var(--color-primary-element);\n\t/* The second pixel of the 2px selected border, without moving the content. */\n\tbox-shadow: inset 0 0 0 1px var(--color-primary-element);\n\tbackground: var(--color-primary-element-light);\n}\n\n/* An option this record does not allow: the grey ground and the muted ink,\n still legible (DqVolgendeStap), not faded. */\n.cn-choice-cards__row--disabled,\n.cn-choice-cards__row--disabled:hover {\n\tborder-color: var(--color-border);\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\tcursor: default;\n}\n\n/* The title takes the muted ink itself: a theme that colours every label\n with `!important` (thematiq) would otherwise hand it the label colour. */\n.cn-choice-cards__row--disabled .cn-choice-cards__row-title {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n/* The chosen option's fields share the selected row's frame: the cell draws\n the 2px primary border and the ground, the row inside it draws none, and\n the fields sit under the text, indented to it (DqVolgendeStap). */\n.cn-choice-cards__cell--with-details {\n\tborder: 1px solid var(--color-primary-element);\n\tborder-radius: var(--border-radius-large, 8px);\n\tbox-shadow: inset 0 0 0 1px var(--color-primary-element);\n\tbackground: var(--color-primary-element-light);\n}\n\n.cn-choice-cards__cell--with-details .cn-choice-cards__row,\n.cn-choice-cards__cell--with-details .cn-choice-cards__row:hover {\n\tborder-color: transparent;\n\tbox-shadow: none;\n\tbackground: transparent;\n}\n\n.cn-choice-cards__details {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 14px;\n\tpadding: 0 14px 14px 44px;\n}\n\n.cn-choice-cards__row--readonly {\n\tcursor: default;\n}\n\n.cn-choice-cards__row--readonly:not(.cn-choice-cards__row--selected):hover {\n\tborder-color: var(--color-border);\n}\n\n.cn-choice-cards__row:has(.cn-choice-cards__row-input:focus-visible) {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-choice-cards__row-input {\n\tflex: none;\n\tmargin: 2px 0 0;\n\taccent-color: var(--color-primary-element);\n\tcursor: inherit;\n}\n\n.cn-choice-cards__row-icon {\n\tflex: none;\n}\n\n.cn-choice-cards__row-text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-choice-cards__row-title {\n\tfont-size: 15px;\n\tfont-weight: 600;\n}\n\n/* Regular weight on the span itself: a theme that sets every label bold\n with `!important` (thematiq) reaches a span that only inherits. */\n.cn-choice-cards__row-description,\n.cn-choice-cards__reason {\n\tfont-size: 13px;\n\tfont-weight: 400;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-choice-cards__reason--card {\n\tdisplay: block;\n\tmargin-block-start: 4px;\n}\n</style>\n"],"names":["CnCard","CnIcon","t"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnChoiceCards.vue2.js","sources":["../../../../src/components/CnChoiceCards/CnChoiceCards.vue"],"sourcesContent":["<!-- SPDX-License-Identifier: EUPL-1.2 -->\n<!-- SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl> -->\n<template>\n\t<fieldset class=\"cn-choice-cards\" :aria-busy=\"loading ? 'true' : 'false'\">\n\t\t<legend v-if=\"label\" class=\"cn-choice-cards__legend\" :class=\"{ 'hidden-visually': hideLabel }\">\n\t\t\t{{ label }}\n\t\t</legend>\n\n\t\t<div v-if=\"loading\" class=\"cn-choice-cards__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t</div>\n\n\t\t<NcNoteCard v-else-if=\"normalizedOptions.length === 0\" type=\"warning\">\n\t\t\t{{ emptyText }}\n\t\t</NcNoteCard>\n\n\t\t<!-- Rows: one full-width option per row, the input on the left and the\n\t\t title over a muted description, no card chrome. -->\n\t\t<div v-else-if=\"layout === 'rows'\"\n\t\t\tclass=\"cn-choice-cards__rows\"\n\t\t\t:style=\"rowsStyle\"\n\t\t\tdata-testid=\"cn-choice-cards-rows\">\n\t\t\t<div\n\t\t\t\tv-for=\"option in normalizedOptions\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-choice-cards__cell\"\n\t\t\t\t:class=\"{ 'cn-choice-cards__cell--with-details': hasDetails(option) }\">\n\t\t\t\t<label\n\t\t\t\t\tclass=\"cn-choice-cards__row\"\n\t\t\t\t\t:class=\"{ 'cn-choice-cards__row--selected': isSelected(option), 'cn-choice-cards__row--disabled': isDisabled(option), 'cn-choice-cards__row--readonly': readonly }\">\n\t\t\t\t\t<input\n\t\t\t\t\t\tclass=\"cn-choice-cards__row-input\"\n\t\t\t\t\t\t:type=\"multiple ? 'checkbox' : 'radio'\"\n\t\t\t\t\t\t:name=\"groupName\"\n\t\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t\t:checked=\"isSelected(option)\"\n\t\t\t\t\t\t:disabled=\"isDisabled(option) || readonly\"\n\t\t\t\t\t\t:aria-describedby=\"option.disabled && option.disabledReason ? reasonId(option) : null\"\n\t\t\t\t\t\t@change=\"onToggle(option)\">\n\t\t\t\t\t<CnIcon v-if=\"option.icon\"\n\t\t\t\t\t\t:name=\"option.icon\"\n\t\t\t\t\t\t:size=\"20\"\n\t\t\t\t\t\tclass=\"cn-choice-cards__row-icon\" />\n\t\t\t\t\t<span class=\"cn-choice-cards__row-text\">\n\t\t\t\t\t\t<span class=\"cn-choice-cards__row-title\">{{ option.label }}</span>\n\t\t\t\t\t\t<span v-if=\"option.description\" class=\"cn-choice-cards__row-description\">{{ option.description }}</span>\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tv-if=\"option.disabled && option.disabledReason\"\n\t\t\t\t\t\t\t:id=\"reasonId(option)\"\n\t\t\t\t\t\t\tclass=\"cn-choice-cards__reason\"\n\t\t\t\t\t\t\tdata-testid=\"cn-choice-cards-reason\">{{ option.disabledReason }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<div v-if=\"hasDetails(option)\"\n\t\t\t\t\tclass=\"cn-choice-cards__details\"\n\t\t\t\t\t:data-testid=\"`cn-choice-cards-details-${option.value}`\">\n\t\t\t\t\t<!-- @slot option-details The chosen option's own fields, drawn\n\t\t\t\t\t inside the selected row's frame under its text (DqVolgendeStap:\n\t\t\t\t\t Opschorten's reason, days and return date). Rendered only for a\n\t\t\t\t\t selected option, outside the label so a field is not part of\n\t\t\t\t\t the radio's label. Scope: `{ option }`. -->\n\t\t\t\t\t<slot name=\"option-details\" :option=\"option\" />\n\t\t\t\t</div>\n\t\t\t\t<div v-if=\"$slots['option-actions']\" class=\"cn-choice-cards__actions\">\n\t\t\t\t\t<slot name=\"option-actions\" :option=\"option\" :selected=\"isSelected(option)\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div v-else class=\"cn-choice-cards__grid\" :style=\"gridStyle\">\n\t\t\t<!-- One cell per option: the card's label, and below it whatever the\n\t\t\t `#option-actions` slot adds. The slot sits OUTSIDE the label on\n\t\t\t purpose: a button inside a label is interactive content inside\n\t\t\t interactive content, and a click on it would toggle the radio. -->\n\t\t\t<div\n\t\t\t\tv-for=\"option in normalizedOptions\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-choice-cards__cell\">\n\t\t\t\t<label\n\t\t\t\t\tclass=\"cn-choice-cards__option\"\n\t\t\t\t\t:class=\"{ 'cn-choice-cards__option--selected': isSelected(option), 'cn-choice-cards__option--disabled': isDisabled(option) }\">\n\t\t\t\t\t<!-- A REAL, VISIBLE input, not a styled div with a click handler.\n\t\t\t\t\t It carries the checked state to assistive technology and to\n\t\t\t\t\t Windows high-contrast mode for free, and it keeps the\n\t\t\t\t\t selection perceivable without colour (WCAG 1.4.1) — the\n\t\t\t\t\t card's active border alone would not be. -->\n\t\t\t\t\t<input\n\t\t\t\t\t\tclass=\"cn-choice-cards__input\"\n\t\t\t\t\t\t:type=\"multiple ? 'checkbox' : 'radio'\"\n\t\t\t\t\t\t:name=\"groupName\"\n\t\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t\t:checked=\"isSelected(option)\"\n\t\t\t\t\t\t:disabled=\"isDisabled(option) || readonly\"\n\t\t\t\t\t\t:aria-describedby=\"option.disabled && option.disabledReason ? reasonId(option) : null\"\n\t\t\t\t\t\t@change=\"onToggle(option)\">\n\t\t\t\t\t<CnCard\n\t\t\t\t\t\tclass=\"cn-choice-cards__card\"\n\t\t\t\t\t\t:title=\"option.label\"\n\t\t\t\t\t\ttitleTag=\"span\"\n\t\t\t\t\t\t:description=\"option.description\"\n\t\t\t\t\t\t:stats=\"option.stats || []\"\n\t\t\t\t\t\t:tags=\"option.tags || []\"\n\t\t\t\t\t\t:active=\"isSelected(option)\"\n\t\t\t\t\t\tactiveVariant=\"primary\"\n\t\t\t\t\t\t:descriptionLines=\"descriptionLines\">\n\t\t\t\t\t\t<template v-if=\"option.icon\" #icon>\n\t\t\t\t\t\t\t<CnIcon :name=\"option.icon\" :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</CnCard>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"option.disabled && option.disabledReason\"\n\t\t\t\t\t\t:id=\"reasonId(option)\"\n\t\t\t\t\t\tclass=\"cn-choice-cards__reason cn-choice-cards__reason--card\"\n\t\t\t\t\t\tdata-testid=\"cn-choice-cards-reason\">{{ option.disabledReason }}</span>\n\t\t\t\t</label>\n\t\t\t\t<div v-if=\"$slots['option-actions']\" class=\"cn-choice-cards__actions\">\n\t\t\t\t\t<!-- @slot option-actions Content below one card, outside its label\n\t\t\t\t\t (e.g. a per-option Load button). Scope: `{ option, selected }`. -->\n\t\t\t\t\t<slot name=\"option-actions\" :option=\"option\" :selected=\"isSelected(option)\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</fieldset>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport CnCard from '../CnCard/CnCard.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\n\nlet groupSeq = 0\n\n/**\n * CnChoiceCards — pick one option, or several, from a grid of cards.\n *\n * A card carries what a dropdown entry cannot: a description of what the\n * option actually is, and a few numbers about it. Use it where the choice\n * deserves explaining — the setup wizard's example-data step, an onboarding\n * template picker — and a plain `NcSelect` where a label already says enough.\n *\n * Each card is a `<label>` around a real radio (or checkbox when `multiple`),\n * so keyboard navigation, the checked state and high-contrast rendering are\n * the browser's, not a reimplementation.\n *\n * ```vue\n * <CnChoiceCards\n * v-model=\"picked\"\n * label=\"Which kind of organisation is this for?\"\n * :options=\"[\n * { value: 'municipality', label: 'Municipality',\n * description: 'A city council with committees and a decision list.',\n * stats: [{ label: 'Objects', value: 170 }] },\n * { value: 'none', label: 'None, I will set this up myself' },\n * ]\" />\n * ```\n */\nexport default {\n\tname: 'CnChoiceCards',\n\n\tcomponents: {\n\t\tCnCard,\n\t\tCnIcon,\n\t\tNcLoadingIcon,\n\t\tNcNoteCard,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The options to offer. Each: `{ value, label, description?, stats?,\n\t\t * tags?, icon?, disabled?, disabledReason? }` — `icon` is a PascalCase\n\t\t * MDI name resolved by CnIcon; a `disabled` option is greyed out and\n\t\t * cannot be picked, with `disabledReason` shown under it.\n\t\t */\n\t\toptions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The selected value: a scalar, or an array when `multiple`.\n\t\t *\n\t\t * @type {string|number|boolean|Array|null}\n\t\t */\n\t\tmodelValue: {\n\t\t\t// eslint-disable-next-line vue/prefer-prop-type-boolean-first -- an option value of '' must stay a string\n\t\t\ttype: [String, Number, Boolean, Array, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Allow selecting several cards; the model becomes an array. */\n\t\tmultiple: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Group label, rendered as the fieldset's legend. */\n\t\tlabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Keep the legend for assistive tech only, where the dialog's title\n\t\t * already says what is chosen (DqZaakSamenwerken).\n\t\t */\n\t\thideLabel: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Disable every card (e.g. a dependent choice with no parent value yet). */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Show the choice without letting it change: every option at full\n\t\t * contrast, the selected one marked, nothing pickable (DqBesluit's\n\t\t * recorded outcome). `disabled` greys the options out instead.\n\t\t */\n\t\treadonly: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Show a spinner instead of the grid while options are being fetched. */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Message shown when there is nothing to choose from. */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Nothing to choose from here.'),\n\t\t},\n\n\t\t/**\n\t\t * `cards`: a grid of cards with stats and tags. `rows`: one option per\n\t\t * row, full width, the input on the left and the title over a muted\n\t\t * description; with `multiple` and one option it is a checkbox card.\n\t\t */\n\t\tlayout: {\n\t\t\ttype: String,\n\t\t\tdefault: 'cards',\n\t\t\tvalidator: (value) => ['cards', 'rows'].includes(value),\n\t\t},\n\n\t\t/**\n\t\t * Columns of the card grid; 0 lets the grid fit as many as the width\n\t\t * allows. In `rows`, more than 1 sets the rows side by side\n\t\t * (DqZaakSamenwerken: Share | Hand over); 0 or 1 stacks them.\n\t\t */\n\t\tcolumns: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\n\t\t},\n\n\t\t/** Lines of description shown before clamping. */\n\t\tdescriptionLines: {\n\t\t\ttype: Number,\n\t\t\tdefault: 4,\n\t\t},\n\t},\n\n\temits: ['update:modelValue'],\n\n\tdata() {\n\t\tgroupSeq += 1\n\t\treturn {\n\t\t\t// Radios only group by `name`, so two card groups on one page would\n\t\t\t// otherwise share a selection.\n\t\t\tgroupName: 'cn-choice-cards-' + groupSeq,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Options with a usable `value` and `label`, tolerating the shapes a\n\t\t * server list arrives in: `id` instead of `value`, `name` instead of\n\t\t * `label`, and a bare string for a value that is its own label.\n\t\t *\n\t\t * @return {Array<object>} The normalised options.\n\t\t */\n\t\tnormalizedOptions() {\n\t\t\treturn (this.options || [])\n\t\t\t\t.filter((option) => option !== null && option !== undefined)\n\t\t\t\t.map((option) => {\n\t\t\t\t\tif (typeof option !== 'object') {\n\t\t\t\t\t\treturn { value: option, label: String(option) }\n\t\t\t\t\t}\n\t\t\t\t\tconst value = option.value !== undefined ? option.value : option.id\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...option,\n\t\t\t\t\t\tvalue,\n\t\t\t\t\t\tlabel: option.label || option.name || String(value),\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.filter((option) => option.value !== undefined)\n\t\t},\n\n\t\t/**\n\t\t * The current selection as an array of values, whatever the model shape.\n\t\t *\n\t\t * @return {Array} The selected values.\n\t\t */\n\t\tselectedValues() {\n\t\t\tif (this.multiple) {\n\t\t\t\treturn Array.isArray(this.modelValue) ? this.modelValue : []\n\t\t\t}\n\t\t\treturn (this.modelValue === null || this.modelValue === undefined || this.modelValue === '')\n\t\t\t\t? []\n\t\t\t\t: [this.modelValue]\n\t\t},\n\n\t\t/**\n\t\t * Inline override for the rows' columns: side by side as a grid when\n\t\t * `columns` is above 1, else the stylesheet's single column (`{}`).\n\t\t *\n\t\t * @return {object} A grid override, or `{}`.\n\t\t */\n\t\trowsStyle() {\n\t\t\tif (this.columns > 1) {\n\t\t\t\treturn { display: 'grid', gridTemplateColumns: `repeat(${this.columns}, minmax(0, 1fr))` }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\n\t\t/**\n\t\t * Inline override for the grid's column count. `auto-fit` with a\n\t\t * 260px minimum wraps a small option count onto its own row well\n\t\t * before the container is actually too narrow for them side by\n\t\t * side — two cards would stack at ~550px wide even though halving\n\t\t * the available width still leaves each one legible. A binary (or\n\t\t * near-binary) choice should always sit in one row and fill the\n\t\t * available width; a larger option count keeps the responsive\n\t\t * `auto-fit` wrapping from the stylesheet (`{}` leaves it alone).\n\t\t *\n\t\t * @return {object} A `gridTemplateColumns` override, or `{}`.\n\t\t */\n\t\tgridStyle() {\n\t\t\tif (this.columns > 0) {\n\t\t\t\treturn { gridTemplateColumns: `repeat(${this.columns}, minmax(0, 1fr))` }\n\t\t\t}\n\t\t\tconst count = this.normalizedOptions.length\n\t\t\tif (count > 0 && count <= 2) {\n\t\t\t\treturn { gridTemplateColumns: `repeat(${count}, minmax(0, 1fr))` }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Whether an option is currently selected.\n\t\t *\n\t\t * Compares as strings: a value that made the round trip through the\n\t\t * server as `\"1\"` must still match the `1` it was sent as.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {boolean} True when selected.\n\t\t */\n\t\t/**\n\t\t * Whether a rows option draws the `option-details` slot in its frame:\n\t\t * the slot is given and the option is the chosen one.\n\t\t *\n\t\t * @param {object} option The option.\n\t\t * @return {boolean}\n\t\t */\n\t\thasDetails(option) {\n\t\t\treturn Boolean(this.$slots['option-details']) && this.isSelected(option)\n\t\t},\n\n\t\t/**\n\t\t * Whether an option cannot be picked: the whole group is disabled, or the option is.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {boolean} True when disabled.\n\t\t */\n\t\tisDisabled(option) {\n\t\t\treturn this.disabled || option.disabled === true\n\t\t},\n\n\t\t/**\n\t\t * Id of an option's disabled reason, for `aria-describedby`.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {string} The id.\n\t\t */\n\t\treasonId(option) {\n\t\t\treturn `${this.groupName}-reason-${String(option.value).replace(/[^A-Za-z0-9_-]/g, '-')}`\n\t\t},\n\n\t\tisSelected(option) {\n\t\t\treturn this.selectedValues.some((v) => String(v) === String(option.value))\n\t\t},\n\n\t\t/**\n\t\t * Select (single) or toggle (multiple) an option and emit the new model.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {void}\n\t\t */\n\t\tonToggle(option) {\n\t\t\tif (this.readonly || this.isDisabled(option)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.multiple) {\n\t\t\t\t/**\n\t\t\t\t * @event update:modelValue The selected value, or the array of values when `multiple`.\n\t\t\t\t * @type {string|number|boolean|Array}\n\t\t\t\t */\n\t\t\t\t// Never the option OBJECT: a parent has to be able to hand back\n\t\t\t\t// what it receives as `modelValue`.\n\t\t\t\tthis.$emit('update:modelValue', option.value)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = this.selectedValues.filter((v) => String(v) !== String(option.value))\n\t\t\tif (next.length === this.selectedValues.length) {\n\t\t\t\tnext.push(option.value)\n\t\t\t}\n\t\t\tthis.$emit('update:modelValue', next)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped lang=\"scss\">\n.cn-choice-cards {\n\tborder: none;\n\tpadding: 0;\n\tmargin: 0;\n\tmin-inline-size: 0;\n}\n\n.cn-choice-cards__legend {\n\tpadding: 0;\n\tmargin-block-end: 8px;\n\tfont-weight: bold;\n}\n\n.cn-choice-cards__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 24px 0;\n}\n\n.cn-choice-cards__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fit, minmax(260px, 1fr));\n\tgap: 12px;\n\t/* Room for the card shadow. `src/css/card.css` is loaded globally by every\n\t consumer and gives EVERY `.cn-card` a `box-shadow` — including these — so\n\t a grid flush against its container gets the shadow shaved off wherever\n\t that container clips, which the setup wizard's dialog body does on the\n\t sides and the bottom. 12px clears the widest of those shadows. */\n\tpadding: 12px;\n}\n\n.cn-choice-cards__cell {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmin-inline-size: 0;\n}\n\n.cn-choice-cards__actions {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-choice-cards__option {\n\tposition: relative;\n\tdisplay: block;\n\tcursor: pointer;\n}\n\n.cn-choice-cards__input {\n\tposition: absolute;\n\tinset-block-start: 16px;\n\tinset-inline-end: 14px;\n\tz-index: 1;\n\tmargin: 0;\n\tcursor: pointer;\n}\n\n/* Room for the input, so a long title never runs underneath it. */\n.cn-choice-cards__option :deep(.cn-card) {\n\tpadding-inline-end: 44px;\n\ttransition: box-shadow 0.2s ease, border-color 0.2s ease;\n}\n\n/* The same global `card.css` also scales `.cn-card` up on hover. A transform\n counts towards the scrollable overflow of the nearest scroll container, so in\n a dialog body sized to its content a scrollbar appears the moment the pointer\n touches a card, and the grid twitches. The border colour below already\n carries hover, so the scale is dropped: at (0,3,0) this beats the global\n `.cn-card:hover` at (0,2,0). */\n.cn-choice-cards__option:hover :deep(.cn-card) {\n\tborder-color: var(--color-primary-element);\n\ttransform: none;\n}\n\n/* The focus ring belongs on the card, not on the small input inside it. */\n.cn-choice-cards__input:focus-visible + :deep(.cn-card) {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-choice-cards__input:disabled {\n\tcursor: default;\n}\n\n.cn-choice-cards__option--disabled .cn-choice-cards__input + :deep(.cn-card) {\n\topacity: 0.6;\n}\n\n.cn-choice-cards__rows {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n}\n\n/* 12px 14px, as the boards draw an option row (DqVolgendeStap, DqBesluit). */\n.cn-choice-cards__row {\n\tdisplay: flex;\n\talign-items: flex-start;\n\tgap: 12px;\n\tpadding: 12px 14px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n\tbackground: var(--color-main-background);\n\tcursor: pointer;\n}\n\n.cn-choice-cards__row:hover {\n\tborder-color: var(--color-border-dark);\n}\n\n.cn-choice-cards__row--selected,\n.cn-choice-cards__row--selected:hover {\n\tborder-color: var(--color-primary-element);\n\t/* The second pixel of the 2px selected border, without moving the content. */\n\tbox-shadow: inset 0 0 0 1px var(--color-primary-element);\n\tbackground: var(--color-primary-element-light);\n}\n\n/* An option this record does not allow: the grey ground and the muted ink,\n still legible (DqVolgendeStap), not faded. */\n.cn-choice-cards__row--disabled,\n.cn-choice-cards__row--disabled:hover {\n\tborder-color: var(--color-border);\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\tcursor: default;\n}\n\n/* The title takes the muted ink itself: a theme that colours every label\n with `!important` (thematiq) would otherwise hand it the label colour. */\n.cn-choice-cards__row--disabled .cn-choice-cards__row-title {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n/* The chosen option's fields share the selected row's frame: the cell draws\n the 2px primary border and the ground, the row inside it draws none, and\n the fields sit under the text, indented to it (DqVolgendeStap). */\n.cn-choice-cards__cell--with-details {\n\tborder: 1px solid var(--color-primary-element);\n\tborder-radius: var(--border-radius-large, 8px);\n\tbox-shadow: inset 0 0 0 1px var(--color-primary-element);\n\tbackground: var(--color-primary-element-light);\n}\n\n.cn-choice-cards__cell--with-details .cn-choice-cards__row,\n.cn-choice-cards__cell--with-details .cn-choice-cards__row:hover {\n\tborder-color: transparent;\n\tbox-shadow: none;\n\tbackground: transparent;\n}\n\n.cn-choice-cards__details {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 14px;\n\tpadding: 0 14px 14px 44px;\n}\n\n.cn-choice-cards__row--readonly {\n\tcursor: default;\n}\n\n.cn-choice-cards__row--readonly:not(.cn-choice-cards__row--selected):hover {\n\tborder-color: var(--color-border);\n}\n\n.cn-choice-cards__row:has(.cn-choice-cards__row-input:focus-visible) {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-choice-cards__row-input {\n\tflex: none;\n\tmargin: 2px 0 0;\n\taccent-color: var(--color-primary-element);\n\tcursor: inherit;\n}\n\n.cn-choice-cards__row-icon {\n\tflex: none;\n}\n\n.cn-choice-cards__row-text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-choice-cards__row-title {\n\tfont-size: 15px;\n\tfont-weight: 600;\n\tline-height: 1.2;\n}\n\n/* Regular weight on the span itself: a theme that sets every label bold\n with `!important` (thematiq) reaches a span that only inherits. */\n.cn-choice-cards__row-description,\n.cn-choice-cards__reason {\n\tfont-size: 13px;\n\tfont-weight: 400;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-choice-cards__reason--card {\n\tdisplay: block;\n\tmargin-block-start: 4px;\n}\n</style>\n"],"names":["CnCard","CnIcon","t"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnChoiceCards.vue3.js","sources":["../../../../src/components/CnChoiceCards/CnChoiceCards.vue?vue&type=template&id=cfc2a11a&lang.js"],"sourcesContent":["<!-- SPDX-License-Identifier: EUPL-1.2 -->\n<!-- SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl> -->\n<template>\n\t<fieldset class=\"cn-choice-cards\" :aria-busy=\"loading ? 'true' : 'false'\">\n\t\t<legend v-if=\"label\" class=\"cn-choice-cards__legend\" :class=\"{ 'hidden-visually': hideLabel }\">\n\t\t\t{{ label }}\n\t\t</legend>\n\n\t\t<div v-if=\"loading\" class=\"cn-choice-cards__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t</div>\n\n\t\t<NcNoteCard v-else-if=\"normalizedOptions.length === 0\" type=\"warning\">\n\t\t\t{{ emptyText }}\n\t\t</NcNoteCard>\n\n\t\t<!-- Rows: one full-width option per row, the input on the left and the\n\t\t title over a muted description, no card chrome. -->\n\t\t<div v-else-if=\"layout === 'rows'\"\n\t\t\tclass=\"cn-choice-cards__rows\"\n\t\t\t:style=\"rowsStyle\"\n\t\t\tdata-testid=\"cn-choice-cards-rows\">\n\t\t\t<div\n\t\t\t\tv-for=\"option in normalizedOptions\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-choice-cards__cell\"\n\t\t\t\t:class=\"{ 'cn-choice-cards__cell--with-details': hasDetails(option) }\">\n\t\t\t\t<label\n\t\t\t\t\tclass=\"cn-choice-cards__row\"\n\t\t\t\t\t:class=\"{ 'cn-choice-cards__row--selected': isSelected(option), 'cn-choice-cards__row--disabled': isDisabled(option), 'cn-choice-cards__row--readonly': readonly }\">\n\t\t\t\t\t<input\n\t\t\t\t\t\tclass=\"cn-choice-cards__row-input\"\n\t\t\t\t\t\t:type=\"multiple ? 'checkbox' : 'radio'\"\n\t\t\t\t\t\t:name=\"groupName\"\n\t\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t\t:checked=\"isSelected(option)\"\n\t\t\t\t\t\t:disabled=\"isDisabled(option) || readonly\"\n\t\t\t\t\t\t:aria-describedby=\"option.disabled && option.disabledReason ? reasonId(option) : null\"\n\t\t\t\t\t\t@change=\"onToggle(option)\">\n\t\t\t\t\t<CnIcon v-if=\"option.icon\"\n\t\t\t\t\t\t:name=\"option.icon\"\n\t\t\t\t\t\t:size=\"20\"\n\t\t\t\t\t\tclass=\"cn-choice-cards__row-icon\" />\n\t\t\t\t\t<span class=\"cn-choice-cards__row-text\">\n\t\t\t\t\t\t<span class=\"cn-choice-cards__row-title\">{{ option.label }}</span>\n\t\t\t\t\t\t<span v-if=\"option.description\" class=\"cn-choice-cards__row-description\">{{ option.description }}</span>\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tv-if=\"option.disabled && option.disabledReason\"\n\t\t\t\t\t\t\t:id=\"reasonId(option)\"\n\t\t\t\t\t\t\tclass=\"cn-choice-cards__reason\"\n\t\t\t\t\t\t\tdata-testid=\"cn-choice-cards-reason\">{{ option.disabledReason }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<div v-if=\"hasDetails(option)\"\n\t\t\t\t\tclass=\"cn-choice-cards__details\"\n\t\t\t\t\t:data-testid=\"`cn-choice-cards-details-${option.value}`\">\n\t\t\t\t\t<!-- @slot option-details The chosen option's own fields, drawn\n\t\t\t\t\t inside the selected row's frame under its text (DqVolgendeStap:\n\t\t\t\t\t Opschorten's reason, days and return date). Rendered only for a\n\t\t\t\t\t selected option, outside the label so a field is not part of\n\t\t\t\t\t the radio's label. Scope: `{ option }`. -->\n\t\t\t\t\t<slot name=\"option-details\" :option=\"option\" />\n\t\t\t\t</div>\n\t\t\t\t<div v-if=\"$slots['option-actions']\" class=\"cn-choice-cards__actions\">\n\t\t\t\t\t<slot name=\"option-actions\" :option=\"option\" :selected=\"isSelected(option)\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div v-else class=\"cn-choice-cards__grid\" :style=\"gridStyle\">\n\t\t\t<!-- One cell per option: the card's label, and below it whatever the\n\t\t\t `#option-actions` slot adds. The slot sits OUTSIDE the label on\n\t\t\t purpose: a button inside a label is interactive content inside\n\t\t\t interactive content, and a click on it would toggle the radio. -->\n\t\t\t<div\n\t\t\t\tv-for=\"option in normalizedOptions\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-choice-cards__cell\">\n\t\t\t\t<label\n\t\t\t\t\tclass=\"cn-choice-cards__option\"\n\t\t\t\t\t:class=\"{ 'cn-choice-cards__option--selected': isSelected(option), 'cn-choice-cards__option--disabled': isDisabled(option) }\">\n\t\t\t\t\t<!-- A REAL, VISIBLE input, not a styled div with a click handler.\n\t\t\t\t\t It carries the checked state to assistive technology and to\n\t\t\t\t\t Windows high-contrast mode for free, and it keeps the\n\t\t\t\t\t selection perceivable without colour (WCAG 1.4.1) — the\n\t\t\t\t\t card's active border alone would not be. -->\n\t\t\t\t\t<input\n\t\t\t\t\t\tclass=\"cn-choice-cards__input\"\n\t\t\t\t\t\t:type=\"multiple ? 'checkbox' : 'radio'\"\n\t\t\t\t\t\t:name=\"groupName\"\n\t\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t\t:checked=\"isSelected(option)\"\n\t\t\t\t\t\t:disabled=\"isDisabled(option) || readonly\"\n\t\t\t\t\t\t:aria-describedby=\"option.disabled && option.disabledReason ? reasonId(option) : null\"\n\t\t\t\t\t\t@change=\"onToggle(option)\">\n\t\t\t\t\t<CnCard\n\t\t\t\t\t\tclass=\"cn-choice-cards__card\"\n\t\t\t\t\t\t:title=\"option.label\"\n\t\t\t\t\t\ttitleTag=\"span\"\n\t\t\t\t\t\t:description=\"option.description\"\n\t\t\t\t\t\t:stats=\"option.stats || []\"\n\t\t\t\t\t\t:tags=\"option.tags || []\"\n\t\t\t\t\t\t:active=\"isSelected(option)\"\n\t\t\t\t\t\tactiveVariant=\"primary\"\n\t\t\t\t\t\t:descriptionLines=\"descriptionLines\">\n\t\t\t\t\t\t<template v-if=\"option.icon\" #icon>\n\t\t\t\t\t\t\t<CnIcon :name=\"option.icon\" :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</CnCard>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"option.disabled && option.disabledReason\"\n\t\t\t\t\t\t:id=\"reasonId(option)\"\n\t\t\t\t\t\tclass=\"cn-choice-cards__reason cn-choice-cards__reason--card\"\n\t\t\t\t\t\tdata-testid=\"cn-choice-cards-reason\">{{ option.disabledReason }}</span>\n\t\t\t\t</label>\n\t\t\t\t<div v-if=\"$slots['option-actions']\" class=\"cn-choice-cards__actions\">\n\t\t\t\t\t<!-- @slot option-actions Content below one card, outside its label\n\t\t\t\t\t (e.g. a per-option Load button). Scope: `{ option, selected }`. -->\n\t\t\t\t\t<slot name=\"option-actions\" :option=\"option\" :selected=\"isSelected(option)\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</fieldset>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport CnCard from '../CnCard/CnCard.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\n\nlet groupSeq = 0\n\n/**\n * CnChoiceCards — pick one option, or several, from a grid of cards.\n *\n * A card carries what a dropdown entry cannot: a description of what the\n * option actually is, and a few numbers about it. Use it where the choice\n * deserves explaining — the setup wizard's example-data step, an onboarding\n * template picker — and a plain `NcSelect` where a label already says enough.\n *\n * Each card is a `<label>` around a real radio (or checkbox when `multiple`),\n * so keyboard navigation, the checked state and high-contrast rendering are\n * the browser's, not a reimplementation.\n *\n * ```vue\n * <CnChoiceCards\n * v-model=\"picked\"\n * label=\"Which kind of organisation is this for?\"\n * :options=\"[\n * { value: 'municipality', label: 'Municipality',\n * description: 'A city council with committees and a decision list.',\n * stats: [{ label: 'Objects', value: 170 }] },\n * { value: 'none', label: 'None, I will set this up myself' },\n * ]\" />\n * ```\n */\nexport default {\n\tname: 'CnChoiceCards',\n\n\tcomponents: {\n\t\tCnCard,\n\t\tCnIcon,\n\t\tNcLoadingIcon,\n\t\tNcNoteCard,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The options to offer. Each: `{ value, label, description?, stats?,\n\t\t * tags?, icon?, disabled?, disabledReason? }` — `icon` is a PascalCase\n\t\t * MDI name resolved by CnIcon; a `disabled` option is greyed out and\n\t\t * cannot be picked, with `disabledReason` shown under it.\n\t\t */\n\t\toptions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The selected value: a scalar, or an array when `multiple`.\n\t\t *\n\t\t * @type {string|number|boolean|Array|null}\n\t\t */\n\t\tmodelValue: {\n\t\t\t// eslint-disable-next-line vue/prefer-prop-type-boolean-first -- an option value of '' must stay a string\n\t\t\ttype: [String, Number, Boolean, Array, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Allow selecting several cards; the model becomes an array. */\n\t\tmultiple: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Group label, rendered as the fieldset's legend. */\n\t\tlabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Keep the legend for assistive tech only, where the dialog's title\n\t\t * already says what is chosen (DqZaakSamenwerken).\n\t\t */\n\t\thideLabel: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Disable every card (e.g. a dependent choice with no parent value yet). */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Show the choice without letting it change: every option at full\n\t\t * contrast, the selected one marked, nothing pickable (DqBesluit's\n\t\t * recorded outcome). `disabled` greys the options out instead.\n\t\t */\n\t\treadonly: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Show a spinner instead of the grid while options are being fetched. */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Message shown when there is nothing to choose from. */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Nothing to choose from here.'),\n\t\t},\n\n\t\t/**\n\t\t * `cards`: a grid of cards with stats and tags. `rows`: one option per\n\t\t * row, full width, the input on the left and the title over a muted\n\t\t * description; with `multiple` and one option it is a checkbox card.\n\t\t */\n\t\tlayout: {\n\t\t\ttype: String,\n\t\t\tdefault: 'cards',\n\t\t\tvalidator: (value) => ['cards', 'rows'].includes(value),\n\t\t},\n\n\t\t/**\n\t\t * Columns of the card grid; 0 lets the grid fit as many as the width\n\t\t * allows. In `rows`, more than 1 sets the rows side by side\n\t\t * (DqZaakSamenwerken: Share | Hand over); 0 or 1 stacks them.\n\t\t */\n\t\tcolumns: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\n\t\t},\n\n\t\t/** Lines of description shown before clamping. */\n\t\tdescriptionLines: {\n\t\t\ttype: Number,\n\t\t\tdefault: 4,\n\t\t},\n\t},\n\n\temits: ['update:modelValue'],\n\n\tdata() {\n\t\tgroupSeq += 1\n\t\treturn {\n\t\t\t// Radios only group by `name`, so two card groups on one page would\n\t\t\t// otherwise share a selection.\n\t\t\tgroupName: 'cn-choice-cards-' + groupSeq,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Options with a usable `value` and `label`, tolerating the shapes a\n\t\t * server list arrives in: `id` instead of `value`, `name` instead of\n\t\t * `label`, and a bare string for a value that is its own label.\n\t\t *\n\t\t * @return {Array<object>} The normalised options.\n\t\t */\n\t\tnormalizedOptions() {\n\t\t\treturn (this.options || [])\n\t\t\t\t.filter((option) => option !== null && option !== undefined)\n\t\t\t\t.map((option) => {\n\t\t\t\t\tif (typeof option !== 'object') {\n\t\t\t\t\t\treturn { value: option, label: String(option) }\n\t\t\t\t\t}\n\t\t\t\t\tconst value = option.value !== undefined ? option.value : option.id\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...option,\n\t\t\t\t\t\tvalue,\n\t\t\t\t\t\tlabel: option.label || option.name || String(value),\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.filter((option) => option.value !== undefined)\n\t\t},\n\n\t\t/**\n\t\t * The current selection as an array of values, whatever the model shape.\n\t\t *\n\t\t * @return {Array} The selected values.\n\t\t */\n\t\tselectedValues() {\n\t\t\tif (this.multiple) {\n\t\t\t\treturn Array.isArray(this.modelValue) ? this.modelValue : []\n\t\t\t}\n\t\t\treturn (this.modelValue === null || this.modelValue === undefined || this.modelValue === '')\n\t\t\t\t? []\n\t\t\t\t: [this.modelValue]\n\t\t},\n\n\t\t/**\n\t\t * Inline override for the rows' columns: side by side as a grid when\n\t\t * `columns` is above 1, else the stylesheet's single column (`{}`).\n\t\t *\n\t\t * @return {object} A grid override, or `{}`.\n\t\t */\n\t\trowsStyle() {\n\t\t\tif (this.columns > 1) {\n\t\t\t\treturn { display: 'grid', gridTemplateColumns: `repeat(${this.columns}, minmax(0, 1fr))` }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\n\t\t/**\n\t\t * Inline override for the grid's column count. `auto-fit` with a\n\t\t * 260px minimum wraps a small option count onto its own row well\n\t\t * before the container is actually too narrow for them side by\n\t\t * side — two cards would stack at ~550px wide even though halving\n\t\t * the available width still leaves each one legible. A binary (or\n\t\t * near-binary) choice should always sit in one row and fill the\n\t\t * available width; a larger option count keeps the responsive\n\t\t * `auto-fit` wrapping from the stylesheet (`{}` leaves it alone).\n\t\t *\n\t\t * @return {object} A `gridTemplateColumns` override, or `{}`.\n\t\t */\n\t\tgridStyle() {\n\t\t\tif (this.columns > 0) {\n\t\t\t\treturn { gridTemplateColumns: `repeat(${this.columns}, minmax(0, 1fr))` }\n\t\t\t}\n\t\t\tconst count = this.normalizedOptions.length\n\t\t\tif (count > 0 && count <= 2) {\n\t\t\t\treturn { gridTemplateColumns: `repeat(${count}, minmax(0, 1fr))` }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Whether an option is currently selected.\n\t\t *\n\t\t * Compares as strings: a value that made the round trip through the\n\t\t * server as `\"1\"` must still match the `1` it was sent as.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {boolean} True when selected.\n\t\t */\n\t\t/**\n\t\t * Whether a rows option draws the `option-details` slot in its frame:\n\t\t * the slot is given and the option is the chosen one.\n\t\t *\n\t\t * @param {object} option The option.\n\t\t * @return {boolean}\n\t\t */\n\t\thasDetails(option) {\n\t\t\treturn Boolean(this.$slots['option-details']) && this.isSelected(option)\n\t\t},\n\n\t\t/**\n\t\t * Whether an option cannot be picked: the whole group is disabled, or the option is.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {boolean} True when disabled.\n\t\t */\n\t\tisDisabled(option) {\n\t\t\treturn this.disabled || option.disabled === true\n\t\t},\n\n\t\t/**\n\t\t * Id of an option's disabled reason, for `aria-describedby`.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {string} The id.\n\t\t */\n\t\treasonId(option) {\n\t\t\treturn `${this.groupName}-reason-${String(option.value).replace(/[^A-Za-z0-9_-]/g, '-')}`\n\t\t},\n\n\t\tisSelected(option) {\n\t\t\treturn this.selectedValues.some((v) => String(v) === String(option.value))\n\t\t},\n\n\t\t/**\n\t\t * Select (single) or toggle (multiple) an option and emit the new model.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {void}\n\t\t */\n\t\tonToggle(option) {\n\t\t\tif (this.readonly || this.isDisabled(option)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.multiple) {\n\t\t\t\t/**\n\t\t\t\t * @event update:modelValue The selected value, or the array of values when `multiple`.\n\t\t\t\t * @type {string|number|boolean|Array}\n\t\t\t\t */\n\t\t\t\t// Never the option OBJECT: a parent has to be able to hand back\n\t\t\t\t// what it receives as `modelValue`.\n\t\t\t\tthis.$emit('update:modelValue', option.value)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = this.selectedValues.filter((v) => String(v) !== String(option.value))\n\t\t\tif (next.length === this.selectedValues.length) {\n\t\t\t\tnext.push(option.value)\n\t\t\t}\n\t\t\tthis.$emit('update:modelValue', next)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped lang=\"scss\">\n.cn-choice-cards {\n\tborder: none;\n\tpadding: 0;\n\tmargin: 0;\n\tmin-inline-size: 0;\n}\n\n.cn-choice-cards__legend {\n\tpadding: 0;\n\tmargin-block-end: 8px;\n\tfont-weight: bold;\n}\n\n.cn-choice-cards__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 24px 0;\n}\n\n.cn-choice-cards__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fit, minmax(260px, 1fr));\n\tgap: 12px;\n\t/* Room for the card shadow. `src/css/card.css` is loaded globally by every\n\t consumer and gives EVERY `.cn-card` a `box-shadow` — including these — so\n\t a grid flush against its container gets the shadow shaved off wherever\n\t that container clips, which the setup wizard's dialog body does on the\n\t sides and the bottom. 12px clears the widest of those shadows. */\n\tpadding: 12px;\n}\n\n.cn-choice-cards__cell {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmin-inline-size: 0;\n}\n\n.cn-choice-cards__actions {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-choice-cards__option {\n\tposition: relative;\n\tdisplay: block;\n\tcursor: pointer;\n}\n\n.cn-choice-cards__input {\n\tposition: absolute;\n\tinset-block-start: 16px;\n\tinset-inline-end: 14px;\n\tz-index: 1;\n\tmargin: 0;\n\tcursor: pointer;\n}\n\n/* Room for the input, so a long title never runs underneath it. */\n.cn-choice-cards__option :deep(.cn-card) {\n\tpadding-inline-end: 44px;\n\ttransition: box-shadow 0.2s ease, border-color 0.2s ease;\n}\n\n/* The same global `card.css` also scales `.cn-card` up on hover. A transform\n counts towards the scrollable overflow of the nearest scroll container, so in\n a dialog body sized to its content a scrollbar appears the moment the pointer\n touches a card, and the grid twitches. The border colour below already\n carries hover, so the scale is dropped: at (0,3,0) this beats the global\n `.cn-card:hover` at (0,2,0). */\n.cn-choice-cards__option:hover :deep(.cn-card) {\n\tborder-color: var(--color-primary-element);\n\ttransform: none;\n}\n\n/* The focus ring belongs on the card, not on the small input inside it. */\n.cn-choice-cards__input:focus-visible + :deep(.cn-card) {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-choice-cards__input:disabled {\n\tcursor: default;\n}\n\n.cn-choice-cards__option--disabled .cn-choice-cards__input + :deep(.cn-card) {\n\topacity: 0.6;\n}\n\n.cn-choice-cards__rows {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n}\n\n/* 12px 14px, as the boards draw an option row (DqVolgendeStap, DqBesluit). */\n.cn-choice-cards__row {\n\tdisplay: flex;\n\talign-items: flex-start;\n\tgap: 12px;\n\tpadding: 12px 14px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n\tbackground: var(--color-main-background);\n\tcursor: pointer;\n}\n\n.cn-choice-cards__row:hover {\n\tborder-color: var(--color-border-dark);\n}\n\n.cn-choice-cards__row--selected,\n.cn-choice-cards__row--selected:hover {\n\tborder-color: var(--color-primary-element);\n\t/* The second pixel of the 2px selected border, without moving the content. */\n\tbox-shadow: inset 0 0 0 1px var(--color-primary-element);\n\tbackground: var(--color-primary-element-light);\n}\n\n/* An option this record does not allow: the grey ground and the muted ink,\n still legible (DqVolgendeStap), not faded. */\n.cn-choice-cards__row--disabled,\n.cn-choice-cards__row--disabled:hover {\n\tborder-color: var(--color-border);\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\tcursor: default;\n}\n\n/* The title takes the muted ink itself: a theme that colours every label\n with `!important` (thematiq) would otherwise hand it the label colour. */\n.cn-choice-cards__row--disabled .cn-choice-cards__row-title {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n/* The chosen option's fields share the selected row's frame: the cell draws\n the 2px primary border and the ground, the row inside it draws none, and\n the fields sit under the text, indented to it (DqVolgendeStap). */\n.cn-choice-cards__cell--with-details {\n\tborder: 1px solid var(--color-primary-element);\n\tborder-radius: var(--border-radius-large, 8px);\n\tbox-shadow: inset 0 0 0 1px var(--color-primary-element);\n\tbackground: var(--color-primary-element-light);\n}\n\n.cn-choice-cards__cell--with-details .cn-choice-cards__row,\n.cn-choice-cards__cell--with-details .cn-choice-cards__row:hover {\n\tborder-color: transparent;\n\tbox-shadow: none;\n\tbackground: transparent;\n}\n\n.cn-choice-cards__details {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 14px;\n\tpadding: 0 14px 14px 44px;\n}\n\n.cn-choice-cards__row--readonly {\n\tcursor: default;\n}\n\n.cn-choice-cards__row--readonly:not(.cn-choice-cards__row--selected):hover {\n\tborder-color: var(--color-border);\n}\n\n.cn-choice-cards__row:has(.cn-choice-cards__row-input:focus-visible) {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-choice-cards__row-input {\n\tflex: none;\n\tmargin: 2px 0 0;\n\taccent-color: var(--color-primary-element);\n\tcursor: inherit;\n}\n\n.cn-choice-cards__row-icon {\n\tflex: none;\n}\n\n.cn-choice-cards__row-text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-choice-cards__row-title {\n\tfont-size: 15px;\n\tfont-weight: 600;\n}\n\n/* Regular weight on the span itself: a theme that sets every label bold\n with `!important` (thematiq) reaches a span that only inherits. */\n.cn-choice-cards__row-description,\n.cn-choice-cards__reason {\n\tfont-size: 13px;\n\tfont-weight: 400;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-choice-cards__reason--card {\n\tdisplay: block;\n\tmargin-block-start: 4px;\n}\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_openBlock","_createBlock","_createElementVNode","_toDisplayString","_renderSlot","_Fragment","_renderList","_createVNode"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnChoiceCards.vue3.js","sources":["../../../../src/components/CnChoiceCards/CnChoiceCards.vue?vue&type=template&id=51150930&lang.js"],"sourcesContent":["<!-- SPDX-License-Identifier: EUPL-1.2 -->\n<!-- SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl> -->\n<template>\n\t<fieldset class=\"cn-choice-cards\" :aria-busy=\"loading ? 'true' : 'false'\">\n\t\t<legend v-if=\"label\" class=\"cn-choice-cards__legend\" :class=\"{ 'hidden-visually': hideLabel }\">\n\t\t\t{{ label }}\n\t\t</legend>\n\n\t\t<div v-if=\"loading\" class=\"cn-choice-cards__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t</div>\n\n\t\t<NcNoteCard v-else-if=\"normalizedOptions.length === 0\" type=\"warning\">\n\t\t\t{{ emptyText }}\n\t\t</NcNoteCard>\n\n\t\t<!-- Rows: one full-width option per row, the input on the left and the\n\t\t title over a muted description, no card chrome. -->\n\t\t<div v-else-if=\"layout === 'rows'\"\n\t\t\tclass=\"cn-choice-cards__rows\"\n\t\t\t:style=\"rowsStyle\"\n\t\t\tdata-testid=\"cn-choice-cards-rows\">\n\t\t\t<div\n\t\t\t\tv-for=\"option in normalizedOptions\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-choice-cards__cell\"\n\t\t\t\t:class=\"{ 'cn-choice-cards__cell--with-details': hasDetails(option) }\">\n\t\t\t\t<label\n\t\t\t\t\tclass=\"cn-choice-cards__row\"\n\t\t\t\t\t:class=\"{ 'cn-choice-cards__row--selected': isSelected(option), 'cn-choice-cards__row--disabled': isDisabled(option), 'cn-choice-cards__row--readonly': readonly }\">\n\t\t\t\t\t<input\n\t\t\t\t\t\tclass=\"cn-choice-cards__row-input\"\n\t\t\t\t\t\t:type=\"multiple ? 'checkbox' : 'radio'\"\n\t\t\t\t\t\t:name=\"groupName\"\n\t\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t\t:checked=\"isSelected(option)\"\n\t\t\t\t\t\t:disabled=\"isDisabled(option) || readonly\"\n\t\t\t\t\t\t:aria-describedby=\"option.disabled && option.disabledReason ? reasonId(option) : null\"\n\t\t\t\t\t\t@change=\"onToggle(option)\">\n\t\t\t\t\t<CnIcon v-if=\"option.icon\"\n\t\t\t\t\t\t:name=\"option.icon\"\n\t\t\t\t\t\t:size=\"20\"\n\t\t\t\t\t\tclass=\"cn-choice-cards__row-icon\" />\n\t\t\t\t\t<span class=\"cn-choice-cards__row-text\">\n\t\t\t\t\t\t<span class=\"cn-choice-cards__row-title\">{{ option.label }}</span>\n\t\t\t\t\t\t<span v-if=\"option.description\" class=\"cn-choice-cards__row-description\">{{ option.description }}</span>\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tv-if=\"option.disabled && option.disabledReason\"\n\t\t\t\t\t\t\t:id=\"reasonId(option)\"\n\t\t\t\t\t\t\tclass=\"cn-choice-cards__reason\"\n\t\t\t\t\t\t\tdata-testid=\"cn-choice-cards-reason\">{{ option.disabledReason }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t</label>\n\t\t\t\t<div v-if=\"hasDetails(option)\"\n\t\t\t\t\tclass=\"cn-choice-cards__details\"\n\t\t\t\t\t:data-testid=\"`cn-choice-cards-details-${option.value}`\">\n\t\t\t\t\t<!-- @slot option-details The chosen option's own fields, drawn\n\t\t\t\t\t inside the selected row's frame under its text (DqVolgendeStap:\n\t\t\t\t\t Opschorten's reason, days and return date). Rendered only for a\n\t\t\t\t\t selected option, outside the label so a field is not part of\n\t\t\t\t\t the radio's label. Scope: `{ option }`. -->\n\t\t\t\t\t<slot name=\"option-details\" :option=\"option\" />\n\t\t\t\t</div>\n\t\t\t\t<div v-if=\"$slots['option-actions']\" class=\"cn-choice-cards__actions\">\n\t\t\t\t\t<slot name=\"option-actions\" :option=\"option\" :selected=\"isSelected(option)\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div v-else class=\"cn-choice-cards__grid\" :style=\"gridStyle\">\n\t\t\t<!-- One cell per option: the card's label, and below it whatever the\n\t\t\t `#option-actions` slot adds. The slot sits OUTSIDE the label on\n\t\t\t purpose: a button inside a label is interactive content inside\n\t\t\t interactive content, and a click on it would toggle the radio. -->\n\t\t\t<div\n\t\t\t\tv-for=\"option in normalizedOptions\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-choice-cards__cell\">\n\t\t\t\t<label\n\t\t\t\t\tclass=\"cn-choice-cards__option\"\n\t\t\t\t\t:class=\"{ 'cn-choice-cards__option--selected': isSelected(option), 'cn-choice-cards__option--disabled': isDisabled(option) }\">\n\t\t\t\t\t<!-- A REAL, VISIBLE input, not a styled div with a click handler.\n\t\t\t\t\t It carries the checked state to assistive technology and to\n\t\t\t\t\t Windows high-contrast mode for free, and it keeps the\n\t\t\t\t\t selection perceivable without colour (WCAG 1.4.1) — the\n\t\t\t\t\t card's active border alone would not be. -->\n\t\t\t\t\t<input\n\t\t\t\t\t\tclass=\"cn-choice-cards__input\"\n\t\t\t\t\t\t:type=\"multiple ? 'checkbox' : 'radio'\"\n\t\t\t\t\t\t:name=\"groupName\"\n\t\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t\t:checked=\"isSelected(option)\"\n\t\t\t\t\t\t:disabled=\"isDisabled(option) || readonly\"\n\t\t\t\t\t\t:aria-describedby=\"option.disabled && option.disabledReason ? reasonId(option) : null\"\n\t\t\t\t\t\t@change=\"onToggle(option)\">\n\t\t\t\t\t<CnCard\n\t\t\t\t\t\tclass=\"cn-choice-cards__card\"\n\t\t\t\t\t\t:title=\"option.label\"\n\t\t\t\t\t\ttitleTag=\"span\"\n\t\t\t\t\t\t:description=\"option.description\"\n\t\t\t\t\t\t:stats=\"option.stats || []\"\n\t\t\t\t\t\t:tags=\"option.tags || []\"\n\t\t\t\t\t\t:active=\"isSelected(option)\"\n\t\t\t\t\t\tactiveVariant=\"primary\"\n\t\t\t\t\t\t:descriptionLines=\"descriptionLines\">\n\t\t\t\t\t\t<template v-if=\"option.icon\" #icon>\n\t\t\t\t\t\t\t<CnIcon :name=\"option.icon\" :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</CnCard>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"option.disabled && option.disabledReason\"\n\t\t\t\t\t\t:id=\"reasonId(option)\"\n\t\t\t\t\t\tclass=\"cn-choice-cards__reason cn-choice-cards__reason--card\"\n\t\t\t\t\t\tdata-testid=\"cn-choice-cards-reason\">{{ option.disabledReason }}</span>\n\t\t\t\t</label>\n\t\t\t\t<div v-if=\"$slots['option-actions']\" class=\"cn-choice-cards__actions\">\n\t\t\t\t\t<!-- @slot option-actions Content below one card, outside its label\n\t\t\t\t\t (e.g. a per-option Load button). Scope: `{ option, selected }`. -->\n\t\t\t\t\t<slot name=\"option-actions\" :option=\"option\" :selected=\"isSelected(option)\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</fieldset>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport CnCard from '../CnCard/CnCard.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\n\nlet groupSeq = 0\n\n/**\n * CnChoiceCards — pick one option, or several, from a grid of cards.\n *\n * A card carries what a dropdown entry cannot: a description of what the\n * option actually is, and a few numbers about it. Use it where the choice\n * deserves explaining — the setup wizard's example-data step, an onboarding\n * template picker — and a plain `NcSelect` where a label already says enough.\n *\n * Each card is a `<label>` around a real radio (or checkbox when `multiple`),\n * so keyboard navigation, the checked state and high-contrast rendering are\n * the browser's, not a reimplementation.\n *\n * ```vue\n * <CnChoiceCards\n * v-model=\"picked\"\n * label=\"Which kind of organisation is this for?\"\n * :options=\"[\n * { value: 'municipality', label: 'Municipality',\n * description: 'A city council with committees and a decision list.',\n * stats: [{ label: 'Objects', value: 170 }] },\n * { value: 'none', label: 'None, I will set this up myself' },\n * ]\" />\n * ```\n */\nexport default {\n\tname: 'CnChoiceCards',\n\n\tcomponents: {\n\t\tCnCard,\n\t\tCnIcon,\n\t\tNcLoadingIcon,\n\t\tNcNoteCard,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The options to offer. Each: `{ value, label, description?, stats?,\n\t\t * tags?, icon?, disabled?, disabledReason? }` — `icon` is a PascalCase\n\t\t * MDI name resolved by CnIcon; a `disabled` option is greyed out and\n\t\t * cannot be picked, with `disabledReason` shown under it.\n\t\t */\n\t\toptions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The selected value: a scalar, or an array when `multiple`.\n\t\t *\n\t\t * @type {string|number|boolean|Array|null}\n\t\t */\n\t\tmodelValue: {\n\t\t\t// eslint-disable-next-line vue/prefer-prop-type-boolean-first -- an option value of '' must stay a string\n\t\t\ttype: [String, Number, Boolean, Array, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Allow selecting several cards; the model becomes an array. */\n\t\tmultiple: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Group label, rendered as the fieldset's legend. */\n\t\tlabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Keep the legend for assistive tech only, where the dialog's title\n\t\t * already says what is chosen (DqZaakSamenwerken).\n\t\t */\n\t\thideLabel: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Disable every card (e.g. a dependent choice with no parent value yet). */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Show the choice without letting it change: every option at full\n\t\t * contrast, the selected one marked, nothing pickable (DqBesluit's\n\t\t * recorded outcome). `disabled` greys the options out instead.\n\t\t */\n\t\treadonly: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Show a spinner instead of the grid while options are being fetched. */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Message shown when there is nothing to choose from. */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Nothing to choose from here.'),\n\t\t},\n\n\t\t/**\n\t\t * `cards`: a grid of cards with stats and tags. `rows`: one option per\n\t\t * row, full width, the input on the left and the title over a muted\n\t\t * description; with `multiple` and one option it is a checkbox card.\n\t\t */\n\t\tlayout: {\n\t\t\ttype: String,\n\t\t\tdefault: 'cards',\n\t\t\tvalidator: (value) => ['cards', 'rows'].includes(value),\n\t\t},\n\n\t\t/**\n\t\t * Columns of the card grid; 0 lets the grid fit as many as the width\n\t\t * allows. In `rows`, more than 1 sets the rows side by side\n\t\t * (DqZaakSamenwerken: Share | Hand over); 0 or 1 stacks them.\n\t\t */\n\t\tcolumns: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\n\t\t},\n\n\t\t/** Lines of description shown before clamping. */\n\t\tdescriptionLines: {\n\t\t\ttype: Number,\n\t\t\tdefault: 4,\n\t\t},\n\t},\n\n\temits: ['update:modelValue'],\n\n\tdata() {\n\t\tgroupSeq += 1\n\t\treturn {\n\t\t\t// Radios only group by `name`, so two card groups on one page would\n\t\t\t// otherwise share a selection.\n\t\t\tgroupName: 'cn-choice-cards-' + groupSeq,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Options with a usable `value` and `label`, tolerating the shapes a\n\t\t * server list arrives in: `id` instead of `value`, `name` instead of\n\t\t * `label`, and a bare string for a value that is its own label.\n\t\t *\n\t\t * @return {Array<object>} The normalised options.\n\t\t */\n\t\tnormalizedOptions() {\n\t\t\treturn (this.options || [])\n\t\t\t\t.filter((option) => option !== null && option !== undefined)\n\t\t\t\t.map((option) => {\n\t\t\t\t\tif (typeof option !== 'object') {\n\t\t\t\t\t\treturn { value: option, label: String(option) }\n\t\t\t\t\t}\n\t\t\t\t\tconst value = option.value !== undefined ? option.value : option.id\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...option,\n\t\t\t\t\t\tvalue,\n\t\t\t\t\t\tlabel: option.label || option.name || String(value),\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.filter((option) => option.value !== undefined)\n\t\t},\n\n\t\t/**\n\t\t * The current selection as an array of values, whatever the model shape.\n\t\t *\n\t\t * @return {Array} The selected values.\n\t\t */\n\t\tselectedValues() {\n\t\t\tif (this.multiple) {\n\t\t\t\treturn Array.isArray(this.modelValue) ? this.modelValue : []\n\t\t\t}\n\t\t\treturn (this.modelValue === null || this.modelValue === undefined || this.modelValue === '')\n\t\t\t\t? []\n\t\t\t\t: [this.modelValue]\n\t\t},\n\n\t\t/**\n\t\t * Inline override for the rows' columns: side by side as a grid when\n\t\t * `columns` is above 1, else the stylesheet's single column (`{}`).\n\t\t *\n\t\t * @return {object} A grid override, or `{}`.\n\t\t */\n\t\trowsStyle() {\n\t\t\tif (this.columns > 1) {\n\t\t\t\treturn { display: 'grid', gridTemplateColumns: `repeat(${this.columns}, minmax(0, 1fr))` }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\n\t\t/**\n\t\t * Inline override for the grid's column count. `auto-fit` with a\n\t\t * 260px minimum wraps a small option count onto its own row well\n\t\t * before the container is actually too narrow for them side by\n\t\t * side — two cards would stack at ~550px wide even though halving\n\t\t * the available width still leaves each one legible. A binary (or\n\t\t * near-binary) choice should always sit in one row and fill the\n\t\t * available width; a larger option count keeps the responsive\n\t\t * `auto-fit` wrapping from the stylesheet (`{}` leaves it alone).\n\t\t *\n\t\t * @return {object} A `gridTemplateColumns` override, or `{}`.\n\t\t */\n\t\tgridStyle() {\n\t\t\tif (this.columns > 0) {\n\t\t\t\treturn { gridTemplateColumns: `repeat(${this.columns}, minmax(0, 1fr))` }\n\t\t\t}\n\t\t\tconst count = this.normalizedOptions.length\n\t\t\tif (count > 0 && count <= 2) {\n\t\t\t\treturn { gridTemplateColumns: `repeat(${count}, minmax(0, 1fr))` }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Whether an option is currently selected.\n\t\t *\n\t\t * Compares as strings: a value that made the round trip through the\n\t\t * server as `\"1\"` must still match the `1` it was sent as.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {boolean} True when selected.\n\t\t */\n\t\t/**\n\t\t * Whether a rows option draws the `option-details` slot in its frame:\n\t\t * the slot is given and the option is the chosen one.\n\t\t *\n\t\t * @param {object} option The option.\n\t\t * @return {boolean}\n\t\t */\n\t\thasDetails(option) {\n\t\t\treturn Boolean(this.$slots['option-details']) && this.isSelected(option)\n\t\t},\n\n\t\t/**\n\t\t * Whether an option cannot be picked: the whole group is disabled, or the option is.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {boolean} True when disabled.\n\t\t */\n\t\tisDisabled(option) {\n\t\t\treturn this.disabled || option.disabled === true\n\t\t},\n\n\t\t/**\n\t\t * Id of an option's disabled reason, for `aria-describedby`.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {string} The id.\n\t\t */\n\t\treasonId(option) {\n\t\t\treturn `${this.groupName}-reason-${String(option.value).replace(/[^A-Za-z0-9_-]/g, '-')}`\n\t\t},\n\n\t\tisSelected(option) {\n\t\t\treturn this.selectedValues.some((v) => String(v) === String(option.value))\n\t\t},\n\n\t\t/**\n\t\t * Select (single) or toggle (multiple) an option and emit the new model.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {void}\n\t\t */\n\t\tonToggle(option) {\n\t\t\tif (this.readonly || this.isDisabled(option)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.multiple) {\n\t\t\t\t/**\n\t\t\t\t * @event update:modelValue The selected value, or the array of values when `multiple`.\n\t\t\t\t * @type {string|number|boolean|Array}\n\t\t\t\t */\n\t\t\t\t// Never the option OBJECT: a parent has to be able to hand back\n\t\t\t\t// what it receives as `modelValue`.\n\t\t\t\tthis.$emit('update:modelValue', option.value)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = this.selectedValues.filter((v) => String(v) !== String(option.value))\n\t\t\tif (next.length === this.selectedValues.length) {\n\t\t\t\tnext.push(option.value)\n\t\t\t}\n\t\t\tthis.$emit('update:modelValue', next)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped lang=\"scss\">\n.cn-choice-cards {\n\tborder: none;\n\tpadding: 0;\n\tmargin: 0;\n\tmin-inline-size: 0;\n}\n\n.cn-choice-cards__legend {\n\tpadding: 0;\n\tmargin-block-end: 8px;\n\tfont-weight: bold;\n}\n\n.cn-choice-cards__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 24px 0;\n}\n\n.cn-choice-cards__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fit, minmax(260px, 1fr));\n\tgap: 12px;\n\t/* Room for the card shadow. `src/css/card.css` is loaded globally by every\n\t consumer and gives EVERY `.cn-card` a `box-shadow` — including these — so\n\t a grid flush against its container gets the shadow shaved off wherever\n\t that container clips, which the setup wizard's dialog body does on the\n\t sides and the bottom. 12px clears the widest of those shadows. */\n\tpadding: 12px;\n}\n\n.cn-choice-cards__cell {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmin-inline-size: 0;\n}\n\n.cn-choice-cards__actions {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-choice-cards__option {\n\tposition: relative;\n\tdisplay: block;\n\tcursor: pointer;\n}\n\n.cn-choice-cards__input {\n\tposition: absolute;\n\tinset-block-start: 16px;\n\tinset-inline-end: 14px;\n\tz-index: 1;\n\tmargin: 0;\n\tcursor: pointer;\n}\n\n/* Room for the input, so a long title never runs underneath it. */\n.cn-choice-cards__option :deep(.cn-card) {\n\tpadding-inline-end: 44px;\n\ttransition: box-shadow 0.2s ease, border-color 0.2s ease;\n}\n\n/* The same global `card.css` also scales `.cn-card` up on hover. A transform\n counts towards the scrollable overflow of the nearest scroll container, so in\n a dialog body sized to its content a scrollbar appears the moment the pointer\n touches a card, and the grid twitches. The border colour below already\n carries hover, so the scale is dropped: at (0,3,0) this beats the global\n `.cn-card:hover` at (0,2,0). */\n.cn-choice-cards__option:hover :deep(.cn-card) {\n\tborder-color: var(--color-primary-element);\n\ttransform: none;\n}\n\n/* The focus ring belongs on the card, not on the small input inside it. */\n.cn-choice-cards__input:focus-visible + :deep(.cn-card) {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-choice-cards__input:disabled {\n\tcursor: default;\n}\n\n.cn-choice-cards__option--disabled .cn-choice-cards__input + :deep(.cn-card) {\n\topacity: 0.6;\n}\n\n.cn-choice-cards__rows {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n}\n\n/* 12px 14px, as the boards draw an option row (DqVolgendeStap, DqBesluit). */\n.cn-choice-cards__row {\n\tdisplay: flex;\n\talign-items: flex-start;\n\tgap: 12px;\n\tpadding: 12px 14px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n\tbackground: var(--color-main-background);\n\tcursor: pointer;\n}\n\n.cn-choice-cards__row:hover {\n\tborder-color: var(--color-border-dark);\n}\n\n.cn-choice-cards__row--selected,\n.cn-choice-cards__row--selected:hover {\n\tborder-color: var(--color-primary-element);\n\t/* The second pixel of the 2px selected border, without moving the content. */\n\tbox-shadow: inset 0 0 0 1px var(--color-primary-element);\n\tbackground: var(--color-primary-element-light);\n}\n\n/* An option this record does not allow: the grey ground and the muted ink,\n still legible (DqVolgendeStap), not faded. */\n.cn-choice-cards__row--disabled,\n.cn-choice-cards__row--disabled:hover {\n\tborder-color: var(--color-border);\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\tcursor: default;\n}\n\n/* The title takes the muted ink itself: a theme that colours every label\n with `!important` (thematiq) would otherwise hand it the label colour. */\n.cn-choice-cards__row--disabled .cn-choice-cards__row-title {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n/* The chosen option's fields share the selected row's frame: the cell draws\n the 2px primary border and the ground, the row inside it draws none, and\n the fields sit under the text, indented to it (DqVolgendeStap). */\n.cn-choice-cards__cell--with-details {\n\tborder: 1px solid var(--color-primary-element);\n\tborder-radius: var(--border-radius-large, 8px);\n\tbox-shadow: inset 0 0 0 1px var(--color-primary-element);\n\tbackground: var(--color-primary-element-light);\n}\n\n.cn-choice-cards__cell--with-details .cn-choice-cards__row,\n.cn-choice-cards__cell--with-details .cn-choice-cards__row:hover {\n\tborder-color: transparent;\n\tbox-shadow: none;\n\tbackground: transparent;\n}\n\n.cn-choice-cards__details {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 14px;\n\tpadding: 0 14px 14px 44px;\n}\n\n.cn-choice-cards__row--readonly {\n\tcursor: default;\n}\n\n.cn-choice-cards__row--readonly:not(.cn-choice-cards__row--selected):hover {\n\tborder-color: var(--color-border);\n}\n\n.cn-choice-cards__row:has(.cn-choice-cards__row-input:focus-visible) {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-choice-cards__row-input {\n\tflex: none;\n\tmargin: 2px 0 0;\n\taccent-color: var(--color-primary-element);\n\tcursor: inherit;\n}\n\n.cn-choice-cards__row-icon {\n\tflex: none;\n}\n\n.cn-choice-cards__row-text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-choice-cards__row-title {\n\tfont-size: 15px;\n\tfont-weight: 600;\n\tline-height: 1.2;\n}\n\n/* Regular weight on the span itself: a theme that sets every label bold\n with `!important` (thematiq) reaches a span that only inherits. */\n.cn-choice-cards__row-description,\n.cn-choice-cards__reason {\n\tfont-size: 13px;\n\tfont-weight: 400;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-choice-cards__reason--card {\n\tdisplay: block;\n\tmargin-block-start: 4px;\n}\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_openBlock","_createBlock","_createElementVNode","_toDisplayString","_renderSlot","_Fragment","_renderList","_createVNode"],"mappings":"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"}
|