@cocoar/vue-ui 3.1.0 → 3.2.0-beta.6
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/components/checkbox/CoarCheckbox.vue.d.ts +1 -1
- package/dist/components/checkbox/CoarCheckbox.vue.d.ts.map +1 -1
- package/dist/components/code-block/CoarCodeBlock.vue.d.ts +1 -1
- package/dist/components/code-block/CoarCodeBlock.vue.d.ts.map +1 -1
- package/dist/components/data-list/CoarDataList.vue.d.ts +158 -0
- package/dist/components/data-list/CoarDataList.vue.d.ts.map +1 -0
- package/dist/components/data-list/CoarDataListToolbar.vue.d.ts +30 -0
- package/dist/components/data-list/CoarDataListToolbar.vue.d.ts.map +1 -0
- package/dist/components/data-list/data-list-builder.d.ts +288 -0
- package/dist/components/data-list/data-list-builder.d.ts.map +1 -0
- package/dist/components/data-list/index.d.ts +12 -0
- package/dist/components/data-list/index.d.ts.map +1 -0
- package/dist/components/data-list/internal/compare.d.ts +14 -0
- package/dist/components/data-list/internal/compare.d.ts.map +1 -0
- package/dist/components/data-list/internal/reorder-core.d.ts +46 -0
- package/dist/components/data-list/internal/reorder-core.d.ts.map +1 -0
- package/dist/components/data-list/internal/search.d.ts +16 -0
- package/dist/components/data-list/internal/search.d.ts.map +1 -0
- package/dist/components/data-list/internal/useDataListLines.d.ts +92 -0
- package/dist/components/data-list/internal/useDataListLines.d.ts.map +1 -0
- package/dist/components/data-list/internal/useDataListReorder.d.ts +68 -0
- package/dist/components/data-list/internal/useDataListReorder.d.ts.map +1 -0
- package/dist/components/data-list/internal/useSearchHighlight.d.ts +19 -0
- package/dist/components/data-list/internal/useSearchHighlight.d.ts.map +1 -0
- package/dist/components/data-list/types.d.ts +185 -0
- package/dist/components/data-list/types.d.ts.map +1 -0
- package/dist/components/data-list/useDataListModel.d.ts +79 -0
- package/dist/components/data-list/useDataListModel.d.ts.map +1 -0
- package/dist/components/date-time/_shared/maskito-config.d.ts +7 -2
- package/dist/components/date-time/_shared/maskito-config.d.ts.map +1 -1
- package/dist/components/date-time/_shared/time-helpers.d.ts +11 -4
- package/dist/components/date-time/_shared/time-helpers.d.ts.map +1 -1
- package/dist/components/date-time/_shared/use-date-picker-base.d.ts +1 -1
- package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts +1 -1
- package/dist/components/date-time/plain-date-picker/CoarPlainDatePicker.vue.d.ts.map +1 -1
- package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts +1 -1
- package/dist/components/date-time/plain-date-time-picker/CoarPlainDateTimePicker.vue.d.ts.map +1 -1
- package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts +1 -1
- package/dist/components/date-time/zoned-date-time-picker/CoarZonedDateTimePicker.vue.d.ts.map +1 -1
- package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts +3 -0
- package/dist/components/dual-listbox/CoarDualListbox.vue.d.ts.map +1 -1
- package/dist/components/form-field/CoarFormField.vue.d.ts +1 -1
- package/dist/components/form-field/CoarFormField.vue.d.ts.map +1 -1
- package/dist/components/icon/CoarIcon.vue.d.ts +2 -2
- package/dist/components/icon/CoarIcon.vue.d.ts.map +1 -1
- package/dist/components/listbox/CoarListbox.vue.d.ts +7 -0
- package/dist/components/listbox/CoarListbox.vue.d.ts.map +1 -1
- package/dist/components/number-input/CoarNumberInput.vue.d.ts +1 -1
- package/dist/components/number-input/CoarNumberInput.vue.d.ts.map +1 -1
- package/dist/components/otp-input/CoarOtpInput.vue.d.ts +1 -1
- package/dist/components/otp-input/CoarOtpInput.vue.d.ts.map +1 -1
- package/dist/components/password-input/CoarPasswordInput.vue.d.ts +1 -1
- package/dist/components/password-input/CoarPasswordInput.vue.d.ts.map +1 -1
- package/dist/components/switch/CoarSwitch.vue.d.ts +1 -1
- package/dist/components/switch/CoarSwitch.vue.d.ts.map +1 -1
- package/dist/components/text-input/CoarTextInput.vue.d.ts +1 -1
- package/dist/components/text-input/CoarTextInput.vue.d.ts.map +1 -1
- package/dist/composables/dragRegistry.d.ts +12 -0
- package/dist/composables/dragRegistry.d.ts.map +1 -1
- package/dist/composables/index.d.ts +2 -2
- package/dist/composables/index.d.ts.map +1 -1
- package/dist/composables/useDragDrop.d.ts +62 -6
- package/dist/composables/useDragDrop.d.ts.map +1 -1
- package/dist/composables/useVirtualList.d.ts +21 -1
- package/dist/composables/useVirtualList.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4508 -2039
- package/package.json +5 -3
- package/skills/cocoar-vue-ui/SKILL.md +244 -0
- package/skills/cocoar-vue-ui/references/components/avatar.md +178 -0
- package/skills/cocoar-vue-ui/references/components/badge.md +290 -0
- package/skills/cocoar-vue-ui/references/components/breadcrumb.md +238 -0
- package/skills/cocoar-vue-ui/references/components/button.md +252 -0
- package/skills/cocoar-vue-ui/references/components/calendar/agenda-view.md +353 -0
- package/skills/cocoar-vue-ui/references/components/calendar/coar-calendar.md +1645 -0
- package/skills/cocoar-vue-ui/references/components/calendar/day-view.md +361 -0
- package/skills/cocoar-vue-ui/references/components/calendar/index.md +312 -0
- package/skills/cocoar-vue-ui/references/components/calendar/month-view.md +475 -0
- package/skills/cocoar-vue-ui/references/components/calendar/performance.md +186 -0
- package/skills/cocoar-vue-ui/references/components/calendar/timeline-view.md +301 -0
- package/skills/cocoar-vue-ui/references/components/calendar/week-view.md +382 -0
- package/skills/cocoar-vue-ui/references/components/calendar/work-week-view.md +145 -0
- package/skills/cocoar-vue-ui/references/components/calendar/year-view.md +63 -0
- package/skills/cocoar-vue-ui/references/components/card.md +151 -0
- package/skills/cocoar-vue-ui/references/components/checkbox-group.md +103 -0
- package/skills/cocoar-vue-ui/references/components/checkbox.md +279 -0
- package/skills/cocoar-vue-ui/references/components/code-block.md +241 -0
- package/skills/cocoar-vue-ui/references/components/context-menu.md +355 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/checkbox.md +213 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/date-columns.md +272 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/editing.md +236 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/multi-select.md +193 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/number.md +147 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/select.md +202 -0
- package/skills/cocoar-vue-ui/references/components/data-grid/text.md +127 -0
- package/skills/cocoar-vue-ui/references/components/data-grid.md +1225 -0
- package/skills/cocoar-vue-ui/references/components/data-list.md +2121 -0
- package/skills/cocoar-vue-ui/references/components/date-or-time-picker.md +200 -0
- package/skills/cocoar-vue-ui/references/components/date-picker.md +203 -0
- package/skills/cocoar-vue-ui/references/components/date-time-picker.md +154 -0
- package/skills/cocoar-vue-ui/references/components/date-views.md +212 -0
- package/skills/cocoar-vue-ui/references/components/dialog.md +148 -0
- package/skills/cocoar-vue-ui/references/components/divider.md +157 -0
- package/skills/cocoar-vue-ui/references/components/document-viewer/annotations.md +400 -0
- package/skills/cocoar-vue-ui/references/components/document-viewer/coar-document-viewer.md +327 -0
- package/skills/cocoar-vue-ui/references/components/document-viewer/index.md +234 -0
- package/skills/cocoar-vue-ui/references/components/document-viewer/toolbar.md +221 -0
- package/skills/cocoar-vue-ui/references/components/drag-drop.md +267 -0
- package/skills/cocoar-vue-ui/references/components/dual-listbox.md +449 -0
- package/skills/cocoar-vue-ui/references/components/file-explorer/asset-store.md +327 -0
- package/skills/cocoar-vue-ui/references/components/file-explorer/in-memory-store.md +176 -0
- package/skills/cocoar-vue-ui/references/components/file-explorer/index.md +311 -0
- package/skills/cocoar-vue-ui/references/components/file-explorer/use-file-explorer.md +300 -0
- package/skills/cocoar-vue-ui/references/components/form-field.md +1025 -0
- package/skills/cocoar-vue-ui/references/components/fragment-parser.md +298 -0
- package/skills/cocoar-vue-ui/references/components/link.md +235 -0
- package/skills/cocoar-vue-ui/references/components/listbox.md +575 -0
- package/skills/cocoar-vue-ui/references/components/map/editor.md +448 -0
- package/skills/cocoar-vue-ui/references/components/map/index.md +351 -0
- package/skills/cocoar-vue-ui/references/components/markdown-diagrams.md +210 -0
- package/skills/cocoar-vue-ui/references/components/markdown-editor.md +1478 -0
- package/skills/cocoar-vue-ui/references/components/markdown-embeds.md +387 -0
- package/skills/cocoar-vue-ui/references/components/markdown-form.md +447 -0
- package/skills/cocoar-vue-ui/references/components/markdown.md +276 -0
- package/skills/cocoar-vue-ui/references/components/menu.md +380 -0
- package/skills/cocoar-vue-ui/references/components/mermaid.md +172 -0
- package/skills/cocoar-vue-ui/references/components/navbar.md +147 -0
- package/skills/cocoar-vue-ui/references/components/note.md +96 -0
- package/skills/cocoar-vue-ui/references/components/notice.md +131 -0
- package/skills/cocoar-vue-ui/references/components/number-input.md +215 -0
- package/skills/cocoar-vue-ui/references/components/otp-input.md +324 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/authoring-contract.md +271 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-builder.md +476 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/coar-page-renderer.md +677 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/custom-elements.md +441 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/idp-integration.md +122 -0
- package/skills/cocoar-vue-ui/references/components/page-builder/index.md +573 -0
- package/skills/cocoar-vue-ui/references/components/pagination.md +150 -0
- package/skills/cocoar-vue-ui/references/components/panel-layout.md +372 -0
- package/skills/cocoar-vue-ui/references/components/password-input.md +175 -0
- package/skills/cocoar-vue-ui/references/components/popconfirm.md +186 -0
- package/skills/cocoar-vue-ui/references/components/popover.md +173 -0
- package/skills/cocoar-vue-ui/references/components/progress-bar.md +178 -0
- package/skills/cocoar-vue-ui/references/components/radio-group.md +225 -0
- package/skills/cocoar-vue-ui/references/components/script-editor.md +1271 -0
- package/skills/cocoar-vue-ui/references/components/segmented-control.md +238 -0
- package/skills/cocoar-vue-ui/references/components/select.md +463 -0
- package/skills/cocoar-vue-ui/references/components/sidebar.md +421 -0
- package/skills/cocoar-vue-ui/references/components/spinner.md +132 -0
- package/skills/cocoar-vue-ui/references/components/switch.md +195 -0
- package/skills/cocoar-vue-ui/references/components/table.md +170 -0
- package/skills/cocoar-vue-ui/references/components/tabs.md +382 -0
- package/skills/cocoar-vue-ui/references/components/tag.md +178 -0
- package/skills/cocoar-vue-ui/references/components/text-input.md +256 -0
- package/skills/cocoar-vue-ui/references/components/toast.md +160 -0
- package/skills/cocoar-vue-ui/references/components/tooltip.md +121 -0
- package/skills/cocoar-vue-ui/references/components/transitions.md +193 -0
- package/skills/cocoar-vue-ui/references/components/tree.md +2388 -0
- package/skills/cocoar-vue-ui/references/components/virtual-list.md +212 -0
- package/skills/cocoar-vue-ui/references/components/wizard.md +251 -0
- package/skills/cocoar-vue-ui/references/components/zoned-date-time-picker.md +177 -0
- package/skills/cocoar-vue-ui/references/foundations/colors.md +708 -0
- package/skills/cocoar-vue-ui/references/foundations/design-principles.md +115 -0
- package/skills/cocoar-vue-ui/references/foundations/icons.md +381 -0
- package/skills/cocoar-vue-ui/references/foundations/localization/formatting.md +530 -0
- package/skills/cocoar-vue-ui/references/foundations/localization/setup.md +86 -0
- package/skills/cocoar-vue-ui/references/foundations/localization/timezones.md +182 -0
- package/skills/cocoar-vue-ui/references/foundations/localization/translations.md +305 -0
- package/skills/cocoar-vue-ui/references/foundations/motion.md +549 -0
- package/skills/cocoar-vue-ui/references/foundations/spacing.md +330 -0
- package/skills/cocoar-vue-ui/references/foundations/theming.md +140 -0
- package/skills/cocoar-vue-ui/references/foundations/typography.md +206 -0
- package/skills/cocoar-vue-ui/references/guide/error-handling.md +162 -0
- package/skills/cocoar-vue-ui/references/guide/getting-started.md +119 -0
- package/skills/cocoar-vue-ui/references/guide/migration-page-builder-3.md +207 -0
- package/skills/cocoar-vue-ui/references/guide/migration.md +140 -0
- package/skills/cocoar-vue-ui/references/guide/theming.md +98 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
<!-- Generated from apps/docs/components/dual-listbox.md by apps/docs/scripts/sync-skill.mjs. Do not edit; edit the docs page. -->
|
|
2
|
+
|
|
3
|
+
# Dual Listbox
|
|
4
|
+
|
|
5
|
+
Two [`CoarListbox`](./listbox.md) columns side-by-side with move buttons — the classic pattern for moving items between "available" and "selected" sets. All the search, grouping, and custom-render features of `CoarListbox` carry over automatically.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { CoarDualListbox } from '@cocoar/vue-ui';
|
|
9
|
+
import type { CoarListboxOption } from '@cocoar/vue-ui';
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Basic
|
|
13
|
+
|
|
14
|
+
`v-model` holds the currently-selected values (right column). Everything else from `options` lives in the left column.
|
|
15
|
+
|
|
16
|
+
**Demo — `dual-listbox/demos/BasicDualListbox.vue`**
|
|
17
|
+
|
|
18
|
+
```vue
|
|
19
|
+
<template>
|
|
20
|
+
<div style="height: 320px;">
|
|
21
|
+
<CoarDualListbox
|
|
22
|
+
v-model="selected"
|
|
23
|
+
:options="options"
|
|
24
|
+
available-label="Available"
|
|
25
|
+
selected-label="Selected"
|
|
26
|
+
/>
|
|
27
|
+
<p style="margin-top: 8px; font-size: 13px; color: #64748b;">
|
|
28
|
+
Selected: {{ selected.join(', ') || 'none' }}
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup lang="ts">
|
|
34
|
+
import { ref } from 'vue';
|
|
35
|
+
import { CoarDualListbox } from '@cocoar/vue-ui';
|
|
36
|
+
import type { CoarListboxOption } from '@cocoar/vue-ui';
|
|
37
|
+
|
|
38
|
+
const options: CoarListboxOption<string>[] = [
|
|
39
|
+
{ value: 'read', label: 'Read' },
|
|
40
|
+
{ value: 'write', label: 'Write' },
|
|
41
|
+
{ value: 'delete', label: 'Delete' },
|
|
42
|
+
{ value: 'admin', label: 'Admin' },
|
|
43
|
+
{ value: 'publish', label: 'Publish' },
|
|
44
|
+
{ value: 'review', label: 'Review' },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const selected = ref<string[]>(['read']);
|
|
48
|
+
</script>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Grouped
|
|
52
|
+
|
|
53
|
+
Group headings appear in both columns. Search and sorting apply per column.
|
|
54
|
+
|
|
55
|
+
**Demo — `dual-listbox/demos/GroupedDualListbox.vue`**
|
|
56
|
+
|
|
57
|
+
```vue
|
|
58
|
+
<template>
|
|
59
|
+
<div style="height: 360px;">
|
|
60
|
+
<CoarDualListbox
|
|
61
|
+
v-model="selected"
|
|
62
|
+
:options="options"
|
|
63
|
+
available-label="Roles"
|
|
64
|
+
selected-label="Assigned"
|
|
65
|
+
:search-fields="['label', 'group']"
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
69
|
+
|
|
70
|
+
<script setup lang="ts">
|
|
71
|
+
import { ref } from 'vue';
|
|
72
|
+
import { CoarDualListbox } from '@cocoar/vue-ui';
|
|
73
|
+
import type { CoarListboxOption } from '@cocoar/vue-ui';
|
|
74
|
+
|
|
75
|
+
const options: CoarListboxOption<string>[] = [
|
|
76
|
+
{ value: 'admin', label: 'Admin', group: 'System' },
|
|
77
|
+
{ value: 'auditor', label: 'Auditor', group: 'System' },
|
|
78
|
+
{ value: 'editor', label: 'Editor', group: 'Content' },
|
|
79
|
+
{ value: 'publisher', label: 'Publisher', group: 'Content' },
|
|
80
|
+
{ value: 'reviewer', label: 'Reviewer', group: 'Content' },
|
|
81
|
+
{ value: 'viewer', label: 'Viewer', group: 'Content' },
|
|
82
|
+
{ value: 'billing', label: 'Billing', group: 'Finance' },
|
|
83
|
+
{ value: 'reporting', label: 'Reporting', group: 'Finance' },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const selected = ref<string[]>(['viewer']);
|
|
87
|
+
</script>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Custom item components
|
|
91
|
+
|
|
92
|
+
Pass `itemComponents` and they're used on both sides. The component receives a `side` prop (`'available' | 'selected'`) so the same renderer can adapt per column if needed.
|
|
93
|
+
|
|
94
|
+
**Demo — `dual-listbox/demos/CustomComponentDualListbox.vue`**
|
|
95
|
+
|
|
96
|
+
```vue
|
|
97
|
+
<template>
|
|
98
|
+
<div style="height: 360px;">
|
|
99
|
+
<CoarDualListbox
|
|
100
|
+
v-model="selectedIds"
|
|
101
|
+
:options="items"
|
|
102
|
+
:item-components="{ user: UserItem }"
|
|
103
|
+
:compare-with="byId"
|
|
104
|
+
available-label="Everyone"
|
|
105
|
+
selected-label="Group members"
|
|
106
|
+
:search-by="(i) => `${i.label} ${(i.value as UserRow).email}`"
|
|
107
|
+
/>
|
|
108
|
+
<p style="margin-top: 8px; font-size: 13px; color: #64748b;">
|
|
109
|
+
Group: {{ selectedIds.map(u => u.name).join(', ') || '—' }}
|
|
110
|
+
</p>
|
|
111
|
+
</div>
|
|
112
|
+
</template>
|
|
113
|
+
|
|
114
|
+
<script setup lang="ts">
|
|
115
|
+
import { ref } from 'vue';
|
|
116
|
+
import { CoarDualListbox } from '@cocoar/vue-ui';
|
|
117
|
+
import type { CoarListboxOption } from '@cocoar/vue-ui';
|
|
118
|
+
import UserItem from '../../listbox/demos/UserItem.vue';
|
|
119
|
+
|
|
120
|
+
interface UserRow { id: string; name: string; email: string; role: string }
|
|
121
|
+
|
|
122
|
+
const users: UserRow[] = [
|
|
123
|
+
{ id: '1', name: 'Alice Müller', email: 'alice@x.com', role: 'Admin' },
|
|
124
|
+
{ id: '2', name: 'Bob Meier', email: 'bob@x.com', role: 'Editor' },
|
|
125
|
+
{ id: '3', name: 'Clara Schmid', email: 'clara@x.com', role: 'Viewer' },
|
|
126
|
+
{ id: '4', name: 'Dan Roth', email: 'dan@x.com', role: 'Editor' },
|
|
127
|
+
{ id: '5', name: 'Eva Horn', email: 'eva@x.com', role: 'Admin' },
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
const items: CoarListboxOption<UserRow>[] = users.map(u => ({
|
|
131
|
+
value: u,
|
|
132
|
+
label: u.name,
|
|
133
|
+
kind: 'user',
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
// Compare users by id — protects against reactivity-induced reference changes.
|
|
137
|
+
const byId = (a: UserRow, b: UserRow) => a.id === b.id;
|
|
138
|
+
|
|
139
|
+
const selectedIds = ref<UserRow[]>([users[0]]);
|
|
140
|
+
</script>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Inline remove button (right side)
|
|
144
|
+
|
|
145
|
+
A common pattern: render an × button inside each selected item, and clicking it moves the user back to the available column. Since the right column is just `options ∩ modelValue`, "moving back" is simply *dropping the id from v-model* — the item re-appears on the left automatically.
|
|
146
|
+
|
|
147
|
+
**Demo — `dual-listbox/demos/RemoveBackToAvailable.vue`**
|
|
148
|
+
|
|
149
|
+
```vue
|
|
150
|
+
<template>
|
|
151
|
+
<div style="height: 360px;">
|
|
152
|
+
<CoarDualListbox
|
|
153
|
+
v-model="selectedIds"
|
|
154
|
+
:options="items"
|
|
155
|
+
:item-components="{ user: UserCard }"
|
|
156
|
+
:compare-with="byId"
|
|
157
|
+
available-label="Everyone"
|
|
158
|
+
selected-label="Group members"
|
|
159
|
+
:search-by="(i) => `${i.label} ${(i.value as UserRow).email}`"
|
|
160
|
+
@item-remove="onItemRemove"
|
|
161
|
+
/>
|
|
162
|
+
<p style="margin-top: 8px; font-size: 13px; color: #64748b;">
|
|
163
|
+
Group ({{ selectedIds.length }}): {{ selectedIds.map(u => u.name).join(', ') || '—' }}
|
|
164
|
+
</p>
|
|
165
|
+
</div>
|
|
166
|
+
</template>
|
|
167
|
+
|
|
168
|
+
<script setup lang="ts">
|
|
169
|
+
import { ref } from 'vue';
|
|
170
|
+
import { CoarDualListbox } from '@cocoar/vue-ui';
|
|
171
|
+
import type { CoarListboxOption } from '@cocoar/vue-ui';
|
|
172
|
+
import UserCard from './UserCard.vue';
|
|
173
|
+
|
|
174
|
+
interface UserRow { id: string; name: string; email: string; role: string }
|
|
175
|
+
|
|
176
|
+
const users: UserRow[] = [
|
|
177
|
+
{ id: '1', name: 'Alice Müller', email: 'alice@x.com', role: 'Admin' },
|
|
178
|
+
{ id: '2', name: 'Bob Meier', email: 'bob@x.com', role: 'Editor' },
|
|
179
|
+
{ id: '3', name: 'Clara Schmid', email: 'clara@x.com', role: 'Viewer' },
|
|
180
|
+
{ id: '4', name: 'Dan Roth', email: 'dan@x.com', role: 'Editor' },
|
|
181
|
+
{ id: '5', name: 'Eva Horn', email: 'eva@x.com', role: 'Admin' },
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
const items: CoarListboxOption<UserRow>[] = users.map((u) => ({
|
|
185
|
+
value: u, label: u.name, kind: 'user',
|
|
186
|
+
}));
|
|
187
|
+
|
|
188
|
+
const byId = (a: UserRow, b: UserRow) => a.id === b.id;
|
|
189
|
+
|
|
190
|
+
const selectedIds = ref<UserRow[]>([users[0], users[2]]);
|
|
191
|
+
|
|
192
|
+
// When a user clicks × on the right side, it's a "move back to available"
|
|
193
|
+
// — just drop the item from v-model and DualListbox re-computes the columns.
|
|
194
|
+
function onItemRemove(payload: { item: CoarListboxOption<UserRow>; side: 'available' | 'selected' }) {
|
|
195
|
+
if (payload.side !== 'selected') return;
|
|
196
|
+
selectedIds.value = selectedIds.value.filter((u) => !byId(u, payload.item.value));
|
|
197
|
+
}
|
|
198
|
+
</script>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The renderer uses the `side` prop to show the × only on the selected side; clicking it calls `api.remove()`, which bubbles up as `item-remove` with `side: 'selected'`, and the parent filters v-model:
|
|
202
|
+
|
|
203
|
+
```vue
|
|
204
|
+
<!-- UserCard.vue -->
|
|
205
|
+
<button v-if="side === 'selected'" @click.stop="api.remove()">×</button>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```vue
|
|
209
|
+
<!-- Parent -->
|
|
210
|
+
<CoarDualListbox v-model="selected" :options="..." @item-remove="onRemove" />
|
|
211
|
+
|
|
212
|
+
<script setup lang="ts">
|
|
213
|
+
function onRemove({ item, side }) {
|
|
214
|
+
if (side !== 'selected') return
|
|
215
|
+
selected.value = selected.value.filter(u => u.id !== item.value.id)
|
|
216
|
+
}
|
|
217
|
+
</script>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
See [`CoarListboxItemApi`](./listbox.md#item-api-inline-actions) for the full api surface.
|
|
221
|
+
|
|
222
|
+
## Drag & drop
|
|
223
|
+
|
|
224
|
+
Set the `drag-drop` prop and items become draggable between the two columns. A unique drag group is wired up internally so the two sides exchange items with each other but not with unrelated listboxes on the page.
|
|
225
|
+
|
|
226
|
+
**Demo — `dual-listbox/demos/DragDropDualListbox.vue`**
|
|
227
|
+
|
|
228
|
+
```vue
|
|
229
|
+
<template>
|
|
230
|
+
<div style="height: 320px;">
|
|
231
|
+
<CoarDualListbox
|
|
232
|
+
v-model="selected"
|
|
233
|
+
:options="options"
|
|
234
|
+
available-label="Roles"
|
|
235
|
+
selected-label="Assigned"
|
|
236
|
+
drag-drop
|
|
237
|
+
/>
|
|
238
|
+
<p style="margin-top: 8px; font-size: 13px; color: #64748b;">
|
|
239
|
+
Tip: drag from one column to the other. Or Ctrl/Shift-click to pick several first, then drag.
|
|
240
|
+
</p>
|
|
241
|
+
</div>
|
|
242
|
+
</template>
|
|
243
|
+
|
|
244
|
+
<script setup lang="ts">
|
|
245
|
+
import { ref } from 'vue';
|
|
246
|
+
import { CoarDualListbox } from '@cocoar/vue-ui';
|
|
247
|
+
import type { CoarListboxOption } from '@cocoar/vue-ui';
|
|
248
|
+
|
|
249
|
+
const options: CoarListboxOption<string>[] = [
|
|
250
|
+
{ value: 'admin', label: 'Admin' },
|
|
251
|
+
{ value: 'editor', label: 'Editor' },
|
|
252
|
+
{ value: 'publisher', label: 'Publisher' },
|
|
253
|
+
{ value: 'reviewer', label: 'Reviewer' },
|
|
254
|
+
{ value: 'viewer', label: 'Viewer' },
|
|
255
|
+
{ value: 'billing', label: 'Billing' },
|
|
256
|
+
{ value: 'reporting', label: 'Reporting' },
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
const selected = ref<string[]>(['viewer']);
|
|
260
|
+
</script>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Selection-aware: if the dragged item is part of a multi-highlight, the whole highlighted set moves at once.
|
|
264
|
+
|
|
265
|
+
For a standalone two-listbox setup (or three+ lists), see the same feature on [`CoarListbox`](./listbox.md#drag-drop-between-lists).
|
|
266
|
+
|
|
267
|
+
## Virtual scrolling (large datasets)
|
|
268
|
+
|
|
269
|
+
For directories with thousands of entries, enable `virtual` — only the rows in and around the viewport are rendered. Group headings scroll naturally in this mode (they are not sticky).
|
|
270
|
+
|
|
271
|
+
The demo below loads 10,000 synthetic principals (users, groups, service accounts) client-side. Scroll, search, highlight, drag, and the inline × all still work.
|
|
272
|
+
|
|
273
|
+
**Demo — `dual-listbox/demos/VirtualLargeList.vue`**
|
|
274
|
+
|
|
275
|
+
```vue
|
|
276
|
+
<template>
|
|
277
|
+
<div style="height: 440px;">
|
|
278
|
+
<CoarDualListbox
|
|
279
|
+
v-model="assigned"
|
|
280
|
+
:options="principals"
|
|
281
|
+
:item-components="{ principal: PrincipalRow }"
|
|
282
|
+
:compare-with="byId"
|
|
283
|
+
:search-by="(i) => `${i.label} ${(i.value as Principal).sub} ${(i.value as Principal).kind}`"
|
|
284
|
+
available-label="Directory"
|
|
285
|
+
selected-label="Assigned"
|
|
286
|
+
virtual
|
|
287
|
+
:item-height="44"
|
|
288
|
+
:group-heading-height="28"
|
|
289
|
+
drag-drop
|
|
290
|
+
@item-remove="onRemove"
|
|
291
|
+
/>
|
|
292
|
+
<p style="margin-top: 8px; font-size: 13px; color: #64748b;">
|
|
293
|
+
Directory: {{ principals.length.toLocaleString() }} principals. Assigned: {{ assigned.length }}.
|
|
294
|
+
</p>
|
|
295
|
+
</div>
|
|
296
|
+
</template>
|
|
297
|
+
|
|
298
|
+
<script setup lang="ts">
|
|
299
|
+
import { computed, ref } from 'vue';
|
|
300
|
+
import { CoarDualListbox } from '@cocoar/vue-ui';
|
|
301
|
+
import type { CoarListboxOption } from '@cocoar/vue-ui';
|
|
302
|
+
import PrincipalRow, { type Principal } from './PrincipalRow.vue';
|
|
303
|
+
|
|
304
|
+
// Generate 10k synthetic IPrincipals: 8k users, 1.5k groups, 500 system users.
|
|
305
|
+
function makePrincipals(): Principal[] {
|
|
306
|
+
const out: Principal[] = [];
|
|
307
|
+
const firstNames = ['Alex', 'Sam', 'Jordan', 'Taylor', 'Morgan', 'Riley', 'Casey', 'Quinn', 'Avery', 'Rowan', 'Maya', 'Leo', 'Nora', 'Ivo', 'Yara', 'Milo', 'Luna', 'Finn', 'Zoe', 'Theo'];
|
|
308
|
+
const lastNames = ['Müller', 'Schmid', 'Meier', 'Weber', 'Keller', 'Huber', 'Fischer', 'Baumann', 'Steiner', 'Brunner', 'Roth', 'Zürcher', 'Frei', 'Hofer', 'Widmer', 'Kaufmann', 'Bosshart', 'Stucki', 'Graf', 'Winkler'];
|
|
309
|
+
for (let i = 0; i < 8000; i++) {
|
|
310
|
+
const fn = firstNames[i % firstNames.length];
|
|
311
|
+
const ln = lastNames[(i * 7) % lastNames.length];
|
|
312
|
+
out.push({
|
|
313
|
+
id: `u${i}`,
|
|
314
|
+
kind: 'user',
|
|
315
|
+
name: `${fn} ${ln}`,
|
|
316
|
+
sub: `${fn.toLowerCase()}.${ln.toLowerCase()}@corp.example`,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
const teams = ['Engineering', 'Design', 'Product', 'Sales', 'Marketing', 'Finance', 'Legal', 'HR', 'Support', 'Operations'];
|
|
320
|
+
for (let i = 0; i < 1500; i++) {
|
|
321
|
+
const t = teams[i % teams.length];
|
|
322
|
+
out.push({
|
|
323
|
+
id: `g${i}`,
|
|
324
|
+
kind: 'group',
|
|
325
|
+
name: `${t} #${Math.floor(i / teams.length) + 1}`,
|
|
326
|
+
sub: `${(8 + (i % 24))} members`,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
for (let i = 0; i < 500; i++) {
|
|
330
|
+
out.push({
|
|
331
|
+
id: `s${i}`,
|
|
332
|
+
kind: 'system',
|
|
333
|
+
name: `svc-${String(i).padStart(4, '0')}`,
|
|
334
|
+
sub: 'service account',
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const rawPrincipals = makePrincipals();
|
|
341
|
+
|
|
342
|
+
const principals = computed<CoarListboxOption<Principal>[]>(() =>
|
|
343
|
+
rawPrincipals.map((p) => ({
|
|
344
|
+
value: p,
|
|
345
|
+
label: p.name,
|
|
346
|
+
kind: 'principal',
|
|
347
|
+
})),
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const byId = (a: Principal, b: Principal) => a.id === b.id;
|
|
351
|
+
|
|
352
|
+
const assigned = ref<Principal[]>([rawPrincipals[3], rawPrincipals[42], rawPrincipals[8005]]);
|
|
353
|
+
|
|
354
|
+
function onRemove(payload: { item: CoarListboxOption<Principal>; side: 'available' | 'selected' }) {
|
|
355
|
+
if (payload.side !== 'selected') return;
|
|
356
|
+
assigned.value = assigned.value.filter((p) => !byId(p, payload.item.value));
|
|
357
|
+
}
|
|
358
|
+
</script>
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
```vue
|
|
362
|
+
<CoarDualListbox
|
|
363
|
+
v-model="assigned"
|
|
364
|
+
:options="principals"
|
|
365
|
+
virtual
|
|
366
|
+
:item-height="44"
|
|
367
|
+
:group-heading-height="28"
|
|
368
|
+
/>
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
- Set `item-height` to the pixel height of a row — must be fixed, but you can pick whatever matches your custom item component.
|
|
372
|
+
- `overscan` controls how many extra rows are rendered above/below the viewport (default 5). Raise it if you see blank flashes on fast scrolls.
|
|
373
|
+
- Virtual mode uses the [`useVirtualList`](./virtual-list.md) composable, which is also exported for your own components.
|
|
374
|
+
|
|
375
|
+
## Interaction
|
|
376
|
+
|
|
377
|
+
- **Click**: highlight a single item
|
|
378
|
+
- **Ctrl / ⌘ + Click**: add/remove from highlight
|
|
379
|
+
- **Shift + Click**: range select
|
|
380
|
+
- **Double-click / Enter**: move that single item across
|
|
381
|
+
- **→ / ← buttons**: move all currently-highlighted items
|
|
382
|
+
- **≫ / ≪ buttons**: move all currently-visible items (respects the column's search filter). Hide with `hideMoveAll`.
|
|
383
|
+
|
|
384
|
+
## Slots
|
|
385
|
+
|
|
386
|
+
Shared slots apply to both columns; side-specific slots override only one side.
|
|
387
|
+
|
|
388
|
+
| Slot | Applies to | Scope |
|
|
389
|
+
|------|------------|-------|
|
|
390
|
+
| `item` | both | `{ item, highlighted, selectable, side }` |
|
|
391
|
+
| `item-<kind>` | both | `{ item, highlighted, selectable, side }` |
|
|
392
|
+
| `group-heading` | both | `{ group, items }` |
|
|
393
|
+
| `header-available` | left | `{ label, count, total }` |
|
|
394
|
+
| `header-selected` | right | `{ label, count, total }` |
|
|
395
|
+
| `empty-available` | left | — |
|
|
396
|
+
| `empty-selected` | right | — |
|
|
397
|
+
| `actions` | center | `{ moveRight, moveLeft, moveAllRight, moveAllLeft, canMoveRight, canMoveLeft, canMoveAllRight, canMoveAllLeft }` |
|
|
398
|
+
|
|
399
|
+
## API
|
|
400
|
+
|
|
401
|
+
### Props
|
|
402
|
+
|
|
403
|
+
| Prop | Type | Default | Description |
|
|
404
|
+
|------|------|---------|-------------|
|
|
405
|
+
| `v-model` | `T[]` | `[]` | Selected values (right column contents, in order) |
|
|
406
|
+
| `options` | `CoarListboxOption<T>[]` | `[]` | All items — split into columns by `modelValue` membership |
|
|
407
|
+
| `availableLabel` | `string` | `'Available'` | Left column header |
|
|
408
|
+
| `selectedLabel` | `string` | `'Selected'` | Right column header |
|
|
409
|
+
| `height` | `string` | *flex* | Fixed list height for both columns |
|
|
410
|
+
| `disabled` / `readonly` | `boolean` | `false` | Standard form states |
|
|
411
|
+
| `hideSearch` | `boolean` | `false` | Hide the search input in both columns |
|
|
412
|
+
| `searchPlaceholder` | `string` | `'Search…'` | Placeholder for both search inputs |
|
|
413
|
+
| `searchFields` / `searchBy` / `filterWith` | — | — | Search config, forwarded to both columns — see [Listbox docs](./listbox.md#flexible-search) |
|
|
414
|
+
| `sortGroups` / `sortOptions` / `hideGroupHeadings` | — | — | Grouping / sorting config, forwarded to both columns |
|
|
415
|
+
| `itemComponents` / `kindBy` | — | — | Custom rendering, forwarded to both columns |
|
|
416
|
+
| `compareWith` | `(a, b) => boolean` | `===` | Value equality |
|
|
417
|
+
| `hideMoveAll` | `boolean` | `false` | Hide the ≫ / ≪ buttons |
|
|
418
|
+
| `hideCounts` | `boolean` | `false` | Hide count badges in headers |
|
|
419
|
+
| `emptyAvailable` | `string` | `'No items'` | Empty state for left column |
|
|
420
|
+
| `emptySelected` | `string` | `'None selected'` | Empty state for right column |
|
|
421
|
+
| `sortSelectedBySource` | `boolean` | `false` | When `true`, the right column is re-sorted by the order of items in `options` after every move. Default keeps the order in which the user moved items across |
|
|
422
|
+
| `dragDrop` | `boolean` | `false` | Enable drag-and-drop between the two columns |
|
|
423
|
+
| `dragEngine` | `'native' \| 'pointer' \| 'auto'` | `'native'` | HTML5 drag events, or Pointer Events for touch — see [Drag & Drop → Engines](./drag-drop.md#engines) |
|
|
424
|
+
| `canDrag` | `(item) => boolean` | — | Per-item source permission, applied to both columns. See [Listbox docs](./listbox.md#per-item-validation) |
|
|
425
|
+
| `canDrop` | `(payload) => boolean` | — | Runtime drop validation, applied to both columns |
|
|
426
|
+
| `virtual` | `boolean` | `false` | Enable virtual scrolling — render only rows in/near the viewport |
|
|
427
|
+
| `itemHeight` | `number` | `32` | Row height in pixels (used only when `virtual` is on) |
|
|
428
|
+
| `groupHeadingHeight` | `number` | `28` | Heading height when `virtual` is on |
|
|
429
|
+
| `overscan` | `number` | `5` | Extra rows rendered above/below the viewport |
|
|
430
|
+
|
|
431
|
+
### Events
|
|
432
|
+
|
|
433
|
+
| Event | Payload | When |
|
|
434
|
+
|-------|---------|------|
|
|
435
|
+
| `update:modelValue` | `T[]` | Selection changed |
|
|
436
|
+
| `move` | `{ direction: 'right' \| 'left', values: T[] }` | Any move action (button or double-click) |
|
|
437
|
+
| `item-remove` | `{ item, side }` | A custom renderer called `api.remove()` in either column |
|
|
438
|
+
| `item-action` | `{ item, name, payload?, side }` | A custom renderer called `api.action(...)` in either column |
|
|
439
|
+
|
|
440
|
+
### Exposed methods
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
const dual = useTemplateRef('dual')
|
|
444
|
+
dual.value?.moveRight()
|
|
445
|
+
dual.value?.moveLeft()
|
|
446
|
+
dual.value?.moveAllRight()
|
|
447
|
+
dual.value?.moveAllLeft()
|
|
448
|
+
dual.value?.clearHighlight()
|
|
449
|
+
```
|