@@ -10,6 +10,7 @@ export interface TagInputProps {
1010 tagBgColor? : string ;
1111 tagClass? : string ;
1212 customDelimiter? : string [] | string ;
13+ singleLine: boolean
1314}
1415
1516const props = withDefaults (defineProps <TagInputProps >(), {
@@ -21,12 +22,14 @@ const props = withDefaults(defineProps<TagInputProps>(), {
2122 tagBgColor: " rgb(120, 54, 10)" ,
2223 tagClass: " " ,
2324 customDelimiter : () => [],
25+ singleLine: false
2426});
2527const emit = defineEmits ([" update:modelValue" ]);
2628// Tags
2729const tags = ref <string []>(props .modelValue );
2830const tagsClass = ref (props .tagClass );
2931const newTag = ref (" " );
32+ const focused = ref (false );
3033const id = Math .random ().toString (36 ).substring (7 );
3134const customDelimiter = computed <string [] | string >(() => [
3235 ... new Set (
@@ -79,14 +82,10 @@ const removeTag = (index: number) => {
7982};
8083
8184// positioning and handling tag change
82- const paddingLeft = ref (10 );
8385const tagsUl = ref <HTMLUListElement | null >(null );
8486const onTagsChange = () => {
85- // position cursor
86- const extraCushion = 15 ;
8787 tagsUl .value ?.style .setProperty (" --tagBgColor" , props .tagBgColor );
8888 tagsUl .value ?.style .setProperty (" --tagTextColor" , props .tagTextColor );
89- paddingLeft .value = (tagsUl .value ?.clientWidth || 0 ) + extraCushion ;
9089 // scroll to end of tags
9190 tagsUl .value ?.scrollTo (tagsUl .value .scrollWidth , 0 );
9291 // emit value on tags change
@@ -117,50 +116,37 @@ const deleteLastTag = () => {
117116 }
118117 }
119118};
119+ const inputElId = ` tag-input${Math .random ()} `
120120 </script >
121121
122122<template >
123- <div
124- class =" tag-input"
125- :class =" { 'with-count': showCount, duplicate: noMatchingTag }"
126- >
127- <input
128- v-model =" newTag"
129- type =" text"
130- :list =" id"
131- autocomplete =" off"
132- @keydown.enter =" addTag(newTag)"
133- @keydown.prevent.tab =" addTag(newTag)"
134- @keydown.delete =" deleteLastTag()"
135- @input =" addTagIfDelem(newTag)"
136- :style =" { 'padding-left': `${paddingLeft}px` }"
137- />
138-
139- <datalist v-if =" options" :id =" id" >
140- <option v-for =" option in availableOptions" :key =" option" :value =" option" >
141- {{ option }}
142- </option >
143- </datalist >
144-
145- <ul class =" tags" ref =" tagsUl" >
146- <li
147- v-for =" (tag, index) in tags"
148- :key =" tag"
149- :class =" {
150- duplicate: tag === duplicate,
151- tag: tagsClass.length == 0,
152- del: shouldDelete && index === tags.length - 1,
153- [tagsClass]: true,
154- }"
155- >
123+ <label :for =" inputElId" >
124+ <ul class =" tags" ref =" tagsUl" tabindex =" 0" :class =" { duplicate, focused, noMatchingTag, singleLine }" >
125+ <li v-for =" (tag, index) in tags" :key =" tag" :class =" {
126+ duplicate: tag === duplicate,
127+ tag: tagsClass.length == 0,
128+ del: shouldDelete && index === tags.length - 1,
129+ [tagsClass]: true,
130+ }" >
156131 {{ tag }}
157132 <button class =" delete" @click =" removeTag(index)" >x</button >
158133 </li >
134+ <div class =" tag-input" >
135+ <input v-model =" newTag" :id =" inputElId" type =" text" :list =" id" autocomplete =" off" @keydown.enter =" addTag(newTag)"
136+ @keydown.prevent.tab =" addTag(newTag)" @keydown.delete =" deleteLastTag()" @input =" addTagIfDelem(newTag)"
137+ placeholder =" Enter tag" @focus =" focused = true" @blur =" focused = false" />
138+
139+ <datalist v-if =" options" :id =" id" >
140+ <option v-for =" option in availableOptions" :key =" option" :value =" option" >
141+ {{ option }}
142+ </option >
143+ </datalist >
144+ </div >
145+ <div v-if =" showCount" class =" count" >
146+ <span >{{ tags.length }}</span > tags
147+ </div >
159148 </ul >
160- <div v-if =" showCount" class =" count" >
161- <span >{{ tags.length }}</span > tags
162- </div >
163- </div >
149+ </label >
164150 <small v-show =" noMatchingTag" class =" err" >Custom tags not allowed</small >
165151</template >
166152
@@ -171,23 +157,42 @@ const deleteLastTag = () => {
171157
172158.tag-input {
173159 position : relative ;
160+ width : 250px ;
174161}
175162
176- ul {
163+ .tags {
177164 --tagBgColor : rgb (250 , 104 , 104 );
178165 --tagTextColor : white ;
179166 list-style : none ;
180167 display : flex ;
168+ flex-wrap : wrap ;
181169 align-items : center ;
182170 gap : 7px ;
183171 margin : 0 ;
184- padding : 0 ;
185- position : absolute ;
186- top : 0 ;
187- bottom : 0 ;
172+ padding : 10px ;
188173 left : 10px ;
189174 max-width : 75% ;
190- overflow-x : auto ;
175+ border-bottom : 1px solid #5558 ;
176+ cursor : text ;
177+
178+ &.singleLine {
179+ flex-wrap : nowrap ;
180+ overflow : auto ;
181+ }
182+
183+ &.focused {
184+ border-bottom : 2px solid #55fa ;
185+ }
186+
187+ &.duplicate {
188+ border-bottom : 1px solid rgb (235 , 27 , 27 );
189+ }
190+
191+ &.noMatchingTag {
192+ outline : rgb (235 , 27 , 27 );
193+ border : 1px solid rgb (235 , 27 , 27 );
194+ animation : shake1 0.5s ;
195+ }
191196}
192197
193198.tag {
@@ -216,25 +221,15 @@ ul {
216221 animation : shake 1s ;
217222}
218223
219- .duplicate input {
220- outline : rgb (235 , 27 , 27 );
221- border : 1px solid rgb (235 , 27 , 27 );
222- animation : shake1 0.5s ;
223- }
224-
225224input {
226- width : 100% ;
227- padding : 10px ;
225+ all : unset ;
228226}
229227
230228.count {
231- position : absolute ;
232- top : 50% ;
233- transform : translateY (-50% );
234- right : 10px ;
235- display : block ;
236229 font-size : 0.8rem ;
237230 white-space : nowrap ;
231+ flex-grow : 1 ;
232+ text-align : end ;
238233}
239234
240235.count span {
@@ -243,51 +238,52 @@ input {
243238 border-radius : 2px ;
244239}
245240
246- .with-count input {
247- padding-right : 60px ;
248- }
249-
250- .with-count ul {
251- max-width : 60% ;
252- }
253-
254241.err {
255242 color : red ;
256243}
257244
258245@keyframes shake {
246+
259247 10% ,
260248 90% {
261249 transform : scale (0.9 ) translate3d (-1px , 0 , 0 );
262250 }
251+
263252 20% ,
264253 80% {
265254 transform : scale (0.9 ) translate3d (2px , 0 , 0 );
266255 }
256+
267257 30% ,
268258 50% ,
269259 70% {
270260 transform : scale (0.9 ) translate3d (-4px , 0 , 0 );
271261 }
262+
272263 40% ,
273264 60% {
274265 transform : scale (0.9 ) translate3d (4px , 0 , 0 );
275266 }
276267}
268+
277269@keyframes shake1 {
270+
278271 10% ,
279272 90% {
280273 transform : scale (0.99 ) translate3d (-1px , 0 , 0 );
281274 }
275+
282276 20% ,
283277 80% {
284278 transform : scale (0.98 ) translate3d (2px , 0 , 0 );
285279 }
280+
286281 30% ,
287282 50% ,
288283 70% {
289284 transform : scale (1 ) translate3d (-4px , 0 , 0 );
290285 }
286+
291287 40% ,
292288 60% {
293289 transform : scale (0.98 ) translate3d (4px , 0 , 0 );
0 commit comments