add cheap autocomplete for tags

This commit is contained in:
Zutatensuppe 2021-06-03 23:30:08 +02:00
parent f303a78631
commit df7584f19d
4 changed files with 135 additions and 13 deletions

View file

@ -23,7 +23,7 @@
<!-- TODO: autocomplete tags --> <!-- TODO: autocomplete tags -->
<td><label>Tags</label></td> <td><label>Tags</label></td>
<td> <td>
<tags-input v-model="tags" /> <tags-input v-model="tags" :autocompleteTags="autocompleteTags" />
</td> </td>
</tr> </tr>
</table> </table>
@ -54,6 +54,9 @@ export default defineComponent({
type: Object as PropType<Image>, type: Object as PropType<Image>,
required: true, required: true,
}, },
autocompleteTags: {
type: Function,
},
}, },
emits: { emits: {
bgclick: null, bgclick: null,

View file

@ -36,7 +36,7 @@ gallery", if possible!
<!-- TODO: autocomplete tags --> <!-- TODO: autocomplete tags -->
<td><label>Tags</label></td> <td><label>Tags</label></td>
<td> <td>
<tags-input v-model="tags" /> <tags-input v-model="tags" :autocompleteTags="autocompleteTags" />
</td> </td>
</tr> </tr>
</table> </table>
@ -62,6 +62,11 @@ export default defineComponent({
ResponsiveImage, ResponsiveImage,
TagsInput, TagsInput,
}, },
props: {
autocompleteTags: {
type: Function,
},
},
emits: { emits: {
bgclick: null, bgclick: null,
setupGameClick: null, setupGameClick: null,

View file

@ -1,19 +1,42 @@
<template> <template>
<div> <div>
<input class="input" type="text" v-model="input" placeholder="Plants, People" @keydown.enter="add" @keyup="onKeyUp" /> <input
ref="input"
class="input"
type="text"
v-model="input"
placeholder="Plants, People"
@change="onChange"
@keydown.enter="add"
@keyup="onKeyUp"
/>
<div v-if="autocomplete.values" class="autocomplete">
<ul>
<li
v-for="(val,idx) in autocomplete.values"
:key="idx"
:class="{active: idx===autocomplete.idx}"
@click="addVal(val)"
>{{val}}</li>
</ul>
</div>
<span v-for="(tag,idx) in values" :key="idx" class="bit" @click="rm(tag)">{{tag}} </span> <span v-for="(tag,idx) in values" :key="idx" class="bit" @click="rm(tag)">{{tag}} </span>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue' import { defineComponent, PropType } from 'vue'
import { Tag } from '../../common/Types'
export default defineComponent({ export default defineComponent({
name: 'tags-input', name: 'tags-input',
props: { props: {
modelValue: { modelValue: {
type: Array as PropType<Array<string>>, type: Array as PropType<string[]>,
required: true, required: true,
}, },
autocompleteTags: {
type: Function,
},
}, },
emits: { emits: {
'update:modelValue': null, 'update:modelValue': null,
@ -21,7 +44,11 @@ export default defineComponent({
data () { data () {
return { return {
input: '', input: '',
values: [] as Array<string>, values: [] as string[],
autocomplete: {
idx: -1,
values: [] as string[],
},
} }
}, },
created () { created () {
@ -29,14 +56,39 @@ export default defineComponent({
}, },
methods: { methods: {
onKeyUp (ev: KeyboardEvent) { onKeyUp (ev: KeyboardEvent) {
if (ev.key === 'ArrowDown' && this.autocomplete.values.length > 0) {
if (this.autocomplete.idx < this.autocomplete.values.length - 1) {
this.autocomplete.idx++
}
ev.stopPropagation()
return false
}
if (ev.key === 'ArrowUp' && this.autocomplete.values.length > 0) {
if (this.autocomplete.idx > 0) {
this.autocomplete.idx--
}
ev.stopPropagation()
return false
}
if (ev.key === ',') { if (ev.key === ',') {
this.add() this.add()
ev.stopPropagation() ev.stopPropagation()
return false return false
} }
if (this.input && this.autocompleteTags) {
this.autocomplete.values = this.autocompleteTags(
this.input,
this.values
)
this.autocomplete.idx = -1
} else {
this.autocomplete.values = []
this.autocomplete.idx = -1
}
}, },
add () { addVal (value: string) {
const newval = this.input.replace(/,/g, '').trim() const newval = value.replace(/,/g, '').trim()
if (!newval) { if (!newval) {
return return
} }
@ -44,7 +96,16 @@ export default defineComponent({
this.values.push(newval) this.values.push(newval)
} }
this.input = '' this.input = ''
this.autocomplete.values = []
this.autocomplete.idx = -1
this.$emit('update:modelValue', this.values) this.$emit('update:modelValue', this.values)
;(this.$refs.input as HTMLInputElement).focus()
},
add () {
const value = this.autocomplete.idx >= 0
? this.autocomplete.values[this.autocomplete.idx]
: this.input
this.addVal(value)
}, },
rm (val: string) { rm (val: string) {
this.values = this.values.filter(v => v !== val) this.values = this.values.filter(v => v !== val)
@ -57,4 +118,31 @@ export default defineComponent({
.input { .input {
margin-bottom: .5em; margin-bottom: .5em;
} }
.autocomplete {
position: relative;
}
.autocomplete ul { list-style: none;
padding: 0;
margin: 0;
position: absolute;
left: 0;
right: 0;
background: #333230;
top: -.5em;
}
.autocomplete ul li {
position: relative;
padding: .5em .5em .5em 1.5em;
cursor: pointer;
}
.autocomplete ul li.active {
color: var(--link-hover-color);
background: var(--input-bg-color);
}
.autocomplete ul li.active:before {
content: '▶';
display: block;
position: absolute;
left: .5em;
}
</style> </style>

View file

@ -31,15 +31,32 @@ in jigsawpuzzles.io
</select> </select>
</label> </label>
</div> </div>
<image-library :images="images" @imageClicked="onImageClicked" @imageEditClicked="onImageEditClicked" /> <image-library
<new-image-dialog v-if="dialog==='new-image'" @bgclick="dialog=''" @postToGalleryClick="postToGalleryClick" @setupGameClick="setupGameClick" /> :images="images"
<edit-image-dialog v-if="dialog==='edit-image'" @bgclick="dialog=''" @saveClick="onSaveImageClick" :image="image" /> @imageClicked="onImageClicked"
<new-game-dialog v-if="image && dialog==='new-game'" @bgclick="dialog=''" @newGame="onNewGame" :image="image" /> @imageEditClicked="onImageEditClicked" />
<new-image-dialog
v-if="dialog==='new-image'"
:autocompleteTags="autocompleteTags"
@bgclick="dialog=''"
@postToGalleryClick="postToGalleryClick"
@setupGameClick="setupGameClick" />
<edit-image-dialog
v-if="dialog==='edit-image'"
:autocompleteTags="autocompleteTags"
@bgclick="dialog=''"
@saveClick="onSaveImageClick"
:image="image" />
<new-game-dialog
v-if="image && dialog==='new-game'"
@bgclick="dialog=''"
@newGame="onNewGame"
:image="image" />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue' import { defineComponent, PropType } from 'vue'
import ImageLibrary from './../components/ImageLibrary.vue' import ImageLibrary from './../components/ImageLibrary.vue'
import NewImageDialog from './../components/NewImageDialog.vue' import NewImageDialog from './../components/NewImageDialog.vue'
@ -62,7 +79,7 @@ export default defineComponent({
tags: [] as string[], tags: [] as string[],
}, },
images: [], images: [],
tags: [], tags: [] as Tag[],
image: { image: {
id: 0, id: 0,
@ -81,6 +98,15 @@ export default defineComponent({
await this.loadImages() await this.loadImages()
}, },
methods: { methods: {
autocompleteTags (input: string, exclude: string[]): string[] {
return this.tags
.filter((tag: Tag) => {
return !exclude.includes(tag.title)
&& tag.title.toLowerCase().startsWith(input.toLowerCase())
})
.slice(0, 10)
.map((tag: Tag) => tag.title)
},
toggleTag (t: Tag) { toggleTag (t: Tag) {
if (this.filters.tags.includes(t.slug)) { if (this.filters.tags.includes(t.slug)) {
this.filters.tags = this.filters.tags.filter(slug => slug !== t.slug) this.filters.tags = this.filters.tags.filter(slug => slug !== t.slug)