Sharkey/src/client/app/common/views/components/autocomplete.vue

358 lines
7.8 KiB
Vue
Raw Normal View History

2018-02-20 03:29:52 -08:00
<template>
2018-02-24 08:55:49 -08:00
<div class="mk-autocomplete" @contextmenu.prevent="() => {}">
<ol class="users" ref="suggests" v-if="users.length > 0">
<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1">
2018-07-24 07:43:14 -07:00
<img class="avatar" :src="user.avatarUrl" alt=""/>
2018-04-09 02:52:29 -07:00
<span class="name">{{ user | userName }}</span>
<span class="username">@{{ user | acct }}</span>
2018-02-20 03:29:52 -08:00
</li>
</ol>
2018-07-17 15:19:24 -07:00
<ol class="hashtags" ref="suggests" v-if="hashtags.length > 0">
<li v-for="hashtag in hashtags" @click="complete(type, hashtag)" @keydown="onKeydown" tabindex="-1">
<span class="name">{{ hashtag }}</span>
</li>
</ol>
2018-02-24 08:55:49 -08:00
<ol class="emojis" ref="suggests" v-if="emojis.length > 0">
2018-02-25 00:03:39 -08:00
<li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1">
<span class="emoji">{{ emoji.emoji }}</span>
<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span>
<span class="alias" v-if="emoji.alias">({{ emoji.alias }})</span>
2018-02-24 08:55:49 -08:00
</li>
</ol>
2018-02-20 03:29:52 -08:00
</div>
</template>
<script lang="ts">
import Vue from 'vue';
2018-02-25 00:03:39 -08:00
import * as emojilib from 'emojilib';
2018-02-20 03:29:52 -08:00
import contains from '../../../common/scripts/contains';
2018-02-25 00:03:39 -08:00
const lib = Object.entries(emojilib.lib).filter((x: any) => {
return x[1].category != 'flags';
});
2018-04-09 02:52:29 -07:00
2018-02-25 00:03:39 -08:00
const emjdb = lib.map((x: any) => ({
emoji: x[1].char,
name: x[0],
alias: null
}));
2018-04-09 02:52:29 -07:00
2018-02-25 00:03:39 -08:00
lib.forEach((x: any) => {
if (x[1].keywords) {
x[1].keywords.forEach(k => {
emjdb.push({
emoji: x[1].char,
name: k,
alias: x[0]
});
});
}
});
2018-04-09 02:52:29 -07:00
2018-02-25 00:03:39 -08:00
emjdb.sort((a, b) => a.name.length - b.name.length);
2018-02-20 03:29:52 -08:00
export default Vue.extend({
2018-02-24 09:57:19 -08:00
props: ['type', 'q', 'textarea', 'complete', 'close', 'x', 'y'],
2018-07-17 15:19:24 -07:00
2018-02-20 03:29:52 -08:00
data() {
return {
fetching: true,
users: [],
2018-07-17 15:19:24 -07:00
hashtags: [],
2018-02-24 08:55:49 -08:00
emojis: [],
select: -1,
2018-02-25 00:03:39 -08:00
emojilib
2018-02-24 08:55:49 -08:00
}
},
2018-07-17 15:19:24 -07:00
2018-02-24 08:55:49 -08:00
computed: {
items(): HTMLCollection {
return (this.$refs.suggests as Element).children;
2018-02-20 03:29:52 -08:00
}
},
2018-07-17 15:19:24 -07:00
2018-02-24 09:57:19 -08:00
updated() {
//#region 位置調整
2018-07-17 15:19:24 -07:00
if (this.x + this.$el.offsetWidth > window.innerWidth) {
this.$el.style.left = (window.innerWidth - this.$el.offsetWidth) + 'px';
2018-02-24 09:57:19 -08:00
} else {
this.$el.style.left = this.x + 'px';
}
2018-07-17 15:19:24 -07:00
if (this.y + this.$el.offsetHeight > window.innerHeight) {
2018-02-24 09:57:19 -08:00
this.$el.style.top = (this.y - this.$el.offsetHeight) + 'px';
this.$el.style.marginTop = '0';
} else {
this.$el.style.top = this.y + 'px';
this.$el.style.marginTop = 'calc(1em + 8px)';
}
//#endregion
},
2018-07-17 15:19:24 -07:00
2018-02-20 03:29:52 -08:00
mounted() {
this.textarea.addEventListener('keydown', this.onKeydown);
Array.from(document.querySelectorAll('body *')).forEach(el => {
el.addEventListener('mousedown', this.onMousedown);
});
2018-02-25 01:40:39 -08:00
this.$nextTick(() => {
this.exec();
2018-02-25 00:38:16 -08:00
2018-02-25 01:40:39 -08:00
this.$watch('q', () => {
this.$nextTick(() => {
this.exec();
});
});
});
2018-02-20 03:29:52 -08:00
},
2018-07-17 15:19:24 -07:00
2018-02-20 03:29:52 -08:00
beforeDestroy() {
this.textarea.removeEventListener('keydown', this.onKeydown);
Array.from(document.querySelectorAll('body *')).forEach(el => {
el.removeEventListener('mousedown', this.onMousedown);
});
},
2018-07-17 15:19:24 -07:00
2018-02-20 03:29:52 -08:00
methods: {
2018-02-25 00:38:16 -08:00
exec() {
2018-02-25 01:40:39 -08:00
this.select = -1;
if (this.$refs.suggests) {
Array.from(this.items).forEach(el => {
el.removeAttribute('data-selected');
});
}
2018-02-25 00:38:16 -08:00
if (this.type == 'user') {
2018-07-17 15:19:24 -07:00
const cacheKey = 'autocomplete:user:' + this.q;
const cache = sessionStorage.getItem(cacheKey);
2018-02-25 00:38:16 -08:00
if (cache) {
const users = JSON.parse(cache);
this.users = users;
this.fetching = false;
} else {
(this as any).api('users/search_by_username', {
query: this.q,
limit: 30
}).then(users => {
this.users = users;
this.fetching = false;
// キャッシュ
2018-07-17 15:19:24 -07:00
sessionStorage.setItem(cacheKey, JSON.stringify(users));
});
}
} else if (this.type == 'hashtag') {
2018-07-20 11:15:31 -07:00
if (this.q == null || this.q == '') {
this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]');
2018-07-17 15:19:24 -07:00
this.fetching = false;
} else {
2018-07-20 11:15:31 -07:00
const cacheKey = 'autocomplete:hashtag:' + this.q;
const cache = sessionStorage.getItem(cacheKey);
if (cache) {
const hashtags = JSON.parse(cache);
2018-07-17 15:19:24 -07:00
this.hashtags = hashtags;
this.fetching = false;
2018-07-20 11:15:31 -07:00
} else {
(this as any).api('hashtags/search', {
query: this.q,
limit: 30
}).then(hashtags => {
this.hashtags = hashtags;
this.fetching = false;
// キャッシュ
sessionStorage.setItem(cacheKey, JSON.stringify(hashtags));
});
}
2018-02-25 00:38:16 -08:00
}
} else if (this.type == 'emoji') {
const matched = [];
emjdb.some(x => {
2018-02-26 21:11:18 -08:00
if (x.name.indexOf(this.q) == 0 && !x.alias && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
2018-02-25 00:38:16 -08:00
return matched.length == 30;
});
2018-02-26 21:11:18 -08:00
if (matched.length < 30) {
emjdb.some(x => {
if (x.name.indexOf(this.q) == 0 && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
return matched.length == 30;
});
}
if (matched.length < 30) {
emjdb.some(x => {
if (x.name.indexOf(this.q) > -1 && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
return matched.length == 30;
});
}
2018-02-25 00:38:16 -08:00
this.emojis = matched;
}
},
2018-02-20 03:29:52 -08:00
onMousedown(e) {
if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close();
},
onKeydown(e) {
const cancel = () => {
e.preventDefault();
e.stopPropagation();
};
switch (e.which) {
case 10: // [ENTER]
case 13: // [ENTER]
if (this.select !== -1) {
cancel();
2018-02-24 08:55:49 -08:00
(this.items[this.select] as any).click();
2018-02-20 03:29:52 -08:00
} else {
this.close();
}
break;
case 27: // [ESC]
cancel();
this.close();
break;
case 38: // [↑]
if (this.select !== -1) {
cancel();
this.selectPrev();
} else {
this.close();
}
break;
case 9: // [TAB]
case 40: // [↓]
cancel();
this.selectNext();
break;
2018-02-25 01:40:39 -08:00
default:
e.stopPropagation();
this.textarea.focus();
2018-02-20 03:29:52 -08:00
}
},
selectNext() {
2018-02-24 08:55:49 -08:00
if (++this.select >= this.items.length) this.select = 0;
2018-02-20 03:29:52 -08:00
this.applySelect();
},
selectPrev() {
2018-02-24 08:55:49 -08:00
if (--this.select < 0) this.select = this.items.length - 1;
2018-02-20 03:29:52 -08:00
this.applySelect();
},
applySelect() {
2018-02-24 08:55:49 -08:00
Array.from(this.items).forEach(el => {
2018-02-20 03:29:52 -08:00
el.removeAttribute('data-selected');
});
2018-02-24 08:55:49 -08:00
this.items[this.select].setAttribute('data-selected', 'true');
(this.items[this.select] as any).focus();
2018-02-20 03:29:52 -08:00
}
}
});
</script>
<style lang="stylus" scoped>
2018-03-02 20:47:55 -08:00
@import '~const.styl'
root(isDark)
2018-02-24 09:57:19 -08:00
position fixed
2018-02-20 03:29:52 -08:00
z-index 65535
2018-07-18 11:25:37 -07:00
max-width 100%
2018-02-20 03:29:52 -08:00
margin-top calc(1em + 8px)
overflow hidden
background isDark ? #313543 : #fff
2018-04-28 16:51:17 -07:00
border solid 1px rgba(#000, 0.1)
2018-02-20 03:29:52 -08:00
border-radius 4px
2018-02-25 00:38:16 -08:00
transition top 0.1s ease, left 0.1s ease
2018-02-20 03:29:52 -08:00
2018-02-24 08:55:49 -08:00
> ol
2018-02-20 03:29:52 -08:00
display block
margin 0
padding 4px 0
max-height 190px
max-width 500px
overflow auto
list-style none
> li
2018-07-18 12:50:46 -07:00
display flex
align-items center
2018-02-20 03:29:52 -08:00
padding 4px 12px
white-space nowrap
overflow hidden
font-size 0.9em
2018-04-28 16:51:17 -07:00
color rgba(#000, 0.8)
2018-02-20 03:29:52 -08:00
cursor default
&, *
user-select none
2018-07-18 12:50:46 -07:00
*
overflow hidden
text-overflow ellipsis
2018-02-20 03:29:52 -08:00
&:hover
2018-07-17 15:19:24 -07:00
background isDark ? rgba(#fff, 0.1) : rgba(#000, 0.1)
2018-02-20 03:29:52 -08:00
&[data-selected='true']
background $theme-color
2018-02-24 08:55:49 -08:00
&, *
color #fff !important
2018-02-20 03:29:52 -08:00
&:active
background darken($theme-color, 10%)
2018-02-24 08:55:49 -08:00
&, *
color #fff !important
> .users > li
.avatar
min-width 28px
min-height 28px
max-width 28px
max-height 28px
margin 0 8px 0 0
border-radius 100%
.name
margin 0 8px 0 0
color isDark ? rgba(#fff, 0.8) : rgba(#000, 0.8)
2018-02-24 08:55:49 -08:00
.username
color isDark ? rgba(#fff, 0.3) : rgba(#000, 0.3)
2018-02-24 08:55:49 -08:00
2018-07-17 15:19:24 -07:00
> .hashtags > li
.name
color isDark ? rgba(#fff, 0.8) : rgba(#000, 0.8)
2018-02-24 08:55:49 -08:00
> .emojis > li
.emoji
display inline-block
margin 0 4px 0 0
width 24px
.name
2018-07-17 15:19:24 -07:00
color isDark ? rgba(#fff, 0.8) : rgba(#000, 0.8)
2018-02-20 03:29:52 -08:00
2018-02-25 00:03:39 -08:00
.alias
margin 0 0 0 8px
2018-07-17 15:19:24 -07:00
color isDark ? rgba(#fff, 0.3) : rgba(#000, 0.3)
2018-02-25 00:03:39 -08:00
.mk-autocomplete[data-darkmode]
root(true)
.mk-autocomplete:not([data-darkmode])
root(false)
2018-02-20 03:29:52 -08:00
</style>