pimelon-ui 0.1.23 → 0.1.31
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/package.json +2 -2
- package/src/components/Autocomplete.story.vue +92 -14
- package/src/components/Autocomplete.vue +214 -99
- package/src/components/Dialog.vue +1 -1
- package/src/components/ListView/ListHeader.vue +3 -0
- package/src/components/ListView/ListHeaderItem.vue +76 -5
- package/src/components/ListView/ListRow.vue +12 -8
- package/src/components/ListView/ListView.vue +2 -0
- package/src/components/ListView.story.md +2 -0
- package/src/components/ListView.story.vue +8 -4
- package/src/components/Progress.vue +7 -5
- package/src/resources/plugin.js +8 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pimelon-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"description": "A set of components and utilities for rapid UI development",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"autoprefixer": "^10.4.13",
|
|
68
68
|
"cross-fetch": "^3.1.5",
|
|
69
69
|
"histoire": "^0.16.2",
|
|
70
|
-
"husky": "^9.0.
|
|
70
|
+
"husky": "^9.0.11",
|
|
71
71
|
"lint-staged": ">=10",
|
|
72
72
|
"postcss": "^8.4.21",
|
|
73
73
|
"prettier": "2.7.1",
|
|
@@ -2,24 +2,102 @@
|
|
|
2
2
|
import { ref } from 'vue'
|
|
3
3
|
import Autocomplete from './Autocomplete.vue'
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const single = ref('')
|
|
6
|
+
const people = ref(null)
|
|
6
7
|
const options = [
|
|
7
|
-
{
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
{
|
|
8
|
+
{
|
|
9
|
+
label: 'John Doe',
|
|
10
|
+
value: 'john-doe',
|
|
11
|
+
image: 'https://randomuser.me/api/portraits/men/59.jpg',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
label: 'Jane Doe',
|
|
15
|
+
value: 'jane-doe',
|
|
16
|
+
image: 'https://randomuser.me/api/portraits/women/58.jpg',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
label: 'John Smith',
|
|
20
|
+
value: 'john-smith',
|
|
21
|
+
image: 'https://randomuser.me/api/portraits/men/59.jpg',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: 'Jane Smith',
|
|
25
|
+
value: 'jane-smith',
|
|
26
|
+
image: 'https://randomuser.me/api/portraits/women/59.jpg',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
label: 'John Wayne',
|
|
30
|
+
value: 'john-wayne',
|
|
31
|
+
image: 'https://randomuser.me/api/portraits/men/57.jpg',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
label: 'Jane Wayne',
|
|
35
|
+
value: 'jane-wayne',
|
|
36
|
+
image: 'https://randomuser.me/api/portraits/women/51.jpg',
|
|
37
|
+
},
|
|
13
38
|
]
|
|
14
39
|
</script>
|
|
15
40
|
<template>
|
|
16
41
|
<Story :layout="{ width: 500, type: 'grid' }" autoPropsDisabled>
|
|
17
|
-
<
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
42
|
+
<Variant title="Single option">
|
|
43
|
+
<div class="p-2">
|
|
44
|
+
<Autocomplete
|
|
45
|
+
:options="options"
|
|
46
|
+
v-model="single"
|
|
47
|
+
placeholder="Select person"
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
</Variant>
|
|
51
|
+
<Variant title="Single option with prefix slots">
|
|
52
|
+
<div class="p-2">
|
|
53
|
+
<Autocomplete
|
|
54
|
+
:options="options"
|
|
55
|
+
v-model="single"
|
|
56
|
+
placeholder="Select person"
|
|
57
|
+
>
|
|
58
|
+
<template #prefix>
|
|
59
|
+
<img
|
|
60
|
+
v-if="single"
|
|
61
|
+
:src="single.image"
|
|
62
|
+
class="mr-2 h-4 w-4 rounded-full"
|
|
63
|
+
/>
|
|
64
|
+
</template>
|
|
65
|
+
<template #item-prefix="{ option }">
|
|
66
|
+
<img :src="option.image" class="h-4 w-4 rounded-full" />
|
|
67
|
+
</template>
|
|
68
|
+
</Autocomplete>
|
|
69
|
+
</div>
|
|
70
|
+
</Variant>
|
|
71
|
+
<Variant title="Single option without search">
|
|
72
|
+
<div class="p-2">
|
|
73
|
+
<Autocomplete
|
|
74
|
+
:options="options"
|
|
75
|
+
v-model="single"
|
|
76
|
+
placeholder="Select person"
|
|
77
|
+
hide-search="true"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
</Variant>
|
|
81
|
+
<Variant title="Multiple options">
|
|
82
|
+
<div class="p-2">
|
|
83
|
+
<Autocomplete
|
|
84
|
+
:options="options"
|
|
85
|
+
v-model="people"
|
|
86
|
+
placeholder="Select people"
|
|
87
|
+
multiple="true"
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
</Variant>
|
|
91
|
+
<Variant title="Multiple options without search">
|
|
92
|
+
<div class="p-2">
|
|
93
|
+
<Autocomplete
|
|
94
|
+
:options="options"
|
|
95
|
+
v-model="people"
|
|
96
|
+
placeholder="Select people"
|
|
97
|
+
multiple="true"
|
|
98
|
+
hide-search="true"
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
</Variant>
|
|
24
102
|
</Story>
|
|
25
103
|
</template>
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<Combobox
|
|
2
|
+
<Combobox
|
|
3
|
+
v-model="selectedValue"
|
|
4
|
+
:multiple="multiple"
|
|
5
|
+
nullable
|
|
6
|
+
v-slot="{ open: isComboboxOpen }"
|
|
7
|
+
>
|
|
3
8
|
<Popover class="w-full" v-model:show="showOptions">
|
|
4
9
|
<template #target="{ open: openPopover, togglePopover }">
|
|
5
10
|
<slot name="target" v-bind="{ open: openPopover, togglePopover }">
|
|
@@ -9,15 +14,12 @@
|
|
|
9
14
|
:class="{ 'bg-gray-200': isComboboxOpen }"
|
|
10
15
|
@click="() => togglePopover()"
|
|
11
16
|
>
|
|
12
|
-
<div class="flex items-center">
|
|
17
|
+
<div class="flex items-center overflow-hidden">
|
|
13
18
|
<slot name="prefix" />
|
|
14
|
-
<span
|
|
15
|
-
class="overflow-hidden text-ellipsis whitespace-nowrap text-base leading-5"
|
|
16
|
-
v-if="selectedValue"
|
|
17
|
-
>
|
|
19
|
+
<span class="truncate text-base leading-5" v-if="selectedValue">
|
|
18
20
|
{{ displayValue(selectedValue) }}
|
|
19
21
|
</span>
|
|
20
|
-
<span class="text-base leading-5 text-gray-
|
|
22
|
+
<span class="text-base leading-5 text-gray-600" v-else>
|
|
21
23
|
{{ placeholder || '' }}
|
|
22
24
|
</span>
|
|
23
25
|
</div>
|
|
@@ -30,97 +32,161 @@
|
|
|
30
32
|
</div>
|
|
31
33
|
</slot>
|
|
32
34
|
</template>
|
|
33
|
-
<template #body="{ isOpen }">
|
|
35
|
+
<template #body="{ isOpen, togglePopover }">
|
|
34
36
|
<div v-show="isOpen">
|
|
35
|
-
<
|
|
36
|
-
class="mt-1
|
|
37
|
-
|
|
37
|
+
<div
|
|
38
|
+
class="relative mt-1 rounded-lg bg-white text-base shadow-2xl"
|
|
39
|
+
:class="bodyClasses"
|
|
38
40
|
>
|
|
39
|
-
<
|
|
40
|
-
class="
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
<ComboboxInput
|
|
44
|
-
ref="search"
|
|
45
|
-
class="form-input w-full"
|
|
46
|
-
type="text"
|
|
47
|
-
@change="
|
|
48
|
-
(e) => {
|
|
49
|
-
query = e.target.value
|
|
50
|
-
}
|
|
51
|
-
"
|
|
52
|
-
:value="query"
|
|
53
|
-
autocomplete="off"
|
|
54
|
-
placeholder="Search"
|
|
55
|
-
/>
|
|
56
|
-
<button
|
|
57
|
-
class="absolute right-0 inline-flex h-7 w-7 items-center justify-center"
|
|
58
|
-
@click="selectedValue = null"
|
|
59
|
-
>
|
|
60
|
-
<FeatherIcon name="x" class="w-4" />
|
|
61
|
-
</button>
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
<div
|
|
65
|
-
class="mt-1.5"
|
|
66
|
-
v-for="group in groups"
|
|
67
|
-
:key="group.key"
|
|
68
|
-
v-show="group.items.length > 0"
|
|
41
|
+
<ComboboxOptions
|
|
42
|
+
class="max-h-[15rem] overflow-y-auto px-1.5 pb-1.5"
|
|
43
|
+
:class="{ 'pt-1.5': hideSearch }"
|
|
44
|
+
static
|
|
69
45
|
>
|
|
70
46
|
<div
|
|
71
|
-
v-if="
|
|
72
|
-
class="
|
|
47
|
+
v-if="!hideSearch"
|
|
48
|
+
class="sticky top-0 z-10 flex items-stretch space-x-1.5 bg-white py-1.5"
|
|
73
49
|
>
|
|
74
|
-
|
|
50
|
+
<div class="relative w-full">
|
|
51
|
+
<ComboboxInput
|
|
52
|
+
ref="searchInput"
|
|
53
|
+
class="form-input w-full"
|
|
54
|
+
type="text"
|
|
55
|
+
@change="
|
|
56
|
+
(e) => {
|
|
57
|
+
query = e.target.value
|
|
58
|
+
}
|
|
59
|
+
"
|
|
60
|
+
:value="query"
|
|
61
|
+
autocomplete="off"
|
|
62
|
+
placeholder="Search"
|
|
63
|
+
/>
|
|
64
|
+
<button
|
|
65
|
+
class="absolute right-0 inline-flex h-7 w-7 items-center justify-center"
|
|
66
|
+
@click="selectedValue = null"
|
|
67
|
+
>
|
|
68
|
+
<FeatherIcon name="x" class="w-4" />
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
75
71
|
</div>
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
:value="option"
|
|
81
|
-
v-slot="{ active, selected }"
|
|
72
|
+
<div
|
|
73
|
+
v-for="group in groups"
|
|
74
|
+
:key="group.key"
|
|
75
|
+
v-show="group.items.length > 0"
|
|
82
76
|
>
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
77
|
+
<div
|
|
78
|
+
v-if="group.group && !group.hideLabel"
|
|
79
|
+
class="sticky top-10 truncate bg-white px-2.5 py-1.5 text-sm font-medium text-gray-600"
|
|
80
|
+
>
|
|
81
|
+
{{ group.group }}
|
|
82
|
+
</div>
|
|
83
|
+
<ComboboxOption
|
|
84
|
+
as="template"
|
|
85
|
+
v-for="(option, idx) in group.items.slice(0, 50)"
|
|
86
|
+
:key="option?.value || idx"
|
|
87
|
+
:value="option"
|
|
88
|
+
v-slot="{ active, selected }"
|
|
88
89
|
>
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
<li
|
|
91
|
+
:class="[
|
|
92
|
+
'flex cursor-pointer items-center justify-between rounded px-2.5 py-1.5 text-base',
|
|
93
|
+
{ 'bg-gray-100': active },
|
|
94
|
+
]"
|
|
95
|
+
>
|
|
96
|
+
<div class="flex flex-1 gap-2 overflow-hidden">
|
|
97
|
+
<div
|
|
98
|
+
v-if="$slots['item-prefix'] || $props.multiple"
|
|
99
|
+
class="flex-shrink-0"
|
|
100
|
+
>
|
|
101
|
+
<slot
|
|
102
|
+
name="item-prefix"
|
|
103
|
+
v-bind="{ active, selected, option }"
|
|
104
|
+
>
|
|
105
|
+
<FeatherIcon
|
|
106
|
+
name="check"
|
|
107
|
+
v-if="isOptionSelected(option)"
|
|
108
|
+
class="h-4 w-4 text-gray-700"
|
|
109
|
+
/>
|
|
110
|
+
<div v-else class="h-4 w-4" />
|
|
111
|
+
</slot>
|
|
112
|
+
</div>
|
|
113
|
+
<span class="flex-1 truncate">
|
|
114
|
+
{{ getLabel(option) }}
|
|
115
|
+
</span>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div
|
|
119
|
+
v-if="$slots['item-suffix'] || option?.description"
|
|
120
|
+
class="ml-2 flex-shrink-0"
|
|
121
|
+
>
|
|
122
|
+
<slot
|
|
123
|
+
name="item-suffix"
|
|
124
|
+
v-bind="{ active, selected, option }"
|
|
125
|
+
>
|
|
126
|
+
<div
|
|
127
|
+
v-if="option?.description"
|
|
128
|
+
class="text-sm text-gray-600"
|
|
129
|
+
>
|
|
130
|
+
{{ option.description }}
|
|
131
|
+
</div>
|
|
132
|
+
</slot>
|
|
133
|
+
</div>
|
|
134
|
+
</li>
|
|
135
|
+
</ComboboxOption>
|
|
136
|
+
</div>
|
|
137
|
+
<li
|
|
138
|
+
v-if="groups.length == 0"
|
|
139
|
+
class="rounded-md px-2.5 py-1.5 text-base text-gray-600"
|
|
140
|
+
>
|
|
141
|
+
No results found
|
|
142
|
+
</li>
|
|
143
|
+
</ComboboxOptions>
|
|
144
|
+
|
|
145
|
+
<div v-if="$slots.footer || multiple" class="border-t p-1">
|
|
146
|
+
<slot name="footer" v-bind="{ togglePopover }">
|
|
147
|
+
<div v-if="multiple" class="flex items-center justify-end">
|
|
148
|
+
<Button
|
|
149
|
+
v-if="!areAllOptionsSelected"
|
|
150
|
+
label="Select All"
|
|
151
|
+
@click.stop="selectAll"
|
|
92
152
|
/>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
153
|
+
<Button
|
|
154
|
+
v-if="areAllOptionsSelected"
|
|
155
|
+
label="Clear All"
|
|
156
|
+
@click.stop="clearAll"
|
|
157
|
+
/></div
|
|
158
|
+
></slot>
|
|
96
159
|
</div>
|
|
97
|
-
|
|
98
|
-
v-if="groups.length == 0"
|
|
99
|
-
class="rounded-md px-2.5 py-1.5 text-base text-gray-600"
|
|
100
|
-
>
|
|
101
|
-
No results found
|
|
102
|
-
</li>
|
|
103
|
-
</ComboboxOptions>
|
|
160
|
+
</div>
|
|
104
161
|
</div>
|
|
105
162
|
</template>
|
|
106
163
|
</Popover>
|
|
107
164
|
</Combobox>
|
|
108
165
|
</template>
|
|
166
|
+
|
|
109
167
|
<script>
|
|
110
168
|
import {
|
|
111
169
|
Combobox,
|
|
170
|
+
ComboboxButton,
|
|
112
171
|
ComboboxInput,
|
|
113
|
-
ComboboxOptions,
|
|
114
172
|
ComboboxOption,
|
|
115
|
-
|
|
173
|
+
ComboboxOptions,
|
|
116
174
|
} from '@headlessui/vue'
|
|
175
|
+
import { nextTick } from 'vue'
|
|
117
176
|
import Popover from './Popover.vue'
|
|
118
177
|
import Button from './Button.vue'
|
|
119
178
|
import FeatherIcon from './FeatherIcon.vue'
|
|
120
179
|
|
|
121
180
|
export default {
|
|
122
181
|
name: 'Autocomplete',
|
|
123
|
-
props: [
|
|
182
|
+
props: [
|
|
183
|
+
'modelValue',
|
|
184
|
+
'options',
|
|
185
|
+
'placeholder',
|
|
186
|
+
'bodyClasses',
|
|
187
|
+
'multiple',
|
|
188
|
+
'hideSearch',
|
|
189
|
+
],
|
|
124
190
|
emits: ['update:modelValue', 'update:query', 'change'],
|
|
125
191
|
components: {
|
|
126
192
|
Popover,
|
|
@@ -132,6 +198,7 @@ export default {
|
|
|
132
198
|
ComboboxOption,
|
|
133
199
|
ComboboxButton,
|
|
134
200
|
},
|
|
201
|
+
expose: ['togglePopover'],
|
|
135
202
|
data() {
|
|
136
203
|
return {
|
|
137
204
|
query: '',
|
|
@@ -139,19 +206,25 @@ export default {
|
|
|
139
206
|
}
|
|
140
207
|
},
|
|
141
208
|
computed: {
|
|
142
|
-
valuePropPassed() {
|
|
143
|
-
return 'value' in this.$attrs
|
|
144
|
-
},
|
|
145
209
|
selectedValue: {
|
|
146
210
|
get() {
|
|
147
|
-
|
|
211
|
+
if (!this.multiple) {
|
|
212
|
+
return this.findOption(this.modelValue)
|
|
213
|
+
}
|
|
214
|
+
// in case of `multiple`, modelValue is an array of values
|
|
215
|
+
// if the modelValue is a list of values, convert them to options
|
|
216
|
+
return isOptionOrValue(this.modelValue?.[0]) === 'value'
|
|
217
|
+
? this.modelValue?.map((v) => this.findOption(v))
|
|
218
|
+
: this.modelValue
|
|
148
219
|
},
|
|
149
220
|
set(val) {
|
|
150
221
|
this.query = ''
|
|
151
|
-
if (val)
|
|
152
|
-
|
|
222
|
+
if (val && !this.multiple) this.showOptions = false
|
|
223
|
+
if (!this.multiple) {
|
|
224
|
+
this.$emit('update:modelValue', val)
|
|
225
|
+
return
|
|
153
226
|
}
|
|
154
|
-
this.$emit(
|
|
227
|
+
this.$emit('update:modelValue', val)
|
|
155
228
|
},
|
|
156
229
|
},
|
|
157
230
|
groups() {
|
|
@@ -159,7 +232,7 @@ export default {
|
|
|
159
232
|
|
|
160
233
|
let groups = this.options[0]?.group
|
|
161
234
|
? this.options
|
|
162
|
-
: [{ group: '', items: this.options }]
|
|
235
|
+
: [{ group: '', items: this.sanitizeOptions(this.options) }]
|
|
163
236
|
|
|
164
237
|
return groups
|
|
165
238
|
.map((group, i) => {
|
|
@@ -167,47 +240,89 @@ export default {
|
|
|
167
240
|
key: i,
|
|
168
241
|
group: group.group,
|
|
169
242
|
hideLabel: group.hideLabel || false,
|
|
170
|
-
items: this.filterOptions(group.items),
|
|
243
|
+
items: this.filterOptions(this.sanitizeOptions(group.items)),
|
|
171
244
|
}
|
|
172
245
|
})
|
|
173
246
|
.filter((group) => group.items.length > 0)
|
|
174
247
|
},
|
|
248
|
+
allOptions() {
|
|
249
|
+
return this.groups.flatMap((group) => group.items)
|
|
250
|
+
},
|
|
251
|
+
areAllOptionsSelected() {
|
|
252
|
+
if (!this.multiple) return false
|
|
253
|
+
return this.allOptions.length === this.selectedValue?.length
|
|
254
|
+
},
|
|
175
255
|
},
|
|
176
256
|
watch: {
|
|
177
257
|
query(q) {
|
|
178
258
|
this.$emit('update:query', q)
|
|
179
259
|
},
|
|
180
260
|
showOptions(val) {
|
|
181
|
-
if (val)
|
|
182
|
-
this.$nextTick(() => {
|
|
183
|
-
this.$refs.search.el.focus()
|
|
184
|
-
})
|
|
185
|
-
}
|
|
261
|
+
if (val) nextTick(() => this.$refs.searchInput?.$el?.focus())
|
|
186
262
|
},
|
|
187
263
|
},
|
|
188
264
|
methods: {
|
|
265
|
+
togglePopover(val) {
|
|
266
|
+
this.showOptions = val ?? !this.showOptions
|
|
267
|
+
},
|
|
268
|
+
findOption(option) {
|
|
269
|
+
if (!option) return option
|
|
270
|
+
const value = isOptionOrValue(option) === 'value' ? option : option.value
|
|
271
|
+
return this.allOptions.find((o) => o.value === value)
|
|
272
|
+
},
|
|
189
273
|
filterOptions(options) {
|
|
190
|
-
if (!this.query)
|
|
191
|
-
return options
|
|
192
|
-
}
|
|
274
|
+
if (!this.query) return options
|
|
193
275
|
return options.filter((option) => {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
(
|
|
197
|
-
.toString()
|
|
198
|
-
.toLowerCase()
|
|
199
|
-
.includes(this.query.toLowerCase())
|
|
276
|
+
return (
|
|
277
|
+
option.label.toLowerCase().includes(this.query.toLowerCase()) ||
|
|
278
|
+
option.value.toLowerCase().includes(this.query.toLowerCase())
|
|
200
279
|
)
|
|
201
280
|
})
|
|
202
281
|
},
|
|
203
282
|
displayValue(option) {
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return
|
|
283
|
+
if (!option) return ''
|
|
284
|
+
|
|
285
|
+
if (!this.multiple) {
|
|
286
|
+
return this.getLabel(this.findOption(option))
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!Array.isArray(option)) return ''
|
|
290
|
+
|
|
291
|
+
// in case of `multiple`, option is an array of values
|
|
292
|
+
// so the display value should be comma separated labels
|
|
293
|
+
return option.map((v) => this.getLabel(this.findOption(v))).join(', ')
|
|
294
|
+
},
|
|
295
|
+
getLabel(option) {
|
|
296
|
+
if (isOptionOrValue(option) === 'value') return option
|
|
297
|
+
return option?.label || option?.value || 'No label'
|
|
298
|
+
},
|
|
299
|
+
sanitizeOptions(options) {
|
|
300
|
+
if (!options) return []
|
|
301
|
+
// in case the options are just values, convert them to objects
|
|
302
|
+
return options.map((option) => {
|
|
303
|
+
return isOptionOrValue(option) === 'option'
|
|
304
|
+
? option
|
|
305
|
+
: { label: option, value: option }
|
|
306
|
+
})
|
|
307
|
+
},
|
|
308
|
+
isOptionSelected(option) {
|
|
309
|
+
if (!this.selectedValue) return false
|
|
310
|
+
const value = isOptionOrValue(option) === 'value' ? option : option.value
|
|
311
|
+
if (!this.multiple) {
|
|
312
|
+
return this.selectedValue?.value === value
|
|
208
313
|
}
|
|
209
|
-
return
|
|
314
|
+
return this.selectedValue?.find((v) => v && v.value === value)
|
|
315
|
+
},
|
|
316
|
+
selectAll() {
|
|
317
|
+
this.selectedValue = this.allOptions
|
|
318
|
+
},
|
|
319
|
+
clearAll() {
|
|
320
|
+
this.selectedValue = []
|
|
210
321
|
},
|
|
211
322
|
},
|
|
212
323
|
}
|
|
324
|
+
|
|
325
|
+
function isOptionOrValue(optionOrValue) {
|
|
326
|
+
return typeof optionOrValue === 'object' ? 'option' : 'value'
|
|
327
|
+
}
|
|
213
328
|
</script>
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
<slot name="body-main">
|
|
54
54
|
<div class="bg-white px-4 pb-6 pt-5 sm:px-6">
|
|
55
55
|
<div class="flex">
|
|
56
|
-
<div class="flex-1">
|
|
56
|
+
<div class="w-full flex-1">
|
|
57
57
|
<div class="mb-6 flex items-center justify-between">
|
|
58
58
|
<div class="flex items-center space-x-2">
|
|
59
59
|
<div
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
v-for="column in list.columns"
|
|
20
20
|
:key="column.key"
|
|
21
21
|
:item="column"
|
|
22
|
+
@columnWidthUpdated="emit('columnWidthUpdated', column)"
|
|
22
23
|
/>
|
|
23
24
|
</slot>
|
|
24
25
|
</div>
|
|
@@ -30,5 +31,7 @@ import ListHeaderItem from './ListHeaderItem.vue'
|
|
|
30
31
|
import { getGridTemplateColumns } from './utils'
|
|
31
32
|
import { inject } from 'vue'
|
|
32
33
|
|
|
34
|
+
const emit = defineEmits(['columnWidthUpdated'])
|
|
35
|
+
|
|
33
36
|
const list = inject('list')
|
|
34
37
|
</script>
|
|
@@ -1,22 +1,93 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
-
|
|
3
|
+
ref="columnRef"
|
|
4
|
+
class="group flex items-center justify-between"
|
|
4
5
|
:class="alignmentMap[item.align]"
|
|
5
6
|
>
|
|
6
|
-
<
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
<div
|
|
8
|
+
class="flex items-center space-x-2 truncate text-sm text-gray-600"
|
|
9
|
+
:class="$attrs.class"
|
|
10
|
+
>
|
|
11
|
+
<slot name="prefix" v-bind="{ item }" />
|
|
12
|
+
<div class="truncate">
|
|
13
|
+
{{ item.label }}
|
|
14
|
+
</div>
|
|
15
|
+
<slot name="suffix" v-bind="{ item }" />
|
|
9
16
|
</div>
|
|
10
|
-
<slot name="
|
|
17
|
+
<slot v-if="list.options.resizeColumn" name="resizer" v-bind="{ item }">
|
|
18
|
+
<div
|
|
19
|
+
class="flex h-4 w-2 cursor-col-resize justify-center"
|
|
20
|
+
@mousedown="startResizing"
|
|
21
|
+
>
|
|
22
|
+
<div
|
|
23
|
+
ref="resizer"
|
|
24
|
+
class="h-full w-[2px] rounded-full transition-all duration-300 ease-in-out group-hover:bg-gray-400"
|
|
25
|
+
/>
|
|
26
|
+
</div>
|
|
27
|
+
</slot>
|
|
11
28
|
</div>
|
|
12
29
|
</template>
|
|
13
30
|
|
|
14
31
|
<script setup>
|
|
15
32
|
import { alignmentMap } from './utils'
|
|
33
|
+
import { useDebounceFn } from '@vueuse/core'
|
|
34
|
+
import { ref, computed, inject } from 'vue'
|
|
35
|
+
|
|
16
36
|
const props = defineProps({
|
|
17
37
|
item: {
|
|
18
38
|
type: Object,
|
|
19
39
|
required: true,
|
|
20
40
|
},
|
|
41
|
+
debounce: {
|
|
42
|
+
type: Number,
|
|
43
|
+
default: 1000,
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const emit = defineEmits(['columnWidthUpdated'])
|
|
48
|
+
|
|
49
|
+
const resizer = ref(null)
|
|
50
|
+
const columnRef = ref(null)
|
|
51
|
+
|
|
52
|
+
const widthInPx = computed(() => {
|
|
53
|
+
if (typeof props.item.width === 'string') {
|
|
54
|
+
const parsedWidth = parseInt(props.item.width)
|
|
55
|
+
if (props.item.width.includes('rem')) {
|
|
56
|
+
return parsedWidth * 16
|
|
57
|
+
} else if (props.item.width.includes('px')) {
|
|
58
|
+
return parsedWidth
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return columnRef.value.offsetWidth
|
|
21
62
|
})
|
|
63
|
+
|
|
64
|
+
const startResizing = (e) => {
|
|
65
|
+
const initialX = e.clientX
|
|
66
|
+
const initialWidth = widthInPx.value
|
|
67
|
+
const onMouseMove = (e) => {
|
|
68
|
+
document.body.classList.add('select-none')
|
|
69
|
+
document.body.classList.add('cursor-col-resize')
|
|
70
|
+
resizer.value.style.backgroundColor = 'rgb(199 199 199)'
|
|
71
|
+
let newWidth = initialWidth + (e.clientX - initialX)
|
|
72
|
+
|
|
73
|
+
props.item.width = `${newWidth < 50 ? 50 : newWidth}px`
|
|
74
|
+
updateWidth(props.item.width)
|
|
75
|
+
}
|
|
76
|
+
const onMouseUp = () => {
|
|
77
|
+
document.body.classList.remove('select-none')
|
|
78
|
+
document.body.classList.remove('cursor-col-resize')
|
|
79
|
+
resizer.value.style.backgroundColor = ''
|
|
80
|
+
window.removeEventListener('mousemove', onMouseMove)
|
|
81
|
+
window.removeEventListener('mouseup', onMouseUp)
|
|
82
|
+
}
|
|
83
|
+
window.addEventListener('mousemove', onMouseMove)
|
|
84
|
+
window.addEventListener('mouseup', onMouseUp)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const updateWidth = useDebounceFn((width) => {
|
|
88
|
+
props.item.width = width
|
|
89
|
+
emit('columnWidthUpdated')
|
|
90
|
+
}, props.debounce)
|
|
91
|
+
|
|
92
|
+
const list = inject('list')
|
|
22
93
|
</script>
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
<component
|
|
3
3
|
:is="list.options.getRowRoute ? 'router-link' : 'div'"
|
|
4
4
|
class="flex cursor-pointer flex-col transition-all duration-300 ease-in-out"
|
|
5
|
-
v-bind="
|
|
6
|
-
list.options.getRowRoute
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
v-bind="{
|
|
6
|
+
to: list.options.getRowRoute ? list.options.getRowRoute(row) : undefined,
|
|
7
|
+
onClick: list.options.onRowClick
|
|
8
|
+
? () => list.options.onRowClick(row)
|
|
9
|
+
: undefined,
|
|
10
|
+
}"
|
|
10
11
|
>
|
|
11
12
|
<component
|
|
12
13
|
:is="list.options.getRowRoute ? 'template' : 'button'"
|
|
@@ -33,11 +34,14 @@
|
|
|
33
34
|
class="cursor-pointer duration-300"
|
|
34
35
|
/>
|
|
35
36
|
<div
|
|
36
|
-
v-for="column in list.columns"
|
|
37
|
+
v-for="(column, i) in list.columns"
|
|
37
38
|
:key="column.key"
|
|
38
|
-
:class="
|
|
39
|
+
:class="[
|
|
40
|
+
alignmentMap[column.align],
|
|
41
|
+
i == 0 ? 'text-gray-900' : 'text-gray-700',
|
|
42
|
+
]"
|
|
39
43
|
>
|
|
40
|
-
<slot v-bind="{ column, item: row[column.key] }">
|
|
44
|
+
<slot v-bind="{ idx: i, column, item: row[column.key] }">
|
|
41
45
|
<ListRowItem :item="row[column.key]" :align="column.align" />
|
|
42
46
|
</slot>
|
|
43
47
|
</div>
|
|
@@ -42,6 +42,7 @@ const props = defineProps({
|
|
|
42
42
|
onRowClick: null,
|
|
43
43
|
showTooltip: true,
|
|
44
44
|
selectable: true,
|
|
45
|
+
resizeColumn: true,
|
|
45
46
|
},
|
|
46
47
|
},
|
|
47
48
|
})
|
|
@@ -64,6 +65,7 @@ let _options = computed(() => {
|
|
|
64
65
|
onRowClick: props.options.onRowClick || null,
|
|
65
66
|
showTooltip: defaultTrue(props.options.showTooltip),
|
|
66
67
|
selectable: defaultTrue(props.options.selectable),
|
|
68
|
+
resizeColumn: defaultTrue(props.options.resizeColumn),
|
|
67
69
|
}
|
|
68
70
|
})
|
|
69
71
|
|
|
@@ -77,6 +77,8 @@ required to be passed in the `row` object.
|
|
|
77
77
|
select/multiselect rows and perform some action on them - default is true
|
|
78
78
|
4. showTooltip (Boolean) - if true, tooltip will be shown on hover of row -
|
|
79
79
|
default is true
|
|
80
|
+
5. resizeColumn (Boolean) - if true, column can be resized by dragging the
|
|
81
|
+
resizer on the right side of the column header - default is true
|
|
80
82
|
|
|
81
83
|
---
|
|
82
84
|
|
|
@@ -15,9 +15,10 @@ import { reactive } from 'vue'
|
|
|
15
15
|
const state = reactive({
|
|
16
16
|
selectable: true,
|
|
17
17
|
showTooltip: true,
|
|
18
|
+
resizeColumn: true,
|
|
18
19
|
})
|
|
19
20
|
|
|
20
|
-
const simple_columns = [
|
|
21
|
+
const simple_columns = reactive([
|
|
21
22
|
{
|
|
22
23
|
label: 'Name',
|
|
23
24
|
key: 'name',
|
|
@@ -36,7 +37,7 @@ const simple_columns = [
|
|
|
36
37
|
label: 'Status',
|
|
37
38
|
key: 'status',
|
|
38
39
|
},
|
|
39
|
-
]
|
|
40
|
+
])
|
|
40
41
|
|
|
41
42
|
const simple_rows = [
|
|
42
43
|
{
|
|
@@ -55,7 +56,7 @@ const simple_rows = [
|
|
|
55
56
|
},
|
|
56
57
|
]
|
|
57
58
|
|
|
58
|
-
const custom_columns = [
|
|
59
|
+
const custom_columns = reactive([
|
|
59
60
|
{
|
|
60
61
|
label: 'Name',
|
|
61
62
|
key: 'name',
|
|
@@ -78,7 +79,7 @@ const custom_columns = [
|
|
|
78
79
|
key: 'status',
|
|
79
80
|
icon: 'check-circle',
|
|
80
81
|
},
|
|
81
|
-
]
|
|
82
|
+
])
|
|
82
83
|
|
|
83
84
|
const custom_rows = [
|
|
84
85
|
{
|
|
@@ -127,6 +128,7 @@ const custom_rows = [
|
|
|
127
128
|
getRowRoute: (row) => ({ name: 'User', params: { userId: row.id } }),
|
|
128
129
|
selectable: state.selectable,
|
|
129
130
|
showTooltip: state.showTooltip,
|
|
131
|
+
resizeColumn: state.resizeColumn,
|
|
130
132
|
}"
|
|
131
133
|
row-key="id"
|
|
132
134
|
/>
|
|
@@ -140,6 +142,7 @@ const custom_rows = [
|
|
|
140
142
|
onRowClick: (row) => console.log(row),
|
|
141
143
|
selectable: state.selectable,
|
|
142
144
|
showTooltip: state.showTooltip,
|
|
145
|
+
resizeColumn: state.resizeColumn,
|
|
143
146
|
}"
|
|
144
147
|
row-key="id"
|
|
145
148
|
>
|
|
@@ -203,6 +206,7 @@ const custom_rows = [
|
|
|
203
206
|
<template #controls>
|
|
204
207
|
<HstCheckbox v-model="state.selectable" title="Selectable" />
|
|
205
208
|
<HstCheckbox v-model="state.showTooltip" title="Show tooltip" />
|
|
209
|
+
<HstCheckbox v-model="state.resizeColumn" title="Resize Column" />
|
|
206
210
|
</template>
|
|
207
211
|
</Story>
|
|
208
212
|
</template>
|
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
<!-- Empty for alignment -->
|
|
11
11
|
<span v-else></span>
|
|
12
12
|
|
|
13
|
-
<span
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
<span class="self-end" v-if="props.hint || $slots.hint">
|
|
14
|
+
<slot name="hint">
|
|
15
|
+
<span class="text-base font-medium text-gray-500">
|
|
16
|
+
{{ props.value }}%
|
|
17
|
+
</span>
|
|
18
|
+
</slot>
|
|
19
|
+
</span>
|
|
18
20
|
</div>
|
|
19
21
|
|
|
20
22
|
<div
|
package/src/resources/plugin.js
CHANGED
|
@@ -16,14 +16,18 @@ let createMixin = (mixinOptions) => ({
|
|
|
16
16
|
if (typeof options == 'function') {
|
|
17
17
|
watch(
|
|
18
18
|
() => {
|
|
19
|
+
let out = null
|
|
19
20
|
try {
|
|
20
|
-
|
|
21
|
+
out = options.call(this)
|
|
21
22
|
} catch (error) {
|
|
22
23
|
console.warn('Failed to get resource options\n\n', error)
|
|
23
|
-
|
|
24
|
+
out = null
|
|
24
25
|
}
|
|
26
|
+
return JSON.stringify(out)
|
|
25
27
|
},
|
|
26
|
-
(
|
|
28
|
+
(_options, _oldOptions) => {
|
|
29
|
+
let options = _options ? JSON.parse(_options) : null
|
|
30
|
+
let oldOptions = _oldOptions ? JSON.parse(_oldOptions) : null
|
|
27
31
|
if (!options) {
|
|
28
32
|
return
|
|
29
33
|
}
|
|
@@ -37,6 +41,7 @@ let createMixin = (mixinOptions) => ({
|
|
|
37
41
|
},
|
|
38
42
|
{
|
|
39
43
|
immediate: true,
|
|
44
|
+
deep: true,
|
|
40
45
|
}
|
|
41
46
|
)
|
|
42
47
|
} else {
|