sprintify-ui 0.0.11 → 0.0.13
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/README.md +8 -7
- package/dist/sprintify-ui.es.js +5911 -3760
- package/dist/style.css +1 -1
- package/dist/tailwindcss/index.js +1 -2
- package/dist/types/src/components/BaseCharacterCounter.vue.d.ts +143 -0
- package/dist/types/src/components/BaseHasMany.vue.d.ts +277 -0
- package/dist/types/src/components/BaseInput.vue.d.ts +39 -5
- package/dist/types/src/components/BaseLoadingCover.vue.d.ts +72 -0
- package/dist/types/src/components/{BaseMediaLibraryItem.vue.d.ts → BaseMediaItem.vue.d.ts} +26 -4
- package/dist/types/src/components/BaseMediaLibrary.vue.d.ts +23 -15
- package/dist/types/src/components/BaseMediaPreview.vue.d.ts +97 -0
- package/dist/types/src/components/BaseModalCenter.vue.d.ts +8 -8
- package/dist/types/src/components/BaseModalSide.vue.d.ts +8 -8
- package/dist/types/src/components/BasePagination.vue.d.ts +105 -13
- package/dist/types/src/components/BaseSelect.vue.d.ts +130 -26
- package/dist/types/src/components/BaseSideNavigationItem.vue.d.ts +20 -1
- package/dist/types/src/components/BaseSwitch.vue.d.ts +15 -8
- package/dist/types/src/components/BaseTabItem.vue.d.ts +45 -4
- package/dist/types/src/components/BaseTagAutocomplete.vue.d.ts +25 -17
- package/dist/types/src/components/BaseTagAutocompleteFetch.vue.d.ts +37 -21
- package/dist/types/src/components/BaseTextareaAutoresize.vue.d.ts +175 -21
- package/dist/types/src/components/index.d.ts +30 -1
- package/dist/types/src/index.d.ts +4 -0
- package/package.json +1 -1
- package/src/components/BaseAppDialogs.vue +2 -2
- package/src/components/BaseAppNotifications.vue +1 -1
- package/src/components/BaseAutocomplete.vue +16 -18
- package/src/components/BaseBelongsTo.vue +1 -0
- package/src/components/BaseCharacterCounter.stories.js +30 -0
- package/src/components/BaseCharacterCounter.vue +60 -0
- package/src/components/BaseClipboard.vue +1 -1
- package/src/components/BaseDataIterator.stories.js +2 -2
- package/src/components/BaseDataIterator.vue +32 -38
- package/src/components/BaseDataTable.stories.js +2 -2
- package/src/components/BaseFileUploader.vue +4 -0
- package/src/components/BaseHasMany.vue +92 -0
- package/src/components/BaseInput.stories.js +46 -0
- package/src/components/BaseInput.vue +10 -2
- package/src/components/BaseInputLabel.stories.js +31 -0
- package/src/components/BaseInputLabel.vue +1 -1
- package/src/components/BaseLoadingCover.stories.js +55 -0
- package/src/components/BaseLoadingCover.vue +19 -1
- package/src/components/BaseMediaItem.stories.js +41 -0
- package/src/components/BaseMediaItem.vue +71 -0
- package/src/components/BaseMediaLibrary.stories.js +80 -0
- package/src/components/BaseMediaLibrary.vue +67 -68
- package/src/components/BaseMediaPreview.stories.js +72 -0
- package/src/components/BaseMediaPreview.vue +90 -0
- package/src/components/BaseMenu.stories.js +125 -0
- package/src/components/BaseMenu.vue +1 -1
- package/src/components/BaseModalCenter.stories.js +61 -0
- package/src/components/BaseModalCenter.vue +2 -2
- package/src/components/BaseModalSide.stories.js +55 -0
- package/src/components/BaseModalSide.vue +2 -2
- package/src/components/BaseNavbar.stories.js +150 -0
- package/src/components/BaseNavbar.vue +3 -0
- package/src/components/BaseNavbarItem.vue +1 -0
- package/src/components/BaseNavbarItemContent.vue +3 -0
- package/src/components/BasePagination.stories.js +32 -0
- package/src/components/BasePagination.vue +126 -40
- package/src/components/BasePanel.stories.js +56 -0
- package/src/components/BasePassword.stories.js +36 -0
- package/src/components/BasePassword.vue +11 -5
- package/src/components/BaseProcessRing.stories.js +27 -0
- package/src/components/BaseReadMore.stories.js +30 -0
- package/src/components/BaseReadMore.vue +1 -1
- package/src/components/BaseSelect.stories.js +67 -0
- package/src/components/BaseSelect.vue +144 -44
- package/src/components/BaseSideNavigation.stories.js +55 -0
- package/src/components/BaseSideNavigation.vue +7 -2
- package/src/components/BaseSideNavigationItem.vue +21 -5
- package/src/components/BaseSkeleton.stories.js +36 -0
- package/src/components/BaseSwitch.stories.js +101 -0
- package/src/components/BaseSwitch.vue +90 -12
- package/src/components/BaseSystemAlert.stories.js +63 -0
- package/src/components/BaseTabItem.vue +29 -6
- package/src/components/BaseTable.vue +2 -2
- package/src/components/BaseTabs.stories.js +54 -0
- package/src/components/BaseTabs.vue +3 -3
- package/src/components/BaseTagAutocomplete.stories.js +129 -0
- package/src/components/BaseTagAutocomplete.vue +155 -57
- package/src/components/BaseTagAutocompleteFetch.stories.js +130 -0
- package/src/components/BaseTagAutocompleteFetch.vue +36 -25
- package/src/components/BaseTextarea.stories.js +35 -0
- package/src/components/BaseTextarea.vue +1 -1
- package/src/components/BaseTextareaAutoresize.stories.js +49 -0
- package/src/components/BaseTextareaAutoresize.vue +83 -87
- package/src/components/HasMany.stories.js +135 -0
- package/src/components/index.ts +58 -0
- package/src/lang/en.json +2 -1
- package/src/lang/fr.json +2 -1
- package/dist/types/src/components/BasePaginationSimple.vue.d.ts +0 -25
- package/dist/types/src/components/BaseWordCount.vue.d.ts +0 -31
- package/src/components/BaseMediaLibraryItem.vue +0 -92
- package/src/components/BasePaginationSimple.vue +0 -60
- package/src/components/BaseWordCount.vue +0 -36
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import BaseTagAutocompleteFetch from './BaseTagAutocompleteFetch.vue';
|
|
2
|
+
import BaseApp from './BaseApp.vue';
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: 'Form/BaseTagAutocompleteFetch',
|
|
6
|
+
component: BaseTagAutocompleteFetch,
|
|
7
|
+
argTypes: {},
|
|
8
|
+
args: {
|
|
9
|
+
url: 'https://effettandem.com/api/content/articles',
|
|
10
|
+
labelKey: 'title',
|
|
11
|
+
valueKey: 'id',
|
|
12
|
+
disabled: false,
|
|
13
|
+
},
|
|
14
|
+
decorators: [() => ({ template: '<div class="mb-36"><story/></div>' })],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const Template = (args) => {
|
|
18
|
+
return {
|
|
19
|
+
components: { BaseTagAutocompleteFetch, BaseApp },
|
|
20
|
+
setup() {
|
|
21
|
+
const value = ref([]);
|
|
22
|
+
return { args, value };
|
|
23
|
+
},
|
|
24
|
+
template: `
|
|
25
|
+
<BaseTagAutocompleteFetch
|
|
26
|
+
v-model="value"
|
|
27
|
+
v-bind="args"
|
|
28
|
+
></BaseTagAutocompleteFetch>
|
|
29
|
+
<p class="mt-5 text-sm">Value: <span class="bg-slate-200 font-mono px-1 py-px rounded">{{ value ?? 'NULL' }}</span></p>
|
|
30
|
+
<BaseApp />
|
|
31
|
+
`,
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const Demo = Template.bind({});
|
|
36
|
+
Demo.args = {};
|
|
37
|
+
|
|
38
|
+
export const Disabled = Template.bind({});
|
|
39
|
+
Disabled.args = {
|
|
40
|
+
modelValue: [{ label: 'Dark Maul', value: '1' }],
|
|
41
|
+
labelKey: 'label',
|
|
42
|
+
valueKey: 'value',
|
|
43
|
+
disabled: true,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const Maximum = Template.bind({});
|
|
47
|
+
Maximum.args = {
|
|
48
|
+
max: 3,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const SlotOption = (args) => {
|
|
52
|
+
return {
|
|
53
|
+
components: { BaseTagAutocompleteFetch },
|
|
54
|
+
setup() {
|
|
55
|
+
const value = ref([]);
|
|
56
|
+
return { args, value };
|
|
57
|
+
},
|
|
58
|
+
template: `
|
|
59
|
+
<div class="mb-20">
|
|
60
|
+
<BaseTagAutocompleteFetch
|
|
61
|
+
v-model="value"
|
|
62
|
+
v-bind="args"
|
|
63
|
+
>
|
|
64
|
+
<template #option="{ option, active, selected }">
|
|
65
|
+
<div
|
|
66
|
+
class="rounded px-2 py-1"
|
|
67
|
+
:class="{
|
|
68
|
+
'hover:bg-slate-100': !active && !selected,
|
|
69
|
+
'bg-slate-200 hover:bg-slate-300': active && !selected,
|
|
70
|
+
'bg-blue-500 text-white hover:bg-blue-600': !active && selected,
|
|
71
|
+
'bg-blue-600 text-white hover:bg-blue-700': active && selected,
|
|
72
|
+
}"
|
|
73
|
+
>
|
|
74
|
+
<p class="text-sm font-medium">{{ option.title }}</p>
|
|
75
|
+
<p class="opacity-60 text-xs">{{ option.owner?.name }}</p>
|
|
76
|
+
</div>
|
|
77
|
+
</template>
|
|
78
|
+
</BaseTagAutocompleteFetch>
|
|
79
|
+
</div>
|
|
80
|
+
`,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const SlotFooter = (args) => {
|
|
85
|
+
return {
|
|
86
|
+
components: { BaseTagAutocompleteFetch },
|
|
87
|
+
setup() {
|
|
88
|
+
const value = ref([]);
|
|
89
|
+
function onClick() {
|
|
90
|
+
alert(1);
|
|
91
|
+
}
|
|
92
|
+
return { args, value, onClick };
|
|
93
|
+
},
|
|
94
|
+
template: `
|
|
95
|
+
<BaseTagAutocompleteFetch
|
|
96
|
+
v-model="value"
|
|
97
|
+
v-bind="args"
|
|
98
|
+
>
|
|
99
|
+
<template #footer>
|
|
100
|
+
<div class="text-center p-2 border-t">
|
|
101
|
+
<button @click=onClick class="btn btn-sm w-full btn-slate-200-outline">This is the footer 💯</button>
|
|
102
|
+
</div>
|
|
103
|
+
</template>
|
|
104
|
+
</BaseTagAutocompleteFetch>
|
|
105
|
+
`,
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const SlotEmpty = (args) => {
|
|
110
|
+
return {
|
|
111
|
+
components: { BaseTagAutocompleteFetch },
|
|
112
|
+
setup() {
|
|
113
|
+
const value = ref([]);
|
|
114
|
+
return { args, value };
|
|
115
|
+
},
|
|
116
|
+
template: `
|
|
117
|
+
<BaseTagAutocompleteFetch
|
|
118
|
+
v-model="value"
|
|
119
|
+
v-bind="args"
|
|
120
|
+
>
|
|
121
|
+
<template #empty="props">
|
|
122
|
+
<div>
|
|
123
|
+
<div v-if="props.firstSearch" class="text-center py-10 p-6">🤓🤓🤓</div>
|
|
124
|
+
<div v-else class="text-center p-6">Start your search... 🔎</div>
|
|
125
|
+
</div>
|
|
126
|
+
</template>
|
|
127
|
+
</BaseTagAutocompleteFetch>
|
|
128
|
+
`,
|
|
129
|
+
};
|
|
130
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<BaseTagAutocomplete
|
|
3
|
-
:loading="
|
|
3
|
+
:loading="showLoading && page == 1"
|
|
4
4
|
:model-value="modelValue"
|
|
5
5
|
:disabled="disabled"
|
|
6
6
|
:placeholder="placeholder"
|
|
@@ -8,22 +8,30 @@
|
|
|
8
8
|
:value-key="valueKey"
|
|
9
9
|
:label-key="labelKey"
|
|
10
10
|
:input-class="inputClass"
|
|
11
|
-
:min="min"
|
|
12
11
|
:max="max"
|
|
12
|
+
:filter="() => true"
|
|
13
13
|
@focus="onFocus"
|
|
14
14
|
@typing="onTyping"
|
|
15
15
|
@scroll-bottom="scrollBottom"
|
|
16
16
|
@update:model-value="$emit('update:modelValue', $event)"
|
|
17
17
|
>
|
|
18
|
-
<template #
|
|
19
|
-
<
|
|
20
|
-
|
|
18
|
+
<template #option="optionProps">
|
|
19
|
+
<slot name="option" v-bind="optionProps" />
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<template #footer="footerProps">
|
|
23
|
+
<slot name="footer" v-bind="footerProps" :keywords="keywords"> </slot>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<template #empty="emptyProps">
|
|
27
|
+
<slot name="empty" v-bind="emptyProps" :first-search="firstSearch">
|
|
28
|
+
<div
|
|
29
|
+
v-if="firstSearch"
|
|
30
|
+
class="flex h-[80px] items-center justify-center px-3 text-center text-base leading-tight text-slate-600"
|
|
31
|
+
>
|
|
21
32
|
{{ $t('sui.nothing_found') }}
|
|
22
|
-
</
|
|
23
|
-
|
|
24
|
-
{{ $t('sui.autocomplete_placeholder') }}
|
|
25
|
-
</span>
|
|
26
|
-
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</slot>
|
|
27
35
|
</template>
|
|
28
36
|
</BaseTagAutocomplete>
|
|
29
37
|
</template>
|
|
@@ -33,6 +41,7 @@ import { debounce } from 'lodash';
|
|
|
33
41
|
import { config } from '@/index';
|
|
34
42
|
import { PropType, Ref } from 'vue';
|
|
35
43
|
import { Option } from '@/types/types';
|
|
44
|
+
import BaseTagAutocomplete from './BaseTagAutocomplete.vue';
|
|
36
45
|
|
|
37
46
|
const props = defineProps({
|
|
38
47
|
modelValue: {
|
|
@@ -67,21 +76,22 @@ const props = defineProps({
|
|
|
67
76
|
default: false,
|
|
68
77
|
type: Boolean,
|
|
69
78
|
},
|
|
70
|
-
min: {
|
|
71
|
-
default: undefined,
|
|
72
|
-
type: Number,
|
|
73
|
-
},
|
|
74
79
|
max: {
|
|
75
80
|
default: undefined,
|
|
76
81
|
type: Number,
|
|
77
82
|
},
|
|
83
|
+
queryKey: {
|
|
84
|
+
default: 'search',
|
|
85
|
+
type: String,
|
|
86
|
+
},
|
|
78
87
|
});
|
|
79
88
|
|
|
80
89
|
defineEmits(['update:modelValue', 'typing', 'focus', 'scrollBottom']);
|
|
81
90
|
|
|
82
91
|
const http = config.http;
|
|
83
92
|
|
|
84
|
-
const
|
|
93
|
+
const showLoading = ref(false);
|
|
94
|
+
const fetching = ref(false);
|
|
85
95
|
const firstSearch = ref(false);
|
|
86
96
|
const reachedEnd = ref(false);
|
|
87
97
|
const keywords = ref('');
|
|
@@ -94,18 +104,15 @@ const onTyping = (query: string) => {
|
|
|
94
104
|
|
|
95
105
|
if (keywords.value != query) {
|
|
96
106
|
keywords.value = query;
|
|
107
|
+
showLoading.value = true;
|
|
97
108
|
debouncedSearch();
|
|
98
109
|
}
|
|
99
110
|
};
|
|
100
111
|
|
|
101
112
|
const onFocus = () => {
|
|
102
|
-
if (firstSearch.value) {
|
|
103
|
-
|
|
113
|
+
if (!firstSearch.value) {
|
|
114
|
+
search();
|
|
104
115
|
}
|
|
105
|
-
|
|
106
|
-
search();
|
|
107
|
-
|
|
108
|
-
firstSearch.value = true;
|
|
109
116
|
};
|
|
110
117
|
|
|
111
118
|
const scrollBottom = () => {
|
|
@@ -116,17 +123,18 @@ const scrollBottom = () => {
|
|
|
116
123
|
};
|
|
117
124
|
|
|
118
125
|
const search = () => {
|
|
119
|
-
if (
|
|
126
|
+
if (fetching.value) {
|
|
120
127
|
return;
|
|
121
128
|
}
|
|
122
129
|
|
|
123
|
-
|
|
130
|
+
fetching.value = true;
|
|
131
|
+
showLoading.value = true;
|
|
124
132
|
firstSearch.value = true;
|
|
125
133
|
|
|
126
134
|
http
|
|
127
135
|
.get(props.url, {
|
|
128
136
|
params: {
|
|
129
|
-
|
|
137
|
+
[props.queryKey]: keywords.value,
|
|
130
138
|
page: page.value,
|
|
131
139
|
},
|
|
132
140
|
})
|
|
@@ -142,9 +150,12 @@ const search = () => {
|
|
|
142
150
|
} else {
|
|
143
151
|
options.value.push(...data);
|
|
144
152
|
}
|
|
153
|
+
|
|
154
|
+
firstSearch.value = true;
|
|
145
155
|
})
|
|
146
156
|
.finally(() => {
|
|
147
|
-
|
|
157
|
+
fetching.value = false;
|
|
158
|
+
showLoading.value = false;
|
|
148
159
|
});
|
|
149
160
|
};
|
|
150
161
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import BaseTextarea from './BaseTextarea.vue';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
title: 'Form/BaseTextarea',
|
|
5
|
+
component: BaseTextarea,
|
|
6
|
+
args: {
|
|
7
|
+
name: 'bio',
|
|
8
|
+
required: true,
|
|
9
|
+
placeholder: 'Describe your complete life in 4 sentences...',
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const Template = (args) => ({
|
|
14
|
+
components: {
|
|
15
|
+
BaseTextarea,
|
|
16
|
+
},
|
|
17
|
+
setup() {
|
|
18
|
+
const value = ref('');
|
|
19
|
+
return { args, value };
|
|
20
|
+
},
|
|
21
|
+
template: `
|
|
22
|
+
<form @submit.prevent="" class="border-none">
|
|
23
|
+
<BaseTextarea v-model="value" v-bind="args" class="w-full"></BaseTextarea>
|
|
24
|
+
</form>
|
|
25
|
+
`,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const Demo = Template.bind({});
|
|
29
|
+
Demo.args = {};
|
|
30
|
+
|
|
31
|
+
export const Disabled = Template.bind({});
|
|
32
|
+
Disabled.args = {
|
|
33
|
+
modelValue: 'Lorem ipsum...',
|
|
34
|
+
disabled: true,
|
|
35
|
+
};
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
:disabled="disabled"
|
|
8
8
|
:required="required"
|
|
9
9
|
:rows="rows"
|
|
10
|
-
class="mb-0 block rounded"
|
|
10
|
+
class="mb-0 block rounded border-slate-300 disabled:cursor-not-allowed disabled:text-slate-300"
|
|
11
11
|
@input="$emit('update:modelValue', transformInputValue($event))"
|
|
12
12
|
/>
|
|
13
13
|
</template>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import BaseTextareaAutoresize from './BaseTextareaAutoresize.vue';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
title: 'Form/BaseTextareaAutoresize',
|
|
5
|
+
component: BaseTextareaAutoresize,
|
|
6
|
+
args: {
|
|
7
|
+
name: 'bio',
|
|
8
|
+
placeholder: 'Describe your complete life in 4 sentences...',
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const Template = (args) => ({
|
|
13
|
+
components: {
|
|
14
|
+
BaseTextareaAutoresize,
|
|
15
|
+
},
|
|
16
|
+
setup() {
|
|
17
|
+
function onSubmit() {
|
|
18
|
+
alert('submit');
|
|
19
|
+
}
|
|
20
|
+
const value = ref('');
|
|
21
|
+
return { args, value, onSubmit };
|
|
22
|
+
},
|
|
23
|
+
template: `
|
|
24
|
+
<form @submit.prevent="onSubmit" class="border-none">
|
|
25
|
+
<BaseTextareaAutoresize
|
|
26
|
+
v-model="value"
|
|
27
|
+
v-bind="args"
|
|
28
|
+
class="w-full"
|
|
29
|
+
@submit="onSubmit"
|
|
30
|
+
></BaseTextareaAutoresize>
|
|
31
|
+
</form>
|
|
32
|
+
|
|
33
|
+
<pre class="mt-4 bg-slate-800 font-light text-xs p-3 rounded whitespace-pre-wrap text-white">{{ {value} }}</pre>
|
|
34
|
+
`,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const Demo = Template.bind({});
|
|
38
|
+
Demo.args = {};
|
|
39
|
+
|
|
40
|
+
export const SubmitOnEnter = Template.bind({});
|
|
41
|
+
SubmitOnEnter.args = {
|
|
42
|
+
submitOnEnter: true,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const Disabled = Template.bind({});
|
|
46
|
+
Disabled.args = {
|
|
47
|
+
modelValue: 'Lorem ipsum...',
|
|
48
|
+
disabled: true,
|
|
49
|
+
};
|
|
@@ -1,117 +1,113 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div
|
|
3
|
+
ref="wrapper"
|
|
4
|
+
class="wrapper grid"
|
|
5
|
+
:style="{ maxHeight: maxHeight + 'px' }"
|
|
6
|
+
>
|
|
3
7
|
<textarea
|
|
4
8
|
:value="modelValue"
|
|
5
9
|
:name="name"
|
|
6
10
|
:placeholder="placeholder"
|
|
11
|
+
:disabled="disabled"
|
|
12
|
+
class="resize-none"
|
|
13
|
+
:class="BASE_TEXTAREA_CLASSES"
|
|
14
|
+
:style="{ maxHeight: maxHeight + 'px', gridArea: BASE_GRID_AREA }"
|
|
7
15
|
rows="1"
|
|
8
|
-
class="focus:outline-none focus:ring-0"
|
|
9
16
|
@input="onInput"
|
|
10
17
|
@keyup="onKeyUp"
|
|
11
18
|
@keydown="onKeyDown"
|
|
12
19
|
@focus="onFocus"
|
|
13
20
|
/>
|
|
21
|
+
<div
|
|
22
|
+
class="invisible whitespace-pre-wrap"
|
|
23
|
+
:class="BASE_TEXTAREA_CLASSES"
|
|
24
|
+
:style="{
|
|
25
|
+
content: DIV_CONTENT,
|
|
26
|
+
maxHeight: maxHeight + 'px',
|
|
27
|
+
gridArea: BASE_GRID_AREA,
|
|
28
|
+
}"
|
|
29
|
+
>
|
|
30
|
+
{{ modelValue }} {{ ' ' }}
|
|
31
|
+
</div>
|
|
14
32
|
</div>
|
|
15
33
|
</template>
|
|
16
34
|
|
|
17
|
-
<script lang="ts">
|
|
18
|
-
|
|
19
|
-
import { defineComponent, inject } from 'vue';
|
|
35
|
+
<script lang="ts" setup>
|
|
36
|
+
import { Ref } from 'vue';
|
|
20
37
|
|
|
21
|
-
const
|
|
38
|
+
const BASE_TEXTAREA_CLASSES =
|
|
39
|
+
'py-2 px-3 font-normal text-base disabled:cursor-not-allowed disabled:text-slate-300 font-sans rounded leading-normal tracking-normal border border-slate-300';
|
|
40
|
+
|
|
41
|
+
const BASE_GRID_AREA = '1 / 1 / 2 / 2';
|
|
22
42
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
default: '',
|
|
31
|
-
type: String,
|
|
32
|
-
},
|
|
33
|
-
name: {
|
|
34
|
-
required: true,
|
|
35
|
-
type: String,
|
|
36
|
-
},
|
|
43
|
+
/* Note the weird space! Needed to prevent jumpy behavior */
|
|
44
|
+
const DIV_CONTENT = "attr(data-replicated-value) ' '";
|
|
45
|
+
|
|
46
|
+
const props = defineProps({
|
|
47
|
+
modelValue: {
|
|
48
|
+
default: '',
|
|
49
|
+
type: String,
|
|
37
50
|
},
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
mobile,
|
|
43
|
-
};
|
|
51
|
+
placeholder: {
|
|
52
|
+
default: '',
|
|
53
|
+
type: String,
|
|
44
54
|
},
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
},
|
|
55
|
+
name: {
|
|
56
|
+
required: true,
|
|
57
|
+
type: String,
|
|
49
58
|
},
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (keys['Enter'] && !keys['Shift'] && !this.mobile) {
|
|
66
|
-
this.$emit('submit');
|
|
67
|
-
event.preventDefault();
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
onKeyUp(event: any) {
|
|
71
|
-
delete keys[event.key];
|
|
72
|
-
},
|
|
73
|
-
onFocus(event: any) {
|
|
74
|
-
this.$emit('focus', event);
|
|
75
|
-
},
|
|
59
|
+
maxHeight: {
|
|
60
|
+
default: 100,
|
|
61
|
+
type: Number,
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* Setting this to true will trigger the 'submit' event while pressing Enter.
|
|
65
|
+
* Users will be able to add a line break while pressing Shift + Enter.
|
|
66
|
+
*/
|
|
67
|
+
submitOnEnter: {
|
|
68
|
+
default: false,
|
|
69
|
+
type: Boolean,
|
|
70
|
+
},
|
|
71
|
+
disabled: {
|
|
72
|
+
default: false,
|
|
73
|
+
type: Boolean,
|
|
76
74
|
},
|
|
77
75
|
});
|
|
78
|
-
</script>
|
|
79
76
|
|
|
80
|
-
|
|
81
|
-
.wrapper {
|
|
82
|
-
/* easy way to plop the elements on top of each other and have them both sized based on the tallest one's height */
|
|
83
|
-
display: grid;
|
|
84
|
-
}
|
|
85
|
-
.wrapper::after {
|
|
86
|
-
/* Note the weird space! Needed to prevent jumpy behavior */
|
|
87
|
-
content: attr(data-replicated-value) ' ';
|
|
77
|
+
const emit = defineEmits(['update:modelValue', 'submit', 'focus', 'input']);
|
|
88
78
|
|
|
89
|
-
|
|
90
|
-
white-space: pre-wrap;
|
|
79
|
+
const wrapper = ref(null) as Ref<null | HTMLDivElement>;
|
|
91
80
|
|
|
92
|
-
|
|
93
|
-
|
|
81
|
+
const keys = {} as { [key: string]: boolean };
|
|
82
|
+
|
|
83
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
84
|
+
function onInput(e: any) {
|
|
85
|
+
const value = e.target.value ?? '';
|
|
86
|
+
emit('update:modelValue', value);
|
|
87
|
+
emit('input', e);
|
|
94
88
|
}
|
|
95
|
-
.wrapper > textarea {
|
|
96
|
-
/* You could leave this, but after a user resizes, then it ruins the auto sizing */
|
|
97
|
-
resize: none;
|
|
98
89
|
|
|
99
|
-
|
|
100
|
-
|
|
90
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
91
|
+
keys[event.key] = true;
|
|
92
|
+
|
|
93
|
+
if (keys['Enter'] && !keys['Shift'] && props.submitOnEnter) {
|
|
94
|
+
// If submit triggers alert, Enter wont be delete from keys,
|
|
95
|
+
// manually remove it here
|
|
96
|
+
delete keys['Enter'];
|
|
97
|
+
|
|
98
|
+
// Submit event to listen to
|
|
99
|
+
emit('submit');
|
|
100
|
+
|
|
101
|
+
// Prevent adding an EOL the the textarea
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
}
|
|
101
104
|
}
|
|
102
|
-
.wrapper > textarea,
|
|
103
|
-
.wrapper::after {
|
|
104
|
-
/* Identical styling required!! */
|
|
105
|
-
padding: 0.5rem;
|
|
106
|
-
font: inherit;
|
|
107
105
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
@apply bg-slate-100;
|
|
112
|
-
@apply rounded-lg;
|
|
106
|
+
function onKeyUp(event: KeyboardEvent) {
|
|
107
|
+
delete keys[event.key];
|
|
108
|
+
}
|
|
113
109
|
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
function onFocus(event: FocusEvent) {
|
|
111
|
+
emit('focus', event);
|
|
116
112
|
}
|
|
117
|
-
</
|
|
113
|
+
</script>
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import BaseHasMany from './BaseHasMany.vue';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
title: 'Form/BaseHasMany',
|
|
5
|
+
component: BaseHasMany,
|
|
6
|
+
argTypes: {},
|
|
7
|
+
args: {
|
|
8
|
+
url: 'https://effettandem.com/api/content/articles',
|
|
9
|
+
field: 'title',
|
|
10
|
+
foreignKey: 'id',
|
|
11
|
+
},
|
|
12
|
+
decorators: [() => ({ template: '<div class="mb-36"><story/></div>' })],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const Template = (args) => {
|
|
16
|
+
return {
|
|
17
|
+
components: { BaseHasMany },
|
|
18
|
+
setup() {
|
|
19
|
+
const value = ref([]);
|
|
20
|
+
return { args, value };
|
|
21
|
+
},
|
|
22
|
+
template: `
|
|
23
|
+
<BaseHasMany
|
|
24
|
+
v-model="value"
|
|
25
|
+
v-bind="args"
|
|
26
|
+
></BaseHasMany>
|
|
27
|
+
<p class="mt-5 text-sm">Value: <span class="bg-slate-200 font-mono px-1 py-px rounded">{{ value ?? '[]' }}</span></p>
|
|
28
|
+
`,
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const Demo = Template.bind({});
|
|
33
|
+
Demo.args = {};
|
|
34
|
+
|
|
35
|
+
export const Disabled = (args) => {
|
|
36
|
+
return {
|
|
37
|
+
components: { BaseHasMany },
|
|
38
|
+
setup() {
|
|
39
|
+
const value = ref([]);
|
|
40
|
+
return { args, value };
|
|
41
|
+
},
|
|
42
|
+
template: `<BaseHasMany
|
|
43
|
+
v-bind="args"
|
|
44
|
+
v-model="value"
|
|
45
|
+
:current-models="[{title: 'Dark Vader', id: 1}]"
|
|
46
|
+
:disabled="true"
|
|
47
|
+
></BaseHasMany>`,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const Maximum = Template.bind({});
|
|
52
|
+
Maximum.args = {
|
|
53
|
+
max: 3,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const SlotOption = (args) => {
|
|
57
|
+
return {
|
|
58
|
+
components: { BaseHasMany },
|
|
59
|
+
setup() {
|
|
60
|
+
const value = ref([]);
|
|
61
|
+
return { args, value };
|
|
62
|
+
},
|
|
63
|
+
template: `
|
|
64
|
+
<div class="mb-20">
|
|
65
|
+
<BaseHasMany
|
|
66
|
+
v-model="value"
|
|
67
|
+
v-bind="args"
|
|
68
|
+
>
|
|
69
|
+
<template #option="{ option, active, selected }">
|
|
70
|
+
<div
|
|
71
|
+
class="rounded px-2 py-1"
|
|
72
|
+
:class="{
|
|
73
|
+
'hover:bg-slate-100': !active && !selected,
|
|
74
|
+
'bg-slate-200 hover:bg-slate-300': active && !selected,
|
|
75
|
+
'bg-blue-500 text-white hover:bg-blue-600': !active && selected,
|
|
76
|
+
'bg-blue-600 text-white hover:bg-blue-700': active && selected,
|
|
77
|
+
}"
|
|
78
|
+
>
|
|
79
|
+
<p class="text-sm font-medium">{{ option.title }}</p>
|
|
80
|
+
<p class="opacity-60 text-xs">{{ option.owner?.name }}</p>
|
|
81
|
+
</div>
|
|
82
|
+
</template>
|
|
83
|
+
</BaseHasMany>
|
|
84
|
+
</div>
|
|
85
|
+
`,
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const SlotFooter = (args) => {
|
|
90
|
+
return {
|
|
91
|
+
components: { BaseHasMany },
|
|
92
|
+
setup() {
|
|
93
|
+
const value = ref([]);
|
|
94
|
+
function onClick() {
|
|
95
|
+
alert(1);
|
|
96
|
+
}
|
|
97
|
+
return { args, value, onClick };
|
|
98
|
+
},
|
|
99
|
+
template: `
|
|
100
|
+
<BaseHasMany
|
|
101
|
+
v-model="value"
|
|
102
|
+
v-bind="args"
|
|
103
|
+
>
|
|
104
|
+
<template #footer>
|
|
105
|
+
<div class="text-center p-2 border-t">
|
|
106
|
+
<button @click=onClick class="btn btn-sm w-full btn-slate-200-outline">This is the footer 💯</button>
|
|
107
|
+
</div>
|
|
108
|
+
</template>
|
|
109
|
+
</BaseHasMany>
|
|
110
|
+
`,
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const SlotEmpty = (args) => {
|
|
115
|
+
return {
|
|
116
|
+
components: { BaseHasMany },
|
|
117
|
+
setup() {
|
|
118
|
+
const value = ref([]);
|
|
119
|
+
return { args, value };
|
|
120
|
+
},
|
|
121
|
+
template: `
|
|
122
|
+
<BaseHasMany
|
|
123
|
+
v-model="value"
|
|
124
|
+
v-bind="args"
|
|
125
|
+
>
|
|
126
|
+
<template #empty="props">
|
|
127
|
+
<div>
|
|
128
|
+
<div v-if="props.firstSearch" class="text-center py-10 p-6">🤓🤓🤓</div>
|
|
129
|
+
<div v-else class="text-center px-6 py-20">Start your search... 🔎</div>
|
|
130
|
+
</div>
|
|
131
|
+
</template>
|
|
132
|
+
</BaseHasMany>
|
|
133
|
+
`,
|
|
134
|
+
};
|
|
135
|
+
};
|