refactor: Composition APIへ移行 (#8121)

* components/abuse-report-window.vue

* use <script setup>

* ✌️

* components/analog-clock.vue

* wip components/autocomplete.vue

* ✌️

* ✌️

* fix

* wip components/captcha.vue

* clean up

* components/channel-follow-button

* components/channel-preview.vue

* components/core-core.vue

* components/code.vue

* wip components/date-separated-list.vue

* fix

* fix autocomplete.vue

* ✌️

* remove global property

* use <script setup>

* components/dialog.vue

* clena up

* fix dialog.vue

* Resolve https://github.com/misskey-dev/misskey/pull/8121#discussion_r781250966
This commit is contained in:
tamaina 2022-01-11 00:05:18 +09:00 committed by GitHub
parent 06125e6820
commit 8855a5fffb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 614 additions and 756 deletions

View file

@ -1,5 +1,5 @@
<template>
<div class="swhvrteh _popup _shadow" :style="{ zIndex }" @contextmenu.prevent="() => {}">
<div ref="rootEl" class="swhvrteh _popup _shadow" :style="{ zIndex }" @contextmenu.prevent="() => {}">
<ol v-if="type === 'user'" ref="suggests" class="users">
<li v-for="user in users" tabindex="-1" class="user" @click="complete(type, user)" @keydown="onKeydown">
<img class="avatar" :src="user.avatarUrl"/>
@ -8,7 +8,7 @@
</span>
<span class="username">@{{ acct(user) }}</span>
</li>
<li tabindex="-1" class="choose" @click="chooseUser()" @keydown="onKeydown">{{ $ts.selectUser }}</li>
<li tabindex="-1" class="choose" @click="chooseUser()" @keydown="onKeydown">{{ i18n.locale.selectUser }}</li>
</ol>
<ol v-else-if="hashtags.length > 0" ref="suggests" class="hashtags">
<li v-for="hashtag in hashtags" tabindex="-1" @click="complete(type, hashtag)" @keydown="onKeydown">
@ -17,8 +17,8 @@
</ol>
<ol v-else-if="emojis.length > 0" ref="suggests" class="emojis">
<li v-for="emoji in emojis" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown">
<span v-if="emoji.isCustomEmoji" class="emoji"><img :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
<span v-else-if="!$store.state.useOsNativeEmojis" class="emoji"><img :src="emoji.url" :alt="emoji.emoji"/></span>
<span v-if="emoji.isCustomEmoji" class="emoji"><img :src="defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
<span v-else-if="!defaultStore.state.useOsNativeEmojis" class="emoji"><img :src="emoji.url" :alt="emoji.emoji"/></span>
<span v-else class="emoji">{{ emoji.emoji }}</span>
<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span>
<span v-if="emoji.aliasOf" class="alias">({{ emoji.aliasOf }})</span>
@ -33,15 +33,17 @@
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
import { emojilist } from '@/scripts/emojilist';
import { markRaw, ref, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import contains from '@/scripts/contains';
import { twemojiSvgBase } from '@/scripts/twemoji-base';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import { acct } from '@/filters/user';
import * as os from '@/os';
import { instance } from '@/instance';
import { MFM_TAGS } from '@/scripts/mfm-tags';
import { defaultStore } from '@/store';
import { emojilist } from '@/scripts/emojilist';
import { instance } from '@/instance';
import { twemojiSvgBase } from '@/scripts/twemoji-base';
import { i18n } from '@/i18n';
type EmojiDef = {
emoji: string;
@ -54,16 +56,14 @@ type EmojiDef = {
const lib = emojilist.filter(x => x.category !== 'flags');
const char2file = (char: string) => {
let codes = Array.from(char).map(x => x.codePointAt(0).toString(16));
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('-');
return codes.filter(x => x && x.length).join('-');
};
const emjdb: EmojiDef[] = lib.map(x => ({
emoji: x.char,
name: x.name,
aliasOf: null,
url: `${twemojiSvgBase}/${char2file(x.char)}.svg`
}));
@ -112,291 +112,270 @@ emojiDefinitions.sort((a, b) => a.name.length - b.name.length);
const emojiDb = markRaw(emojiDefinitions.concat(emjdb));
//#endregion
export default defineComponent({
props: {
type: {
type: String,
required: true,
},
export default {
emojiDb,
emojiDefinitions,
emojilist,
customEmojis,
};
</script>
q: {
type: String,
required: false,
},
<script lang="ts" setup>
const props = defineProps<{
type: string;
q: string | null;
textarea: HTMLTextAreaElement;
close: () => void;
x: number;
y: number;
}>();
textarea: {
type: HTMLTextAreaElement,
required: true,
},
const emit = defineEmits<{
(e: 'done', v: { type: string; value: any }): void;
(e: 'closed'): void;
}>();
close: {
type: Function,
required: true,
},
const suggests = ref<Element>();
const rootEl = ref<HTMLDivElement>();
x: {
type: Number,
required: true,
},
const fetching = ref(true);
const users = ref<any[]>([]);
const hashtags = ref<any[]>([]);
const emojis = ref<(EmojiDef)[]>([]);
const items = ref<Element[] | HTMLCollection>([]);
const mfmTags = ref<string[]>([]);
const select = ref(-1);
const zIndex = os.claimZIndex('high');
y: {
type: Number,
required: true,
},
},
function complete(type: string, value: any) {
emit('done', { type, value });
emit('closed');
if (type === 'emoji') {
let recents = defaultStore.state.recentlyUsedEmojis;
recents = recents.filter((e: any) => e !== value);
recents.unshift(value);
defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
}
}
emits: ['done', 'closed'],
function setPosition() {
if (!rootEl.value) return;
if (props.x + rootEl.value.offsetWidth > window.innerWidth) {
rootEl.value.style.left = (window.innerWidth - rootEl.value.offsetWidth) + 'px';
} else {
rootEl.value.style.left = `${props.x}px`;
}
if (props.y + rootEl.value.offsetHeight > window.innerHeight) {
rootEl.value.style.top = (props.y - rootEl.value.offsetHeight) + 'px';
rootEl.value.style.marginTop = '0';
} else {
rootEl.value.style.top = props.y + 'px';
rootEl.value.style.marginTop = 'calc(1em + 8px)';
}
}
data() {
return {
getStaticImageUrl,
fetching: true,
users: [],
hashtags: [],
emojis: [],
items: [],
mfmTags: [],
select: -1,
zIndex: os.claimZIndex('high'),
function exec() {
select.value = -1;
if (suggests.value) {
for (const el of Array.from(items.value)) {
el.removeAttribute('data-selected');
}
},
updated() {
this.setPosition();
this.items = (this.$refs.suggests as Element | undefined)?.children || [];
},
mounted() {
this.setPosition();
this.textarea.addEventListener('keydown', this.onKeydown);
for (const el of Array.from(document.querySelectorAll('body *'))) {
el.addEventListener('mousedown', this.onMousedown);
}
if (props.type === 'user') {
if (!props.q) {
users.value = [];
fetching.value = false;
return;
}
this.$nextTick(() => {
this.exec();
const cacheKey = `autocomplete:user:${props.q}`;
const cache = sessionStorage.getItem(cacheKey);
this.$watch('q', () => {
this.$nextTick(() => {
this.exec();
if (cache) {
const users = JSON.parse(cache);
users.value = users;
fetching.value = false;
} else {
os.api('users/search-by-username-and-host', {
username: props.q,
limit: 10,
detail: false
}).then(searchedUsers => {
users.value = searchedUsers as any[];
fetching.value = false;
//
sessionStorage.setItem(cacheKey, JSON.stringify(searchedUsers));
});
}
} else if (props.type === 'hashtag') {
if (!props.q || props.q == '') {
hashtags.value = JSON.parse(localStorage.getItem('hashtags') || '[]');
fetching.value = false;
} else {
const cacheKey = `autocomplete:hashtag:${props.q}`;
const cache = sessionStorage.getItem(cacheKey);
if (cache) {
const hashtags = JSON.parse(cache);
hashtags.value = hashtags;
fetching.value = false;
} else {
os.api('hashtags/search', {
query: props.q,
limit: 30
}).then(searchedHashtags => {
hashtags.value = searchedHashtags as any[];
fetching.value = false;
//
sessionStorage.setItem(cacheKey, JSON.stringify(searchedHashtags));
});
}
}
} else if (props.type === 'emoji') {
if (!props.q || props.q == '') {
// 使
emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.find(e => e.emoji == emoji)).filter(x => x) as EmojiDef[];
return;
}
const matched: EmojiDef[] = [];
const max = 30;
emojiDb.some(x => {
if (x.name.startsWith(props.q || '') && !x.aliasOf && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
return matched.length == max;
});
if (matched.length < max) {
emojiDb.some(x => {
if (x.name.startsWith(props.q || '') && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
return matched.length == max;
});
}
if (matched.length < max) {
emojiDb.some(x => {
if (x.name.includes(props.q || '') && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
return matched.length == max;
});
}
emojis.value = matched;
} else if (props.type === 'mfmTag') {
if (!props.q || props.q == '') {
mfmTags.value = MFM_TAGS;
return;
}
mfmTags.value = MFM_TAGS.filter(tag => tag.startsWith(props.q || ''));
}
}
function onMousedown(e: Event) {
if (!contains(rootEl.value, e.target) && (rootEl.value != e.target)) props.close();
}
function onKeydown(e: KeyboardEvent) {
const cancel = () => {
e.preventDefault();
e.stopPropagation();
};
switch (e.key) {
case 'Enter':
if (select.value !== -1) {
cancel();
(items.value[select.value] as any).click();
} else {
props.close();
}
break;
case 'Escape':
cancel();
props.close();
break;
case 'ArrowUp':
if (select.value !== -1) {
cancel();
selectPrev();
} else {
props.close();
}
break;
case 'Tab':
case 'ArrowDown':
cancel();
selectNext();
break;
default:
e.stopPropagation();
props.textarea.focus();
}
}
function selectNext() {
if (++select.value >= items.value.length) select.value = 0;
if (items.value.length === 0) select.value = -1;
applySelect();
}
function selectPrev() {
if (--select.value < 0) select.value = items.value.length - 1;
applySelect();
}
function applySelect() {
for (const el of Array.from(items.value)) {
el.removeAttribute('data-selected');
}
if (select.value !== -1) {
items.value[select.value].setAttribute('data-selected', 'true');
(items.value[select.value] as any).focus();
}
}
function chooseUser() {
props.close();
os.selectUser().then(user => {
complete('user', user);
props.textarea.focus();
});
}
onUpdated(() => {
setPosition();
items.value = suggests.value?.children || [];
});
onMounted(() => {
setPosition();
props.textarea.addEventListener('keydown', onKeydown);
for (const el of Array.from(document.querySelectorAll('body *'))) {
el.addEventListener('mousedown', onMousedown);
}
nextTick(() => {
exec();
watch(() => props.q, () => {
nextTick(() => {
exec();
});
});
},
});
});
beforeUnmount() {
this.textarea.removeEventListener('keydown', this.onKeydown);
onBeforeUnmount(() => {
props.textarea.removeEventListener('keydown', onKeydown);
for (const el of Array.from(document.querySelectorAll('body *'))) {
el.removeEventListener('mousedown', this.onMousedown);
}
},
methods: {
complete(type, value) {
this.$emit('done', { type, value });
this.$emit('closed');
if (type === 'emoji') {
let recents = this.$store.state.recentlyUsedEmojis;
recents = recents.filter((e: any) => e !== value);
recents.unshift(value);
this.$store.set('recentlyUsedEmojis', recents.splice(0, 32));
}
},
setPosition() {
if (this.x + this.$el.offsetWidth > window.innerWidth) {
this.$el.style.left = (window.innerWidth - this.$el.offsetWidth) + 'px';
} else {
this.$el.style.left = this.x + 'px';
}
if (this.y + this.$el.offsetHeight > window.innerHeight) {
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)';
}
},
exec() {
this.select = -1;
if (this.$refs.suggests) {
for (const el of Array.from(this.items)) {
el.removeAttribute('data-selected');
}
}
if (this.type === 'user') {
if (this.q == null) {
this.users = [];
this.fetching = false;
return;
}
const cacheKey = `autocomplete:user:${this.q}`;
const cache = sessionStorage.getItem(cacheKey);
if (cache) {
const users = JSON.parse(cache);
this.users = users;
this.fetching = false;
} else {
os.api('users/search-by-username-and-host', {
username: this.q,
limit: 10,
detail: false
}).then(users => {
this.users = users;
this.fetching = false;
//
sessionStorage.setItem(cacheKey, JSON.stringify(users));
});
}
} else if (this.type === 'hashtag') {
if (this.q == null || this.q == '') {
this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]');
this.fetching = false;
} else {
const cacheKey = `autocomplete:hashtag:${this.q}`;
const cache = sessionStorage.getItem(cacheKey);
if (cache) {
const hashtags = JSON.parse(cache);
this.hashtags = hashtags;
this.fetching = false;
} else {
os.api('hashtags/search', {
query: this.q,
limit: 30
}).then(hashtags => {
this.hashtags = hashtags;
this.fetching = false;
//
sessionStorage.setItem(cacheKey, JSON.stringify(hashtags));
});
}
}
} else if (this.type === 'emoji') {
if (this.q == null || this.q == '') {
// 使
this.emojis = this.$store.state.recentlyUsedEmojis.map(emoji => emojiDb.find(e => e.emoji == emoji)).filter(x => x != null);
return;
}
const matched = [];
const max = 30;
emojiDb.some(x => {
if (x.name.startsWith(this.q) && !x.aliasOf && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
return matched.length == max;
});
if (matched.length < max) {
emojiDb.some(x => {
if (x.name.startsWith(this.q) && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
return matched.length == max;
});
}
if (matched.length < max) {
emojiDb.some(x => {
if (x.name.includes(this.q) && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
return matched.length == max;
});
}
this.emojis = matched;
} else if (this.type === 'mfmTag') {
if (this.q == null || this.q == '') {
this.mfmTags = MFM_TAGS;
return;
}
this.mfmTags = MFM_TAGS.filter(tag => tag.startsWith(this.q));
}
},
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();
(this.items[this.select] as any).click();
} 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;
default:
e.stopPropagation();
this.textarea.focus();
}
},
selectNext() {
if (++this.select >= this.items.length) this.select = 0;
if (this.items.length === 0) this.select = -1;
this.applySelect();
},
selectPrev() {
if (--this.select < 0) this.select = this.items.length - 1;
this.applySelect();
},
applySelect() {
for (const el of Array.from(this.items)) {
el.removeAttribute('data-selected');
}
if (this.select !== -1) {
this.items[this.select].setAttribute('data-selected', 'true');
(this.items[this.select] as any).focus();
}
},
chooseUser() {
this.close();
os.selectUser().then(user => {
this.complete('user', user);
this.textarea.focus();
});
},
acct
for (const el of Array.from(document.querySelectorAll('body *'))) {
el.removeEventListener('mousedown', onMousedown);
}
});
</script>