@conduction/nextcloud-vue 2.100.0 → 2.102.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.
Files changed (168) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +22 -0
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +8 -6
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  5. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  6. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +29 -4
  7. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  8. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +9 -1
  9. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  10. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  11. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +22 -1
  12. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  13. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +26 -16
  14. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  16. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +70 -6
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +105 -74
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  20. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue.js +9 -0
  21. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue.js.map +1 -0
  22. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue2.js +139 -0
  23. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue2.js.map +1 -0
  24. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue3.js +76 -0
  25. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue3.js.map +1 -0
  26. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  27. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +160 -2
  28. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  29. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +206 -94
  30. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  31. package/dist/esm/components/CnFilesBrowser/filesReview.js +61 -0
  32. package/dist/esm/components/CnFilesBrowser/filesReview.js.map +1 -0
  33. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js +9 -0
  34. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js.map +1 -0
  35. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js +279 -0
  36. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js.map +1 -0
  37. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js +122 -0
  38. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js.map +1 -0
  39. package/dist/esm/components/CnGroupedCardsWidget/index.js +29 -0
  40. package/dist/esm/components/CnGroupedCardsWidget/index.js.map +1 -0
  41. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +320 -74
  42. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  43. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +323 -213
  44. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/useNamedSource.js +30 -3
  46. package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
  47. package/dist/esm/components/CnMapWidget/CnMapWidget.vue.js +1 -1
  48. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js +15 -0
  49. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js.map +1 -1
  50. package/dist/esm/components/CnMapWidget/CnMapWidget.vue3.js.map +1 -1
  51. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue.js +1 -1
  52. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue2.js +25 -1
  53. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue2.js.map +1 -1
  54. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue3.js.map +1 -1
  55. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  56. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +14 -1
  57. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  58. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +14 -6
  59. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  60. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  61. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +10 -0
  62. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  63. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +6 -1
  64. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  65. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +1 -1
  66. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +91 -4
  67. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -1
  68. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +63 -38
  69. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -1
  70. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue.js +1 -1
  71. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +106 -4
  72. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  73. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +70 -25
  74. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  75. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js +9 -0
  76. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js.map +1 -0
  77. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js +175 -0
  78. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js.map +1 -0
  79. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js +77 -0
  80. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js.map +1 -0
  81. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
  82. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +35 -2
  83. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  84. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  85. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +4 -0
  86. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  87. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  88. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  89. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  90. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +2 -23
  91. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  92. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  93. package/dist/esm/composables/indexSources.js +176 -20
  94. package/dist/esm/composables/indexSources.js.map +1 -1
  95. package/dist/esm/composables/useEndpointSource.js +28 -6
  96. package/dist/esm/composables/useEndpointSource.js.map +1 -1
  97. package/dist/esm/index.js +1 -0
  98. package/dist/esm/index.js.map +1 -1
  99. package/dist/esm/l10n/en.json.js +41 -1
  100. package/dist/esm/l10n/en.json.js.map +1 -1
  101. package/dist/esm/l10n/nl.json.js +39 -1
  102. package/dist/esm/l10n/nl.json.js.map +1 -1
  103. package/dist/esm/library-style.js +1 -1
  104. package/dist/esm/nextcloud-vue.css +939 -376
  105. package/dist/esm/utils/builtInFormatters.js +24 -4
  106. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  107. package/dist/esm/utils/languageMapText.js +50 -0
  108. package/dist/esm/utils/languageMapText.js.map +1 -0
  109. package/dist/esm/utils/libraryWidgetKeys.js +3 -0
  110. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  111. package/dist/esm/utils/mapList.js +71 -0
  112. package/dist/esm/utils/mapList.js.map +1 -0
  113. package/dist/esm/utils/rowClassRules.js +43 -0
  114. package/dist/esm/utils/rowClassRules.js.map +1 -0
  115. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  116. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  117. package/dist/esm/utils/visibleWhen.js +19 -2
  118. package/dist/esm/utils/visibleWhen.js.map +1 -1
  119. package/dist/nextcloud-vue.cjs.js +9681 -7136
  120. package/dist/nextcloud-vue.cjs.js.map +2 -2
  121. package/dist/nextcloud-vue.css +939 -376
  122. package/l10n/en.json +41 -1
  123. package/l10n/nl.json +39 -1
  124. package/package.json +1 -1
  125. package/src/components/CnActionsBar/CnActionsBar.md +1 -0
  126. package/src/components/CnActionsBar/CnActionsBar.vue +31 -4
  127. package/src/components/CnChartWidget/CnChartWidget.md +3 -2
  128. package/src/components/CnChartWidget/CnChartWidget.vue +42 -4
  129. package/src/components/CnChoiceCards/CnChoiceCards.md +39 -0
  130. package/src/components/CnChoiceCards/CnChoiceCards.vue +93 -16
  131. package/src/components/CnDashboardPage/CnDashboardPage.vue +110 -21
  132. package/src/components/CnExportsWidget/CnExportsWidget.vue +233 -0
  133. package/src/components/CnExportsWidget/index.js +7 -0
  134. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +324 -0
  135. package/src/components/CnFilesBrowser/filesReview.js +57 -0
  136. package/src/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue +433 -0
  137. package/src/components/CnGroupedCardsWidget/index.js +28 -0
  138. package/src/components/CnIndexPage/CnIndexPage.md +5 -1
  139. package/src/components/CnIndexPage/CnIndexPage.vue +313 -9
  140. package/src/components/CnIndexPage/useNamedSource.js +30 -3
  141. package/src/components/CnMapWidget/CnMapWidget.vue +37 -0
  142. package/src/components/CnMenuWidget/CnMenuWidget.vue +25 -1
  143. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +29 -1
  144. package/src/components/CnObjectSidebar/CnFilesTab.vue +15 -0
  145. package/src/components/CnOrderedListField/CnOrderedListField.vue +151 -5
  146. package/src/components/CnQuickFilterBar/CnQuickFilterBar.md +1 -0
  147. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +159 -5
  148. package/src/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue +272 -0
  149. package/src/components/CnSavedViewAlertWidget/index.js +7 -0
  150. package/src/components/CnSetupWizard/CnSetupWizard.vue +35 -2
  151. package/src/components/CnWidgetGrid/builtInWidgets.js +4 -0
  152. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  153. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +2 -23
  154. package/src/components/index.js +3 -0
  155. package/src/composables/indexSources.js +175 -19
  156. package/src/composables/useEndpointSource.js +28 -6
  157. package/src/css/index-page.css +117 -0
  158. package/src/css/look-board-index.css +10 -0
  159. package/src/css/look-board.css +44 -0
  160. package/src/index.js +1 -0
  161. package/src/schemas/app-manifest-v2.schema.json +85 -3
  162. package/src/utils/builtInFormatters.js +24 -4
  163. package/src/utils/languageMapText.js +46 -0
  164. package/src/utils/libraryWidgetKeys.js +3 -0
  165. package/src/utils/mapList.js +67 -0
  166. package/src/utils/rowClassRules.js +39 -0
  167. package/src/utils/validateManifestV2.compiled.js +3 -3
  168. package/src/utils/visibleWhen.js +19 -2
