add cheap autocomplete for tags
This commit is contained in:
parent
f303a78631
commit
df7584f19d
4 changed files with 135 additions and 13 deletions
|
|
@ -23,7 +23,7 @@
|
|||
<!-- TODO: autocomplete tags -->
|
||||
<td><label>Tags</label></td>
|
||||
<td>
|
||||
<tags-input v-model="tags" />
|
||||
<tags-input v-model="tags" :autocompleteTags="autocompleteTags" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -54,6 +54,9 @@ export default defineComponent({
|
|||
type: Object as PropType<Image>,
|
||||
required: true,
|
||||
},
|
||||
autocompleteTags: {
|
||||
type: Function,
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
bgclick: null,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ gallery", if possible!
|
|||
<!-- TODO: autocomplete tags -->
|
||||
<td><label>Tags</label></td>
|
||||
<td>
|
||||
<tags-input v-model="tags" />
|
||||
<tags-input v-model="tags" :autocompleteTags="autocompleteTags" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -62,6 +62,11 @@ export default defineComponent({
|
|||
ResponsiveImage,
|
||||
TagsInput,
|
||||
},
|
||||
props: {
|
||||
autocompleteTags: {
|
||||
type: Function,
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
bgclick: null,
|
||||
setupGameClick: null,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,42 @@
|
|||
<template>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
import { Tag } from '../../common/Types'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'tags-input',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array as PropType<Array<string>>,
|
||||
type: Array as PropType<string[]>,
|
||||
required: true,
|
||||
},
|
||||
autocompleteTags: {
|
||||
type: Function,
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
'update:modelValue': null,
|
||||
|
|
@ -21,7 +44,11 @@ export default defineComponent({
|
|||
data () {
|
||||
return {
|
||||
input: '',
|
||||
values: [] as Array<string>,
|
||||
values: [] as string[],
|
||||
autocomplete: {
|
||||
idx: -1,
|
||||
values: [] as string[],
|
||||
},
|
||||
}
|
||||
},
|
||||
created () {
|
||||
|
|
@ -29,14 +56,39 @@ export default defineComponent({
|
|||
},
|
||||
methods: {
|
||||
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 === ',') {
|
||||
this.add()
|
||||
ev.stopPropagation()
|
||||
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 () {
|
||||
const newval = this.input.replace(/,/g, '').trim()
|
||||
addVal (value: string) {
|
||||
const newval = value.replace(/,/g, '').trim()
|
||||
if (!newval) {
|
||||
return
|
||||
}
|
||||
|
|
@ -44,7 +96,16 @@ export default defineComponent({
|
|||
this.values.push(newval)
|
||||
}
|
||||
this.input = ''
|
||||
this.autocomplete.values = []
|
||||
this.autocomplete.idx = -1
|
||||
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) {
|
||||
this.values = this.values.filter(v => v !== val)
|
||||
|
|
@ -57,4 +118,31 @@ export default defineComponent({
|
|||
.input {
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -31,15 +31,32 @@ in jigsawpuzzles.io
|
|||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<image-library :images="images" @imageClicked="onImageClicked" @imageEditClicked="onImageEditClicked" />
|
||||
<new-image-dialog v-if="dialog==='new-image'" @bgclick="dialog=''" @postToGalleryClick="postToGalleryClick" @setupGameClick="setupGameClick" />
|
||||
<edit-image-dialog v-if="dialog==='edit-image'" @bgclick="dialog=''" @saveClick="onSaveImageClick" :image="image" />
|
||||
<new-game-dialog v-if="image && dialog==='new-game'" @bgclick="dialog=''" @newGame="onNewGame" :image="image" />
|
||||
<image-library
|
||||
:images="images"
|
||||
@imageClicked="onImageClicked"
|
||||
@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>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { defineComponent, PropType } from 'vue'
|
||||
|
||||
import ImageLibrary from './../components/ImageLibrary.vue'
|
||||
import NewImageDialog from './../components/NewImageDialog.vue'
|
||||
|
|
@ -62,7 +79,7 @@ export default defineComponent({
|
|||
tags: [] as string[],
|
||||
},
|
||||
images: [],
|
||||
tags: [],
|
||||
tags: [] as Tag[],
|
||||
|
||||
image: {
|
||||
id: 0,
|
||||
|
|
@ -81,6 +98,15 @@ export default defineComponent({
|
|||
await this.loadImages()
|
||||
},
|
||||
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) {
|
||||
if (this.filters.tags.includes(t.slug)) {
|
||||
this.filters.tags = this.filters.tags.filter(slug => slug !== t.slug)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue