@conduction/nextcloud-vue 2.91.0 → 2.93.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/CnAppRoot/CnAppRoot.vue2.js +57 -4
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +44 -17
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +44 -2
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +140 -59
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +20 -6
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +3 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +10 -39
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +5 -3
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +6 -4
- 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/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js +46 -0
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js +6 -2
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +82 -9
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js +104 -90
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +9 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js.map +1 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +355 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +462 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +39 -10
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +418 -399
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
- package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js +1 -1
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js +156 -3
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js +168 -52
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue.js +9 -0
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue.js.map +1 -0
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue2.js +83 -0
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue2.js.map +1 -0
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue3.js +119 -0
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue3.js.map +1 -0
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +22 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +20 -14
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue.js +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js +2 -0
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js.map +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js +15 -5
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js.map +1 -1
- package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue.js +1 -1
- package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js +8 -30
- package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js.map +1 -1
- package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -2
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTextWidget/CnTextWidget.vue.js +1 -1
- package/dist/esm/components/CnTextWidget/CnTextWidget.vue2.js +63 -4
- package/dist/esm/components/CnTextWidget/CnTextWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTextWidget/CnTextWidget.vue3.js +19 -2
- package/dist/esm/components/CnTextWidget/CnTextWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +9 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js.map +1 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +164 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +93 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -0
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +12 -0
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +45 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +44 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +1182 -328
- package/dist/esm/utils/boardDate.js +35 -1
- package/dist/esm/utils/boardDate.js.map +1 -1
- package/dist/esm/utils/featureCatalog.js +135 -0
- package/dist/esm/utils/featureCatalog.js.map +1 -0
- package/dist/esm/utils/fillTemplate.js +84 -4
- package/dist/esm/utils/fillTemplate.js.map +1 -1
- package/dist/esm/utils/headerMeta.js +62 -12
- package/dist/esm/utils/headerMeta.js.map +1 -1
- package/dist/esm/utils/roadmapIssues.js +86 -0
- package/dist/esm/utils/roadmapIssues.js.map +1 -0
- package/dist/esm/utils/userDisplayName.js +47 -0
- package/dist/esm/utils/userDisplayName.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +15618 -13097
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1182 -328
- package/l10n/en.json +45 -1
- package/l10n/nl.json +44 -1
- package/package.json +1 -1
- package/src/components/CnAppRoot/CnAppRoot.md +2 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +66 -5
- package/src/components/CnChoiceCards/CnChoiceCards.md +27 -0
- package/src/components/CnChoiceCards/CnChoiceCards.vue +168 -3
- package/src/components/CnDashboardPage/CnDashboardPage.vue +45 -6
- package/src/components/CnDataTable/CnDataTable.vue +10 -39
- package/src/components/CnDetailPage/CnDetailPage.vue +5 -3
- package/src/components/CnFactsWidget/CnFactsWidget.vue +6 -4
- package/src/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue +51 -1
- package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +98 -13
- package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +1053 -0
- package/src/components/CnFormDialog/CnFormDialog.md +1 -0
- package/src/components/CnFormDialog/CnFormDialog.vue +619 -562
- package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +338 -5
- package/src/components/CnModuleCard/CnModuleCard.vue +258 -0
- package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +23 -2
- package/src/components/CnNotificationPreferences/CnNotificationPreferences.vue +5 -0
- package/src/components/CnRoadmapTab/CnRoadmapTab.vue +6 -28
- package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -2
- package/src/components/CnTextWidget/CnTextWidget.vue +89 -4
- package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +305 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +12 -0
- package/src/css/dialog.css +47 -0
- package/src/css/look-board-detail.css +8 -0
- package/src/css/look-board.css +33 -0
- package/src/schemas/app-manifest-v2.schema.json +67 -4
- package/src/utils/boardDate.js +34 -0
- package/src/utils/featureCatalog.js +132 -0
- package/src/utils/fillTemplate.js +83 -3
- package/src/utils/headerMeta.js +62 -12
- package/src/utils/roadmapIssues.js +82 -0
- package/src/utils/userDisplayName.js +54 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnTextWidget.vue3.js","sources":["../../../../src/components/CnTextWidget/CnTextWidget.vue?vue&type=template&id=2d4184ef&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-text-widget\" :style=\"wrapperStyle\">\n\t\t<table\n\t\t\tv-if=\"tableMode\"\n\t\t\tclass=\"cn-text-widget__table\"\n\t\t\t:style=\"tableContainerStyle\">\n\t\t\t<tbody>\n\t\t\t\t<tr v-for=\"(row, rIdx) in tableRows\" :key=\"`r-${rIdx}`\">\n\t\t\t\t\t<template v-for=\"(cell, cIdx) in row\">\n\t\t\t\t\t\t<th\n\t\t\t\t\t\t\tv-if=\"isHeaderCell(rIdx)\"\n\t\t\t\t\t\t\tv-show=\"!isPlaceholderCellAt(rIdx, cIdx)\"\n\t\t\t\t\t\t\t:key=\"`c-${rIdx}-${cIdx}`\"\n\t\t\t\t\t\t\t:rowspan=\"cell.rowSpan || 1\"\n\t\t\t\t\t\t\t:colspan=\"cell.colSpan || 1\"\n\t\t\t\t\t\t\t:style=\"headerCellStyle(cIdx)\">\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"cellHasText(cell)\"\n\t\t\t\t\t\t\t\tv-html=\"sanitizeCell(cell.text)\" /><!-- eslint-disable-line vue/no-v-html -->\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\t\tclass=\"cn-text-widget__cell-placeholder\">\n\t\t\t\t\t\t\t\t{{ emptyCellPlaceholder }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</th>\n\t\t\t\t\t\t<td\n\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\tv-show=\"!isPlaceholderCellAt(rIdx, cIdx)\"\n\t\t\t\t\t\t\t:key=\"`c-${rIdx}-${cIdx}`\"\n\t\t\t\t\t\t\t:rowspan=\"cell.rowSpan || 1\"\n\t\t\t\t\t\t\t:colspan=\"cell.colSpan || 1\"\n\t\t\t\t\t\t\t:style=\"dataCellStyle(cIdx)\">\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"cellHasText(cell)\"\n\t\t\t\t\t\t\t\tv-html=\"sanitizeCell(cell.text)\" /><!-- eslint-disable-line vue/no-v-html -->\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\t\tclass=\"cn-text-widget__cell-placeholder\">\n\t\t\t\t\t\t\t\t{{ emptyCellPlaceholder }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</template>\n\t\t\t\t</tr>\n\t\t\t</tbody>\n\t\t</table>\n\t\t<div\n\t\t\tv-else-if=\"hasText\"\n\t\t\tclass=\"cn-text-widget__content\"\n\t\t\t:class=\"contentClass\"\n\t\t\t:style=\"contentStyle\"\n\t\t\tv-html=\"sanitizedHtml\" /><!-- eslint-disable-line vue/no-v-html -->\n\t\t<span\n\t\t\tv-else\n\t\t\tclass=\"cn-text-widget__placeholder\"\n\t\t\t:style=\"contentStyle\">\n\t\t\t{{ placeholderText }}\n\t\t</span>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport DOMPurify from 'dompurify'\nimport { Marked } from 'marked'\nimport { isPlaceholderCell } from '../../utils/textTable.js'\n\n// Per-module `Marked` instance so configuration is scoped here and does NOT\n// mutate the global `marked` singleton (which would affect any other code in\n// the same app that imports `marked`). `gfm: true` enables tables; `breaks:\n// false` keeps CommonMark-strict newline behaviour.\nconst markedInstance = new Marked({\n\tgfm: true,\n\tbreaks: false,\n})\n\n// DOMPurify allow-list hook: author-supplied `target=_blank` links survive\n// sanitisation (we add `target` to ADD_ATTR below); the hook then appends\n// `rel=\"noopener noreferrer\"` to mitigate reverse-tabnabbing. The hook is\n// idempotent and registered once.\nlet hookRegistered = false\nfunction ensureDomPurifyHook() {\n\tif (hookRegistered) {\n\t\treturn\n\t}\n\tDOMPurify.addHook('afterSanitizeAttributes', (node) => {\n\t\tif (node.tagName === 'A' && node.getAttribute('target') === '_blank') {\n\t\t\tnode.setAttribute('rel', 'noopener noreferrer')\n\t\t}\n\t})\n\thookRegistered = true\n}\n\n/**\n * CnTextWidget — renders user-authored multi-line text (or a small table)\n * inside a dashboard cell. Text content is passed through DOMPurify before\n * injection via `v-html` so common formatting tags survive while XSS vectors\n * (`<script>`, `on*` attributes, `javascript:` URLs) are stripped.\n *\n * Persisted shape: `{text, fontSize, color, backgroundColor, textAlign,\n * contentMode, tableMode, tableData}`. Defaults: `fontSize='14px'`,\n * `color='var(--color-main-text)'`, `backgroundColor='transparent'`,\n * `textAlign='left'`. `contentMode` absent means the legacy `'html'` branch;\n * the literal `'markdown'` parses via marked (GFM tables) then sanitises\n * through the same allow-list — a single trust boundary protects both modes.\n *\n * When `tableMode === true`, the renderer draws an HTML `<table>` from\n * `tableData` instead: header rows promote row 0 to `<th>`, per-column\n * alignment is applied inline, and `colSpan` / `rowSpan` become native HTML\n * attributes. Merge-swallowed cells are hidden via `v-show` so the visual\n * grid stays rectangular.\n *\n * Registered as the `text` dashboard widget type via the renderer's\n * `index.js`.\n */\nexport default {\n\tname: 'CnTextWidget',\n\n\tprops: {\n\t\t/**\n\t\t * Persisted widget content blob: `{text, fontSize, color,\n\t\t * backgroundColor, textAlign, contentMode, tableMode, tableData}`.\n\t\t * All keys are optional; missing keys collapse to documented defaults.\n\t\t *\n\t\t * @type {{text: string, fontSize: string, color: string, backgroundColor: string, textAlign: string, contentMode: string, tableMode: boolean, tableData: object}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\tcomputed: {\n\t\t/** The author text, or '' when absent / non-string. */\n\t\ttext() {\n\t\t\treturn typeof this.content?.text === 'string' ? this.content.text : ''\n\t\t},\n\n\t\t/** Whether the text has any non-whitespace content. */\n\t\thasText() {\n\t\t\treturn this.text.trim() !== ''\n\t\t},\n\n\t\t/** Resolved content mode — only `'markdown'` switches the branch. */\n\t\tcontentMode() {\n\t\t\treturn this.content?.contentMode === 'markdown' ? 'markdown' : 'html'\n\t\t},\n\n\t\t/** CSS class selecting the markdown vs html content-spacing rules. */\n\t\tcontentClass() {\n\t\t\treturn this.contentMode === 'markdown'\n\t\t\t\t? 'cn-text-widget__content--markdown'\n\t\t\t\t: 'cn-text-widget__content--html'\n\t\t},\n\n\t\t/**\n\t\t * The HTML to render: either the raw author HTML (sanitised) or the\n\t\t * markdown rendering of the author text (sanitised). Both branches go\n\t\t * through DOMPurify, which strips `<script>`, `<style>`, `<link>`,\n\t\t * `on*` attributes, and `javascript:` URLs.\n\t\t *\n\t\t * @return {string} sanitised HTML safe for `v-html` injection.\n\t\t */\n\t\tsanitizedHtml() {\n\t\t\tlet html\n\t\t\tif (this.contentMode === 'markdown') {\n\t\t\t\ttry {\n\t\t\t\t\thtml = markedInstance.parse(this.text)\n\t\t\t\t\tif (typeof html !== 'string') {\n\t\t\t\t\t\thtml = String(html)\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Defensive: fall back to the raw text rather than crash\n\t\t\t\t\t// the dashboard if the parser throws.\n\t\t\t\t\thtml = this.text\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\thtml = this.text\n\t\t\t}\n\t\t\treturn DOMPurify.sanitize(html, {\n\t\t\t\tADD_ATTR: ['target'],\n\t\t\t})\n\t\t},\n\n\t\t/** Localised placeholder shown when there is no text content. */\n\t\tplaceholderText() {\n\t\t\treturn t('nextcloud-vue', 'No text content')\n\t\t},\n\n\t\t/** Localised placeholder shown inside an empty table cell. */\n\t\temptyCellPlaceholder() {\n\t\t\treturn t('nextcloud-vue', 'Empty cell')\n\t\t},\n\n\t\t/** Resolved font size (default `14px`). */\n\t\tfontSize() {\n\t\t\treturn this.content?.fontSize || '14px'\n\t\t},\n\n\t\t/** Resolved text colour (default theme main text). */\n\t\tcolor() {\n\t\t\treturn this.content?.color || 'var(--color-main-text)'\n\t\t},\n\n\t\t/** Resolved background colour (default transparent). */\n\t\tbackgroundColor() {\n\t\t\treturn this.content?.backgroundColor || 'transparent'\n\t\t},\n\n\t\t/** Resolved text alignment (default `left`). */\n\t\ttextAlign() {\n\t\t\treturn this.content?.textAlign || 'left'\n\t\t},\n\n\t\t/** Whether the renderer is in table mode. */\n\t\ttableMode() {\n\t\t\treturn this.content?.tableMode === true\n\t\t},\n\n\t\t/** The persisted tableData object, or null. */\n\t\ttableData() {\n\t\t\treturn this.content?.tableData || null\n\t\t},\n\n\t\t/** The table rows array (empty when no table data). */\n\t\ttableRows() {\n\t\t\treturn Array.isArray(this.tableData?.rows) ? this.tableData.rows : []\n\t\t},\n\n\t\t/** Whether row 0 should render as a header row. */\n\t\theaderRow() {\n\t\t\treturn this.tableData?.headerRow === true\n\t\t},\n\n\t\t/** Per-column text-alignment array. */\n\t\tcolumnAlignments() {\n\t\t\tconst aligns = this.tableData?.columnAlignments\n\t\t\treturn Array.isArray(aligns) ? aligns : []\n\t\t},\n\n\t\t/** Inline style for the flex wrapper. */\n\t\twrapperStyle() {\n\t\t\treturn {\n\t\t\t\twidth: '100%',\n\t\t\t\theight: '100%',\n\t\t\t\tpadding: '16px',\n\t\t\t\tdisplay: 'flex',\n\t\t\t\t'align-items': 'center',\n\t\t\t\t'justify-content': 'center',\n\t\t\t\toverflow: 'auto',\n\t\t\t\t'background-color': this.backgroundColor,\n\t\t\t}\n\t\t},\n\n\t\t/** Inline style for the text / placeholder content. */\n\t\tcontentStyle() {\n\t\t\tconst base = {\n\t\t\t\t'font-size': this.fontSize,\n\t\t\t\t'text-align': this.textAlign,\n\t\t\t\tcolor: this.color,\n\t\t\t\twidth: '100%',\n\t\t\t\t'overflow-wrap': 'break-word',\n\t\t\t}\n\t\t\tif (!this.hasText) {\n\t\t\t\tbase['font-style'] = 'italic'\n\t\t\t\tbase.color = 'var(--color-text-maxcontrast)'\n\t\t\t}\n\t\t\treturn base\n\t\t},\n\n\t\t/** Inline style for the table container. */\n\t\ttableContainerStyle() {\n\t\t\treturn {\n\t\t\t\t'font-size': this.fontSize,\n\t\t\t\tcolor: this.color,\n\t\t\t\twidth: '100%',\n\t\t\t}\n\t\t},\n\t},\n\n\tcreated() {\n\t\tensureDomPurifyHook()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Run cell text through DOMPurify so the table-mode render path has\n\t\t * the same XSS-stripping guarantees as the text-mode path.\n\t\t *\n\t\t * @param {string} text raw cell text (may contain HTML).\n\t\t * @return {string} sanitised HTML safe to render via `v-html`.\n\t\t */\n\t\tsanitizeCell(text) {\n\t\t\treturn DOMPurify.sanitize(typeof text === 'string' ? text : '')\n\t\t},\n\n\t\t/**\n\t\t * Whether the given row index renders as `<th>` (header row).\n\t\t *\n\t\t * @param {number} rIdx the 0-based row index.\n\t\t * @return {boolean} true when row 0 and the header flag is set.\n\t\t */\n\t\tisHeaderCell(rIdx) {\n\t\t\treturn this.headerRow && rIdx === 0\n\t\t},\n\n\t\t/**\n\t\t * Whether a cell is covered by another cell's `rowSpan` / `colSpan`\n\t\t * and so must be hidden to keep the grid rectangular.\n\t\t *\n\t\t * @param {number} rIdx the row index.\n\t\t * @param {number} cIdx the column index.\n\t\t * @return {boolean} true when the cell is a covered placeholder.\n\t\t */\n\t\tisPlaceholderCellAt(rIdx, cIdx) {\n\t\t\treturn isPlaceholderCell(this.tableRows, rIdx, cIdx)\n\t\t},\n\n\t\t/**\n\t\t * Whether a cell has any non-whitespace text.\n\t\t *\n\t\t * @param {object} cell the cell object.\n\t\t * @return {boolean} true when the cell has text.\n\t\t */\n\t\tcellHasText(cell) {\n\t\t\treturn typeof cell?.text === 'string' && cell.text.trim() !== ''\n\t\t},\n\n\t\t/**\n\t\t * Inline style for a data cell at a given column.\n\t\t *\n\t\t * @param {number} cIdx the column index.\n\t\t * @return {object} the cell style object.\n\t\t */\n\t\tdataCellStyle(cIdx) {\n\t\t\treturn {\n\t\t\t\t'text-align': this.columnAlignments[cIdx] || 'left',\n\t\t\t\tborder: '1px solid var(--color-border)',\n\t\t\t\tpadding: '8px',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Inline style for a header cell at a given column.\n\t\t *\n\t\t * @param {number} cIdx the column index.\n\t\t * @return {object} the header cell style object.\n\t\t */\n\t\theaderCellStyle(cIdx) {\n\t\t\treturn {\n\t\t\t\t...this.dataCellStyle(cIdx),\n\t\t\t\t'background-color': 'var(--color-background-hover)',\n\t\t\t\t'font-weight': 'bold',\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-text-widget {\n\twidth: 100%;\n\theight: 100%;\n}\n\n.cn-text-widget__content,\n.cn-text-widget__placeholder {\n\t/* Safety net — ensures long URLs / words inside the sanitised HTML wrap\n\t rather than overflowing horizontally. */\n\toverflow-wrap: break-word;\n\tmax-width: 100%;\n}\n\n/* Restore inline-emphasis semantics inside rendered content. Nextcloud's\n server CSS styles bare <em> as muted (var(--color-text-maxcontrast)) and can\n drop the italic, so `_x_` / `**_x_**` looked greyed-out and non-italic. Force\n italic <em> / bold <strong> that inherit the widget's own text colour. */\n.cn-text-widget :deep(em),\n.cn-text-widget :deep(i) {\n\tfont-style: italic;\n\tcolor: inherit;\n}\n\n.cn-text-widget :deep(strong),\n.cn-text-widget :deep(b) {\n\tfont-weight: bold;\n\tcolor: inherit;\n}\n\n/* Nextcloud's server CSS strips list markers/indent (`list-style: none`,\n no padding), so rendered `<ul>`/`<ol>` looked like plain text. Restore\n bullets/numbers and indentation, including nested levels. */\n.cn-text-widget :deep(ul),\n.cn-text-widget :deep(ol) {\n\tmargin: 4px 0;\n\tpadding-left: 1.5em;\n}\n\n.cn-text-widget :deep(ul) {\n\tlist-style: disc outside;\n}\n\n.cn-text-widget :deep(ol) {\n\tlist-style: decimal outside;\n}\n\n.cn-text-widget :deep(ul ul) {\n\tlist-style-type: circle;\n}\n\n.cn-text-widget :deep(ul ul ul) {\n\tlist-style-type: square;\n}\n\n.cn-text-widget :deep(li) {\n\tdisplay: list-item;\n}\n\n/* Nextcloud's server CSS flattens heading sizes (e.g. h1 → 100%, so it fell\n back to the ~14px body size while h2 stayed 1.8em), leaving them oddly\n scaled. Apply a consistent em-based scale that grows with the widget's own\n font size. */\n.cn-text-widget :deep(h1) {\n\tfont-size: 2em;\n}\n\n.cn-text-widget :deep(h2) {\n\tfont-size: 1.5em;\n}\n\n.cn-text-widget :deep(h3) {\n\tfont-size: 1.25em;\n}\n\n.cn-text-widget :deep(h4) {\n\tfont-size: 1.1em;\n}\n\n.cn-text-widget :deep(h5) {\n\tfont-size: 1em;\n}\n\n.cn-text-widget :deep(h6) {\n\tfont-size: 0.85em;\n}\n\n.cn-text-widget__content--markdown :deep(h1),\n.cn-text-widget__content--markdown :deep(h2),\n.cn-text-widget__content--markdown :deep(h3),\n.cn-text-widget__content--markdown :deep(h4),\n.cn-text-widget__content--markdown :deep(h5),\n.cn-text-widget__content--markdown :deep(h6) {\n\tmargin: 8px 0 4px;\n\tfont-weight: 600;\n\tline-height: 1.3;\n}\n\n.cn-text-widget__content--markdown :deep(p),\n.cn-text-widget__content--markdown :deep(ul),\n.cn-text-widget__content--markdown :deep(ol),\n.cn-text-widget__content--markdown :deep(blockquote) {\n\tmargin: 4px 0;\n}\n\n.cn-text-widget__content--markdown :deep(code) {\n\tpadding: 1px 4px;\n\tborder-radius: 3px;\n\tbackground: var(--color-background-hover);\n\tfont-family: var(--font-monospace, monospace);\n\tfont-size: 0.95em;\n}\n\n.cn-text-widget__content--markdown :deep(pre) {\n\tpadding: 8px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n\toverflow: auto;\n}\n\n.cn-text-widget__content--markdown :deep(pre) :deep(code) {\n\tpadding: 0;\n\tbackground: transparent;\n}\n\n.cn-text-widget__content--markdown :deep(blockquote) {\n\tpadding-left: 8px;\n\tborder-left: 3px solid var(--color-border);\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-text-widget__content--markdown :deep(table) {\n\tborder-collapse: collapse;\n\tmargin: 4px 0;\n}\n\n.cn-text-widget__content--markdown :deep(th),\n.cn-text-widget__content--markdown :deep(td) {\n\tpadding: 4px 8px;\n\tborder: 1px solid var(--color-border);\n}\n\n.cn-text-widget__table {\n\tborder-collapse: collapse;\n\twidth: 100%;\n}\n\n.cn-text-widget__table th,\n.cn-text-widget__table td {\n\toverflow-wrap: break-word;\n\tvertical-align: top;\n}\n\n.cn-text-widget__cell-placeholder {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-style: italic;\n}\n</style>\n"],"names":["_createElementBlock","_createElementVNode","_Fragment","_renderList","_normalizeStyle","_toDisplayString","_normalizeClass"],"mappings":";;;;;;AA+BQ,EAAA,KAAI,EAAA,kCAAA;;;;;;AAgBJ,EAAA,KAAA,EAAA,kCAAA;;;;;uBApCLA;AAAG,IAAA,KAAG,EAAA,gBAAA;AAAA,IAAA,KAAA,iBAAA,QAAA,CAAA,YAAA,CAAA;;KAEA,QAAS,CAAA,SAAA;wCAyCR,CAAA,OAAA,EAAA;;AAxCJ,UAAA,KAAE,EAAA,uBAAA;eACC,iBAAE,QAAmB,CAAA,mBAAA,CAAA;;UACvBC,kBAqCI,CAAA,OAAA,EAAA,IAAA,EAAA;iDADFC,QAnCqB,EAAA,IAAA,EAAAC,UAAA,CAAA,QAAA,CAAA,SAAE,EAAhB,CAAA,GAAG,EAAE,IAAI,KAAA;qDAmChB,CAAA,IAAA,EAAA;AAnCuB,gBAAA,GAAA,OAAA,IAAA,CAAA,CAAA;;qDAkChBD,QAjCkB,EAAA,IAAA,EAAAC,UAAA,CAAA,GAAA,EAAX,CAAA,IAAI,EAAC,IAAK,KAAA;;AAEpB,oBAAA,CAAA,QAAA,CAAA,YAAY,CAAC,IAAI,CAAA;uEAcnB,CAAA,IAAA,EAAA;0BAZH,GAAG,EAAA,CAAA,EAAA,EAAO,IAAI,CAAA,CAAA,EAAI,IAAI,CAAA,CAAA;0BACtB,OAAO,EAAE,IAAI,CAAC,OAAO,IAAA,CAAA;0BACrB,OAAG,EAAA,IAAA,CAAA,OAAA,IAAA,CAAA;0BACH,KAAK,EAAAC,cAAA,CAAA,QAAA,CAAA,eAAA,CAAA,IAAA,CAAA,CAAA;;AAEC,0BAAA,CAAA,QAAA,CAAA,WAAW,CAAC,IAAI,CAAA;4CADtBJ,kBAEK,CAAA,MAAA,EAAA;;AAAL,gCAAA,SAAK,EAAA,QAAA,CAAA,YAAA,CAAA,IAAA,CAAA,IAAA,CAAA;;4CACNA,kBAIO,CAAA,MAAA,EAJP,UAIO,EAAAK,eAAA,CADH,QAAoB,CAAA,oBAAA,CAAA,EAAA,CAAA,YAAA,CAAA;;mCAXpB,QAAA,CAAA,mBAAA,CAAA,IAAA,EAAA,IAAA,CAAA,CAAA;;uEA6BN,CAAA,IAAA,EAAA;0BAZG,GAAG,EAAA,CAAA,EAAA,EAAO,IAAI,CAAA,CAAA,EAAI,IAAI,CAAA,CAAA;0BACtB,OAAO,EAAE,IAAI,CAAC,OAAO,IAAA,CAAA;0BACrB,OAAG,EAAA,IAAA,CAAA,OAAA,IAAA,CAAA;0BACH,KAAK,EAAAD,cAAA,CAAA,QAAA,CAAA,aAAA,CAAA,IAAA,CAAA,CAAA;;AAEC,0BAAA,CAAA,QAAA,CAAA,WAAW,CAAC,IAAI,CAAA;4CADtBJ,kBAEK,CAAA,MAAA,EAAA;;AAAL,gCAAA,SAAK,EAAA,QAAA,CAAA,YAAA,CAAA,IAAA,CAAA,IAAA,CAAA;;4CACNA,kBAIE,CAAA,MAAA,EAJF,UAIE,EAAAK,eAAA,CADC,QAAA,CAAA,oBAAA,CAAA,EAAA,CAAA,YAAA,CAAA;;mCAXC,QAAA,CAAA,mBAAA,CAAA,IAAA,EAAA,IAAA,CAAA,CAAA;;;;;;;;SAmBL,QAAA,CAAA,OAAA;wBADHL,kBAKoB,CAAA,KAAA,EAAA;;YAHpB,KAAK,EAAAM,cAAA,CAAA,CAAA,yBAAA,EACG,QAAY,CAAA,YAAA,CAAA,CAAA;AACnB,YAAA,KAAK,iBAAE,QAAY,CAAA,YAAA,CAAA;AACpB,YAAA,SAAoB,EAAZ,QAAY,CAAA,aAAA;;wBACrBN;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"CnTextWidget.vue3.js","sources":["../../../../src/components/CnTextWidget/CnTextWidget.vue?vue&type=template&id=f00a6820&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-text-widget\" :style=\"wrapperStyle\">\n\t\t<table\n\t\t\tv-if=\"tableMode\"\n\t\t\tclass=\"cn-text-widget__table\"\n\t\t\t:style=\"tableContainerStyle\">\n\t\t\t<tbody>\n\t\t\t\t<tr v-for=\"(row, rIdx) in tableRows\" :key=\"`r-${rIdx}`\">\n\t\t\t\t\t<template v-for=\"(cell, cIdx) in row\">\n\t\t\t\t\t\t<th\n\t\t\t\t\t\t\tv-if=\"isHeaderCell(rIdx)\"\n\t\t\t\t\t\t\tv-show=\"!isPlaceholderCellAt(rIdx, cIdx)\"\n\t\t\t\t\t\t\t:key=\"`c-${rIdx}-${cIdx}`\"\n\t\t\t\t\t\t\t:rowspan=\"cell.rowSpan || 1\"\n\t\t\t\t\t\t\t:colspan=\"cell.colSpan || 1\"\n\t\t\t\t\t\t\t:style=\"headerCellStyle(cIdx)\">\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"cellHasText(cell)\"\n\t\t\t\t\t\t\t\tv-html=\"sanitizeCell(cell.text)\" /><!-- eslint-disable-line vue/no-v-html -->\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\t\tclass=\"cn-text-widget__cell-placeholder\">\n\t\t\t\t\t\t\t\t{{ emptyCellPlaceholder }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</th>\n\t\t\t\t\t\t<td\n\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\tv-show=\"!isPlaceholderCellAt(rIdx, cIdx)\"\n\t\t\t\t\t\t\t:key=\"`c-${rIdx}-${cIdx}`\"\n\t\t\t\t\t\t\t:rowspan=\"cell.rowSpan || 1\"\n\t\t\t\t\t\t\t:colspan=\"cell.colSpan || 1\"\n\t\t\t\t\t\t\t:style=\"dataCellStyle(cIdx)\">\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"cellHasText(cell)\"\n\t\t\t\t\t\t\t\tv-html=\"sanitizeCell(cell.text)\" /><!-- eslint-disable-line vue/no-v-html -->\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\t\tclass=\"cn-text-widget__cell-placeholder\">\n\t\t\t\t\t\t\t\t{{ emptyCellPlaceholder }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</template>\n\t\t\t\t</tr>\n\t\t\t</tbody>\n\t\t</table>\n\t\t<div\n\t\t\tv-else-if=\"hasText\"\n\t\t\tclass=\"cn-text-widget__content\"\n\t\t\t:class=\"contentClass\"\n\t\t\t:style=\"contentStyle\"\n\t\t\tv-html=\"sanitizedHtml\" /><!-- eslint-disable-line vue/no-v-html -->\n\t\t<span\n\t\t\tv-else\n\t\t\tclass=\"cn-text-widget__placeholder\"\n\t\t\t:style=\"contentStyle\">\n\t\t\t{{ placeholderText }}\n\t\t</span>\n\t\t<a\n\t\t\tv-if=\"link\"\n\t\t\tclass=\"cn-text-widget__link\"\n\t\t\tdata-testid=\"cn-text-widget-link\"\n\t\t\t:href=\"link.url\"\n\t\t\t:target=\"link.external ? '_blank' : null\"\n\t\t\t:rel=\"link.external ? 'noopener noreferrer' : null\"\n\t\t\t@click=\"openLink\">\n\t\t\t{{ link.label }}\n\t\t\t<ChevronRight :size=\"16\" />\n\t\t</a>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport DOMPurify from 'dompurify'\nimport { Marked } from 'marked'\nimport ChevronRight from 'vue-material-design-icons/ChevronRight.vue'\nimport { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'\nimport { isPlaceholderCell } from '../../utils/textTable.js'\n\n// Per-module `Marked` instance so configuration is scoped here and does NOT\n// mutate the global `marked` singleton (which would affect any other code in\n// the same app that imports `marked`). `gfm: true` enables tables; `breaks:\n// false` keeps CommonMark-strict newline behaviour.\nconst markedInstance = new Marked({\n\tgfm: true,\n\tbreaks: false,\n})\n\n// DOMPurify allow-list hook: author-supplied `target=_blank` links survive\n// sanitisation (we add `target` to ADD_ATTR below); the hook then appends\n// `rel=\"noopener noreferrer\"` to mitigate reverse-tabnabbing. The hook is\n// idempotent and registered once.\nlet hookRegistered = false\nfunction ensureDomPurifyHook() {\n\tif (hookRegistered) {\n\t\treturn\n\t}\n\tDOMPurify.addHook('afterSanitizeAttributes', (node) => {\n\t\tif (node.tagName === 'A' && node.getAttribute('target') === '_blank') {\n\t\t\tnode.setAttribute('rel', 'noopener noreferrer')\n\t\t}\n\t})\n\thookRegistered = true\n}\n\n/**\n * CnTextWidget — renders user-authored multi-line text (or a small table)\n * inside a dashboard cell. Text content is passed through DOMPurify before\n * injection via `v-html` so common formatting tags survive while XSS vectors\n * (`<script>`, `on*` attributes, `javascript:` URLs) are stripped.\n *\n * Persisted shape: `{text, fontSize, color, backgroundColor, textAlign,\n * contentMode, tableMode, tableData}`. Defaults: `fontSize='14px'`,\n * `color='var(--color-main-text)'`, `backgroundColor='transparent'`,\n * `textAlign='left'`. `contentMode` absent means the legacy `'html'` branch;\n * the literal `'markdown'` parses via marked (GFM tables) then sanitises\n * through the same allow-list — a single trust boundary protects both modes.\n *\n * When `tableMode === true`, the renderer draws an HTML `<table>` from\n * `tableData` instead: header rows promote row 0 to `<th>`, per-column\n * alignment is applied inline, and `colSpan` / `rowSpan` become native HTML\n * attributes. Merge-swallowed cells are hidden via `v-show` so the visual\n * grid stays rectangular.\n *\n * Registered as the `text` dashboard widget type via the renderer's\n * `index.js`.\n */\nexport default {\n\tname: 'CnTextWidget',\n\n\tcomponents: { ChevronRight },\n\n\tinject: {\n\t\t/** The app's translate function, provided by CnAppRoot. Identity outside one. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted widget content blob: `{text, fontSize, color,\n\t\t * backgroundColor, textAlign, contentMode, tableMode, tableData, link}`.\n\t\t * All keys are optional; missing keys collapse to documented defaults.\n\t\t * `text` and `link.label` go through the app's translate function.\n\t\t * `link` is `{label, route}` (a page id, or a path starting with `/`) or\n\t\t * `{label, href}` (an external URL), drawn under the text.\n\t\t *\n\t\t * @type {{text: string, fontSize: string, color: string, backgroundColor: string, textAlign: string, contentMode: string, tableMode: boolean, tableData: object, link: {label: string, route: string, href: string}}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\tcomputed: {\n\t\t/** The author text through the app's translate function, or '' when absent / non-string. */\n\t\ttext() {\n\t\t\treturn typeof this.content?.text === 'string' ? this.tr(this.content.text) : ''\n\t\t},\n\n\t\t/**\n\t\t * The link under the text, from `content.link`: `{ label, route }`\n\t\t * (a page id, or a router path when it starts with `/`) or\n\t\t * `{ label, href }` (an external URL, opened in a new tab). The label\n\t\t * goes through the app's translate function. Null without a label or\n\t\t * a target.\n\t\t *\n\t\t * @return {{label: string, url: string, location: (object|null), external: boolean}|null} The link.\n\t\t */\n\t\tlink() {\n\t\t\tconst link = this.content?.link\n\t\t\tif (!link || typeof link !== 'object' || typeof link.label !== 'string' || link.label === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (typeof link.route === 'string' && link.route !== '') {\n\t\t\t\tconst location = link.route.startsWith('/') ? { path: link.route } : { name: link.route }\n\t\t\t\tconst url = resolveHref(location, this.$router)\n\t\t\t\treturn url ? { label: this.tr(link.label), url, location, external: false } : null\n\t\t\t}\n\t\t\tif (typeof link.href === 'string' && link.href !== '') {\n\t\t\t\treturn { label: this.tr(link.label), url: link.href, location: null, external: true }\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/** Whether the text has any non-whitespace content. */\n\t\thasText() {\n\t\t\treturn this.text.trim() !== ''\n\t\t},\n\n\t\t/** Resolved content mode — only `'markdown'` switches the branch. */\n\t\tcontentMode() {\n\t\t\treturn this.content?.contentMode === 'markdown' ? 'markdown' : 'html'\n\t\t},\n\n\t\t/** CSS class selecting the markdown vs html content-spacing rules. */\n\t\tcontentClass() {\n\t\t\treturn this.contentMode === 'markdown'\n\t\t\t\t? 'cn-text-widget__content--markdown'\n\t\t\t\t: 'cn-text-widget__content--html'\n\t\t},\n\n\t\t/**\n\t\t * The HTML to render: either the raw author HTML (sanitised) or the\n\t\t * markdown rendering of the author text (sanitised). Both branches go\n\t\t * through DOMPurify, which strips `<script>`, `<style>`, `<link>`,\n\t\t * `on*` attributes, and `javascript:` URLs.\n\t\t *\n\t\t * @return {string} sanitised HTML safe for `v-html` injection.\n\t\t */\n\t\tsanitizedHtml() {\n\t\t\tlet html\n\t\t\tif (this.contentMode === 'markdown') {\n\t\t\t\ttry {\n\t\t\t\t\thtml = markedInstance.parse(this.text)\n\t\t\t\t\tif (typeof html !== 'string') {\n\t\t\t\t\t\thtml = String(html)\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Defensive: fall back to the raw text rather than crash\n\t\t\t\t\t// the dashboard if the parser throws.\n\t\t\t\t\thtml = this.text\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\thtml = this.text\n\t\t\t}\n\t\t\treturn DOMPurify.sanitize(html, {\n\t\t\t\tADD_ATTR: ['target'],\n\t\t\t})\n\t\t},\n\n\t\t/** Localised placeholder shown when there is no text content. */\n\t\tplaceholderText() {\n\t\t\treturn t('nextcloud-vue', 'No text content')\n\t\t},\n\n\t\t/** Localised placeholder shown inside an empty table cell. */\n\t\temptyCellPlaceholder() {\n\t\t\treturn t('nextcloud-vue', 'Empty cell')\n\t\t},\n\n\t\t/** Resolved font size (default `14px`). */\n\t\tfontSize() {\n\t\t\treturn this.content?.fontSize || '14px'\n\t\t},\n\n\t\t/** Resolved text colour (default theme main text). */\n\t\tcolor() {\n\t\t\treturn this.content?.color || 'var(--color-main-text)'\n\t\t},\n\n\t\t/** Resolved background colour (default transparent). */\n\t\tbackgroundColor() {\n\t\t\treturn this.content?.backgroundColor || 'transparent'\n\t\t},\n\n\t\t/** Resolved text alignment (default `left`). */\n\t\ttextAlign() {\n\t\t\treturn this.content?.textAlign || 'left'\n\t\t},\n\n\t\t/** Whether the renderer is in table mode. */\n\t\ttableMode() {\n\t\t\treturn this.content?.tableMode === true\n\t\t},\n\n\t\t/** The persisted tableData object, or null. */\n\t\ttableData() {\n\t\t\treturn this.content?.tableData || null\n\t\t},\n\n\t\t/** The table rows array (empty when no table data). */\n\t\ttableRows() {\n\t\t\treturn Array.isArray(this.tableData?.rows) ? this.tableData.rows : []\n\t\t},\n\n\t\t/** Whether row 0 should render as a header row. */\n\t\theaderRow() {\n\t\t\treturn this.tableData?.headerRow === true\n\t\t},\n\n\t\t/** Per-column text-alignment array. */\n\t\tcolumnAlignments() {\n\t\t\tconst aligns = this.tableData?.columnAlignments\n\t\t\treturn Array.isArray(aligns) ? aligns : []\n\t\t},\n\n\t\t/** Inline style for the flex wrapper. */\n\t\twrapperStyle() {\n\t\t\treturn {\n\t\t\t\twidth: '100%',\n\t\t\t\theight: '100%',\n\t\t\t\tpadding: '16px',\n\t\t\t\tdisplay: 'flex',\n\t\t\t\t'align-items': 'center',\n\t\t\t\t'justify-content': 'center',\n\t\t\t\toverflow: 'auto',\n\t\t\t\t'background-color': this.backgroundColor,\n\t\t\t\t...(this.link ? { 'flex-direction': 'column', gap: '8px', 'align-items': 'flex-start' } : {}),\n\t\t\t}\n\t\t},\n\n\t\t/** Inline style for the text / placeholder content. */\n\t\tcontentStyle() {\n\t\t\tconst base = {\n\t\t\t\t'font-size': this.fontSize,\n\t\t\t\t'text-align': this.textAlign,\n\t\t\t\tcolor: this.color,\n\t\t\t\twidth: '100%',\n\t\t\t\t'overflow-wrap': 'break-word',\n\t\t\t}\n\t\t\tif (!this.hasText) {\n\t\t\t\tbase['font-style'] = 'italic'\n\t\t\t\tbase.color = 'var(--color-text-maxcontrast)'\n\t\t\t}\n\t\t\treturn base\n\t\t},\n\n\t\t/** Inline style for the table container. */\n\t\ttableContainerStyle() {\n\t\t\treturn {\n\t\t\t\t'font-size': this.fontSize,\n\t\t\t\tcolor: this.color,\n\t\t\t\twidth: '100%',\n\t\t\t}\n\t\t},\n\t},\n\n\tcreated() {\n\t\tensureDomPurifyHook()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * A manifest string through the app's translate function.\n\t\t *\n\t\t * @param {string} text The string.\n\t\t * @return {string} The translated string.\n\t\t */\n\t\ttr(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * Route a plain click on an in-app link, so the page does not reload.\n\t\t *\n\t\t * @param {MouseEvent} event The click.\n\t\t */\n\t\topenLink(event) {\n\t\t\tif (this.link && !this.link.external) {\n\t\t\t\tfollowLinkClick(event, this.link.location, this.$router)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Run cell text through DOMPurify so the table-mode render path has\n\t\t * the same XSS-stripping guarantees as the text-mode path.\n\t\t *\n\t\t * @param {string} text raw cell text (may contain HTML).\n\t\t * @return {string} sanitised HTML safe to render via `v-html`.\n\t\t */\n\t\tsanitizeCell(text) {\n\t\t\treturn DOMPurify.sanitize(typeof text === 'string' ? text : '')\n\t\t},\n\n\t\t/**\n\t\t * Whether the given row index renders as `<th>` (header row).\n\t\t *\n\t\t * @param {number} rIdx the 0-based row index.\n\t\t * @return {boolean} true when row 0 and the header flag is set.\n\t\t */\n\t\tisHeaderCell(rIdx) {\n\t\t\treturn this.headerRow && rIdx === 0\n\t\t},\n\n\t\t/**\n\t\t * Whether a cell is covered by another cell's `rowSpan` / `colSpan`\n\t\t * and so must be hidden to keep the grid rectangular.\n\t\t *\n\t\t * @param {number} rIdx the row index.\n\t\t * @param {number} cIdx the column index.\n\t\t * @return {boolean} true when the cell is a covered placeholder.\n\t\t */\n\t\tisPlaceholderCellAt(rIdx, cIdx) {\n\t\t\treturn isPlaceholderCell(this.tableRows, rIdx, cIdx)\n\t\t},\n\n\t\t/**\n\t\t * Whether a cell has any non-whitespace text.\n\t\t *\n\t\t * @param {object} cell the cell object.\n\t\t * @return {boolean} true when the cell has text.\n\t\t */\n\t\tcellHasText(cell) {\n\t\t\treturn typeof cell?.text === 'string' && cell.text.trim() !== ''\n\t\t},\n\n\t\t/**\n\t\t * Inline style for a data cell at a given column.\n\t\t *\n\t\t * @param {number} cIdx the column index.\n\t\t * @return {object} the cell style object.\n\t\t */\n\t\tdataCellStyle(cIdx) {\n\t\t\treturn {\n\t\t\t\t'text-align': this.columnAlignments[cIdx] || 'left',\n\t\t\t\tborder: '1px solid var(--color-border)',\n\t\t\t\tpadding: '8px',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Inline style for a header cell at a given column.\n\t\t *\n\t\t * @param {number} cIdx the column index.\n\t\t * @return {object} the header cell style object.\n\t\t */\n\t\theaderCellStyle(cIdx) {\n\t\t\treturn {\n\t\t\t\t...this.dataCellStyle(cIdx),\n\t\t\t\t'background-color': 'var(--color-background-hover)',\n\t\t\t\t'font-weight': 'bold',\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-text-widget {\n\twidth: 100%;\n\theight: 100%;\n}\n\n.cn-text-widget__content,\n.cn-text-widget__placeholder {\n\t/* Safety net — ensures long URLs / words inside the sanitised HTML wrap\n\t rather than overflowing horizontally. */\n\toverflow-wrap: break-word;\n\tmax-width: 100%;\n}\n\n/* Restore inline-emphasis semantics inside rendered content. Nextcloud's\n server CSS styles bare <em> as muted (var(--color-text-maxcontrast)) and can\n drop the italic, so `_x_` / `**_x_**` looked greyed-out and non-italic. Force\n italic <em> / bold <strong> that inherit the widget's own text colour. */\n.cn-text-widget :deep(em),\n.cn-text-widget :deep(i) {\n\tfont-style: italic;\n\tcolor: inherit;\n}\n\n.cn-text-widget :deep(strong),\n.cn-text-widget :deep(b) {\n\tfont-weight: bold;\n\tcolor: inherit;\n}\n\n/* Nextcloud's server CSS strips list markers/indent (`list-style: none`,\n no padding), so rendered `<ul>`/`<ol>` looked like plain text. Restore\n bullets/numbers and indentation, including nested levels. */\n.cn-text-widget :deep(ul),\n.cn-text-widget :deep(ol) {\n\tmargin: 4px 0;\n\tpadding-left: 1.5em;\n}\n\n.cn-text-widget :deep(ul) {\n\tlist-style: disc outside;\n}\n\n.cn-text-widget :deep(ol) {\n\tlist-style: decimal outside;\n}\n\n.cn-text-widget :deep(ul ul) {\n\tlist-style-type: circle;\n}\n\n.cn-text-widget :deep(ul ul ul) {\n\tlist-style-type: square;\n}\n\n.cn-text-widget :deep(li) {\n\tdisplay: list-item;\n}\n\n/* Nextcloud's server CSS flattens heading sizes (e.g. h1 → 100%, so it fell\n back to the ~14px body size while h2 stayed 1.8em), leaving them oddly\n scaled. Apply a consistent em-based scale that grows with the widget's own\n font size. */\n.cn-text-widget :deep(h1) {\n\tfont-size: 2em;\n}\n\n.cn-text-widget :deep(h2) {\n\tfont-size: 1.5em;\n}\n\n.cn-text-widget :deep(h3) {\n\tfont-size: 1.25em;\n}\n\n.cn-text-widget :deep(h4) {\n\tfont-size: 1.1em;\n}\n\n.cn-text-widget :deep(h5) {\n\tfont-size: 1em;\n}\n\n.cn-text-widget :deep(h6) {\n\tfont-size: 0.85em;\n}\n\n.cn-text-widget__content--markdown :deep(h1),\n.cn-text-widget__content--markdown :deep(h2),\n.cn-text-widget__content--markdown :deep(h3),\n.cn-text-widget__content--markdown :deep(h4),\n.cn-text-widget__content--markdown :deep(h5),\n.cn-text-widget__content--markdown :deep(h6) {\n\tmargin: 8px 0 4px;\n\tfont-weight: 600;\n\tline-height: 1.3;\n}\n\n.cn-text-widget__content--markdown :deep(p),\n.cn-text-widget__content--markdown :deep(ul),\n.cn-text-widget__content--markdown :deep(ol),\n.cn-text-widget__content--markdown :deep(blockquote) {\n\tmargin: 4px 0;\n}\n\n.cn-text-widget__content--markdown :deep(code) {\n\tpadding: 1px 4px;\n\tborder-radius: 3px;\n\tbackground: var(--color-background-hover);\n\tfont-family: var(--font-monospace, monospace);\n\tfont-size: 0.95em;\n}\n\n.cn-text-widget__content--markdown :deep(pre) {\n\tpadding: 8px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n\toverflow: auto;\n}\n\n.cn-text-widget__content--markdown :deep(pre) :deep(code) {\n\tpadding: 0;\n\tbackground: transparent;\n}\n\n.cn-text-widget__content--markdown :deep(blockquote) {\n\tpadding-left: 8px;\n\tborder-left: 3px solid var(--color-border);\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-text-widget__content--markdown :deep(table) {\n\tborder-collapse: collapse;\n\tmargin: 4px 0;\n}\n\n.cn-text-widget__content--markdown :deep(th),\n.cn-text-widget__content--markdown :deep(td) {\n\tpadding: 4px 8px;\n\tborder: 1px solid var(--color-border);\n}\n\n.cn-text-widget__table {\n\tborder-collapse: collapse;\n\twidth: 100%;\n}\n\n.cn-text-widget__table th,\n.cn-text-widget__table td {\n\toverflow-wrap: break-word;\n\tvertical-align: top;\n}\n\n.cn-text-widget__cell-placeholder {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-style: italic;\n}\n\n.cn-text-widget__link {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 2px;\n\tfont-size: 14px;\n\tfont-weight: 600;\n\tcolor: var(--color-primary-element);\n\ttext-decoration: none;\n}\n\n.cn-text-widget__link:hover,\n.cn-text-widget__link:focus-visible {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["_createElementBlock","_createElementVNode","_Fragment","_renderList","_normalizeStyle","_toDisplayString","_normalizeClass"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import script from './CnUserSettingsDialog.vue2.js';
|
|
2
|
+
import { render } from './CnUserSettingsDialog.vue3.js';
|
|
3
|
+
|
|
4
|
+
script.render = render;
|
|
5
|
+
script.__scopeId = "data-v-8ebfd3c8";
|
|
6
|
+
const __sfc_main = script;
|
|
7
|
+
|
|
8
|
+
export { __sfc_main as default };
|
|
9
|
+
//# sourceMappingURL=CnUserSettingsDialog.vue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CnUserSettingsDialog.vue.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { translate } from '@nextcloud/l10n';
|
|
2
|
+
import { NcButton } from '@nextcloud/vue';
|
|
3
|
+
import { markRaw } from 'vue';
|
|
4
|
+
import __sfc_main from '../CnDialog/CnDialog.vue.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The key NcAppSettingsSection injects to register itself with its dialog
|
|
8
|
+
* (`Symbol.for`, so the same symbol `@nextcloud/vue` provides).
|
|
9
|
+
*/
|
|
10
|
+
const REGISTRATION_KEY = Symbol.for('NcAppSettingsDialog:registration');
|
|
11
|
+
|
|
12
|
+
/** Renders a section's icon vnodes as they were registered. */
|
|
13
|
+
const CnVNodes = {
|
|
14
|
+
name: 'CnVNodes',
|
|
15
|
+
props: { vnodes: { type: [Array, Object], default: null } },
|
|
16
|
+
render() {
|
|
17
|
+
return this.vnodes
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* CnUserSettingsDialog - the user settings dialog in the board look (board
|
|
23
|
+
* PqPersoonlijk): eyebrow, title and subtitle, a left navigation listing the
|
|
24
|
+
* sections with their icons, the sections themselves, and a footer with a
|
|
25
|
+
* note that changes save straight away and a Close button.
|
|
26
|
+
*
|
|
27
|
+
* The sections stay the app's: any `NcAppSettingsSection` in the default slot
|
|
28
|
+
* registers itself with this dialog exactly as it does with
|
|
29
|
+
* `NcAppSettingsDialog`, with its name, order and `#icon` slot. CnAppRoot
|
|
30
|
+
* renders this dialog instead of `NcAppSettingsDialog` under the board look.
|
|
31
|
+
*
|
|
32
|
+
* @spec openspec/changes/user-settings-board-dialog/specs/user-settings-board-dialog/spec.md#requirement-the-user-settings-dialog-takes-the-board-look
|
|
33
|
+
*
|
|
34
|
+
* @event update:open Emitted with `false` when the dialog closes.
|
|
35
|
+
*/
|
|
36
|
+
var script = {
|
|
37
|
+
name: 'CnUserSettingsDialog',
|
|
38
|
+
|
|
39
|
+
components: { CnDialog: __sfc_main, CnVNodes, NcButton },
|
|
40
|
+
|
|
41
|
+
provide() {
|
|
42
|
+
return {
|
|
43
|
+
[REGISTRATION_KEY]: {
|
|
44
|
+
registerSection: this.registerSection,
|
|
45
|
+
unregisterSection: this.unregisterSection,
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
props: {
|
|
51
|
+
/** Whether the dialog is open. */
|
|
52
|
+
open: { type: Boolean, default: false },
|
|
53
|
+
/** Dialog title; also its accessible name. */
|
|
54
|
+
title: { type: String, required: true },
|
|
55
|
+
/** Context line above the title. */
|
|
56
|
+
eyebrow: { type: String, default: () => translate('nextcloud-vue', 'Your account') },
|
|
57
|
+
/** Sentence under the title. */
|
|
58
|
+
subtitle: { type: String, default: () => translate('nextcloud-vue', 'Only for you. Colleagues keep their own settings.') },
|
|
59
|
+
/** The app's name, after the footer note. */
|
|
60
|
+
appName: { type: String, default: '' },
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
emits: ['update:open'],
|
|
64
|
+
|
|
65
|
+
data() {
|
|
66
|
+
return {
|
|
67
|
+
sections: [],
|
|
68
|
+
selected: '',
|
|
69
|
+
picked: false,
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
computed: {
|
|
74
|
+
/** @return {object} The board's eyebrow: muted, in sentence case. */
|
|
75
|
+
eyebrowStyle() {
|
|
76
|
+
return { '--cn-dialog-eyebrow-transform': 'none', '--cn-dialog-eyebrow-color': 'var(--color-text-maxcontrast)' }
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/** @return {string} The navigation's accessible name. */
|
|
80
|
+
navLabel() {
|
|
81
|
+
return translate('nextcloud-vue', 'Sections of the settings')
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/** @return {string} The footer note, with the app's name when known. */
|
|
85
|
+
footerNote() {
|
|
86
|
+
const note = translate('nextcloud-vue', 'Changes are saved straight away');
|
|
87
|
+
return this.appName ? `${note} · ${this.appName}` : note
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/** @return {string} The Close button label. */
|
|
91
|
+
closeLabel() {
|
|
92
|
+
return translate('nextcloud-vue', 'Close')
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
methods: {
|
|
97
|
+
/**
|
|
98
|
+
* Add a section to the navigation, kept in `order`; a section
|
|
99
|
+
* without one goes last. Until the reader picks one, the first
|
|
100
|
+
* section is current.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} id The section id.
|
|
103
|
+
* @param {string} name The section name.
|
|
104
|
+
* @param {number} [order] The section's place.
|
|
105
|
+
* @param {Array} [icon] The section's icon vnodes.
|
|
106
|
+
*/
|
|
107
|
+
registerSection(id, name, order, icon) {
|
|
108
|
+
const rest = this.sections.filter((section) => section.id !== id);
|
|
109
|
+
const place = typeof order === 'number' ? order : Math.max(0, ...rest.map((section) => section.order)) + 1;
|
|
110
|
+
this.sections = [...rest, { id, name, order: place, icon: icon ? markRaw(icon) : null }].sort((a, b) => a.order - b.order);
|
|
111
|
+
if (!this.picked) {
|
|
112
|
+
this.selected = this.sections[0].id;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Remove a section from the navigation.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} id The section id.
|
|
120
|
+
*/
|
|
121
|
+
unregisterSection(id) {
|
|
122
|
+
this.sections = this.sections.filter((section) => section.id !== id);
|
|
123
|
+
if (this.selected === id) {
|
|
124
|
+
this.selected = this.sections.length > 0 ? this.sections[0].id : '';
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Mark a section current and scroll it into view.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} id The section id.
|
|
132
|
+
*/
|
|
133
|
+
selectSection(id) {
|
|
134
|
+
this.picked = true;
|
|
135
|
+
this.selected = id;
|
|
136
|
+
const target = typeof document !== 'undefined' ? document.getElementById(`settings-section_${id}`) : null;
|
|
137
|
+
if (target && typeof target.scrollIntoView === 'function') {
|
|
138
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Follow NcDialog's own open state.
|
|
144
|
+
*
|
|
145
|
+
* @param {boolean} value Whether NcDialog is open.
|
|
146
|
+
*/
|
|
147
|
+
onUpdateOpen(value) {
|
|
148
|
+
if (!value) {
|
|
149
|
+
this.close();
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
/** Close the dialog. */
|
|
154
|
+
close() {
|
|
155
|
+
/**
|
|
156
|
+
* @event update:open Emitted with `false` when the dialog closes (Close button, close icon, Escape).
|
|
157
|
+
*/
|
|
158
|
+
this.$emit('update:open', false);
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export { script as default };
|
|
164
|
+
//# sourceMappingURL=CnUserSettingsDialog.vue2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CnUserSettingsDialog.vue2.js","sources":["../../../../src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<CnDialog\n\t\tclass=\"cn-user-settings-dialog\"\n\t\t:name=\"title\"\n\t\tlook=\"board\"\n\t\twidth=\"wizard\"\n\t\t:eyebrow=\"eyebrow\"\n\t\t:subtitle=\"subtitle\"\n\t\t:open=\"open\"\n\t\t:style=\"eyebrowStyle\"\n\t\tdata-testid=\"cn-user-settings-dialog\"\n\t\t@update:open=\"onUpdateOpen\"\n\t\t@closing=\"close\">\n\t\t<div class=\"cn-user-settings-dialog__body\">\n\t\t\t<nav\n\t\t\t\tv-if=\"sections.length > 0\"\n\t\t\t\tclass=\"cn-user-settings-dialog__nav\"\n\t\t\t\t:aria-label=\"navLabel\"\n\t\t\t\tdata-testid=\"cn-user-settings-nav\">\n\t\t\t\t<ul>\n\t\t\t\t\t<li v-for=\"section in sections\" :key=\"section.id\">\n\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t:href=\"`#settings-section_${section.id}`\"\n\t\t\t\t\t\t\tclass=\"cn-user-settings-dialog__nav-link\"\n\t\t\t\t\t\t\t:class=\"{ 'cn-user-settings-dialog__nav-link--active': section.id === selected }\"\n\t\t\t\t\t\t\t:aria-current=\"section.id === selected ? 'true' : undefined\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-user-settings-nav-${section.id}`\"\n\t\t\t\t\t\t\t@click.prevent=\"selectSection(section.id)\">\n\t\t\t\t\t\t\t<span v-if=\"section.icon\" class=\"cn-user-settings-dialog__nav-icon\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t\t<CnVNodes :vnodes=\"section.icon\" />\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span>{{ section.name }}</span>\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</nav>\n\t\t\t<div class=\"cn-user-settings-dialog__sections\">\n\t\t\t\t<!-- @slot default NcAppSettingsSection children; each registers itself in the navigation. -->\n\t\t\t\t<slot />\n\t\t\t</div>\n\t\t</div>\n\t\t<template #actions>\n\t\t\t<p class=\"cn-user-settings-dialog__note\" data-testid=\"cn-user-settings-note\">\n\t\t\t\t{{ footerNote }}\n\t\t\t</p>\n\t\t\t<NcButton variant=\"secondary\" data-testid=\"cn-user-settings-close\" @click=\"close\">\n\t\t\t\t{{ closeLabel }}\n\t\t\t</NcButton>\n\t\t</template>\n\t</CnDialog>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton } from '@nextcloud/vue'\nimport { markRaw } from 'vue'\nimport CnDialog from '../CnDialog/CnDialog.vue'\n\n/**\n * The key NcAppSettingsSection injects to register itself with its dialog\n * (`Symbol.for`, so the same symbol `@nextcloud/vue` provides).\n */\nconst REGISTRATION_KEY = Symbol.for('NcAppSettingsDialog:registration')\n\n/** Renders a section's icon vnodes as they were registered. */\nconst CnVNodes = {\n\tname: 'CnVNodes',\n\tprops: { vnodes: { type: [Array, Object], default: null } },\n\trender() {\n\t\treturn this.vnodes\n\t},\n}\n\n/**\n * CnUserSettingsDialog - the user settings dialog in the board look (board\n * PqPersoonlijk): eyebrow, title and subtitle, a left navigation listing the\n * sections with their icons, the sections themselves, and a footer with a\n * note that changes save straight away and a Close button.\n *\n * The sections stay the app's: any `NcAppSettingsSection` in the default slot\n * registers itself with this dialog exactly as it does with\n * `NcAppSettingsDialog`, with its name, order and `#icon` slot. CnAppRoot\n * renders this dialog instead of `NcAppSettingsDialog` under the board look.\n *\n * @spec openspec/changes/user-settings-board-dialog/specs/user-settings-board-dialog/spec.md#requirement-the-user-settings-dialog-takes-the-board-look\n *\n * @event update:open Emitted with `false` when the dialog closes.\n */\nexport default {\n\tname: 'CnUserSettingsDialog',\n\n\tcomponents: { CnDialog, CnVNodes, NcButton },\n\n\tprovide() {\n\t\treturn {\n\t\t\t[REGISTRATION_KEY]: {\n\t\t\t\tregisterSection: this.registerSection,\n\t\t\t\tunregisterSection: this.unregisterSection,\n\t\t\t},\n\t\t}\n\t},\n\n\tprops: {\n\t\t/** Whether the dialog is open. */\n\t\topen: { type: Boolean, default: false },\n\t\t/** Dialog title; also its accessible name. */\n\t\ttitle: { type: String, required: true },\n\t\t/** Context line above the title. */\n\t\teyebrow: { type: String, default: () => t('nextcloud-vue', 'Your account') },\n\t\t/** Sentence under the title. */\n\t\tsubtitle: { type: String, default: () => t('nextcloud-vue', 'Only for you. Colleagues keep their own settings.') },\n\t\t/** The app's name, after the footer note. */\n\t\tappName: { type: String, default: '' },\n\t},\n\n\temits: ['update:open'],\n\n\tdata() {\n\t\treturn {\n\t\t\tsections: [],\n\t\t\tselected: '',\n\t\t\tpicked: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {object} The board's eyebrow: muted, in sentence case. */\n\t\teyebrowStyle() {\n\t\t\treturn { '--cn-dialog-eyebrow-transform': 'none', '--cn-dialog-eyebrow-color': 'var(--color-text-maxcontrast)' }\n\t\t},\n\n\t\t/** @return {string} The navigation's accessible name. */\n\t\tnavLabel() {\n\t\t\treturn t('nextcloud-vue', 'Sections of the settings')\n\t\t},\n\n\t\t/** @return {string} The footer note, with the app's name when known. */\n\t\tfooterNote() {\n\t\t\tconst note = t('nextcloud-vue', 'Changes are saved straight away')\n\t\t\treturn this.appName ? `${note} · ${this.appName}` : note\n\t\t},\n\n\t\t/** @return {string} The Close button label. */\n\t\tcloseLabel() {\n\t\t\treturn t('nextcloud-vue', 'Close')\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Add a section to the navigation, kept in `order`; a section\n\t\t * without one goes last. Until the reader picks one, the first\n\t\t * section is current.\n\t\t *\n\t\t * @param {string} id The section id.\n\t\t * @param {string} name The section name.\n\t\t * @param {number} [order] The section's place.\n\t\t * @param {Array} [icon] The section's icon vnodes.\n\t\t */\n\t\tregisterSection(id, name, order, icon) {\n\t\t\tconst rest = this.sections.filter((section) => section.id !== id)\n\t\t\tconst place = typeof order === 'number' ? order : Math.max(0, ...rest.map((section) => section.order)) + 1\n\t\t\tthis.sections = [...rest, { id, name, order: place, icon: icon ? markRaw(icon) : null }].sort((a, b) => a.order - b.order)\n\t\t\tif (!this.picked) {\n\t\t\t\tthis.selected = this.sections[0].id\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove a section from the navigation.\n\t\t *\n\t\t * @param {string} id The section id.\n\t\t */\n\t\tunregisterSection(id) {\n\t\t\tthis.sections = this.sections.filter((section) => section.id !== id)\n\t\t\tif (this.selected === id) {\n\t\t\t\tthis.selected = this.sections.length > 0 ? this.sections[0].id : ''\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Mark a section current and scroll it into view.\n\t\t *\n\t\t * @param {string} id The section id.\n\t\t */\n\t\tselectSection(id) {\n\t\t\tthis.picked = true\n\t\t\tthis.selected = id\n\t\t\tconst target = typeof document !== 'undefined' ? document.getElementById(`settings-section_${id}`) : null\n\t\t\tif (target && typeof target.scrollIntoView === 'function') {\n\t\t\t\ttarget.scrollIntoView({ behavior: 'smooth', block: 'start' })\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Follow NcDialog's own open state.\n\t\t *\n\t\t * @param {boolean} value Whether NcDialog is open.\n\t\t */\n\t\tonUpdateOpen(value) {\n\t\t\tif (!value) {\n\t\t\t\tthis.close()\n\t\t\t}\n\t\t},\n\n\t\t/** Close the dialog. */\n\t\tclose() {\n\t\t\t/**\n\t\t\t * @event update:open Emitted with `false` when the dialog closes (Close button, close icon, Escape).\n\t\t\t */\n\t\t\tthis.$emit('update:open', false)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-user-settings-dialog__body {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 0 24px;\n}\n\n.cn-user-settings-dialog__nav {\n\tflex: 0 0 200px;\n\twidth: 200px;\n\tpadding-top: 18px;\n}\n\n.cn-user-settings-dialog__nav ul {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-user-settings-dialog__nav-link {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 10px;\n\tmin-height: 40px;\n\tpadding: 0 12px;\n\tborder-radius: var(--border-radius-element, 8px);\n\tfont-size: 15px;\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n}\n\n.cn-user-settings-dialog__nav-link:hover {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-user-settings-dialog__nav-link:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-user-settings-dialog__nav-link--active,\n.cn-user-settings-dialog__nav-link--active:hover {\n\tbackground: var(--color-primary-element-light);\n\tcolor: var(--color-primary-element-light-text);\n\tfont-weight: 600;\n}\n\n.cn-user-settings-dialog__nav-icon {\n\tdisplay: inline-flex;\n\tflex: none;\n\twidth: 18px;\n\theight: 18px;\n\talign-items: center;\n\tjustify-content: center;\n}\n\n.cn-user-settings-dialog__sections {\n\tdisplay: flex;\n\tflex: 1 1 380px;\n\tflex-direction: column;\n\tmin-width: 0;\n}\n\n.cn-user-settings-dialog__sections :deep(.app-settings-section) {\n\tmargin: 0;\n\tpadding: 18px 0;\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-user-settings-dialog__sections :deep(.app-settings-section__name) {\n\tmargin: 0 0 12px;\n\tfont-size: 17px;\n\tfont-weight: 700;\n}\n\n.cn-user-settings-dialog__note {\n\tflex: 1 1 auto;\n\tmargin: 0;\n\tfont-size: 14px;\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["CnDialog","t"],"mappings":";;;;;AA8DA;AACC;;;AAGD,MAAM,gBAAiB,GAAE,MAAM,CAAC,GAAG,CAAC,kCAAkC,EAAA;;AAEtE;AACA,MAAM,QAAO,GAAI;AAChB,CAAA,IAAI,EAAE,UAAU;CAChB,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,IAAK,EAAA,EAAG;AAC3D,CAAA,MAAM,GAAG;AACR,EAAA,OAAO,IAAI,CAAC,MAAK;EACjB;AACF,EAAA;;AAEA;AACC;AACA;;;AAGA;AACA;;AAEA;AACA;AACA;AACA;AACA;;;AAGD,aAAe;CACd,IAAI,EAAE,sBAAsB;;AAE5B,CAAA,UAAU,EAAE,YAAEA,UAAQ,EAAE,QAAQ,EAAE,UAAU;;AAE5C,CAAA,OAAO,GAAG;AACT,EAAA,OAAO;AACN,GAAA,CAAC,gBAAgB,GAAG;AACnB,IAAA,eAAe,EAAE,IAAI,CAAC,eAAe;AACrC,IAAA,iBAAiB,EAAE,IAAI,CAAC,iBAAiB;IACzC;AACF,GAAA;EACA;;AAED,CAAA,KAAK,EAAE;;AAEN,EAAA,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO;AACvC;AACA,EAAA,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;;AAEvC,EAAA,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMC,SAAC,CAAC,eAAe,EAAE,cAAc,GAAG;;EAE5E,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,mDAAmD,CAAA,EAAG;AAClH;AACA,EAAA,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI;EACtC;;AAED,CAAA,KAAK,EAAE,CAAC,aAAa,CAAC;;AAEtB,CAAA,IAAI,GAAG;AACN,EAAA,OAAO;GACN,QAAQ,EAAE,EAAE;GACZ,QAAQ,EAAE,EAAE;GACZ,MAAM,EAAE,KAAK;AACd,GAAA;EACA;;AAED,CAAA,QAAQ,EAAE;;AAET,EAAA,YAAY,GAAG;GACd,OAAO,EAAE,+BAA+B,EAAE,MAAM,EAAE,2BAA2B,EAAE,+BAAgC,EAAA;GAC/G;;;EAGD,QAAQ,GAAG;GACV,OAAOA,SAAC,CAAC,eAAe,EAAE,0BAA0B,CAAA;GACpD;;;EAGD,UAAU,GAAG;GACZ,MAAM,IAAG,GAAIA,SAAC,CAAC,eAAe,EAAE,iCAAiC,EAAA;GACjE,OAAO,IAAI,CAAC,UAAU,CAAC,EAAE,IAAI,CAAA,GAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAE,GAAE,IAAG;GACvD;;AAED;EACA,UAAU,GAAG;AACZ,GAAA,OAAOA,SAAC,CAAC,eAAe,EAAE,OAAO,CAAA;GACjC;EACD;;AAED,CAAA,OAAO,EAAE;AACR;;;AAGC;AACA;;AAEA;AACA;AACA;;AAED,EAAA,eAAe,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;GACtC,MAAM,IAAG,GAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,EAAC,KAAM,EAAE,EAAA;AAChE,GAAA,MAAM,KAAI,GAAI,OAAO,KAAI,KAAM,QAAO,GAAI,KAAI,GAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,KAAK,CAAC,IAAI,EAAA;GACzG,IAAI,CAAC,WAAW,CAAC,GAAG,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAK,GAAE,OAAO,CAAC,IAAI,CAAA,GAAI,IAAK,EAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAA;AACzH,GAAA,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;AACjB,IAAA,IAAI,CAAC,QAAO,GAAI,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAC;AACnC,IAAA;GACA;;AAED;AACC;AACA;;;AAGD,EAAA,iBAAiB,CAAC,EAAE,EAAE;GACrB,IAAI,CAAC,QAAS,GAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,EAAC,KAAM,EAAE,EAAA;GACnE,IAAI,IAAI,CAAC,QAAS,KAAI,EAAE,EAAE;IACzB,IAAI,CAAC,QAAS,GAAE,IAAI,CAAC,QAAQ,CAAC,MAAK,GAAI,CAAA,GAAI,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAG,GAAE,GAAC;AACnE,IAAA;GACA;;AAED;AACC;AACA;;;AAGD,EAAA,aAAa,CAAC,EAAE,EAAE;AACjB,GAAA,IAAI,CAAC,MAAK,GAAI,KAAG;AACjB,GAAA,IAAI,CAAC,QAAO,GAAI,GAAC;GACjB,MAAM,MAAK,GAAI,OAAO,QAAO,KAAM,WAAY,GAAE,QAAQ,CAAC,cAAc,CAAC,CAAC,iBAAiB,EAAE,EAAE,CAAC,CAAC,IAAI,KAAG;GACxG,IAAI,MAAO,IAAG,OAAO,MAAM,CAAC,cAAe,KAAI,UAAU,EAAE;IAC1D,MAAM,CAAC,cAAc,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAM,EAAG,EAAA;AAC7D,IAAA;GACA;;AAED;;AAEC;AACA;;AAED,EAAA,YAAY,CAAC,KAAK,EAAE;GACnB,IAAI,CAAC,KAAK,EAAE;IACX,IAAI,CAAC,KAAK,GAAC;AACZ,IAAA;GACA;;AAED;AACA,EAAA,KAAK,GAAG;AACP;AACC;;AAED,GAAA,IAAI,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,EAAA;GAC/B;EACD;AACF;;;;"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { resolveComponent, openBlock, createBlock, normalizeStyle, withCtx, createElementVNode, toDisplayString, createVNode, createTextVNode, createElementBlock, Fragment, renderList, normalizeClass, withModifiers, createCommentVNode, renderSlot } from 'vue';
|
|
2
|
+
|
|
3
|
+
const _hoisted_1 = { class: "cn-user-settings-dialog__body" };
|
|
4
|
+
const _hoisted_2 = ["aria-label"];
|
|
5
|
+
const _hoisted_3 = ["href", "aria-current", "data-testid", "onClick"];
|
|
6
|
+
const _hoisted_4 = {
|
|
7
|
+
key: 0,
|
|
8
|
+
class: "cn-user-settings-dialog__nav-icon",
|
|
9
|
+
"aria-hidden": "true"
|
|
10
|
+
};
|
|
11
|
+
const _hoisted_5 = { class: "cn-user-settings-dialog__sections" };
|
|
12
|
+
const _hoisted_6 = {
|
|
13
|
+
class: "cn-user-settings-dialog__note",
|
|
14
|
+
"data-testid": "cn-user-settings-note"
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
18
|
+
const _component_CnVNodes = resolveComponent("CnVNodes");
|
|
19
|
+
const _component_NcButton = resolveComponent("NcButton");
|
|
20
|
+
const _component_CnDialog = resolveComponent("CnDialog");
|
|
21
|
+
|
|
22
|
+
return (openBlock(), createBlock(_component_CnDialog, {
|
|
23
|
+
class: "cn-user-settings-dialog",
|
|
24
|
+
name: $props.title,
|
|
25
|
+
look: "board",
|
|
26
|
+
width: "wizard",
|
|
27
|
+
eyebrow: $props.eyebrow,
|
|
28
|
+
subtitle: $props.subtitle,
|
|
29
|
+
open: $props.open,
|
|
30
|
+
style: normalizeStyle($options.eyebrowStyle),
|
|
31
|
+
"data-testid": "cn-user-settings-dialog",
|
|
32
|
+
"onUpdate:open": $options.onUpdateOpen,
|
|
33
|
+
onClosing: $options.close
|
|
34
|
+
}, {
|
|
35
|
+
actions: withCtx(() => [
|
|
36
|
+
createElementVNode("p", _hoisted_6, toDisplayString($options.footerNote), 1 /* TEXT */),
|
|
37
|
+
createVNode(_component_NcButton, {
|
|
38
|
+
variant: "secondary",
|
|
39
|
+
"data-testid": "cn-user-settings-close",
|
|
40
|
+
onClick: $options.close
|
|
41
|
+
}, {
|
|
42
|
+
default: withCtx(() => [
|
|
43
|
+
createTextVNode(toDisplayString($options.closeLabel), 1 /* TEXT */)
|
|
44
|
+
]),
|
|
45
|
+
_: 1 /* STABLE */
|
|
46
|
+
}, 8 /* PROPS */, ["onClick"])
|
|
47
|
+
]),
|
|
48
|
+
default: withCtx(() => [
|
|
49
|
+
createElementVNode("div", _hoisted_1, [
|
|
50
|
+
($data.sections.length > 0)
|
|
51
|
+
? (openBlock(), createElementBlock("nav", {
|
|
52
|
+
key: 0,
|
|
53
|
+
class: "cn-user-settings-dialog__nav",
|
|
54
|
+
"aria-label": $options.navLabel,
|
|
55
|
+
"data-testid": "cn-user-settings-nav"
|
|
56
|
+
}, [
|
|
57
|
+
createElementVNode("ul", null, [
|
|
58
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList($data.sections, (section) => {
|
|
59
|
+
return (openBlock(), createElementBlock("li", {
|
|
60
|
+
key: section.id
|
|
61
|
+
}, [
|
|
62
|
+
createElementVNode("a", {
|
|
63
|
+
href: `#settings-section_${section.id}`,
|
|
64
|
+
class: normalizeClass(["cn-user-settings-dialog__nav-link", { 'cn-user-settings-dialog__nav-link--active': section.id === $data.selected }]),
|
|
65
|
+
"aria-current": section.id === $data.selected ? 'true' : undefined,
|
|
66
|
+
"data-testid": `cn-user-settings-nav-${section.id}`,
|
|
67
|
+
onClick: withModifiers($event => ($options.selectSection(section.id)), ["prevent"])
|
|
68
|
+
}, [
|
|
69
|
+
(section.icon)
|
|
70
|
+
? (openBlock(), createElementBlock("span", _hoisted_4, [
|
|
71
|
+
createVNode(_component_CnVNodes, {
|
|
72
|
+
vnodes: section.icon
|
|
73
|
+
}, null, 8 /* PROPS */, ["vnodes"])
|
|
74
|
+
]))
|
|
75
|
+
: createCommentVNode("v-if", true),
|
|
76
|
+
createElementVNode("span", null, toDisplayString(section.name), 1 /* TEXT */)
|
|
77
|
+
], 10 /* CLASS, PROPS */, _hoisted_3)
|
|
78
|
+
]))
|
|
79
|
+
}), 128 /* KEYED_FRAGMENT */))
|
|
80
|
+
])
|
|
81
|
+
], 8 /* PROPS */, _hoisted_2))
|
|
82
|
+
: createCommentVNode("v-if", true),
|
|
83
|
+
createElementVNode("div", _hoisted_5, [
|
|
84
|
+
renderSlot(_ctx.$slots, "default")
|
|
85
|
+
])
|
|
86
|
+
])
|
|
87
|
+
]),
|
|
88
|
+
_: 3 /* FORWARDED */
|
|
89
|
+
}, 8 /* PROPS */, ["name", "eyebrow", "subtitle", "open", "style", "onUpdate:open", "onClosing"]))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export { render };
|
|
93
|
+
//# sourceMappingURL=CnUserSettingsDialog.vue3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CnUserSettingsDialog.vue3.js","sources":["../../../../src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue?vue&type=template&id=8ebfd3c8&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<CnDialog\n\t\tclass=\"cn-user-settings-dialog\"\n\t\t:name=\"title\"\n\t\tlook=\"board\"\n\t\twidth=\"wizard\"\n\t\t:eyebrow=\"eyebrow\"\n\t\t:subtitle=\"subtitle\"\n\t\t:open=\"open\"\n\t\t:style=\"eyebrowStyle\"\n\t\tdata-testid=\"cn-user-settings-dialog\"\n\t\t@update:open=\"onUpdateOpen\"\n\t\t@closing=\"close\">\n\t\t<div class=\"cn-user-settings-dialog__body\">\n\t\t\t<nav\n\t\t\t\tv-if=\"sections.length > 0\"\n\t\t\t\tclass=\"cn-user-settings-dialog__nav\"\n\t\t\t\t:aria-label=\"navLabel\"\n\t\t\t\tdata-testid=\"cn-user-settings-nav\">\n\t\t\t\t<ul>\n\t\t\t\t\t<li v-for=\"section in sections\" :key=\"section.id\">\n\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t:href=\"`#settings-section_${section.id}`\"\n\t\t\t\t\t\t\tclass=\"cn-user-settings-dialog__nav-link\"\n\t\t\t\t\t\t\t:class=\"{ 'cn-user-settings-dialog__nav-link--active': section.id === selected }\"\n\t\t\t\t\t\t\t:aria-current=\"section.id === selected ? 'true' : undefined\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-user-settings-nav-${section.id}`\"\n\t\t\t\t\t\t\t@click.prevent=\"selectSection(section.id)\">\n\t\t\t\t\t\t\t<span v-if=\"section.icon\" class=\"cn-user-settings-dialog__nav-icon\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t\t<CnVNodes :vnodes=\"section.icon\" />\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span>{{ section.name }}</span>\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</nav>\n\t\t\t<div class=\"cn-user-settings-dialog__sections\">\n\t\t\t\t<!-- @slot default NcAppSettingsSection children; each registers itself in the navigation. -->\n\t\t\t\t<slot />\n\t\t\t</div>\n\t\t</div>\n\t\t<template #actions>\n\t\t\t<p class=\"cn-user-settings-dialog__note\" data-testid=\"cn-user-settings-note\">\n\t\t\t\t{{ footerNote }}\n\t\t\t</p>\n\t\t\t<NcButton variant=\"secondary\" data-testid=\"cn-user-settings-close\" @click=\"close\">\n\t\t\t\t{{ closeLabel }}\n\t\t\t</NcButton>\n\t\t</template>\n\t</CnDialog>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton } from '@nextcloud/vue'\nimport { markRaw } from 'vue'\nimport CnDialog from '../CnDialog/CnDialog.vue'\n\n/**\n * The key NcAppSettingsSection injects to register itself with its dialog\n * (`Symbol.for`, so the same symbol `@nextcloud/vue` provides).\n */\nconst REGISTRATION_KEY = Symbol.for('NcAppSettingsDialog:registration')\n\n/** Renders a section's icon vnodes as they were registered. */\nconst CnVNodes = {\n\tname: 'CnVNodes',\n\tprops: { vnodes: { type: [Array, Object], default: null } },\n\trender() {\n\t\treturn this.vnodes\n\t},\n}\n\n/**\n * CnUserSettingsDialog - the user settings dialog in the board look (board\n * PqPersoonlijk): eyebrow, title and subtitle, a left navigation listing the\n * sections with their icons, the sections themselves, and a footer with a\n * note that changes save straight away and a Close button.\n *\n * The sections stay the app's: any `NcAppSettingsSection` in the default slot\n * registers itself with this dialog exactly as it does with\n * `NcAppSettingsDialog`, with its name, order and `#icon` slot. CnAppRoot\n * renders this dialog instead of `NcAppSettingsDialog` under the board look.\n *\n * @spec openspec/changes/user-settings-board-dialog/specs/user-settings-board-dialog/spec.md#requirement-the-user-settings-dialog-takes-the-board-look\n *\n * @event update:open Emitted with `false` when the dialog closes.\n */\nexport default {\n\tname: 'CnUserSettingsDialog',\n\n\tcomponents: { CnDialog, CnVNodes, NcButton },\n\n\tprovide() {\n\t\treturn {\n\t\t\t[REGISTRATION_KEY]: {\n\t\t\t\tregisterSection: this.registerSection,\n\t\t\t\tunregisterSection: this.unregisterSection,\n\t\t\t},\n\t\t}\n\t},\n\n\tprops: {\n\t\t/** Whether the dialog is open. */\n\t\topen: { type: Boolean, default: false },\n\t\t/** Dialog title; also its accessible name. */\n\t\ttitle: { type: String, required: true },\n\t\t/** Context line above the title. */\n\t\teyebrow: { type: String, default: () => t('nextcloud-vue', 'Your account') },\n\t\t/** Sentence under the title. */\n\t\tsubtitle: { type: String, default: () => t('nextcloud-vue', 'Only for you. Colleagues keep their own settings.') },\n\t\t/** The app's name, after the footer note. */\n\t\tappName: { type: String, default: '' },\n\t},\n\n\temits: ['update:open'],\n\n\tdata() {\n\t\treturn {\n\t\t\tsections: [],\n\t\t\tselected: '',\n\t\t\tpicked: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {object} The board's eyebrow: muted, in sentence case. */\n\t\teyebrowStyle() {\n\t\t\treturn { '--cn-dialog-eyebrow-transform': 'none', '--cn-dialog-eyebrow-color': 'var(--color-text-maxcontrast)' }\n\t\t},\n\n\t\t/** @return {string} The navigation's accessible name. */\n\t\tnavLabel() {\n\t\t\treturn t('nextcloud-vue', 'Sections of the settings')\n\t\t},\n\n\t\t/** @return {string} The footer note, with the app's name when known. */\n\t\tfooterNote() {\n\t\t\tconst note = t('nextcloud-vue', 'Changes are saved straight away')\n\t\t\treturn this.appName ? `${note} · ${this.appName}` : note\n\t\t},\n\n\t\t/** @return {string} The Close button label. */\n\t\tcloseLabel() {\n\t\t\treturn t('nextcloud-vue', 'Close')\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Add a section to the navigation, kept in `order`; a section\n\t\t * without one goes last. Until the reader picks one, the first\n\t\t * section is current.\n\t\t *\n\t\t * @param {string} id The section id.\n\t\t * @param {string} name The section name.\n\t\t * @param {number} [order] The section's place.\n\t\t * @param {Array} [icon] The section's icon vnodes.\n\t\t */\n\t\tregisterSection(id, name, order, icon) {\n\t\t\tconst rest = this.sections.filter((section) => section.id !== id)\n\t\t\tconst place = typeof order === 'number' ? order : Math.max(0, ...rest.map((section) => section.order)) + 1\n\t\t\tthis.sections = [...rest, { id, name, order: place, icon: icon ? markRaw(icon) : null }].sort((a, b) => a.order - b.order)\n\t\t\tif (!this.picked) {\n\t\t\t\tthis.selected = this.sections[0].id\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove a section from the navigation.\n\t\t *\n\t\t * @param {string} id The section id.\n\t\t */\n\t\tunregisterSection(id) {\n\t\t\tthis.sections = this.sections.filter((section) => section.id !== id)\n\t\t\tif (this.selected === id) {\n\t\t\t\tthis.selected = this.sections.length > 0 ? this.sections[0].id : ''\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Mark a section current and scroll it into view.\n\t\t *\n\t\t * @param {string} id The section id.\n\t\t */\n\t\tselectSection(id) {\n\t\t\tthis.picked = true\n\t\t\tthis.selected = id\n\t\t\tconst target = typeof document !== 'undefined' ? document.getElementById(`settings-section_${id}`) : null\n\t\t\tif (target && typeof target.scrollIntoView === 'function') {\n\t\t\t\ttarget.scrollIntoView({ behavior: 'smooth', block: 'start' })\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Follow NcDialog's own open state.\n\t\t *\n\t\t * @param {boolean} value Whether NcDialog is open.\n\t\t */\n\t\tonUpdateOpen(value) {\n\t\t\tif (!value) {\n\t\t\t\tthis.close()\n\t\t\t}\n\t\t},\n\n\t\t/** Close the dialog. */\n\t\tclose() {\n\t\t\t/**\n\t\t\t * @event update:open Emitted with `false` when the dialog closes (Close button, close icon, Escape).\n\t\t\t */\n\t\t\tthis.$emit('update:open', false)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-user-settings-dialog__body {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 0 24px;\n}\n\n.cn-user-settings-dialog__nav {\n\tflex: 0 0 200px;\n\twidth: 200px;\n\tpadding-top: 18px;\n}\n\n.cn-user-settings-dialog__nav ul {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-user-settings-dialog__nav-link {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 10px;\n\tmin-height: 40px;\n\tpadding: 0 12px;\n\tborder-radius: var(--border-radius-element, 8px);\n\tfont-size: 15px;\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n}\n\n.cn-user-settings-dialog__nav-link:hover {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-user-settings-dialog__nav-link:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-user-settings-dialog__nav-link--active,\n.cn-user-settings-dialog__nav-link--active:hover {\n\tbackground: var(--color-primary-element-light);\n\tcolor: var(--color-primary-element-light-text);\n\tfont-weight: 600;\n}\n\n.cn-user-settings-dialog__nav-icon {\n\tdisplay: inline-flex;\n\tflex: none;\n\twidth: 18px;\n\theight: 18px;\n\talign-items: center;\n\tjustify-content: center;\n}\n\n.cn-user-settings-dialog__sections {\n\tdisplay: flex;\n\tflex: 1 1 380px;\n\tflex-direction: column;\n\tmin-width: 0;\n}\n\n.cn-user-settings-dialog__sections :deep(.app-settings-section) {\n\tmargin: 0;\n\tpadding: 18px 0;\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-user-settings-dialog__sections :deep(.app-settings-section__name) {\n\tmargin: 0 0 12px;\n\tfont-size: 17px;\n\tfont-weight: 700;\n}\n\n.cn-user-settings-dialog__note {\n\tflex: 1 1 auto;\n\tmargin: 0;\n\tfont-size: 14px;\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["_toDisplayString","_createVNode","_createElementVNode","_createElementBlock","_Fragment","_renderList","_withModifiers","_openBlock","_renderSlot"],"mappings":";;AAqBO,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,+BAAY,GAAA;;;;;AAehB,EAAA,KAAA,EAAA,mCAAA;AAAA,EAAA,aAAA,EAAA,MAAA;;AAQF,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,mCAAA,GAAA;;AAMF,EAAA,KAAM,EAAC,+BAAO;AAAA,EAAA,aAAA,EAAA,uBAAA;;;;;;;;;AAxCjB,IAAA,KAAK,EAAC,yBAAW;AAChB,IAAA,IAAI,EAAE,MAAK,CAAA,KAAA;AACZ,IAAA,IAAI,EAAC,OAAM;AACX,IAAA,KAAK,EAAC,QAAQ;AACb,IAAA,OAAO,EAAE,MAAO,CAAA,OAAA;AAChB,IAAA,QAAQ,EAAE,MAAQ,CAAA,QAAA;AAClB,IAAA,IAAI,EAAE,MAAI,CAAA,IAAA;AACV,IAAA,KAAK,iBAAE,QAAY,CAAA,YAAA,CAAA;AACnB,IAAA,aAAG,EAAA,yBAAA;mBACQ,EAAE,QAAY,CAAA,YAAA;AACxB,IAAA,SAAM,EAAE,QAAK,CAAA,KAAA;;AA6BJ,IAAA,OAAO,gBAGb;wBAAA,CAAA,GAAA,YAAA,EAAAA,eAAA,CADA,QAAM,CAAA,UAAA,CAAA,EAAA,CAAA,YAAA;MAEVC;AAAQ,QAAA,OAAA,EAAA,WAAA;AAAA,QAAA,aAAA,EAAA,wBAAA;AAAA,QAAA,OAAA,EAAA,QAAA,CAAA,KAAA;;;;;;;;qBAhCP,MA2BE;AA3BF,MAAAC,kBAAA,CA2BE,OA3BF,UA2BE,EAAA;AAzBC,QAAA,CAAA,KAAA,CAAA,QAAA,CAAA,MAAA,GAAA,CAAA;0BADHC,kBAqBI,CAAA,KAAA,EAAA;;AAnBH,cAAA,KAAI,EAAC,8BAA8B;0BACjC,EAAA,QAAA,CAAA,QAAA;AACA,cAAA,aAAQ,EAAC,sBAAsB;;cAC/BD,kBAeC,CAAA,IAAA,EAAA,IAAA,EAAA;kCAdDC,kBAaG,CAAAC,QAAA,EAAA,IAAA,EAAAC,UAAA,CAbiB,KAAQ,CAAA,QAAA,EAAA,CAAlB,OAAM,KAAA;uCAAhBF,kBAaG,CAAA,IAAA,EAAA;oBAb4B,GAAG,EAAE,OAAO,CAAC,EAAE;;sCAY3C,CAAA,GAAA,EAAA;sBAVF,IAAI,EAAA,CAAA,kBAAA,EAAuB,OAAO,CAAC,EAAE,CAAA,CAAA;AACtC,sBAAA,KAAK,kBAAC,mCAAmC,EAAA,EAAA,2CAAA,EACc,OAAO,CAAC,EAAE,KAAK,KAAQ,CAAA,QAAA,EAAA,CAAA,CAAA;AAC7E,sBAAA,cAAY,EAAE,OAAO,CAAC,EAAE,KAAK,KAAA,CAAA,QAAK,YAAA,SAAA;sBAClC,aAAK,EAAA,CAAA,qBAAA,EAAA,OAAA,CAAA,EAAA,CAAA,CAAA;AACL,sBAAA,OAAK,EAAUG,aAAA,CAAA,MAAA,KAAA,QAAA,CAAA,aAAa,CAAC,OAAA,CAAA,EAAA,CAAA,CAAA,EAAA,CAAA,SAAA,CAAA,CAAA;;AAC5B,sBAAA,CAAA,OAAA,CAAA,IAAA;AAAF,2BAAAC,SAAA,EAAA,EAAAJ,kBAAA,CAEC,QAFD,UAEC,EAAA;4BADAF,WAAC,CAAA,mBAAA,EAAA;8BAAA,MAAA,EAAA,OAAA,CAAA,IAAA;;;;sBAEFC,kBAAC,CAAA,MAAA,EAAA,IAAA,EAAAF,eAAA,CAAA,OAAA,CAAA,IAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;AAKL,QAAAE,kBAAA,CAGM,OAHN,UAGM,EAAA;UADLM,UAAQ,CAAA,IAAA,CAAA,MAAA,EAAA,SAAA,CAAA;;;;;;;;;;"}
|
|
@@ -2,7 +2,7 @@ import script from './CnWidgetWrapper.vue2.js';
|
|
|
2
2
|
import { render } from './CnWidgetWrapper.vue3.js';
|
|
3
3
|
|
|
4
4
|
script.render = render;
|
|
5
|
-
script.__scopeId = "data-v-
|
|
5
|
+
script.__scopeId = "data-v-8ce3140c";
|
|
6
6
|
const __sfc_main = script;
|
|
7
7
|
|
|
8
8
|
export { __sfc_main as default };
|
|
@@ -48,6 +48,18 @@ var script = {
|
|
|
48
48
|
CnActionsMenu: __sfc_main,
|
|
49
49
|
},
|
|
50
50
|
|
|
51
|
+
provide() {
|
|
52
|
+
return {
|
|
53
|
+
/**
|
|
54
|
+
* Whether this card draws a title row, so a widget inside it can
|
|
55
|
+
* leave out a heading of its own (nc-widget-header).
|
|
56
|
+
*
|
|
57
|
+
* @return {boolean}
|
|
58
|
+
*/
|
|
59
|
+
cnWidgetTitled: () => this.titleVisible && Boolean(this.displayTitle),
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
51
63
|
inject: {
|
|
52
64
|
/**
|
|
53
65
|
* Host translate function provided by CnAppRoot as
|