@@ -239,6 +239,11 @@ export default {
239
239
  CnMenuItemIcon,
240
240
  },
241
241
 
242
+ inject: {
243
+ /** Host translate function provided by CnAppRoot, for the item labels. */
244
+ cnTranslate: { default: () => (key) => key },
245
+ },
246
+
242
247
  props: {
243
248
  /**
244
249
  * Persisted widget content `{items, style, orientation, showIcons,
@@ -288,7 +293,26 @@ export default {
288
293
  */
289
294
  items() {
290
295
  const arr = this.content?.items
291
- return Array.isArray(arr) ? arr : []
296
+ if (!Array.isArray(arr)) {
297
+ return []
298
+ }
299
+ // Labels are manifest strings: they go through the app's translate,
300
+ // children included, so "Case types" reads "Zaaktypen" (DqCatalogus).
301
+ const translate = typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key
302
+ const translated = (list) => list.map((item) => {
303
+ if (!item || typeof item !== 'object') {
304
+ return item
305
+ }
306
+ const out = { ...item }
307
+ if (typeof item.label === 'string' && item.label !== '') {
308
+ out.label = translate(item.label)
309
+ }
310
+ if (Array.isArray(item.children)) {
311
+ out.children = translated(item.children)
312
+ }
313
+ return out
314
+ })
315
+ return translated(arr)
292
316
  },
293
317
 
294
318
  /**
@@ -233,6 +233,15 @@
233
233
  {{ moreLabel }}
234
234
  </p>
235
235
  </template>
236
+ <!-- `content.footnote`: a line under the table ("Awb 4:17 en 4:18"),
237
+ through the host translate. Not while the list waits for its
238
+ context or failed to load. -->
239
+ <p
240
+ v-if="resolvedFootnote && !waitingForContext && !error"
241
+ class="cn-object-list-widget__footnote"
242
+ data-testid="cn-object-list-widget-footnote">
243
+ {{ resolvedFootnote }}
244
+ </p>
236
245
  <!-- Create affordance (ADR-062): every collection carries its Add at
237
246
  the bottom of the widget; the host card's Actions menu calls the
238
247
  same openCreate() through the public method. Suppressed while the
@@ -430,8 +439,10 @@ export default {
430
439
  * `hideHeader: true` drops the table's column header row (CnDataTable's
431
440
  * `hideHeader`), for a short list that reads as rows, not as a table.
432
441
  *
442
+ * `footnote` is a line under the table, through the host translate.
443
+ *
433
444
  * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-small-opt-ins-from-the-decidiq-and-learniq-lanes
434
- * @type {{register?: string, schema?: string, filter?: object, sort?: {field?: string, dir?: string}, limit?: number, extend?: Array<string>, columns?: Array, rowActions?: Array<object>, dropZone?: object, upload?: boolean, groupBy?: string, groupLabel?: string, groupLabelResolve?: {register: string, schema: string, labelField?: string}, selectable?: boolean, bulkActions?: Array<object>, sortable?: boolean, facet?: {field: string, label?: string}, rowRoute?: string, prompt?: string, emptyText?: string, viewAllRoute?: string, viewAllQuery?: object, hideHeader?: boolean}}
445
+ * @type {{register?: string, schema?: string, filter?: object, sort?: {field?: string, dir?: string}, limit?: number, extend?: Array<string>, columns?: Array, rowActions?: Array<object>, dropZone?: object, upload?: boolean, groupBy?: string, groupLabel?: string, groupLabelResolve?: {register: string, schema: string, labelField?: string}, selectable?: boolean, bulkActions?: Array<object>, sortable?: boolean, facet?: {field: string, label?: string}, rowRoute?: string, prompt?: string, emptyText?: string, footnote?: string, viewAllRoute?: string, viewAllQuery?: object, hideHeader?: boolean}}
435
446
  */
436
447
  content: {
437
448
  type: Object,
@@ -918,6 +929,17 @@ export default {
918
929
  return this.emptyText ? fn(this.emptyText) : this.emptyText
919
930
  },
920
931
 
932
+ /**
933
+ * `content.footnote` run through the host translate function, or ''.
934
+ *
935
+ * @return {string}
936
+ */
937
+ resolvedFootnote() {
938
+ const note = typeof this.content.footnote === 'string' ? this.content.footnote : ''
939
+ const fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k
940
+ return note ? fn(note) : ''
941
+ },
942
+
921
943
  /**
922
944
  * The rows actually rendered: capped to what fits the host grid cell
923
945
  * (ADR-062 — content adapts to the cell, never a nested scrollbar).
@@ -2027,6 +2049,12 @@ export default {
2027
2049
  padding: 4px;
2028
2050
  }
2029
2051
 
2052
+ .cn-object-list-widget__footnote {
2053
+ color: var(--color-text-maxcontrast);
2054
+ font-size: 13px;
2055
+ margin: 8px 0 0;
2056
+ }
2057
+
2030
2058
  /* Footer Add — same pattern as the integration leaves' footer action
2031
2059
  ("Open in Calendar"): full-width, centered, divider-topped, pinned to
2032
2060
  the card bottom. */
@@ -15,6 +15,11 @@
15
15
  :dropHint="dropHint"
16
16
  :rowActions="rowActions"
17
17
  :newActions="newActions"
18
+ :headerActions="headerActions"
19
+ :rowButton="rowButton"
20
+ :reviewSummary="reviewSummary"
21
+ :footnote="footnote"
22
+ :nameSecondary="nameSecondary"
18
23
  :linkedItems="linkedItems"
19
24
  :columns="columns"
20
25
  :rowData="rowDataSource"
@@ -420,6 +425,16 @@ export default {
420
425
  * @type {string[]}
421
426
  */
422
427
  facets: { type: Array, default: () => [] },
428
+ /** Forwarded to CnFilesBrowser: the host's own buttons in the bar. */
429
+ headerActions: { type: Array, default: () => [] },
430
+ /** Forwarded to CnFilesBrowser: one visible button on every file row. */
431
+ rowButton: { type: Object, default: null },
432
+ /** Forwarded to CnFilesBrowser: the review summary above the list. */
433
+ reviewSummary: { type: Object, default: null },
434
+ /** Forwarded to CnFilesBrowser: the host's own line under the list. */
435
+ footnote: { type: String, default: '' },
436
+ /** Forwarded to CnFilesBrowser: the second line under each file's name. */
437
+ nameSecondary: { type: String, default: '' },
423
438
  /** A dotted path to the list of records in the `rowDataUrl` answer; empty reads the answer itself. */
424
439
  rowDataPath: { type: String, default: '' },
425
440
  /** The record field that holds the Nextcloud file id. */
@@ -17,7 +17,30 @@
17
17
  v-for="(row, index) in rows"
18
18
  :key="row.value"
19
19
  class="cn-ordered-list-field__row"
20
- :data-testid="`cn-ordered-list-row-${row.value}`">
20
+ :class="{
21
+ 'cn-ordered-list-field__row--dragging': dragIndex === index,
22
+ 'cn-ordered-list-field__row--over': dropIndex === index && dragIndex !== index,
23
+ }"
24
+ :data-testid="`cn-ordered-list-row-${row.value}`"
25
+ @dragover.prevent="onDragOver(index)"
26
+ @drop.prevent="onDrop(index)">
27
+ <!-- A drag handle (DqPersoonlijkeInstellingen). It is a button: a
28
+ keyboard user moves the row with the arrow keys on it, and the
29
+ up and down buttons stay for anyone who neither drags nor uses
30
+ arrow keys. -->
31
+ <button
32
+ v-if="!disabled"
33
+ type="button"
34
+ class="cn-ordered-list-field__handle"
35
+ draggable="true"
36
+ :aria-label="t('nextcloud-vue', 'Reorder {item}: drag, or press the up and down arrow keys', { item: row.label })"
37
+ :data-testid="`cn-ordered-list-handle-${row.value}`"
38
+ @dragstart="onDragStart($event, index)"
39
+ @dragend="onDragEnd"
40
+ @keydown.up.prevent="move(index, index - 1, 'handle')"
41
+ @keydown.down.prevent="move(index, index + 1, 'handle')">
42
+ <DragVertical :size="20" />
43
+ </button>
21
44
  <span class="cn-ordered-list-field__position" aria-hidden="true">{{ index + 1 }}</span>
22
45
  <span v-if="row.icon" class="cn-ordered-list-field__tile" aria-hidden="true">
23
46
  <CnIcon :name="row.icon" :size="18" />
@@ -104,6 +127,7 @@ import { NcSelect } from '@nextcloud/vue'
104
127
  import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
105
128
  import ChevronUp from 'vue-material-design-icons/ChevronUp.vue'
106
129
  import Close from 'vue-material-design-icons/Close.vue'
130
+ import DragVertical from 'vue-material-design-icons/DragVertical.vue'
107
131
  import CnIcon from '../CnIcon/CnIcon.vue'
108
132
 
109
133
  let orderedListUid = 0
@@ -134,7 +158,7 @@ let orderedListUid = 0
134
158
  export default {
135
159
  name: 'CnOrderedListField',
136
160
 
137
- components: { ChevronDown, ChevronUp, Close, CnIcon, NcSelect },
161
+ components: { ChevronDown, ChevronUp, Close, CnIcon, DragVertical, NcSelect },
138
162
 
139
163
  props: {
140
164
  /** The values in their order. */
@@ -205,6 +229,12 @@ export default {
205
229
  return {
206
230
  labelId: `cn-ordered-list-label-${orderedListUid}`,
207
231
  announcement: '',
232
+ /** The value whose handle takes focus after the next render, or null. */
233
+ pendingHandleFocus: null,
234
+ /** The index of the row being dragged, or -1. */
235
+ dragIndex: -1,
236
+ /** The index of the row the drag is over, or -1. */
237
+ dropIndex: -1,
208
238
  }
209
239
  },
210
240
 
@@ -244,6 +274,20 @@ export default {
244
274
  },
245
275
  },
246
276
 
277
+ watch: {
278
+ // The parent's new order arrives as new rows; the moved row's handle
279
+ // lost focus when its row moved in the DOM, so it takes it back.
280
+ rows: {
281
+ flush: 'post',
282
+ handler() {
283
+ if (this.pendingHandleFocus !== null) {
284
+ this.focusPendingHandle()
285
+ this.pendingHandleFocus = null
286
+ }
287
+ },
288
+ },
289
+ },
290
+
247
291
  methods: {
248
292
  t,
249
293
 
@@ -270,13 +314,15 @@ export default {
270
314
  },
271
315
 
272
316
  /**
273
- * Move a row to another place and keep focus on the pressed button
274
- * where it still makes sense.
317
+ * Move a row to another place and keep focus on the pressed control
318
+ * where it still makes sense: the row's handle after an arrow key on
319
+ * it, else the up or down button.
275
320
  *
276
321
  * @param {number} from Its index.
277
322
  * @param {number} to The index it moves to.
323
+ * @param {string} [via] `handle` when the move came from the handle.
278
324
  */
279
- move(from, to) {
325
+ move(from, to, via = '') {
280
326
  if (to < 0 || to >= this.rows.length) {
281
327
  return
282
328
  }
@@ -286,7 +332,16 @@ export default {
286
332
  const row = this.rows[from]
287
333
  this.emitValue(values)
288
334
  this.announcement = t('nextcloud-vue', '{item} moved to position {position} of {total}', { item: row.label, position: to + 1, total: values.length })
335
+ if (via === 'handle') {
336
+ // Re-focused once the parent hands the new order back: moving the
337
+ // row in the DOM takes focus off its handle.
338
+ this.pendingHandleFocus = row.value
339
+ }
289
340
  this.$nextTick(() => {
341
+ if (via === 'handle') {
342
+ this.focusPendingHandle()
343
+ return
344
+ }
290
345
  const direction = to < from ? 'up' : 'down'
291
346
  const atEdge = (direction === 'up' && to === 0) || (direction === 'down' && to === values.length - 1)
292
347
  const target = this.$el.querySelector(`[data-testid="cn-ordered-list-${atEdge ? (direction === 'up' ? 'down' : 'up') : direction}-${row.value}"]`)
@@ -296,6 +351,61 @@ export default {
296
351
  })
297
352
  },
298
353
 
354
+ /** Put focus back on the handle of the row just moved by its handle. */
355
+ focusPendingHandle() {
356
+ if (this.pendingHandleFocus === null) {
357
+ return
358
+ }
359
+ const handle = this.$el.querySelector(`[data-testid="cn-ordered-list-handle-${this.pendingHandleFocus}"]`)
360
+ if (handle) {
361
+ handle.focus()
362
+ }
363
+ },
364
+
365
+ /**
366
+ * Start dragging a row by its handle.
367
+ *
368
+ * @param {DragEvent} event The drag event.
369
+ * @param {number} index The row's index.
370
+ */
371
+ onDragStart(event, index) {
372
+ this.dragIndex = index
373
+ if (event && event.dataTransfer) {
374
+ event.dataTransfer.effectAllowed = 'move'
375
+ event.dataTransfer.setData('text/plain', String(this.rows[index].value))
376
+ }
377
+ },
378
+
379
+ /**
380
+ * Mark the row a drag is over as where the row will land.
381
+ *
382
+ * @param {number} index The row's index.
383
+ */
384
+ onDragOver(index) {
385
+ if (this.dragIndex !== -1) {
386
+ this.dropIndex = index
387
+ }
388
+ },
389
+
390
+ /**
391
+ * Drop the dragged row in the place of the row it is over.
392
+ *
393
+ * @param {number} index The row's index.
394
+ */
395
+ onDrop(index) {
396
+ const from = this.dragIndex
397
+ this.onDragEnd()
398
+ if (from !== -1 && from !== index) {
399
+ this.move(from, index, 'handle')
400
+ }
401
+ },
402
+
403
+ /** End a drag, dropped or not. */
404
+ onDragEnd() {
405
+ this.dragIndex = -1
406
+ this.dropIndex = -1
407
+ },
408
+
299
409
  /**
300
410
  * Remove a row.
301
411
  *
@@ -371,6 +481,42 @@ export default {
371
481
  border-top: 1px solid var(--color-border);
372
482
  }
373
483
 
484
+ .cn-ordered-list-field__row--dragging {
485
+ opacity: 0.5;
486
+ }
487
+
488
+ /* Where a dragged row will land: a primary line on the row's top edge. */
489
+ .cn-ordered-list-field__row--over {
490
+ box-shadow: inset 0 2px 0 var(--color-primary-element);
491
+ }
492
+
493
+ .cn-ordered-list-field__handle {
494
+ display: inline-flex;
495
+ flex: none;
496
+ align-items: center;
497
+ justify-content: center;
498
+ width: 28px;
499
+ min-width: 28px;
500
+ height: 34px;
501
+ min-height: 34px;
502
+ margin: 0 -6px 0 -8px;
503
+ padding: 0;
504
+ border: 0;
505
+ border-radius: var(--border-radius-element, 8px);
506
+ background: transparent;
507
+ color: var(--color-text-maxcontrast);
508
+ cursor: grab;
509
+ }
510
+
511
+ .cn-ordered-list-field__handle:active {
512
+ cursor: grabbing;
513
+ }
514
+
515
+ .cn-ordered-list-field__handle:focus-visible {
516
+ outline: 2px solid var(--color-primary-element);
517
+ outline-offset: 1px;
518
+ }
519
+
374
520
  .cn-ordered-list-field__position {
375
521
  flex: none;
376
522
  min-width: 18px;
@@ -59,6 +59,7 @@ In `manifest.json` (the usual entry-point — CnIndexPage mounts the bar for you
59
59
  - `selectLabel` (String, default `'Filter'`) — accessible label / placeholder for the dropdown control.
60
60
  - `placeholder` (String) — placeholder text for the dropdown (falls back to `selectLabel`).
61
61
  - `maxVisible` (Number, default `0`) — chips mode only: how many pills render inline before the rest move behind one more chip, a `⋯` pill that opens a small panel of the hidden lenses (also as chips). `0` renders every tab. The visible set is the first `maxVisible` entries of `tabs`, so their declared order decides what stays in the strip; an active hidden tab moves its label and fill onto that chip. Manifest: `config.quickFilterMaxVisible`.
62
+ - `fit` (Boolean, default `false`) — chips mode only: show as many chips as the bar's own width holds and move the rest behind the overflow chip, measured again when the width, the tabs or the counts change. The bar takes the room its parent gives it; CnIndexPage's `quickFilterPlacement: "toolbar"` gives it what the Filter button and the view switch leave on their row. With `maxVisible` too, the smaller of the two wins.
62
63
  - `counts` (Object or Array, default `null`): how many records each tab matches, keyed by tab index. A tab with a number shows it after its label. CnIndexPage fills this for tabs that set `showCount`.
63
64
 
64
65
  ### Events
@@ -24,8 +24,28 @@
24
24
  </div>
25
25
  <!-- Chips mode (default): the clickable tab strip. -->
26
26
  <div v-else
27
+ ref="chipsRoot"
27
28
  class="cn-quick-filter-bar"
28
- :class="{ 'cn-quick-filter-bar--inline': inline }">
29
+ :class="{ 'cn-quick-filter-bar--inline': inline, 'cn-quick-filter-bar--fit': fit }">
30
+ <!-- With `fit`: an invisible copy of every chip and the overflow chip,
31
+ measured to work out how many chips the bar's width holds. -->
32
+ <div v-if="fit"
33
+ ref="measure"
34
+ class="cn-quick-filter-bar__measure"
35
+ aria-hidden="true">
36
+ <span v-for="entry in entries"
37
+ :key="'m:' + entry.tab.label + ':' + entry.index"
38
+ class="cn-quick-filter-bar__tab"
39
+ :class="{ 'cn-quick-filter-bar__tab--active': isChipActive(entry.index) }"
40
+ data-measure="chip">
41
+ <span class="cn-quick-filter-bar__label">{{ tl(entry.tab.label) }}</span>
42
+ <span v-if="countOf(entry) !== null" class="cn-quick-filter-bar__count">{{ ' ' + countOf(entry) }}</span>
43
+ </span>
44
+ <span class="cn-quick-filter-bar__tab cn-quick-filter-bar__more" data-measure="more">
45
+ <span v-if="overflowLabel" class="cn-quick-filter-bar__label">{{ overflowLabel }}</span>
46
+ <DotsHorizontal :size="16" class="cn-quick-filter-bar__icon" />
47
+ </span>
48
+ </div>
29
49
  <!-- The tablist holds ONLY tabs: the overflow chip is a sibling, not a
30
50
  child, because a role="tablist" may not contain a menu button. -->
31
51
  <!-- Board look: the tabs are chips, pressed buttons in a group, not a
@@ -266,6 +286,19 @@ export default {
266
286
  type: [Object, Array],
267
287
  default: null,
268
288
  },
289
+
290
+ /**
291
+ * Chips mode only: show as many chips as the bar's own width holds and
292
+ * move the rest into the overflow chip, measured again whenever the
293
+ * width, the tabs or the counts change. The bar takes the room it is
294
+ * given (CnIndexPage's `quickFilterPlacement: "toolbar"` gives it what
295
+ * the Filter button and the view switch leave on their row). With
296
+ * `maxVisible` too, the smaller of the two wins.
297
+ */
298
+ fit: {
299
+ type: Boolean,
300
+ default: false,
301
+ },
269
302
  },
270
303
 
271
304
  emits: ['update:active-index', 'update:selected-indices'],
@@ -274,6 +307,8 @@ export default {
274
307
  return {
275
308
  /** Whether the overflow chip's panel is open. */
276
309
  moreOpen: false,
310
+ /** With `fit`: how many chips the width holds; null before the first measure. */
311
+ fitCount: null,
277
312
  }
278
313
  },
279
314
 
@@ -303,10 +338,24 @@ export default {
303
338
  * @return {Array<{tab: object, index: number}>}
304
339
  */
305
340
  visibleEntries() {
306
- if (!this.maxVisible || this.tabs.length <= this.maxVisible) {
341
+ const limit = this.effectiveMax
342
+ if (!limit || this.tabs.length <= limit) {
307
343
  return this.entries
308
344
  }
309
- return this.entries.slice(0, this.maxVisible)
345
+ return this.entries.slice(0, limit)
346
+ },
347
+
348
+ /**
349
+ * How many chips render: `maxVisible`, and with `fit` no more than the
350
+ * width holds; 0 for no limit.
351
+ *
352
+ * @return {number}
353
+ */
354
+ effectiveMax() {
355
+ if (!this.fit || this.fitCount === null) {
356
+ return this.maxVisible
357
+ }
358
+ return this.maxVisible ? Math.min(this.maxVisible, this.fitCount) : this.fitCount
310
359
  },
311
360
 
312
361
  /**
@@ -316,10 +365,11 @@ export default {
316
365
  * @return {Array<{tab: object, index: number}>}
317
366
  */
318
367
  overflowEntries() {
319
- if (!this.maxVisible || this.tabs.length <= this.maxVisible) {
368
+ const limit = this.effectiveMax
369
+ if (!limit || this.tabs.length <= limit) {
320
370
  return []
321
371
  }
322
- return this.entries.slice(this.maxVisible)
372
+ return this.entries.slice(limit)
323
373
  },
324
374
 
325
375
  /**
@@ -399,7 +449,79 @@ export default {
399
449
  },
400
450
  },
401
451
 
452
+ watch: {
453
+ tabs() {
454
+ this.scheduleFit()
455
+ },
456
+
457
+ counts: {
458
+ deep: true,
459
+ handler() {
460
+ this.scheduleFit()
461
+ },
462
+ },
463
+ },
464
+
465
+ mounted() {
466
+ if (this.fit && typeof ResizeObserver === 'function' && this.$refs.chipsRoot) {
467
+ this.fitObserver = new ResizeObserver(() => this.measureFit())
468
+ this.fitObserver.observe(this.$refs.chipsRoot)
469
+ }
470
+ this.scheduleFit()
471
+ },
472
+
473
+ beforeUnmount() {
474
+ if (this.fitObserver) {
475
+ this.fitObserver.disconnect()
476
+ this.fitObserver = null
477
+ }
478
+ },
479
+
402
480
  methods: {
481
+ /** Measure the fit after the next render. */
482
+ scheduleFit() {
483
+ if (this.fit) {
484
+ this.$nextTick(() => this.measureFit())
485
+ }
486
+ },
487
+
488
+ /**
489
+ * Work out how many chips the bar's width holds, from the invisible
490
+ * copy: every chip that fits, and when not all do, room left for the
491
+ * overflow chip. At least one chip always shows.
492
+ */
493
+ measureFit() {
494
+ const box = this.$refs.measure
495
+ const root = this.$refs.chipsRoot
496
+ if (!this.fit || !box || !root) {
497
+ return
498
+ }
499
+ const available = root.clientWidth
500
+ const chips = [...box.querySelectorAll('[data-measure="chip"]')].map((el) => el.offsetWidth)
501
+ const more = box.querySelector('[data-measure="more"]')
502
+ const moreWidth = more ? more.offsetWidth : 0
503
+ const gap = parseFloat(getComputedStyle(box).columnGap) || 8
504
+ if (available <= 0 || chips.length === 0) {
505
+ this.fitCount = null
506
+ return
507
+ }
508
+ const total = chips.reduce((sum, width) => sum + width, 0) + gap * (chips.length - 1)
509
+ if (total <= available) {
510
+ this.fitCount = null
511
+ return
512
+ }
513
+ let used = moreWidth
514
+ let count = 0
515
+ for (const width of chips) {
516
+ if (used + gap + width > available) {
517
+ break
518
+ }
519
+ used += gap + width
520
+ count++
521
+ }
522
+ this.fitCount = Math.max(1, count)
523
+ },
524
+
403
525
  /**
404
526
  * A manifest string through the host's label lookup (`cnTranslate`), so a key
405
527
  * such as "Mine" reads in the user's language.
@@ -531,6 +653,38 @@ export default {
531
653
  /* Inline variant — bare strip for embedding inside another bar (e.g. the
532
654
  CnActionsBar #filters slot), so it reads as a control beside the view toggle
533
655
  rather than a separate bordered row. */
656
+ /* With `fit`: one line, as wide as it is given; the chips it cannot hold
657
+ are in the overflow chip. The measuring copy takes no room and is never
658
+ seen or read. */
659
+ .cn-quick-filter-bar--fit {
660
+ position: relative;
661
+ flex-wrap: nowrap;
662
+ min-width: 0;
663
+ }
664
+
665
+ .cn-quick-filter-bar--fit .cn-quick-filter-bar__tabs {
666
+ flex-wrap: nowrap;
667
+ min-width: 0;
668
+ }
669
+
670
+ .cn-quick-filter-bar__measure {
671
+ position: absolute;
672
+ inset-block-start: 0;
673
+ inset-inline-start: 0;
674
+ display: flex;
675
+ gap: 8px;
676
+ width: 0;
677
+ height: 0;
678
+ overflow: hidden;
679
+ visibility: hidden;
680
+ pointer-events: none;
681
+ white-space: nowrap;
682
+ }
683
+
684
+ .cn-quick-filter-bar__measure > * {
685
+ flex: none;
686
+ }
687
+
534
688
  .cn-quick-filter-bar--inline {
535
689
  padding: 0;
536
690
  border-bottom: none;