@veritree/ui 0.0.1 → 0.1.3
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/index.js +54 -8
- package/package.json +1 -1
- package/src/{vtAccordion/vtAccordion.vue → Accordion/VTAccordion.vue} +1 -1
- package/src/{vtAccordion/vtAccordionButton.vue → Accordion/VTAccordionButton.vue} +1 -1
- package/src/{vtAccordion/vtAccordionGroup.vue → Accordion/VTAccordionGroup.vue} +1 -1
- package/src/{vtAccordion/vtAccordionPanel.vue → Accordion/VTAccordionPanel.vue} +1 -3
- package/src/Alerts/VTAlert.vue +72 -0
- package/src/Button/VTButton.vue +86 -0
- package/src/Button/VTButtonSave.vue +27 -0
- package/src/Input/VTInput.vue +82 -0
- package/src/Input/VTInputDate.vue +36 -0
- package/src/Input/VTInputFile.vue +60 -0
- package/src/Input/VTInputUpload.vue +54 -0
- package/src/Listbox/VTListbox.vue +168 -0
- package/src/Listbox/VTListboxButton.vue +82 -0
- package/src/Listbox/VTListboxOption.vue +121 -0
- package/src/Listbox/VTListboxOptions.vue +120 -0
- package/src/Modal/VTModal.vue +69 -0
- package/src/Spinner/VTSpinner.vue +14 -0
- package/src/Tabs/VTTab.vue +126 -0
- package/src/Tabs/VTTabGroup.vue +100 -0
- package/src/Tabs/VTTabList.vue +11 -0
- package/src/Tabs/VTTabPanel.vue +51 -0
- package/src/Tabs/VTTabPanels.vue +11 -0
- package/src/Textarea/VTTextarea.vue +77 -0
- package/src/utils/ids.js +34 -0
- package/src/utils/objects.js +139 -0
package/index.js
CHANGED
|
@@ -1,12 +1,58 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
|
|
1
|
+
import VTAlert from './src/Alerts/VTAlert.vue';
|
|
2
|
+
|
|
3
|
+
// import VTSpinner from './src/Spinner/VTSpinner.vue';
|
|
4
|
+
|
|
5
|
+
import VTButton from './src/Button/VTButton.vue';
|
|
6
|
+
// import VTButtonSave from './src/Button/VTButtonSave.vue';
|
|
7
|
+
|
|
8
|
+
import VTInput from './src/Input/VTInput.vue';
|
|
9
|
+
import VTInputDate from './src/Input/VTInputDate.vue';
|
|
10
|
+
import VTInputFile from './src/Input/VTInputFile.vue';
|
|
11
|
+
import VTInputUpload from './src/Input/VTInputUpload.vue';
|
|
12
|
+
|
|
13
|
+
import VTTextarea from './src/Textarea/VTTextarea.vue';
|
|
14
|
+
|
|
15
|
+
// import VTListbox from './src/Listbox/VTListbox.vue';
|
|
16
|
+
// import VTListboxButton from './src/Listbox/VTListboxButton.vue';
|
|
17
|
+
// import VTListboxOption from './src/Listbox/VTListboxOption.vue';
|
|
18
|
+
// import VTListboxOptions from './src/Listbox/VTListboxOptions.vue';
|
|
19
|
+
|
|
20
|
+
import VTModal from './src/Modal/VTModal.vue';
|
|
21
|
+
|
|
22
|
+
import VTAccordion from './src/Accordion/VTAccordion.vue';
|
|
23
|
+
import VTAccordionButton from './src/Accordion/VTAccordionButton.vue';
|
|
24
|
+
import VTAccordionGroup from './src/Accordion/VTAccordionGroup.vue';
|
|
25
|
+
import VTAccordionPanel from './src/Accordion/VTAccordionPanel.vue';
|
|
26
|
+
|
|
27
|
+
import VTTab from './src/Tabs/VTTab.vue';
|
|
28
|
+
import VTTabGroup from './src/Tabs/VTTabGroup.vue';
|
|
29
|
+
import VTTabList from './src/Tabs/VTTabList.vue';
|
|
30
|
+
import VTTabPanel from './src/Tabs/VTTabPanel.vue';
|
|
31
|
+
import VTTabPanels from './src/Tabs/VTTabPanels.vue';
|
|
5
32
|
|
|
6
33
|
export {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
34
|
+
VTAlert,
|
|
35
|
+
// VTSpinner,
|
|
36
|
+
VTButton,
|
|
37
|
+
// VTButtonSave,
|
|
38
|
+
VTInput,
|
|
39
|
+
VTInputDate,
|
|
40
|
+
VTInputFile,
|
|
41
|
+
VTInputUpload,
|
|
42
|
+
VTTextarea,
|
|
43
|
+
// VTListbox,
|
|
44
|
+
// VTListboxButton,
|
|
45
|
+
// VTListboxOption,
|
|
46
|
+
// VTListboxOptions,
|
|
47
|
+
VTModal,
|
|
48
|
+
VTAccordion,
|
|
49
|
+
VTAccordionButton,
|
|
50
|
+
VTAccordionGroup,
|
|
51
|
+
VTAccordionPanel,
|
|
52
|
+
VTTab,
|
|
53
|
+
VTTabGroup,
|
|
54
|
+
VTTabList,
|
|
55
|
+
VTTabPanel,
|
|
56
|
+
VTTabPanels,
|
|
11
57
|
}
|
|
12
58
|
|
package/package.json
CHANGED
|
@@ -22,7 +22,7 @@ import IconChevronDown from '../icons/IconChevronDown.vue';
|
|
|
22
22
|
import IconChevronUp from '../icons/IconChevronUp.vue';
|
|
23
23
|
|
|
24
24
|
export default {
|
|
25
|
-
name: '
|
|
25
|
+
name: 'VTAccordionButton',
|
|
26
26
|
components: { IconChevronDown, IconChevronUp },
|
|
27
27
|
inject: ['api'],
|
|
28
28
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="isVisible" class="Alert" :class="classes" role="alert">
|
|
3
|
+
<slot></slot>
|
|
4
|
+
<button v-if="dismissable" class="Alert-close" @click="dismiss">
|
|
5
|
+
<IconClose />
|
|
6
|
+
</button>
|
|
7
|
+
</div>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script>
|
|
11
|
+
import { IconClose } from '@veritree/icons';
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
name: 'VTAlert',
|
|
15
|
+
|
|
16
|
+
components: { IconClose },
|
|
17
|
+
|
|
18
|
+
model: {
|
|
19
|
+
prop: 'value',
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
props: {
|
|
23
|
+
dismissable: {
|
|
24
|
+
type: Boolean,
|
|
25
|
+
default: false,
|
|
26
|
+
},
|
|
27
|
+
variant: {
|
|
28
|
+
type: String,
|
|
29
|
+
default: '',
|
|
30
|
+
validate(value) {
|
|
31
|
+
if (value === '') return true;
|
|
32
|
+
return ['success', 'warning', 'error'].includes(value);
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
value: {
|
|
36
|
+
type: Boolean,
|
|
37
|
+
default: false,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
data() {
|
|
42
|
+
return {
|
|
43
|
+
destroy: false,
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
computed: {
|
|
48
|
+
classes() {
|
|
49
|
+
return {
|
|
50
|
+
[`Alert--${this.variant}`]: this.variant,
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
isVisible() {
|
|
55
|
+
return this.value && !this.destroy;
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
watch: {
|
|
60
|
+
isVisible(newVal) {
|
|
61
|
+
if (newVal === false) this.destroy = false;
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
methods: {
|
|
66
|
+
dismiss() {
|
|
67
|
+
this.destroy = true;
|
|
68
|
+
this.$emit('input', false);
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
</script>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component
|
|
3
|
+
:is="tag"
|
|
4
|
+
:class="classes"
|
|
5
|
+
:data-theme="theme"
|
|
6
|
+
:to="to"
|
|
7
|
+
class="Button"
|
|
8
|
+
type="button"
|
|
9
|
+
v-on="$listeners"
|
|
10
|
+
>
|
|
11
|
+
<slot></slot>
|
|
12
|
+
</component>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script>
|
|
16
|
+
export default {
|
|
17
|
+
name: 'VTButton',
|
|
18
|
+
|
|
19
|
+
props: {
|
|
20
|
+
variant: {
|
|
21
|
+
type: String,
|
|
22
|
+
default: 'primary',
|
|
23
|
+
validator(value) {
|
|
24
|
+
return [
|
|
25
|
+
'primary',
|
|
26
|
+
'secondary',
|
|
27
|
+
'tertiary',
|
|
28
|
+
'danger',
|
|
29
|
+
'custom',
|
|
30
|
+
'icon',
|
|
31
|
+
].includes(value);
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
size: {
|
|
35
|
+
type: String,
|
|
36
|
+
default: 'large',
|
|
37
|
+
validator(value) {
|
|
38
|
+
return ['large', 'small'].includes(value);
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
pill: {
|
|
42
|
+
type: Boolean,
|
|
43
|
+
default: false,
|
|
44
|
+
},
|
|
45
|
+
theme: {
|
|
46
|
+
type: String,
|
|
47
|
+
default: null,
|
|
48
|
+
validator(value) {
|
|
49
|
+
return ['dark'].includes(value);
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
to: {
|
|
53
|
+
type: [String, Object],
|
|
54
|
+
default: null,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
computed: {
|
|
59
|
+
classes() {
|
|
60
|
+
const classes = {};
|
|
61
|
+
|
|
62
|
+
if (this.variant) {
|
|
63
|
+
classes[`Button--${this.variant}`] = true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (this.size) {
|
|
67
|
+
classes[`Button--${this.size}`] = true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (this.pill) {
|
|
71
|
+
classes['Button--pill'] = true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return classes;
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
tag() {
|
|
78
|
+
if (this.to) {
|
|
79
|
+
return 'NuxtLink';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return 'button';
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<VTButton
|
|
3
|
+
class="relative"
|
|
4
|
+
:disabled="saving"
|
|
5
|
+
v-bind="$attrs"
|
|
6
|
+
v-on="$listeners"
|
|
7
|
+
>
|
|
8
|
+
<VTSpinner v-if="saving" class="absolute m-auto" />
|
|
9
|
+
<span :class="{ invisible: saving }"><slot></slot></span>
|
|
10
|
+
</VTButton>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script>
|
|
14
|
+
import VTButton from './VTButton.vue';
|
|
15
|
+
import VTSpinner from '../Spinner/VTSpinner.vue';
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
components: { VTButton, VTSpinner },
|
|
19
|
+
|
|
20
|
+
props: {
|
|
21
|
+
saving: {
|
|
22
|
+
type: Boolean,
|
|
23
|
+
default: false,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
</script>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<input
|
|
3
|
+
:class="classes"
|
|
4
|
+
class="form-control"
|
|
5
|
+
:data-theme="theme"
|
|
6
|
+
:type="type"
|
|
7
|
+
:value="value"
|
|
8
|
+
v-on="listeners"
|
|
9
|
+
/>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script>
|
|
13
|
+
export default {
|
|
14
|
+
name: 'VTInput',
|
|
15
|
+
|
|
16
|
+
props: {
|
|
17
|
+
lazy: {
|
|
18
|
+
type: Boolean,
|
|
19
|
+
default: false,
|
|
20
|
+
},
|
|
21
|
+
type: {
|
|
22
|
+
type: String,
|
|
23
|
+
default: 'text',
|
|
24
|
+
},
|
|
25
|
+
theme: {
|
|
26
|
+
type: String,
|
|
27
|
+
default: null,
|
|
28
|
+
validator(value) {
|
|
29
|
+
return ['dark'].includes(value);
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
variant: {
|
|
33
|
+
type: [String, Object],
|
|
34
|
+
default: '',
|
|
35
|
+
validator(value) {
|
|
36
|
+
if (value === '' || typeof value === 'object') {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return ['success', 'warning', 'error'].includes(value);
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
value: {
|
|
44
|
+
type: [String, Number, Object, Array],
|
|
45
|
+
default: null,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
computed: {
|
|
50
|
+
classes() {
|
|
51
|
+
const classes = {};
|
|
52
|
+
|
|
53
|
+
if (this.variant) {
|
|
54
|
+
classes[`form-control--${this.variant}`] = true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return classes;
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
listeners() {
|
|
61
|
+
// `Object.assign` merges objects together to form a new object
|
|
62
|
+
return Object.assign(
|
|
63
|
+
{},
|
|
64
|
+
// We add all the listeners from the parent
|
|
65
|
+
this.$listeners,
|
|
66
|
+
// Then we can add custom listeners or override the
|
|
67
|
+
// behavior of some listeners.
|
|
68
|
+
{
|
|
69
|
+
// This ensures that the component works with v-model
|
|
70
|
+
input: (event) => {
|
|
71
|
+
if (this.lazy) return;
|
|
72
|
+
this.$emit('input', event.target.value);
|
|
73
|
+
},
|
|
74
|
+
blur: (event) => {
|
|
75
|
+
this.$emit('blur', event);
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
</script>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<VTInput v-model="date" type="date" />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script>
|
|
6
|
+
import VTInput from './VTInput.vue';
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
name: 'VTInputDate',
|
|
10
|
+
|
|
11
|
+
components: { VTInput },
|
|
12
|
+
|
|
13
|
+
model: {
|
|
14
|
+
prop: 'value',
|
|
15
|
+
event: 'input',
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
props: {
|
|
19
|
+
value: {
|
|
20
|
+
type: String,
|
|
21
|
+
default: '',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
computed: {
|
|
26
|
+
date: {
|
|
27
|
+
get() {
|
|
28
|
+
return this.$date.format(this.value, 'YYYY-MM-DD');
|
|
29
|
+
},
|
|
30
|
+
set(newDate) {
|
|
31
|
+
this.$emit('input', newDate);
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
</script>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex items-stretch gap-2">
|
|
3
|
+
<VTInput
|
|
4
|
+
ref="input"
|
|
5
|
+
type="file"
|
|
6
|
+
:value="value"
|
|
7
|
+
:theme="theme"
|
|
8
|
+
v-bind="$attrs"
|
|
9
|
+
@change="onChange"
|
|
10
|
+
/>
|
|
11
|
+
<VTButton :theme="theme" @click.stop="onButtonClick">Browse</VTButton>
|
|
12
|
+
</div>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script>
|
|
16
|
+
import VTButton from '../Button/VTButton.vue';
|
|
17
|
+
import VTInput from './VTInput.vue';
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
name: 'VTInputFile',
|
|
21
|
+
|
|
22
|
+
components: {
|
|
23
|
+
VTInput,
|
|
24
|
+
VTButton,
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
inheritAttrs: false,
|
|
28
|
+
|
|
29
|
+
props: {
|
|
30
|
+
theme: {
|
|
31
|
+
type: String,
|
|
32
|
+
default: null,
|
|
33
|
+
validator(value) {
|
|
34
|
+
return ['dark'].includes(value);
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
multiple: {
|
|
38
|
+
type: Boolean,
|
|
39
|
+
default: false,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
data() {
|
|
44
|
+
return {
|
|
45
|
+
value: null,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
methods: {
|
|
50
|
+
onChange(event) {
|
|
51
|
+
this.value = this.$refs.input.$el.value;
|
|
52
|
+
this.$emit('change', event);
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
onButtonClick() {
|
|
56
|
+
this.$refs.input.$el.click();
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
</script>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<label
|
|
3
|
+
class="flex h-full w-full flex-col items-center justify-center rounded border-2 border-dotted border-white p-4 text-center hover:border-fl-500 hover:bg-fd-500"
|
|
4
|
+
:class="{ 'border-fl-500 bg-fd-500': isDraggingOver }"
|
|
5
|
+
@drop.prevent="onDrop"
|
|
6
|
+
@dragover.prevent="onDragOver"
|
|
7
|
+
@dragleave.prevent="onDragLeave"
|
|
8
|
+
>
|
|
9
|
+
<IconImagePlaceholder class="mb-3" />
|
|
10
|
+
<span>Drop your images here, or click to browse</span>
|
|
11
|
+
<VTInput type="file" class="sr-only" v-bind="$attrs" @change="onChange" />
|
|
12
|
+
</label>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script>
|
|
16
|
+
import { IconImagePlaceholder } from '@veritree/icons';
|
|
17
|
+
import VTInput from './VTInput.vue';
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
name: 'VTInputFile',
|
|
21
|
+
|
|
22
|
+
components: {
|
|
23
|
+
VTInput,
|
|
24
|
+
IconImagePlaceholder,
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
inheritAttrs: false,
|
|
28
|
+
|
|
29
|
+
data() {
|
|
30
|
+
return {
|
|
31
|
+
isDraggingOver: false,
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
methods: {
|
|
36
|
+
onDrop(event) {
|
|
37
|
+
this.isDraggingOver = false;
|
|
38
|
+
this.$emit('drop', event);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
onDragOver() {
|
|
42
|
+
this.isDraggingOver = true;
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
onDragLeave() {
|
|
46
|
+
this.isDraggingOver = false;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
onChange(event) {
|
|
50
|
+
this.$emit('change', event);
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
</script>
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="Listbox"><slot></slot></div>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script>
|
|
6
|
+
export default {
|
|
7
|
+
name: 'VTListbox',
|
|
8
|
+
|
|
9
|
+
provide() {
|
|
10
|
+
return {
|
|
11
|
+
api: () => {
|
|
12
|
+
// Get children components
|
|
13
|
+
const getListbox = () => this.listbox;
|
|
14
|
+
const getListboxValue = () => this.value;
|
|
15
|
+
const getListboxButton = () => this.listboxButton;
|
|
16
|
+
|
|
17
|
+
// Handle registering and unregistering
|
|
18
|
+
const registerListboxButton = (button) => {
|
|
19
|
+
if (!button) return;
|
|
20
|
+
this.listboxButton = button;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const registerListbox = (listbox) => {
|
|
24
|
+
if (!listbox) return;
|
|
25
|
+
this.listbox = listbox;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const registerOption = (option) => {
|
|
29
|
+
if (!option) return;
|
|
30
|
+
_register(this.listboxOptions, option);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const unregisterOption = (id) => {
|
|
34
|
+
_unregister(this.listboxOptions, id);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Register helper functions
|
|
38
|
+
const _register = (arr, item) => {
|
|
39
|
+
arr.push(item);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const _unregister = (arr, id) => {
|
|
43
|
+
const index = _getIndex(arr, id);
|
|
44
|
+
arr.splice(index, 1);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Handle focus and unfocus
|
|
48
|
+
const getFocusedIndex = () => {
|
|
49
|
+
let index = 0;
|
|
50
|
+
|
|
51
|
+
this.listboxOptions.forEach((option, i) => {
|
|
52
|
+
if (option.focused) index = i;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return index;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const focusOptionByFilter = (filter) => {
|
|
59
|
+
const optionIndex = this.listboxOptions.findIndex((option) => {
|
|
60
|
+
const text = option.$el.innerText;
|
|
61
|
+
return text.toLowerCase().indexOf(filter.toLowerCase()) === 0;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (optionIndex >= 0) _focusOption(optionIndex);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const focusFirstOption = () => {
|
|
68
|
+
_focusOption(0);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const focusLastOption = () => {
|
|
72
|
+
_focusOption(this.listboxOptions.length - 1);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const focusPreviousOption = () => {
|
|
76
|
+
const focusedIndex = getFocusedIndex();
|
|
77
|
+
const previousIndex = focusedIndex - 1;
|
|
78
|
+
|
|
79
|
+
if (previousIndex >= 0) _focusOption(previousIndex);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const focusNextOption = () => {
|
|
83
|
+
const focusedIndex = getFocusedIndex();
|
|
84
|
+
const nextIndex = focusedIndex + 1;
|
|
85
|
+
|
|
86
|
+
if (nextIndex < this.listboxOptions.length) _focusOption(nextIndex);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const unfocusOptions = () => {
|
|
90
|
+
this.listboxOptions.forEach((option) => {
|
|
91
|
+
option.unfocus();
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const _focusOption = (index) => {
|
|
96
|
+
unfocusOptions();
|
|
97
|
+
this.listboxOptions[index].focus();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const _getFocusedOption = () => {
|
|
101
|
+
return this.listboxOptions.filter((option) => option.focused)[0];
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// handle selecting and unselecting
|
|
105
|
+
const selectOption = () => {
|
|
106
|
+
const focusedOption = _getFocusedOption();
|
|
107
|
+
|
|
108
|
+
// do nothing if option is already selected
|
|
109
|
+
if (focusedOption.selected) return;
|
|
110
|
+
|
|
111
|
+
// unselect all options
|
|
112
|
+
this.listboxOptions.forEach((option) => option.unselect());
|
|
113
|
+
|
|
114
|
+
// select focused option
|
|
115
|
+
if (focusedOption) {
|
|
116
|
+
focusedOption.select();
|
|
117
|
+
_onSelectedOption(focusedOption.value);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const _onSelectedOption = (value) => {
|
|
122
|
+
this.listbox.hide();
|
|
123
|
+
this.$emit('input', value);
|
|
124
|
+
this.$emit('change');
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Index helper functions
|
|
128
|
+
const _getIndex = (arr, id) => {
|
|
129
|
+
return arr.findIndex((item) => item.id === id);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
getListbox,
|
|
134
|
+
getListboxButton,
|
|
135
|
+
registerListboxButton,
|
|
136
|
+
registerListbox,
|
|
137
|
+
registerOption,
|
|
138
|
+
unregisterOption,
|
|
139
|
+
focusOptionByFilter,
|
|
140
|
+
focusFirstOption,
|
|
141
|
+
focusLastOption,
|
|
142
|
+
focusPreviousOption,
|
|
143
|
+
focusNextOption,
|
|
144
|
+
unfocusOptions,
|
|
145
|
+
selectOption,
|
|
146
|
+
getFocusedIndex,
|
|
147
|
+
getListboxValue,
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
props: {
|
|
154
|
+
value: {
|
|
155
|
+
type: [String, Number, Object],
|
|
156
|
+
default: null,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
data() {
|
|
161
|
+
return {
|
|
162
|
+
listbox: null,
|
|
163
|
+
listboxButton: null,
|
|
164
|
+
listboxOptions: [],
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
</script>
|