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

426 lines
9.7 KiB
Vue
Raw Normal View History

2018-02-20 11:29:52 +00:00
<template>
2018-02-24 16:55:49 +00: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 14:43:14 +00:00
<img class="avatar" :src="user.avatarUrl" alt=""/>
<span class="name">
2019-03-04 23:24:19 +00:00
<mk-user-name :user="user" :key="user.id"/>
</span>
2018-04-09 09:52:29 +00:00
<span class="username">@{{ user | acct }}</span>
2018-02-20 11:29:52 +00:00
</li>
</ol>
2018-07-17 22:19:24 +00: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 16:55:49 +00:00
<ol class="emojis" ref="suggests" v-if="emojis.length > 0">
2018-02-25 08:03:39 +00:00
<li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1">
<span class="emoji" v-if="emoji.isCustomEmoji"><img :src="$store.state.device.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
<span class="emoji" v-else-if="!useOsDefaultEmojis"><img :src="emoji.url" :alt="emoji.emoji"/></span>
2018-11-01 02:51:49 +00:00
<span class="emoji" v-else>{{ emoji.emoji }}</span>
2019-06-10 14:09:36 +00:00
<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span>
2018-11-01 12:28:39 +00:00
<span class="alias" v-if="emoji.aliasOf">({{ emoji.aliasOf }})</span>
2018-02-24 16:55:49 +00:00
</li>
</ol>
2018-02-20 11:29:52 +00:00
</div>
</template>
<script lang="ts">
import Vue from 'vue';
2018-02-25 08:03:39 +00:00
import * as emojilib from 'emojilib';
2018-02-20 11:29:52 +00:00
import contains from '../../../common/scripts/contains';
2019-03-18 13:02:45 +00:00
import { twemojiBase } from '../../../../../misc/twemoji-base';
import { getStaticImageUrl } from '../../../common/scripts/get-static-image-url';
2018-02-20 11:29:52 +00:00
2018-11-01 12:28:39 +00:00
type EmojiDef = {
emoji: string;
name: string;
aliasOf?: string;
url?: string;
isCustomEmoji?: boolean;
2018-11-01 12:28:39 +00:00
};
2018-02-25 08:03:39 +00:00
const lib = Object.entries(emojilib.lib).filter((x: any) => {
return x[1].category != 'flags';
});
2018-04-09 09:52:29 +00:00
const char2file = (char: string) => {
let codes = Array.from(char).map(x => x.codePointAt(0).toString(16));
if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f');
codes = codes.filter(x => x && x.length);
return codes.join('-');
};
2018-11-01 12:28:39 +00:00
const emjdb: EmojiDef[] = lib.map((x: any) => ({
emoji: x[1].char,
2018-02-25 08:03:39 +00:00
name: x[0],
2018-11-05 02:19:40 +00:00
aliasOf: null,
2019-03-18 13:02:45 +00:00
url: `${twemojiBase}/2/svg/${char2file(x[1].char)}.svg`
2018-02-25 08:03:39 +00:00
}));
2018-04-09 09:52:29 +00:00
for (const x of lib as any) {
2018-02-25 08:03:39 +00:00
if (x[1].keywords) {
for (const k of x[1].keywords) {
2018-02-25 08:03:39 +00:00
emjdb.push({
emoji: x[1].char,
2018-02-25 08:03:39 +00:00
name: k,
2018-11-05 02:19:40 +00:00
aliasOf: x[0],
2019-03-18 13:02:45 +00:00
url: `${twemojiBase}/2/svg/${char2file(x[1].char)}.svg`
2018-02-25 08:03:39 +00:00
});
}
2018-02-25 08:03:39 +00:00
}
}
2018-04-09 09:52:29 +00:00
2018-02-25 08:03:39 +00:00
emjdb.sort((a, b) => a.name.length - b.name.length);
2018-02-20 11:29:52 +00:00
export default Vue.extend({
2018-02-24 17:57:19 +00:00
props: ['type', 'q', 'textarea', 'complete', 'close', 'x', 'y'],
2018-07-17 22:19:24 +00:00
2018-02-20 11:29:52 +00:00
data() {
return {
getStaticImageUrl,
2018-02-20 11:29:52 +00:00
fetching: true,
users: [],
2018-07-17 22:19:24 +00:00
hashtags: [],
2018-02-24 16:55:49 +00:00
emojis: [],
select: -1,
2018-11-01 12:28:39 +00:00
emojilib,
emojiDb: [] as EmojiDef[]
2018-02-24 16:55:49 +00:00
}
},
2018-07-17 22:19:24 +00:00
2018-02-24 16:55:49 +00:00
computed: {
items(): HTMLCollection {
return (this.$refs.suggests as Element).children;
},
useOsDefaultEmojis(): boolean {
return this.$store.state.device.useOsDefaultEmojis;
2018-02-20 11:29:52 +00:00
}
},
2018-07-17 22:19:24 +00:00
2018-02-24 17:57:19 +00:00
updated() {
//#region 位置調整
2018-07-17 22:19:24 +00:00
if (this.x + this.$el.offsetWidth > window.innerWidth) {
this.$el.style.left = (window.innerWidth - this.$el.offsetWidth) + 'px';
2018-02-24 17:57:19 +00:00
} else {
this.$el.style.left = this.x + 'px';
}
2018-07-17 22:19:24 +00:00
if (this.y + this.$el.offsetHeight > window.innerHeight) {
2018-02-24 17:57:19 +00: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 22:19:24 +00:00
2018-02-20 11:29:52 +00:00
mounted() {
2018-11-01 12:28:39 +00:00
//#region Construct Emoji DB
2018-11-08 23:13:34 +00:00
const customEmojis = (this.$root.getMetaSync() || { emojis: [] }).emojis || [];
2018-11-01 12:28:39 +00:00
const emojiDefinitions: EmojiDef[] = [];
for (const x of customEmojis) {
2018-11-01 12:28:39 +00:00
emojiDefinitions.push({
name: x.name,
emoji: `:${x.name}:`,
url: x.url,
isCustomEmoji: true
2018-11-01 12:28:39 +00:00
});
if (x.aliases) {
for (const alias of x.aliases) {
2018-11-01 12:28:39 +00:00
emojiDefinitions.push({
name: alias,
aliasOf: x.name,
emoji: `:${x.name}:`,
url: x.url,
isCustomEmoji: true
2018-11-01 12:28:39 +00:00
});
}
2018-11-01 12:28:39 +00:00
}
}
2018-11-01 12:28:39 +00:00
emojiDefinitions.sort((a, b) => a.name.length - b.name.length);
this.emojiDb = emojiDefinitions.concat(emjdb);
//#endregion
2018-02-20 11:29:52 +00:00
this.textarea.addEventListener('keydown', this.onKeydown);
for (const el of Array.from(document.querySelectorAll('body *'))) {
2018-02-20 11:29:52 +00:00
el.addEventListener('mousedown', this.onMousedown);
}
2018-02-20 11:29:52 +00:00
2018-02-25 09:40:39 +00:00
this.$nextTick(() => {
this.exec();
2018-02-25 08:38:16 +00:00
2018-02-25 09:40:39 +00:00
this.$watch('q', () => {
this.$nextTick(() => {
this.exec();
});
});
});
2018-02-20 11:29:52 +00:00
},
2018-07-17 22:19:24 +00:00
2018-02-20 11:29:52 +00:00
beforeDestroy() {
this.textarea.removeEventListener('keydown', this.onKeydown);
for (const el of Array.from(document.querySelectorAll('body *'))) {
2018-02-20 11:29:52 +00:00
el.removeEventListener('mousedown', this.onMousedown);
}
2018-02-20 11:29:52 +00:00
},
2018-07-17 22:19:24 +00:00
2018-02-20 11:29:52 +00:00
methods: {
2018-02-25 08:38:16 +00:00
exec() {
2018-02-25 09:40:39 +00:00
this.select = -1;
if (this.$refs.suggests) {
for (const el of Array.from(this.items)) {
2018-02-25 09:40:39 +00:00
el.removeAttribute('data-selected');
}
2018-02-25 09:40:39 +00:00
}
2018-02-25 08:38:16 +00:00
if (this.type == 'user') {
2018-09-01 14:12:51 +00:00
const cacheKey = `autocomplete:user:${this.q}`;
2018-07-17 22:19:24 +00:00
const cache = sessionStorage.getItem(cacheKey);
2018-02-25 08:38:16 +00:00
if (cache) {
const users = JSON.parse(cache);
this.users = users;
this.fetching = false;
} else {
2018-11-08 23:13:34 +00:00
this.$root.api('users/search', {
2018-02-25 08:38:16 +00:00
query: this.q,
2018-12-01 21:44:18 +00:00
limit: 10,
detail: false
2018-02-25 08:38:16 +00:00
}).then(users => {
this.users = users;
this.fetching = false;
// キャッシュ
2018-07-17 22:19:24 +00:00
sessionStorage.setItem(cacheKey, JSON.stringify(users));
});
}
} else if (this.type == 'hashtag') {
2018-07-20 18:15:31 +00:00
if (this.q == null || this.q == '') {
this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]');
2018-07-17 22:19:24 +00:00
this.fetching = false;
} else {
2018-09-01 14:12:51 +00:00
const cacheKey = `autocomplete:hashtag:${this.q}`;
2018-07-20 18:15:31 +00:00
const cache = sessionStorage.getItem(cacheKey);
if (cache) {
const hashtags = JSON.parse(cache);
2018-07-17 22:19:24 +00:00
this.hashtags = hashtags;
this.fetching = false;
2018-07-20 18:15:31 +00:00
} else {
2018-11-08 23:13:34 +00:00
this.$root.api('hashtags/search', {
2018-07-20 18:15:31 +00:00
query: this.q,
limit: 30
}).then(hashtags => {
this.hashtags = hashtags;
this.fetching = false;
// キャッシュ
sessionStorage.setItem(cacheKey, JSON.stringify(hashtags));
});
}
2018-02-25 08:38:16 +00:00
}
} else if (this.type == 'emoji') {
2018-11-05 17:05:16 +00:00
if (this.q == null || this.q == '') {
this.emojis = this.emojiDb.filter(x => x.isCustomEmoji && !x.aliasOf).sort((a, b) => {
var textA = a.name.toUpperCase();
var textB = b.name.toUpperCase();
return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;
});
2018-11-05 17:05:16 +00:00
return;
}
2018-02-25 08:38:16 +00:00
const matched = [];
2018-11-01 02:51:49 +00:00
const max = 30;
2018-11-01 12:28:39 +00:00
this.emojiDb.some(x => {
if (x.name.startsWith(this.q) && !x.aliasOf && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
2018-11-01 02:51:49 +00:00
return matched.length == max;
2018-02-25 08:38:16 +00:00
});
2018-11-01 02:51:49 +00:00
if (matched.length < max) {
2018-11-01 12:28:39 +00:00
this.emojiDb.some(x => {
2018-11-01 12:01:47 +00:00
if (x.name.startsWith(this.q) && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
2018-11-01 02:51:49 +00:00
return matched.length == max;
2018-02-27 05:11:18 +00:00
});
}
2018-11-01 02:51:49 +00:00
if (matched.length < max) {
2018-11-01 12:28:39 +00:00
this.emojiDb.some(x => {
2018-11-01 12:01:47 +00:00
if (x.name.includes(this.q) && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
2018-11-01 02:51:49 +00:00
return matched.length == max;
2018-02-27 05:11:18 +00:00
});
}
2018-11-01 02:51:49 +00:00
2018-02-25 08:38:16 +00:00
this.emojis = matched;
}
},
2018-02-20 11:29:52 +00: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 16:55:49 +00:00
(this.items[this.select] as any).click();
2018-02-20 11:29:52 +00: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 09:40:39 +00:00
default:
e.stopPropagation();
this.textarea.focus();
2018-02-20 11:29:52 +00:00
}
},
selectNext() {
2018-02-24 16:55:49 +00:00
if (++this.select >= this.items.length) this.select = 0;
2018-02-20 11:29:52 +00:00
this.applySelect();
},
selectPrev() {
2018-02-24 16:55:49 +00:00
if (--this.select < 0) this.select = this.items.length - 1;
2018-02-20 11:29:52 +00:00
this.applySelect();
},
applySelect() {
for (const el of Array.from(this.items)) {
2018-02-20 11:29:52 +00:00
el.removeAttribute('data-selected');
}
2018-02-20 11:29:52 +00:00
2018-02-24 16:55:49 +00:00
this.items[this.select].setAttribute('data-selected', 'true');
(this.items[this.select] as any).focus();
2018-02-20 11:29:52 +00:00
}
}
});
</script>
<style lang="stylus" scoped>
2018-09-26 16:44:05 +00:00
.mk-autocomplete
2018-02-24 17:57:19 +00:00
position fixed
2018-02-20 11:29:52 +00:00
z-index 65535
2018-07-18 18:25:37 +00:00
max-width 100%
2018-02-20 11:29:52 +00:00
margin-top calc(1em + 8px)
overflow hidden
2018-09-26 11:28:13 +00:00
background var(--faceHeader)
2018-04-28 23:51:17 +00:00
border solid 1px rgba(#000, 0.1)
2018-02-20 11:29:52 +00:00
border-radius 4px
2018-02-25 08:38:16 +00:00
transition top 0.1s ease, left 0.1s ease
2018-02-20 11:29:52 +00:00
2018-02-24 16:55:49 +00:00
> ol
2018-02-20 11:29:52 +00:00
display block
margin 0
padding 4px 0
max-height 190px
max-width 500px
overflow auto
list-style none
> li
2018-07-18 19:50:46 +00:00
display flex
align-items center
2018-02-20 11:29:52 +00:00
padding 4px 12px
white-space nowrap
overflow hidden
font-size 0.9em
2018-04-28 23:51:17 +00:00
color rgba(#000, 0.8)
2018-02-20 11:29:52 +00:00
cursor default
&, *
user-select none
2018-07-18 19:50:46 +00:00
*
overflow hidden
text-overflow ellipsis
2018-02-20 11:29:52 +00:00
&:hover
2018-09-26 16:44:05 +00:00
background var(--autocompleteItemHoverBg)
2018-07-17 22:19:24 +00:00
2018-02-20 11:29:52 +00:00
&[data-selected='true']
2018-09-26 11:19:35 +00:00
background var(--primary)
2018-02-20 11:29:52 +00:00
2018-02-24 16:55:49 +00:00
&, *
color #fff !important
2018-02-20 11:29:52 +00:00
&:active
2018-09-26 11:19:35 +00:00
background var(--primaryDarken10)
2018-02-20 11:29:52 +00:00
2018-02-24 16:55:49 +00: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
2018-09-26 16:44:05 +00:00
color var(--autocompleteItemText)
2018-02-24 16:55:49 +00:00
.username
2018-09-26 16:44:05 +00:00
color var(--autocompleteItemTextSub)
2018-02-24 16:55:49 +00:00
2018-07-17 22:19:24 +00:00
> .hashtags > li
.name
2018-09-26 16:44:05 +00:00
color var(--autocompleteItemText)
2018-07-17 22:19:24 +00:00
2018-02-24 16:55:49 +00:00
> .emojis > li
.emoji
display inline-block
margin 0 4px 0 0
width 24px
2018-11-01 02:51:49 +00:00
> img
width 24px
vertical-align bottom
2018-02-24 16:55:49 +00:00
.name
2018-09-26 16:44:05 +00:00
color var(--autocompleteItemText)
2018-02-20 11:29:52 +00:00
2018-02-25 08:03:39 +00:00
.alias
margin 0 0 0 8px
2018-09-26 16:44:05 +00:00
color var(--autocompleteItemTextSub)
2018-02-20 11:29:52 +00:00
</style>