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 -->
|
<!-- 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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue