@nexxtmove/ui 0.1.52 → 0.1.54
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/index.d.ts +460 -145
- package/dist/index.js +2789 -2470
- package/dist/nuxt.js +34 -30
- package/package.json +1 -1
- package/src/assets/images/property_placeholder.jpg +0 -0
- package/src/components/Card/Card.vue +30 -0
- package/src/components/Chip/Chip.vue +28 -15
- package/src/components/Combobox/Combobox.vue +17 -99
- package/src/components/ComboboxPanel/ComboboxPanel.vue +293 -0
- package/src/components/ComboboxPanel/combobox-types.ts +5 -0
- package/src/components/ContactInfoCard/ContactInfoCard.vue +1 -1
- package/src/components/PropertyCard/PropertyCard.vue +39 -0
- package/src/components/PropertyListing/PropertyListing.vue +30 -0
- package/src/components/TimelinePhaseblock/TimelinePhaseblock.vue +8 -8
- package/src/components.json +4 -0
- package/src/index.ts +4 -0
package/dist/nuxt.js
CHANGED
|
@@ -1,38 +1,42 @@
|
|
|
1
1
|
import { defineNuxtModule as c, addComponent as s, addTemplate as a } from "@nuxt/kit";
|
|
2
|
-
const x = "components/AnimatedNumber/AnimatedNumber.vue", i = "components/Avatar/Avatar.vue",
|
|
2
|
+
const x = "components/AnimatedNumber/AnimatedNumber.vue", i = "components/Avatar/Avatar.vue", m = "components/Button/Button.vue", u = "components/Calendar/Calendar.vue", r = "components/Card/Card.vue", p = "components/Checkbox/Checkbox.vue", d = "components/Chip/Chip.vue", l = "components/Combobox/Combobox.vue", v = "components/ComboboxPanel/ComboboxPanel.vue", C = "components/ContactInfoCard/ContactInfoCard.vue", N = "components/CustomerJourneyTile/CustomerJourneyTile.vue", b = "components/DatePicker/DatePicker.vue", T = "components/DoughnutChart/DoughnutChart.vue", P = "components/DropdownButton/DropdownButton.vue", S = "components/Header/Header.vue", h = "components/Icon/Icon.vue", I = "components/InfoBlock/InfoBlock.vue", f = "components/InputField/InputField.vue", g = "components/Modal/Modal.vue", y = "components/Pagination/Pagination.vue", M = "components/PresenceIndicator/PresenceIndicator.vue", k = "components/ProgressBar/ProgressBar.vue", B = "components/PropertyCard/PropertyCard.vue", D = "components/PropertyListing/PropertyListing.vue", A = "components/SocialIcons/SocialIcons.vue", H = "components/SocialMediaCustomTemplate/SocialMediaCustomTemplate.vue", w = "components/SocialMediaTemplate/SocialMediaTemplate.vue", E = "components/SocialMediaType/SocialMediaType.vue", F = "components/Source/Source.vue", J = "components/StepperHeader/StepperHeader.vue", L = "components/TabbedContent/TabbedContent.vue", j = "components/Table/Table.vue", $ = "components/Tabs/Tabs.vue", K = "components/TimelineEvent/TimelineEvent.vue", O = "components/TimelinePhaseblock/TimelinePhaseblock.vue", U = "components/Tooltip/Tooltip.vue", _ = {
|
|
3
3
|
NexxtAnimatedNumber: x,
|
|
4
4
|
NexxtAvatar: i,
|
|
5
|
-
NexxtButton:
|
|
6
|
-
NexxtCalendar:
|
|
5
|
+
NexxtButton: m,
|
|
6
|
+
NexxtCalendar: u,
|
|
7
|
+
NexxtCard: r,
|
|
7
8
|
NexxtCheckbox: p,
|
|
8
|
-
NexxtChip:
|
|
9
|
+
NexxtChip: d,
|
|
9
10
|
NexxtCombobox: l,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
NexxtComboboxPanel: v,
|
|
12
|
+
NexxtContactInfoCard: C,
|
|
13
|
+
NexxtCustomerJourneyTile: N,
|
|
14
|
+
NexxtDatePicker: b,
|
|
15
|
+
NexxtDoughnutChart: T,
|
|
16
|
+
NexxtDropdownButton: P,
|
|
17
|
+
NexxtHeader: S,
|
|
18
|
+
NexxtIcon: h,
|
|
19
|
+
NexxtInfoBlock: I,
|
|
20
|
+
NexxtInputField: f,
|
|
21
|
+
NexxtModal: g,
|
|
22
|
+
NexxtPagination: y,
|
|
21
23
|
NexxtPresenceIndicator: M,
|
|
22
24
|
NexxtProgressBar: k,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
NexxtPropertyCard: B,
|
|
26
|
+
NexxtPropertyListing: D,
|
|
27
|
+
NexxtSocialIcons: A,
|
|
28
|
+
NexxtSocialMediaCustomTemplate: H,
|
|
29
|
+
NexxtSocialMediaTemplate: w,
|
|
30
|
+
NexxtSocialMediaType: E,
|
|
31
|
+
NexxtSource: F,
|
|
32
|
+
NexxtStepperHeader: J,
|
|
33
|
+
NexxtTabbedContent: L,
|
|
34
|
+
NexxtTable: j,
|
|
35
|
+
NexxtTabs: $,
|
|
36
|
+
NexxtTimelineEvent: K,
|
|
37
|
+
NexxtTimelinePhaseblock: O,
|
|
38
|
+
NexxtTooltip: U
|
|
39
|
+
}, z = c({
|
|
36
40
|
meta: {
|
|
37
41
|
name: "@nexxtmove/ui",
|
|
38
42
|
configKey: "nexxtmoveUi",
|
|
@@ -44,7 +48,7 @@ const x = "components/AnimatedNumber/AnimatedNumber.vue", i = "components/Avatar
|
|
|
44
48
|
addCSS: !0
|
|
45
49
|
},
|
|
46
50
|
setup(e, o) {
|
|
47
|
-
for (const [t, n] of Object.entries(
|
|
51
|
+
for (const [t, n] of Object.entries(_))
|
|
48
52
|
s({
|
|
49
53
|
name: t,
|
|
50
54
|
export: "default",
|
|
@@ -63,5 +67,5 @@ const x = "components/AnimatedNumber/AnimatedNumber.vue", i = "components/Avatar
|
|
|
63
67
|
}
|
|
64
68
|
});
|
|
65
69
|
export {
|
|
66
|
-
|
|
70
|
+
z as default
|
|
67
71
|
};
|
package/package.json
CHANGED
|
Binary file
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
image?: string
|
|
4
|
+
imageAlt?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
defineProps<Props>()
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<div
|
|
12
|
+
class="card bg-color-white inline-flex w-80 flex-col items-start justify-start gap-0 rounded-t-lg"
|
|
13
|
+
>
|
|
14
|
+
<slot name="image" v-if="image || $slots.image">
|
|
15
|
+
<img
|
|
16
|
+
v-if="image"
|
|
17
|
+
:src="image"
|
|
18
|
+
:alt="imageAlt ?? 'Card afbeelding'"
|
|
19
|
+
class="block h-40 max-h-40 min-h-40 w-full rounded-tl-lg rounded-tr-lg object-cover"
|
|
20
|
+
/>
|
|
21
|
+
</slot>
|
|
22
|
+
|
|
23
|
+
<div
|
|
24
|
+
class="card-content box-border flex w-full flex-col gap-2 rounded-b-lg border border-gray-200 px-4 py-2"
|
|
25
|
+
:class="{ 'rounded-t-lg border-t': !image && !$slots.image }"
|
|
26
|
+
>
|
|
27
|
+
<slot />
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
@@ -2,41 +2,54 @@
|
|
|
2
2
|
import { computed } from 'vue'
|
|
3
3
|
|
|
4
4
|
interface NexxtChipProps {
|
|
5
|
-
variant?: 'default' | 'warning' | 'success' | 'danger'
|
|
5
|
+
variant?: 'default' | 'warning' | 'success' | 'danger'
|
|
6
|
+
ghost?: boolean
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
defineOptions({
|
|
9
10
|
name: 'NexxtChip',
|
|
10
11
|
})
|
|
11
12
|
|
|
12
|
-
const { variant = 'default' } = defineProps<NexxtChipProps>()
|
|
13
|
+
const { variant = 'default', ghost = false } = defineProps<NexxtChipProps>()
|
|
13
14
|
|
|
14
|
-
const
|
|
15
|
+
const styles = computed(() => {
|
|
16
|
+
if (ghost) {
|
|
17
|
+
switch (variant) {
|
|
18
|
+
case 'warning':
|
|
19
|
+
return { bg: 'bg-orange-50', border: 'border border-orange-500', text: 'text-orange-500' }
|
|
20
|
+
case 'success':
|
|
21
|
+
return { bg: 'bg-green-50', border: 'border border-green-500', text: 'text-green-500' }
|
|
22
|
+
case 'danger':
|
|
23
|
+
return { bg: 'bg-brick-100/20', border: 'border border-brick-500', text: 'text-brick-500' }
|
|
24
|
+
default:
|
|
25
|
+
return {
|
|
26
|
+
bg: 'bg-cornflower-blue-50',
|
|
27
|
+
border: 'border border-cornflower-blue-500',
|
|
28
|
+
text: 'text-cornflower-blue-500',
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
15
32
|
switch (variant) {
|
|
16
33
|
case 'warning':
|
|
17
|
-
return 'bg-orange-500'
|
|
34
|
+
return { bg: 'bg-orange-500', border: '', text: 'text-white' }
|
|
18
35
|
case 'success':
|
|
19
|
-
return 'bg-green-500'
|
|
36
|
+
return { bg: 'bg-green-500', border: '', text: 'text-white' }
|
|
20
37
|
case 'danger':
|
|
21
|
-
return 'bg-brick-500'
|
|
22
|
-
case 'ghost':
|
|
23
|
-
return 'bg-cornflower-blue-50'
|
|
38
|
+
return { bg: 'bg-brick-500', border: '', text: 'text-white' }
|
|
24
39
|
default:
|
|
25
|
-
return 'bg-cornflower-blue-600'
|
|
40
|
+
return { bg: 'bg-cornflower-blue-600', border: '', text: 'text-white' }
|
|
26
41
|
}
|
|
27
42
|
})
|
|
28
43
|
</script>
|
|
29
44
|
|
|
30
45
|
<template>
|
|
31
46
|
<span
|
|
32
|
-
class="inline-flex h-5 flex-col items-center justify-center gap-2.5 rounded-[200px]
|
|
33
|
-
:class="[
|
|
47
|
+
class="inline-flex h-5 flex-col items-center justify-center gap-2.5 rounded-[200px]"
|
|
48
|
+
:class="[styles.bg, styles.border, $slots.close ? 'pr-2 pl-4' : 'px-4']"
|
|
34
49
|
>
|
|
35
|
-
<span
|
|
36
|
-
class="justify-start text-center extra-small-semibold"
|
|
37
|
-
:class="variant === 'ghost' ? 'text-cornflower-blue-500' : 'text-white'"
|
|
38
|
-
>
|
|
50
|
+
<span class="justify-start text-center extra-small-semibold" :class="styles.text">
|
|
39
51
|
<slot>Chip</slot>
|
|
52
|
+
<slot v-if="$slots.close" name="close"></slot>
|
|
40
53
|
</span>
|
|
41
54
|
</span>
|
|
42
55
|
</template>
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
|
|
3
3
|
import NexxtButton from '../Button/Button.vue'
|
|
4
|
-
import NexxtChip from '../Chip/Chip.vue'
|
|
5
4
|
import NexxtIcon from '../Icon/Icon.vue'
|
|
6
|
-
import
|
|
5
|
+
import ComboboxPanel from '../ComboboxPanel/ComboboxPanel.vue'
|
|
6
|
+
import type { ComboboxOptions } from '../ComboboxPanel/combobox-types'
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
type NexxtComboboxProps = {
|
|
9
9
|
variant?: InstanceType<typeof NexxtButton>['variant']
|
|
10
10
|
icon?: InstanceType<typeof NexxtIcon>['name']
|
|
11
11
|
placeholder?: string
|
|
12
12
|
placement?: 'left' | 'right'
|
|
13
|
-
|
|
14
|
-
}
|
|
13
|
+
} & ComboboxOptions
|
|
15
14
|
|
|
16
15
|
const props = defineProps<NexxtComboboxProps>()
|
|
17
16
|
|
|
@@ -25,43 +24,13 @@ const {
|
|
|
25
24
|
const selected = defineModel<(string | number)[]>({ default: [] })
|
|
26
25
|
|
|
27
26
|
const isOpen = ref(false)
|
|
28
|
-
const
|
|
27
|
+
const panelRef = ref<InstanceType<typeof ComboboxPanel> | null>(null)
|
|
29
28
|
const wrapperRef = ref<HTMLElement | null>(null)
|
|
30
|
-
const search = ref('')
|
|
31
|
-
|
|
32
|
-
const getLabel = (option: string | { label: string; value: string | number }) =>
|
|
33
|
-
typeof option === 'string' ? option : option.label
|
|
34
|
-
|
|
35
|
-
const getValue = (option: string | { label: string; value: string | number }) =>
|
|
36
|
-
typeof option === 'string' ? option : option.value
|
|
37
|
-
|
|
38
|
-
const selectedLabels = computed(() =>
|
|
39
|
-
selected.value.map((val) => {
|
|
40
|
-
const match = props.options.find((o) => getValue(o) === val)
|
|
41
|
-
return { value: val, label: match ? getLabel(match) : String(val) }
|
|
42
|
-
}),
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
const filteredOptions = computed(() => {
|
|
46
|
-
const q = search.value.toLowerCase()
|
|
47
|
-
return props.options
|
|
48
|
-
.filter((o) => !selected.value.includes(getValue(o)))
|
|
49
|
-
.filter((o) => getLabel(o).toLowerCase().includes(q))
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
const selectOption = (option: string | { label: string; value: string | number }) => {
|
|
53
|
-
selected.value = [...selected.value, getValue(option)]
|
|
54
|
-
search.value = ''
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const removeSelected = (val: string | number) => {
|
|
58
|
-
selected.value = selected.value.filter((v) => v !== val)
|
|
59
|
-
}
|
|
60
29
|
|
|
61
30
|
watch(isOpen, async (open) => {
|
|
62
31
|
if (open) {
|
|
63
32
|
await nextTick()
|
|
64
|
-
|
|
33
|
+
panelRef.value?.focus()
|
|
65
34
|
}
|
|
66
35
|
})
|
|
67
36
|
|
|
@@ -108,74 +77,23 @@ const transitionClasses = computed(() => {
|
|
|
108
77
|
aria-haspopup="listbox"
|
|
109
78
|
@click="isOpen = !isOpen"
|
|
110
79
|
>
|
|
111
|
-
<span class="flex gap-2"><slot>Combobox</slot><NexxtIcon name="chevron-down"
|
|
80
|
+
<span class="flex gap-2"><slot>Combobox</slot><NexxtIcon name="chevron-down" /></span>
|
|
112
81
|
</NexxtButton>
|
|
113
82
|
<Transition v-bind="transitionClasses">
|
|
114
83
|
<div
|
|
115
84
|
v-if="isOpen"
|
|
116
|
-
class="absolute top-full z-10 mt-2.5
|
|
85
|
+
class="absolute top-full z-10 mt-2.5"
|
|
117
86
|
:class="placement === 'right' ? 'right-0' : 'left-0'"
|
|
118
87
|
>
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
<Transition
|
|
129
|
-
enter-active-class="transition-all duration-200 ease-out overflow-hidden"
|
|
130
|
-
enter-from-class="max-h-0 opacity-0"
|
|
131
|
-
enter-to-class="max-h-40 opacity-100"
|
|
132
|
-
leave-active-class="transition-all duration-150 ease-in overflow-hidden"
|
|
133
|
-
leave-from-class="max-h-40 opacity-100"
|
|
134
|
-
leave-to-class="max-h-0 opacity-0"
|
|
135
|
-
>
|
|
136
|
-
<TransitionGroup
|
|
137
|
-
v-if="selectedLabels.length > 0"
|
|
138
|
-
tag="div"
|
|
139
|
-
class="flex flex-wrap gap-1 border-b border-gray-200 p-2 pt-0"
|
|
140
|
-
enter-active-class="transition-all duration-150 ease-out"
|
|
141
|
-
enter-from-class="opacity-0 scale-75"
|
|
142
|
-
enter-to-class="opacity-100 scale-100"
|
|
143
|
-
leave-active-class="transition-all duration-100 ease-in"
|
|
144
|
-
leave-from-class="opacity-100 scale-100"
|
|
145
|
-
leave-to-class="opacity-0 scale-75"
|
|
146
|
-
>
|
|
147
|
-
<button
|
|
148
|
-
v-for="item in selectedLabels"
|
|
149
|
-
:key="item.value"
|
|
150
|
-
class="origin-left cursor-pointer"
|
|
151
|
-
:aria-label="`Verwijder ${item.label}`"
|
|
152
|
-
@click="removeSelected(item.value)"
|
|
153
|
-
>
|
|
154
|
-
<NexxtChip variant="ghost">
|
|
155
|
-
{{ item.label }}
|
|
156
|
-
<span>
|
|
157
|
-
<NexxtIcon name="xmark" class="text-xs" aria-hidden="true" />
|
|
158
|
-
</span>
|
|
159
|
-
</NexxtChip>
|
|
160
|
-
</button>
|
|
161
|
-
</TransitionGroup>
|
|
162
|
-
</Transition>
|
|
163
|
-
<ul
|
|
164
|
-
role="listbox"
|
|
165
|
-
aria-multiselectable="true"
|
|
166
|
-
class="max-h-56 overflow-y-auto [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-cornflower-blue-200 [&::-webkit-scrollbar-thumb:hover]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent"
|
|
167
|
-
>
|
|
168
|
-
<li
|
|
169
|
-
v-for="option in filteredOptions"
|
|
170
|
-
:key="getValue(option)"
|
|
171
|
-
role="option"
|
|
172
|
-
aria-selected="false"
|
|
173
|
-
class="flex w-full min-w-42 cursor-pointer items-center gap-2 px-6 py-3 text-left text-sm whitespace-nowrap text-gray-700 transition-colors duration-200 hover:bg-gray-100 hover:text-purple-600 focus:bg-gray-100 focus:outline-none"
|
|
174
|
-
@click="selectOption(option)"
|
|
175
|
-
>
|
|
176
|
-
{{ getLabel(option) }}
|
|
177
|
-
</li>
|
|
178
|
-
</ul>
|
|
88
|
+
<ComboboxPanel
|
|
89
|
+
ref="panelRef"
|
|
90
|
+
v-model="selected"
|
|
91
|
+
:options="props.options"
|
|
92
|
+
:allow-new="props.allowNew"
|
|
93
|
+
:icon
|
|
94
|
+
:placeholder
|
|
95
|
+
:floating="false"
|
|
96
|
+
/>
|
|
179
97
|
</div>
|
|
180
98
|
</Transition>
|
|
181
99
|
</div>
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
3
|
+
import NexxtChip from '../Chip/Chip.vue'
|
|
4
|
+
import NexxtIcon from '../Icon/Icon.vue'
|
|
5
|
+
import NexxtInputField from '../InputField/InputField.vue'
|
|
6
|
+
import type { ComboboxOptions } from './combobox-types'
|
|
7
|
+
|
|
8
|
+
type ComboboxPanelProps = {
|
|
9
|
+
icon?: InstanceType<typeof NexxtIcon>['name']
|
|
10
|
+
placeholder?: string
|
|
11
|
+
floating?: boolean
|
|
12
|
+
} & ComboboxOptions
|
|
13
|
+
|
|
14
|
+
const props = defineProps<ComboboxPanelProps>()
|
|
15
|
+
|
|
16
|
+
const { icon = 'tag', placeholder = 'Zoeken...', floating = true } = props
|
|
17
|
+
|
|
18
|
+
const selected = defineModel<(string | number)[]>({ default: [] })
|
|
19
|
+
|
|
20
|
+
const inputFieldRef = ref<InstanceType<typeof NexxtInputField> | null>(null)
|
|
21
|
+
const wrapperRef = ref<HTMLElement | null>(null)
|
|
22
|
+
const search = ref('')
|
|
23
|
+
const isFocused = ref(false)
|
|
24
|
+
|
|
25
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
26
|
+
if (wrapperRef.value && !wrapperRef.value.contains(event.target as Node)) {
|
|
27
|
+
isFocused.value = false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
32
|
+
if (event.key === 'Escape') isFocused.value = false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onMounted(() => {
|
|
36
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
37
|
+
document.addEventListener('keydown', handleKeydown)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
onUnmounted(() => {
|
|
41
|
+
document.removeEventListener('mousedown', handleClickOutside)
|
|
42
|
+
document.removeEventListener('keydown', handleKeydown)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const getLabel = (option: string | { label: string; value: string | number }) =>
|
|
46
|
+
typeof option === 'string' ? option : option.label
|
|
47
|
+
|
|
48
|
+
const getValue = (option: string | { label: string; value: string | number }) =>
|
|
49
|
+
typeof option === 'string' ? option : option.value
|
|
50
|
+
|
|
51
|
+
const selectedLabels = computed(() =>
|
|
52
|
+
selected.value.map((val) => {
|
|
53
|
+
const match = props.options.find((o) => getValue(o) === val)
|
|
54
|
+
return { value: val, label: match ? getLabel(match) : String(val), isCustom: !match }
|
|
55
|
+
}),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const filteredOptions = computed(() => {
|
|
59
|
+
const q = search.value.toLowerCase()
|
|
60
|
+
return props.options
|
|
61
|
+
.filter((o) => !selected.value.includes(getValue(o)))
|
|
62
|
+
.filter((o) => getLabel(o).toLowerCase().includes(q))
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const showAddHint = computed(() => {
|
|
66
|
+
if (!props.allowNew || !search.value) return false
|
|
67
|
+
if (filteredOptions.value.length > 0) return false
|
|
68
|
+
const trimmed = search.value.trim().toLowerCase()
|
|
69
|
+
return !selectedLabels.value.some((item) => item.label.toLowerCase() === trimmed)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const showSuggestions = computed(() => filteredOptions.value.length > 0 || showAddHint.value)
|
|
73
|
+
|
|
74
|
+
const selectOption = (option: string | { label: string; value: string | number }) => {
|
|
75
|
+
selected.value = [...selected.value, getValue(option)]
|
|
76
|
+
search.value = ''
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const handleEnter = () => {
|
|
80
|
+
if (filteredOptions.value.length === 1) {
|
|
81
|
+
selectOption(filteredOptions.value[0])
|
|
82
|
+
} else if (props.allowNew) {
|
|
83
|
+
addNewItem()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const addNewItem = () => {
|
|
88
|
+
const trimmed = search.value.trim()
|
|
89
|
+
if (!trimmed) return
|
|
90
|
+
const lower = trimmed.toLowerCase()
|
|
91
|
+
const alreadySelected = selectedLabels.value.some((item) => item.label.toLowerCase() === lower)
|
|
92
|
+
const existsAsOption = props.options.some((o) => getLabel(o).toLowerCase() === lower)
|
|
93
|
+
if (alreadySelected || existsAsOption) return
|
|
94
|
+
selected.value = [...selected.value, trimmed]
|
|
95
|
+
search.value = ''
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const removeSelected = (val: string | number) => {
|
|
99
|
+
selected.value = selected.value.filter((v) => v !== val)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const focus = () => {
|
|
103
|
+
inputFieldRef.value?.$el.querySelector('input')?.focus()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
defineExpose({ focus })
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<template>
|
|
110
|
+
<!-- Standalone (floating=true): input+chips are a static card, suggestions float below -->
|
|
111
|
+
<div v-if="floating" ref="wrapperRef" class="relative min-w-80 whitespace-nowrap">
|
|
112
|
+
<div class="rounded-lg border border-gray-200 bg-white">
|
|
113
|
+
<div class="p-2">
|
|
114
|
+
<NexxtInputField
|
|
115
|
+
ref="inputFieldRef"
|
|
116
|
+
v-model="search"
|
|
117
|
+
class="w-full"
|
|
118
|
+
:left-icon="icon"
|
|
119
|
+
:placeholder
|
|
120
|
+
:aria-label="placeholder"
|
|
121
|
+
@focus="isFocused = true"
|
|
122
|
+
@keydown.enter="handleEnter"
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
<Transition
|
|
126
|
+
enter-active-class="transition-all duration-200 ease-out overflow-hidden"
|
|
127
|
+
enter-from-class="max-h-0 opacity-0"
|
|
128
|
+
enter-to-class="max-h-40 opacity-100"
|
|
129
|
+
leave-active-class="transition-all duration-150 ease-in overflow-hidden"
|
|
130
|
+
leave-from-class="max-h-40 opacity-100"
|
|
131
|
+
leave-to-class="max-h-0 opacity-0"
|
|
132
|
+
>
|
|
133
|
+
<TransitionGroup
|
|
134
|
+
v-if="selectedLabels.length > 0"
|
|
135
|
+
tag="div"
|
|
136
|
+
class="flex flex-wrap gap-1 p-2 pt-0"
|
|
137
|
+
enter-active-class="transition-all duration-150 ease-out"
|
|
138
|
+
enter-from-class="opacity-0 scale-75"
|
|
139
|
+
enter-to-class="opacity-100 scale-100"
|
|
140
|
+
leave-active-class="transition-all duration-100 ease-in"
|
|
141
|
+
leave-from-class="opacity-100 scale-100"
|
|
142
|
+
leave-to-class="opacity-0 scale-75"
|
|
143
|
+
>
|
|
144
|
+
<button
|
|
145
|
+
v-for="item in selectedLabels"
|
|
146
|
+
:key="item.value"
|
|
147
|
+
class="origin-left cursor-pointer"
|
|
148
|
+
:aria-label="`Verwijder ${item.label}`"
|
|
149
|
+
@click="removeSelected(item.value)"
|
|
150
|
+
>
|
|
151
|
+
<NexxtChip :ghost="true" class="group" :variant="item.isCustom ? 'success' : 'default'">
|
|
152
|
+
{{ item.label }}
|
|
153
|
+
<template #close>
|
|
154
|
+
<span>
|
|
155
|
+
<NexxtIcon
|
|
156
|
+
name="xmark"
|
|
157
|
+
class="text-xs group-hover:text-brick-500"
|
|
158
|
+
aria-hidden="true"
|
|
159
|
+
/>
|
|
160
|
+
</span>
|
|
161
|
+
</template>
|
|
162
|
+
</NexxtChip>
|
|
163
|
+
</button>
|
|
164
|
+
</TransitionGroup>
|
|
165
|
+
</Transition>
|
|
166
|
+
</div>
|
|
167
|
+
<Transition
|
|
168
|
+
enter-active-class="transition-all duration-200 ease-out origin-top"
|
|
169
|
+
leave-active-class="transition-all duration-150 ease-in origin-top"
|
|
170
|
+
enter-from-class="opacity-0 scale-95 -translate-y-2"
|
|
171
|
+
leave-to-class="opacity-0 scale-95 -translate-y-2"
|
|
172
|
+
enter-to-class="opacity-100 scale-100 translate-y-0"
|
|
173
|
+
leave-from-class="opacity-100 scale-100 translate-y-0"
|
|
174
|
+
>
|
|
175
|
+
<div
|
|
176
|
+
v-if="isFocused && showSuggestions"
|
|
177
|
+
class="absolute top-full left-0 z-10 mt-1 w-full rounded-lg border border-gray-200 bg-white"
|
|
178
|
+
@mousedown.prevent
|
|
179
|
+
>
|
|
180
|
+
<ul
|
|
181
|
+
role="listbox"
|
|
182
|
+
aria-multiselectable="true"
|
|
183
|
+
tabindex="0"
|
|
184
|
+
class="max-h-56 overflow-y-auto [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-cornflower-blue-200 [&::-webkit-scrollbar-thumb:hover]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent"
|
|
185
|
+
>
|
|
186
|
+
<li
|
|
187
|
+
v-for="option in filteredOptions"
|
|
188
|
+
:key="getValue(option)"
|
|
189
|
+
role="option"
|
|
190
|
+
aria-selected="false"
|
|
191
|
+
class="flex w-full cursor-pointer items-center gap-2 px-6 py-3 text-left text-sm whitespace-nowrap text-gray-700 transition-colors duration-200 hover:bg-gray-100 hover:text-purple-600 focus:bg-gray-100 focus:outline-none"
|
|
192
|
+
@click="selectOption(option)"
|
|
193
|
+
>
|
|
194
|
+
{{ getLabel(option) }}
|
|
195
|
+
</li>
|
|
196
|
+
</ul>
|
|
197
|
+
<div
|
|
198
|
+
v-if="showAddHint"
|
|
199
|
+
class="flex grow-0 items-center gap-4 border-t border-gray-100 px-6 py-3 whitespace-normal text-gray-700"
|
|
200
|
+
@click="addNewItem()"
|
|
201
|
+
>
|
|
202
|
+
<NexxtIcon name="plus" class="cursor-pointer small-normal" aria-hidden="true" />
|
|
203
|
+
<div>
|
|
204
|
+
Druk op Enter om "<span class="small-semibold text-gray-800">{{ search }}</span
|
|
205
|
+
>" als kenmerk toe te voegen
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</Transition>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<!-- Inside Combobox (floating=false): everything in one flat card -->
|
|
213
|
+
<div v-else class="min-w-80 rounded-lg border border-gray-200 bg-white whitespace-nowrap">
|
|
214
|
+
<div class="p-2">
|
|
215
|
+
<NexxtInputField
|
|
216
|
+
ref="inputFieldRef"
|
|
217
|
+
v-model="search"
|
|
218
|
+
class="w-full"
|
|
219
|
+
:left-icon="icon"
|
|
220
|
+
:placeholder
|
|
221
|
+
@keydown.enter="handleEnter"
|
|
222
|
+
/>
|
|
223
|
+
</div>
|
|
224
|
+
<Transition
|
|
225
|
+
enter-active-class="transition-all duration-200 ease-out overflow-hidden"
|
|
226
|
+
enter-from-class="max-h-0 opacity-0"
|
|
227
|
+
enter-to-class="max-h-40 opacity-100"
|
|
228
|
+
leave-active-class="transition-all duration-150 ease-in overflow-hidden"
|
|
229
|
+
leave-from-class="max-h-40 opacity-100"
|
|
230
|
+
leave-to-class="max-h-0 opacity-0"
|
|
231
|
+
>
|
|
232
|
+
<TransitionGroup
|
|
233
|
+
v-if="selectedLabels.length > 0"
|
|
234
|
+
tag="div"
|
|
235
|
+
class="flex flex-wrap gap-1 border-b border-gray-200 p-2 pt-0"
|
|
236
|
+
enter-active-class="transition-all duration-150 ease-out"
|
|
237
|
+
enter-from-class="opacity-0 scale-75"
|
|
238
|
+
enter-to-class="opacity-100 scale-100"
|
|
239
|
+
leave-active-class="transition-all duration-100 ease-in"
|
|
240
|
+
leave-from-class="opacity-100 scale-100"
|
|
241
|
+
leave-to-class="opacity-0 scale-75"
|
|
242
|
+
>
|
|
243
|
+
<button
|
|
244
|
+
v-for="item in selectedLabels"
|
|
245
|
+
:key="item.value"
|
|
246
|
+
class="origin-left cursor-pointer"
|
|
247
|
+
:aria-label="`Verwijder ${item.label}`"
|
|
248
|
+
@click="removeSelected(item.value)"
|
|
249
|
+
>
|
|
250
|
+
<NexxtChip :ghost="true" class="group" :variant="item.isCustom ? 'success' : 'default'">
|
|
251
|
+
{{ item.label }}
|
|
252
|
+
<template #close>
|
|
253
|
+
<span>
|
|
254
|
+
<NexxtIcon
|
|
255
|
+
name="xmark"
|
|
256
|
+
class="text-xs group-hover:text-brick-500"
|
|
257
|
+
aria-hidden="true"
|
|
258
|
+
/>
|
|
259
|
+
</span>
|
|
260
|
+
</template>
|
|
261
|
+
</NexxtChip>
|
|
262
|
+
</button>
|
|
263
|
+
</TransitionGroup>
|
|
264
|
+
</Transition>
|
|
265
|
+
<ul
|
|
266
|
+
role="listbox"
|
|
267
|
+
aria-multiselectable="true"
|
|
268
|
+
class="max-h-56 overflow-y-auto [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-cornflower-blue-200 [&::-webkit-scrollbar-thumb:hover]:bg-gray-300 [&::-webkit-scrollbar-track]:bg-transparent"
|
|
269
|
+
>
|
|
270
|
+
<li
|
|
271
|
+
v-for="option in filteredOptions"
|
|
272
|
+
:key="getValue(option)"
|
|
273
|
+
role="option"
|
|
274
|
+
aria-selected="false"
|
|
275
|
+
class="flex w-full min-w-42 cursor-pointer items-center gap-2 px-6 py-3 text-left text-sm whitespace-nowrap text-gray-700 transition-colors duration-200 hover:bg-gray-100 hover:text-purple-600 focus:bg-gray-100 focus:outline-none"
|
|
276
|
+
@click="selectOption(option)"
|
|
277
|
+
>
|
|
278
|
+
{{ getLabel(option) }}
|
|
279
|
+
</li>
|
|
280
|
+
</ul>
|
|
281
|
+
<div
|
|
282
|
+
v-if="showAddHint"
|
|
283
|
+
class="flex grow-0 items-center gap-4 border-t border-gray-100 px-6 py-3 whitespace-normal text-gray-700"
|
|
284
|
+
@click="addNewItem()"
|
|
285
|
+
>
|
|
286
|
+
<NexxtIcon name="plus" class="cursor-pointer small-normal" aria-hidden="true" />
|
|
287
|
+
<div>
|
|
288
|
+
Druk op Enter om "<span class="small-semibold text-gray-800">{{ search }}</span
|
|
289
|
+
>" als kenmerk toe te voegen
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
</template>
|