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:
		
							parent
							
								
									06125e6820
								
							
						
					
					
						commit
						8855a5fffb
					
				
					 11 changed files with 614 additions and 756 deletions
				
			
		|  | @ -1,8 +1,8 @@ | ||||||
| <template> | <template> | ||||||
| <XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="$emit('closed')"> | <XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')"> | ||||||
| 	<template #header> | 	<template #header> | ||||||
| 		<i class="fas fa-exclamation-circle" style="margin-right: 0.5em;"></i> | 		<i class="fas fa-exclamation-circle" style="margin-right: 0.5em;"></i> | ||||||
| 		<I18n :src="$ts.reportAbuseOf" tag="span"> | 		<I18n :src="i18n.locale.reportAbuseOf" tag="span"> | ||||||
| 			<template #name> | 			<template #name> | ||||||
| 				<b><MkAcct :user="user"/></b> | 				<b><MkAcct :user="user"/></b> | ||||||
| 			</template> | 			</template> | ||||||
|  | @ -11,65 +11,51 @@ | ||||||
| 	<div class="dpvffvvy _monolithic_"> | 	<div class="dpvffvvy _monolithic_"> | ||||||
| 		<div class="_section"> | 		<div class="_section"> | ||||||
| 			<MkTextarea v-model="comment"> | 			<MkTextarea v-model="comment"> | ||||||
| 				<template #label>{{ $ts.details }}</template> | 				<template #label>{{ i18n.locale.details }}</template> | ||||||
| 				<template #caption>{{ $ts.fillAbuseReportDescription }}</template> | 				<template #caption>{{ i18n.locale.fillAbuseReportDescription }}</template> | ||||||
| 			</MkTextarea> | 			</MkTextarea> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="_section"> | 		<div class="_section"> | ||||||
| 			<MkButton primary full :disabled="comment.length === 0" @click="send">{{ $ts.send }}</MkButton> | 			<MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.locale.send }}</MkButton> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| </XWindow> | </XWindow> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script setup lang="ts"> | ||||||
| import { defineComponent, markRaw } from 'vue'; | import { ref } from 'vue'; | ||||||
|  | import * as Misskey from 'misskey-js'; | ||||||
| import XWindow from '@/components/ui/window.vue'; | import XWindow from '@/components/ui/window.vue'; | ||||||
| import MkTextarea from '@/components/form/textarea.vue'; | import MkTextarea from '@/components/form/textarea.vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
|  | import { i18n } from '@/i18n'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | const props = defineProps<{ | ||||||
| 	components: { | 	user: Misskey.entities.User; | ||||||
| 		XWindow, | 	initialComment?: string; | ||||||
| 		MkTextarea, | }>(); | ||||||
| 		MkButton, |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	props: { | const emit = defineEmits<{ | ||||||
| 		user: { | 	(e: 'closed'): void; | ||||||
| 			type: Object, | }>(); | ||||||
| 			required: true, |  | ||||||
| 		}, |  | ||||||
| 		initialComment: { |  | ||||||
| 			type: String, |  | ||||||
| 			required: false, |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	emits: ['closed'], | const window = ref<InstanceType<typeof XWindow>>(); | ||||||
|  | const comment = ref(props.initialComment || ''); | ||||||
| 
 | 
 | ||||||
| 	data() { | function send() { | ||||||
| 		return { | 	os.apiWithDialog('users/report-abuse', { | ||||||
| 			comment: this.initialComment || '', | 		userId: props.user.id, | ||||||
| 		}; | 		comment: comment.value, | ||||||
| 	}, | 	}, undefined).then(res => { | ||||||
| 
 | 		os.alert({ | ||||||
| 	methods: { | 			type: 'success', | ||||||
| 		send() { | 			text: i18n.locale.abuseReported | ||||||
| 			os.apiWithDialog('users/report-abuse', { | 		}); | ||||||
| 				userId: this.user.id, | 		window.value?.close(); | ||||||
| 				comment: this.comment, | 		emit('closed'); | ||||||
| 			}, undefined, res => { | 	}); | ||||||
| 				os.alert({ | } | ||||||
| 					type: 'success', |  | ||||||
| 					text: this.$ts.abuseReported |  | ||||||
| 				}); |  | ||||||
| 				this.$refs.window.close(); |  | ||||||
| 			}); |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| }); |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -40,106 +40,64 @@ | ||||||
| </svg> | </svg> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { defineComponent } from 'vue'; | import { ref, computed, onMounted, onBeforeUnmount } from 'vue'; | ||||||
| import * as tinycolor from 'tinycolor2'; | import * as tinycolor from 'tinycolor2'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | withDefaults(defineProps<{ | ||||||
| 	props: { | 	thickness: number; | ||||||
| 		thickness: { | }>(), { | ||||||
| 			type: Number, | 	thickness: 0.1, | ||||||
| 			default: 0.1 | }); | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	data() { | const now = ref(new Date()); | ||||||
| 		return { | const enabled = ref(true); | ||||||
| 			now: new Date(), | const graduationsPadding = ref(0.5); | ||||||
| 			enabled: true, | const handsPadding = ref(1); | ||||||
|  | const handsTailLength = ref(0.7); | ||||||
|  | const hHandLengthRatio = ref(0.75); | ||||||
|  | const mHandLengthRatio = ref(1); | ||||||
|  | const sHandLengthRatio = ref(1); | ||||||
|  | const computedStyle = getComputedStyle(document.documentElement); | ||||||
| 
 | 
 | ||||||
| 			graduationsPadding: 0.5, | const dark = computed(() => tinycolor(computedStyle.getPropertyValue('--bg')).isDark()); | ||||||
| 			handsPadding: 1, | const majorGraduationColor = computed(() => dark.value ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)'); | ||||||
| 			handsTailLength: 0.7, | const minorGraduationColor = computed(() => dark.value ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'); | ||||||
| 			hHandLengthRatio: 0.75, | const sHandColor = computed(() => dark.value ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)'); | ||||||
| 			mHandLengthRatio: 1, | const mHandColor = computed(() => tinycolor(computedStyle.getPropertyValue('--fg')).toHexString()); | ||||||
| 			sHandLengthRatio: 1, | const hHandColor = computed(() => tinycolor(computedStyle.getPropertyValue('--accent')).toHexString()); | ||||||
| 
 | const s = computed(() => now.value.getSeconds()); | ||||||
| 			computedStyle: getComputedStyle(document.documentElement) | const m = computed(() => now.value.getMinutes()); | ||||||
| 		}; | const h = computed(() => now.value.getHours()); | ||||||
| 	}, | const hAngle = computed(() => Math.PI * (h.value % 12 + (m.value + s.value / 60) / 60) / 6); | ||||||
| 
 | const mAngle = computed(() => Math.PI * (m.value + s.value / 60) / 30); | ||||||
| 	computed: { | const sAngle = computed(() => Math.PI * s.value / 30); | ||||||
| 		dark(): boolean { | const graduations = computed(() => { | ||||||
| 			return tinycolor(this.computedStyle.getPropertyValue('--bg')).isDark(); | 	const angles: number[] = []; | ||||||
| 		}, | 	for (let i = 0; i < 60; i++) { | ||||||
| 
 | 		const angle = Math.PI * i / 30; | ||||||
| 		majorGraduationColor(): string { | 		angles.push(angle); | ||||||
| 			return this.dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)'; |  | ||||||
| 		}, |  | ||||||
| 		minorGraduationColor(): string { |  | ||||||
| 			return this.dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		sHandColor(): string { |  | ||||||
| 			return this.dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)'; |  | ||||||
| 		}, |  | ||||||
| 		mHandColor(): string { |  | ||||||
| 			return tinycolor(this.computedStyle.getPropertyValue('--fg')).toHexString(); |  | ||||||
| 		}, |  | ||||||
| 		hHandColor(): string { |  | ||||||
| 			return tinycolor(this.computedStyle.getPropertyValue('--accent')).toHexString(); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		s(): number { |  | ||||||
| 			return this.now.getSeconds(); |  | ||||||
| 		}, |  | ||||||
| 		m(): number { |  | ||||||
| 			return this.now.getMinutes(); |  | ||||||
| 		}, |  | ||||||
| 		h(): number { |  | ||||||
| 			return this.now.getHours(); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		hAngle(): number { |  | ||||||
| 			return Math.PI * (this.h % 12 + (this.m + this.s / 60) / 60) / 6; |  | ||||||
| 		}, |  | ||||||
| 		mAngle(): number { |  | ||||||
| 			return Math.PI * (this.m + this.s / 60) / 30; |  | ||||||
| 		}, |  | ||||||
| 		sAngle(): number { |  | ||||||
| 			return Math.PI * this.s / 30; |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		graduations(): any { |  | ||||||
| 			const angles = []; |  | ||||||
| 			for (let i = 0; i < 60; i++) { |  | ||||||
| 				const angle = Math.PI * i / 30; |  | ||||||
| 				angles.push(angle); |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			return angles; |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	mounted() { |  | ||||||
| 		const update = () => { |  | ||||||
| 			if (this.enabled) { |  | ||||||
| 				this.tick(); |  | ||||||
| 				setTimeout(update, 1000); |  | ||||||
| 			} |  | ||||||
| 		}; |  | ||||||
| 		update(); |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	beforeUnmount() { |  | ||||||
| 		this.enabled = false; |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	methods: { |  | ||||||
| 		tick() { |  | ||||||
| 			this.now = new Date(); |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	return angles; | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | function tick() { | ||||||
|  | 	now.value = new Date(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | onMounted(() => { | ||||||
|  | 	const update = () => { | ||||||
|  | 		if (enabled.value) { | ||||||
|  | 			tick(); | ||||||
|  | 			setTimeout(update, 1000); | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | 	update(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | onBeforeUnmount(() => { | ||||||
|  | 	enabled.value = false; | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <template> | <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"> | 	<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"> | 		<li v-for="user in users" tabindex="-1" class="user" @click="complete(type, user)" @keydown="onKeydown"> | ||||||
| 			<img class="avatar" :src="user.avatarUrl"/> | 			<img class="avatar" :src="user.avatarUrl"/> | ||||||
|  | @ -8,7 +8,7 @@ | ||||||
| 			</span> | 			</span> | ||||||
| 			<span class="username">@{{ acct(user) }}</span> | 			<span class="username">@{{ acct(user) }}</span> | ||||||
| 		</li> | 		</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> | ||||||
| 	<ol v-else-if="hashtags.length > 0" ref="suggests" class="hashtags"> | 	<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"> | 		<li v-for="hashtag in hashtags" tabindex="-1" @click="complete(type, hashtag)" @keydown="onKeydown"> | ||||||
|  | @ -17,8 +17,8 @@ | ||||||
| 	</ol> | 	</ol> | ||||||
| 	<ol v-else-if="emojis.length > 0" ref="suggests" class="emojis"> | 	<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"> | 		<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-if="emoji.isCustomEmoji" class="emoji"><img :src="defaultStore.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-else-if="!defaultStore.state.useOsNativeEmojis" class="emoji"><img :src="emoji.url" :alt="emoji.emoji"/></span> | ||||||
| 			<span v-else class="emoji">{{ 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 class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span> | ||||||
| 			<span v-if="emoji.aliasOf" class="alias">({{ emoji.aliasOf }})</span> | 			<span v-if="emoji.aliasOf" class="alias">({{ emoji.aliasOf }})</span> | ||||||
|  | @ -33,15 +33,17 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent, markRaw } from 'vue'; | import { markRaw, ref, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; | ||||||
| import { emojilist } from '@/scripts/emojilist'; |  | ||||||
| import contains from '@/scripts/contains'; | import contains from '@/scripts/contains'; | ||||||
| import { twemojiSvgBase } from '@/scripts/twemoji-base'; |  | ||||||
| import { getStaticImageUrl } from '@/scripts/get-static-image-url'; | import { getStaticImageUrl } from '@/scripts/get-static-image-url'; | ||||||
| import { acct } from '@/filters/user'; | import { acct } from '@/filters/user'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { instance } from '@/instance'; |  | ||||||
| import { MFM_TAGS } from '@/scripts/mfm-tags'; | 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 = { | type EmojiDef = { | ||||||
| 	emoji: string; | 	emoji: string; | ||||||
|  | @ -54,16 +56,14 @@ type EmojiDef = { | ||||||
| const lib = emojilist.filter(x => x.category !== 'flags'); | const lib = emojilist.filter(x => x.category !== 'flags'); | ||||||
| 
 | 
 | ||||||
| const char2file = (char: string) => { | 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'); | 	if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f'); | ||||||
| 	codes = codes.filter(x => x && x.length); | 	return codes.filter(x => x && x.length).join('-'); | ||||||
| 	return codes.join('-'); |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const emjdb: EmojiDef[] = lib.map(x => ({ | const emjdb: EmojiDef[] = lib.map(x => ({ | ||||||
| 	emoji: x.char, | 	emoji: x.char, | ||||||
| 	name: x.name, | 	name: x.name, | ||||||
| 	aliasOf: null, |  | ||||||
| 	url: `${twemojiSvgBase}/${char2file(x.char)}.svg` | 	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)); | const emojiDb = markRaw(emojiDefinitions.concat(emjdb)); | ||||||
| //#endregion | //#endregion | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default { | ||||||
| 	props: { | 	emojiDb, | ||||||
| 		type: { | 	emojiDefinitions, | ||||||
| 			type: String, | 	emojilist, | ||||||
| 			required: true, | 	customEmojis, | ||||||
| 		}, | }; | ||||||
|  | </script> | ||||||
| 
 | 
 | ||||||
| 		q: { | <script lang="ts" setup> | ||||||
| 			type: String, | const props = defineProps<{ | ||||||
| 			required: false, | 	type: string; | ||||||
| 		}, | 	q: string | null; | ||||||
|  | 	textarea: HTMLTextAreaElement; | ||||||
|  | 	close: () => void; | ||||||
|  | 	x: number; | ||||||
|  | 	y: number; | ||||||
|  | }>(); | ||||||
| 
 | 
 | ||||||
| 		textarea: { | const emit = defineEmits<{ | ||||||
| 			type: HTMLTextAreaElement, | 	(e: 'done', v: { type: string; value: any }): void; | ||||||
| 			required: true, | 	(e: 'closed'): void; | ||||||
| 		}, | }>(); | ||||||
| 
 | 
 | ||||||
| 		close: { | const suggests = ref<Element>(); | ||||||
| 			type: Function, | const rootEl = ref<HTMLDivElement>(); | ||||||
| 			required: true, |  | ||||||
| 		}, |  | ||||||
| 
 | 
 | ||||||
| 		x: { | const fetching = ref(true); | ||||||
| 			type: Number, | const users = ref<any[]>([]); | ||||||
| 			required: true, | 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: { | function complete(type: string, value: any) { | ||||||
| 			type: Number, | 	emit('done', { type, value }); | ||||||
| 			required: true, | 	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() { | function exec() { | ||||||
| 		return { | 	select.value = -1; | ||||||
| 			getStaticImageUrl, | 	if (suggests.value) { | ||||||
| 			fetching: true, | 		for (const el of Array.from(items.value)) { | ||||||
| 			users: [], | 			el.removeAttribute('data-selected'); | ||||||
| 			hashtags: [], |  | ||||||
| 			emojis: [], |  | ||||||
| 			items: [], |  | ||||||
| 			mfmTags: [], |  | ||||||
| 			select: -1, |  | ||||||
| 			zIndex: os.claimZIndex('high'), |  | ||||||
| 		} | 		} | ||||||
| 	}, | 	} | ||||||
| 
 | 	if (props.type === 'user') { | ||||||
| 	updated() { | 		if (!props.q) { | ||||||
| 		this.setPosition(); | 			users.value = []; | ||||||
| 		this.items = (this.$refs.suggests as Element | undefined)?.children || []; | 			fetching.value = false; | ||||||
| 	}, | 			return; | ||||||
| 
 |  | ||||||
| 	mounted() { |  | ||||||
| 		this.setPosition(); |  | ||||||
| 
 |  | ||||||
| 		this.textarea.addEventListener('keydown', this.onKeydown); |  | ||||||
| 
 |  | ||||||
| 		for (const el of Array.from(document.querySelectorAll('body *'))) { |  | ||||||
| 			el.addEventListener('mousedown', this.onMousedown); |  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		this.$nextTick(() => { | 		const cacheKey = `autocomplete:user:${props.q}`; | ||||||
| 			this.exec(); | 		const cache = sessionStorage.getItem(cacheKey); | ||||||
| 
 | 
 | ||||||
| 			this.$watch('q', () => { | 		if (cache) { | ||||||
| 				this.$nextTick(() => { | 			const users = JSON.parse(cache); | ||||||
| 					this.exec(); | 			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() { | onBeforeUnmount(() => { | ||||||
| 		this.textarea.removeEventListener('keydown', this.onKeydown); | 	props.textarea.removeEventListener('keydown', onKeydown); | ||||||
| 
 | 
 | ||||||
| 		for (const el of Array.from(document.querySelectorAll('body *'))) { | 	for (const el of Array.from(document.querySelectorAll('body *'))) { | ||||||
| 			el.removeEventListener('mousedown', this.onMousedown); | 		el.removeEventListener('mousedown', 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 |  | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,12 +1,14 @@ | ||||||
| <template> | <template> | ||||||
| <div> | <div> | ||||||
| 	<span v-if="!available">{{ $ts.waiting }}<MkEllipsis/></span> | 	<span v-if="!available">{{ i18n.locale.waiting }}<MkEllipsis/></span> | ||||||
| 	<div ref="captcha"></div> | 	<div ref="captchaEl"></div> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { defineComponent, PropType } from 'vue'; | import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'; | ||||||
|  | import { defaultStore } from '@/store'; | ||||||
|  | import { i18n } from '@/i18n'; | ||||||
| 
 | 
 | ||||||
| type Captcha = { | type Captcha = { | ||||||
| 	render(container: string | Node, options: { | 	render(container: string | Node, options: { | ||||||
|  | @ -14,7 +16,7 @@ type Captcha = { | ||||||
| 	}): string; | 	}): string; | ||||||
| 	remove(id: string): void; | 	remove(id: string): void; | ||||||
| 	execute(id: string): void; | 	execute(id: string): void; | ||||||
| 	reset(id: string): void; | 	reset(id?: string): void; | ||||||
| 	getResponse(id: string): string; | 	getResponse(id: string): string; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -29,95 +31,87 @@ declare global { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | const props = defineProps<{ | ||||||
| 	props: { | 	provider: CaptchaProvider; | ||||||
| 		provider: { | 	sitekey: string; | ||||||
| 			type: String as PropType<CaptchaProvider>, | 	modelValue?: string | null; | ||||||
| 			required: true, | }>(); | ||||||
| 		}, |  | ||||||
| 		sitekey: { |  | ||||||
| 			type: String, |  | ||||||
| 			required: true, |  | ||||||
| 		}, |  | ||||||
| 		modelValue: { |  | ||||||
| 			type: String, |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	data() { | const emit = defineEmits<{ | ||||||
| 		return { | 	(e: 'update:modelValue', v: string | null): void; | ||||||
| 			available: false, | }>(); | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	computed: { | const available = ref(false); | ||||||
| 		variable(): string { |  | ||||||
| 			switch (this.provider) { |  | ||||||
| 				case 'hcaptcha': return 'hcaptcha'; |  | ||||||
| 				case 'recaptcha': return 'grecaptcha'; |  | ||||||
| 			} |  | ||||||
| 		}, |  | ||||||
| 		loaded(): boolean { |  | ||||||
| 			return !!window[this.variable]; |  | ||||||
| 		}, |  | ||||||
| 		src(): string { |  | ||||||
| 			const endpoint = ({ |  | ||||||
| 				hcaptcha: 'https://hcaptcha.com/1', |  | ||||||
| 				recaptcha: 'https://www.recaptcha.net/recaptcha', |  | ||||||
| 			} as Record<CaptchaProvider, string>)[this.provider]; |  | ||||||
| 
 | 
 | ||||||
| 			return `${typeof endpoint === 'string' ? endpoint : 'about:invalid'}/api.js?render=explicit`; | const captchaEl = ref<HTMLDivElement | undefined>(); | ||||||
| 		}, |  | ||||||
| 		captcha(): Captcha { |  | ||||||
| 			return window[this.variable] || {} as unknown as Captcha; |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	created() { | const variable = computed(() => { | ||||||
| 		if (this.loaded) { | 	switch (props.provider) { | ||||||
| 			this.available = true; | 		case 'hcaptcha': return 'hcaptcha'; | ||||||
| 		} else { | 		case 'recaptcha': return 'grecaptcha'; | ||||||
| 			(document.getElementById(this.provider) || document.head.appendChild(Object.assign(document.createElement('script'), { | 	} | ||||||
| 				async: true, |  | ||||||
| 				id: this.provider, |  | ||||||
| 				src: this.src, |  | ||||||
| 			}))) |  | ||||||
| 				.addEventListener('load', () => this.available = true); |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	mounted() { |  | ||||||
| 		if (this.available) { |  | ||||||
| 			this.requestRender(); |  | ||||||
| 		} else { |  | ||||||
| 			this.$watch('available', this.requestRender); |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	beforeUnmount() { |  | ||||||
| 		this.reset(); |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	methods: { |  | ||||||
| 		reset() { |  | ||||||
| 			if (this.captcha?.reset) this.captcha.reset(); |  | ||||||
| 		}, |  | ||||||
| 		requestRender() { |  | ||||||
| 			if (this.captcha.render && this.$refs.captcha instanceof Element) { |  | ||||||
| 				this.captcha.render(this.$refs.captcha, { |  | ||||||
| 					sitekey: this.sitekey, |  | ||||||
| 					theme: this.$store.state.darkMode ? 'dark' : 'light', |  | ||||||
| 					callback: this.callback, |  | ||||||
| 					'expired-callback': this.callback, |  | ||||||
| 					'error-callback': this.callback, |  | ||||||
| 				}); |  | ||||||
| 			} else { |  | ||||||
| 				setTimeout(this.requestRender.bind(this), 1); |  | ||||||
| 			} |  | ||||||
| 		}, |  | ||||||
| 		callback(response?: string) { |  | ||||||
| 			this.$emit('update:modelValue', typeof response == 'string' ? response : null); |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | const loaded = computed(() => !!window[variable.value]); | ||||||
|  | 
 | ||||||
|  | const src = computed(() => { | ||||||
|  | 	const endpoint = ({ | ||||||
|  | 		hcaptcha: 'https://hcaptcha.com/1', | ||||||
|  | 		recaptcha: 'https://www.recaptcha.net/recaptcha', | ||||||
|  | 	} as Record<CaptchaProvider, string>)[props.provider]; | ||||||
|  | 
 | ||||||
|  | 	return `${typeof endpoint === 'string' ? endpoint : 'about:invalid'}/api.js?render=explicit`; | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha); | ||||||
|  | 
 | ||||||
|  | if (loaded.value) { | ||||||
|  | 	available.value = true; | ||||||
|  | } else { | ||||||
|  | 	(document.getElementById(props.provider) || document.head.appendChild(Object.assign(document.createElement('script'), { | ||||||
|  | 		async: true, | ||||||
|  | 		id: props.provider, | ||||||
|  | 		src: src.value, | ||||||
|  | 	}))) | ||||||
|  | 		.addEventListener('load', () => available.value = true); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function reset() { | ||||||
|  | 	if (captcha.value?.reset) captcha.value.reset(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function requestRender() { | ||||||
|  | 	if (captcha.value.render && captchaEl.value instanceof Element) { | ||||||
|  | 		captcha.value.render(captchaEl.value, { | ||||||
|  | 			sitekey: props.sitekey, | ||||||
|  | 			theme: defaultStore.state.darkMode ? 'dark' : 'light', | ||||||
|  | 			callback: callback, | ||||||
|  | 			'expired-callback': callback, | ||||||
|  | 			'error-callback': callback, | ||||||
|  | 		}); | ||||||
|  | 	} else { | ||||||
|  | 		setTimeout(requestRender, 1); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function callback(response?: string) { | ||||||
|  | 	emit('update:modelValue', typeof response == 'string' ? response : null); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | onMounted(() => { | ||||||
|  | 	if (available.value) { | ||||||
|  | 		requestRender(); | ||||||
|  | 	} else { | ||||||
|  | 		watch(available, requestRender); | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | onBeforeUnmount(() => { | ||||||
|  | 	reset(); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | defineExpose({ | ||||||
|  | 	reset, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -6,66 +6,54 @@ | ||||||
| > | > | ||||||
| 	<template v-if="!wait"> | 	<template v-if="!wait"> | ||||||
| 		<template v-if="isFollowing"> | 		<template v-if="isFollowing"> | ||||||
| 			<span v-if="full">{{ $ts.unfollow }}</span><i class="fas fa-minus"></i> | 			<span v-if="full">{{ i18n.locale.unfollow }}</span><i class="fas fa-minus"></i> | ||||||
| 		</template> | 		</template> | ||||||
| 		<template v-else> | 		<template v-else> | ||||||
| 			<span v-if="full">{{ $ts.follow }}</span><i class="fas fa-plus"></i> | 			<span v-if="full">{{ i18n.locale.follow }}</span><i class="fas fa-plus"></i> | ||||||
| 		</template> | 		</template> | ||||||
| 	</template> | 	</template> | ||||||
| 	<template v-else> | 	<template v-else> | ||||||
| 		<span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i> | 		<span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i> | ||||||
| 	</template> | 	</template> | ||||||
| </button> | </button> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { defineComponent } from 'vue'; | import { ref } from 'vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
|  | import { i18n } from '@/i18n'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | const props = withDefaults(defineProps<{ | ||||||
| 	props: { | 	channel: Record<string, any>; | ||||||
| 		channel: { | 	full?: boolean; | ||||||
| 			type: Object, | }>(), { | ||||||
| 			required: true | 	full: false, | ||||||
| 		}, |  | ||||||
| 		full: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			required: false, |  | ||||||
| 			default: false, |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			isFollowing: this.channel.isFollowing, |  | ||||||
| 			wait: false, |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 
 |  | ||||||
| 	methods: { |  | ||||||
| 		async onClick() { |  | ||||||
| 			this.wait = true; |  | ||||||
| 
 |  | ||||||
| 			try { |  | ||||||
| 				if (this.isFollowing) { |  | ||||||
| 					await os.api('channels/unfollow', { |  | ||||||
| 						channelId: this.channel.id |  | ||||||
| 					}); |  | ||||||
| 					this.isFollowing = false; |  | ||||||
| 				} else { |  | ||||||
| 					await os.api('channels/follow', { |  | ||||||
| 						channelId: this.channel.id |  | ||||||
| 					}); |  | ||||||
| 					this.isFollowing = true; |  | ||||||
| 				} |  | ||||||
| 			} catch (e) { |  | ||||||
| 				console.error(e); |  | ||||||
| 			} finally { |  | ||||||
| 				this.wait = false; |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | const isFollowing = ref<boolean>(props.channel.isFollowing); | ||||||
|  | const wait = ref(false); | ||||||
|  | 
 | ||||||
|  | async function onClick() { | ||||||
|  | 	wait.value = true; | ||||||
|  | 
 | ||||||
|  | 	try { | ||||||
|  | 		if (isFollowing.value) { | ||||||
|  | 			await os.api('channels/unfollow', { | ||||||
|  | 				channelId: props.channel.id | ||||||
|  | 			}); | ||||||
|  | 			isFollowing.value = false; | ||||||
|  | 		} else { | ||||||
|  | 			await os.api('channels/follow', { | ||||||
|  | 				channelId: props.channel.id | ||||||
|  | 			}); | ||||||
|  | 			isFollowing.value = true; | ||||||
|  | 		} | ||||||
|  | 	} catch (e) { | ||||||
|  | 		console.error(e); | ||||||
|  | 	} finally { | ||||||
|  | 		wait.value = false; | ||||||
|  | 	} | ||||||
|  | } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ | ||||||
| 		<div class="status"> | 		<div class="status"> | ||||||
| 			<div> | 			<div> | ||||||
| 				<i class="fas fa-users fa-fw"></i> | 				<i class="fas fa-users fa-fw"></i> | ||||||
| 				<I18n :src="$ts._channel.usersCount" tag="span" style="margin-left: 4px;"> | 				<I18n :src="i18n.locale._channel.usersCount" tag="span" style="margin-left: 4px;"> | ||||||
| 					<template #n> | 					<template #n> | ||||||
| 						<b>{{ channel.usersCount }}</b> | 						<b>{{ channel.usersCount }}</b> | ||||||
| 					</template> | 					</template> | ||||||
|  | @ -14,7 +14,7 @@ | ||||||
| 			</div> | 			</div> | ||||||
| 			<div> | 			<div> | ||||||
| 				<i class="fas fa-pencil-alt fa-fw"></i> | 				<i class="fas fa-pencil-alt fa-fw"></i> | ||||||
| 				<I18n :src="$ts._channel.notesCount" tag="span" style="margin-left: 4px;"> | 				<I18n :src="i18n.locale._channel.notesCount" tag="span" style="margin-left: 4px;"> | ||||||
| 					<template #n> | 					<template #n> | ||||||
| 						<b>{{ channel.notesCount }}</b> | 						<b>{{ channel.notesCount }}</b> | ||||||
| 					</template> | 					</template> | ||||||
|  | @ -27,37 +27,26 @@ | ||||||
| 	</article> | 	</article> | ||||||
| 	<footer> | 	<footer> | ||||||
| 		<span v-if="channel.lastNotedAt"> | 		<span v-if="channel.lastNotedAt"> | ||||||
| 			{{ $ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/> | 			{{ i18n.locale.updatedAt }}: <MkTime :time="channel.lastNotedAt"/> | ||||||
| 		</span> | 		</span> | ||||||
| 	</footer> | 	</footer> | ||||||
| </MkA> | </MkA> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { defineComponent } from 'vue'; | import { computed } from 'vue'; | ||||||
|  | import { i18n } from '@/i18n'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | const props = defineProps<{ | ||||||
| 	props: { | 	channel: Record<string, any>; | ||||||
| 		channel: { | }>(); | ||||||
| 			type: Object, |  | ||||||
| 			required: true |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	data() { | const bannerStyle = computed(() => { | ||||||
| 		return { | 	if (props.channel.bannerUrl) { | ||||||
| 		}; | 		return { backgroundImage: `url(${props.channel.bannerUrl})` }; | ||||||
| 	}, | 	} else { | ||||||
| 
 | 		return { backgroundColor: '#4c5e6d' }; | ||||||
| 	computed: { | 	} | ||||||
| 		bannerStyle() { |  | ||||||
| 			if (this.channel.bannerUrl) { |  | ||||||
| 				return { backgroundImage: `url(${this.channel.bannerUrl})` }; |  | ||||||
| 			} else { |  | ||||||
| 				return { backgroundColor: '#4c5e6d' }; |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,33 +3,17 @@ | ||||||
| <pre v-else :class="`language-${prismLang}`"><code :class="`language-${prismLang}`" v-html="html"></code></pre> | <pre v-else :class="`language-${prismLang}`"><code :class="`language-${prismLang}`" v-html="html"></code></pre> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { defineComponent } from 'vue'; | import { computed } from 'vue'; | ||||||
| import 'prismjs'; | import 'prismjs'; | ||||||
| import 'prismjs/themes/prism-okaidia.css'; | import 'prismjs/themes/prism-okaidia.css'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | const props = defineProps<{ | ||||||
| 	props: { | 	code: string; | ||||||
| 		code: { | 	lang?: string; | ||||||
| 			type: String, | 	inline?: boolean; | ||||||
| 			required: true | }>(); | ||||||
| 		}, | 
 | ||||||
| 		lang: { | const prismLang = computed(() => Prism.languages[props.lang] ? props.lang : 'js'); | ||||||
| 			type: String, | const html = computed(() => Prism.highlight(props.code, Prism.languages[prismLang.value], prismLang.value)); | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		inline: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			required: false |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 	computed: { |  | ||||||
| 		prismLang() { |  | ||||||
| 			return Prism.languages[this.lang] ? this.lang : 'js'; |  | ||||||
| 		}, |  | ||||||
| 		html() { |  | ||||||
| 			return Prism.highlight(this.code, Prism.languages[this.prismLang], this.prismLang); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -2,26 +2,14 @@ | ||||||
| <XCode :code="code" :lang="lang" :inline="inline"/> | <XCode :code="code" :lang="lang" :inline="inline"/> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { defineComponent, defineAsyncComponent } from 'vue'; | import { defineAsyncComponent } from 'vue'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | defineProps<{ | ||||||
| 	components: { | 	code: string; | ||||||
| 		XCode: defineAsyncComponent(() => import('./code-core.vue')) | 	lang?: string; | ||||||
| 	}, | 	inline?: boolean; | ||||||
| 	props: { | }>(); | ||||||
| 		code: { | 
 | ||||||
| 			type: String, | const XCode = defineAsyncComponent(() => import('./code-core.vue')); | ||||||
| 			required: true |  | ||||||
| 		}, |  | ||||||
| 		lang: { |  | ||||||
| 			type: String, |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		inline: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			required: false |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| <template> | <template> | ||||||
| <button class="nrvgflfu _button" @click="toggle"> | <button class="nrvgflfu _button" @click="toggle"> | ||||||
| 	<b>{{ modelValue ? $ts._cw.hide : $ts._cw.show }}</b> | 	<b>{{ modelValue ? i18n.locale._cw.hide : i18n.locale._cw.show }}</b> | ||||||
| 	<span v-if="!modelValue">{{ label }}</span> | 	<span v-if="!modelValue">{{ label }}</span> | ||||||
| </button> | </button> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent, h, PropType, TransitionGroup } from 'vue'; | import { defineComponent, h, PropType, TransitionGroup } from 'vue'; | ||||||
| import MkAd from '@/components/global/ad.vue'; | import MkAd from '@/components/global/ad.vue'; | ||||||
|  | import { i18n } from '@/i18n'; | ||||||
|  | import { defaultStore } from '@/store'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
|  | @ -30,29 +32,29 @@ export default defineComponent({ | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	setup(props, { slots, expose }) { | ||||||
| 		getDateText(time: string) { | 		function getDateText(time: string) { | ||||||
| 			const date = new Date(time).getDate(); | 			const date = new Date(time).getDate(); | ||||||
| 			const month = new Date(time).getMonth() + 1; | 			const month = new Date(time).getMonth() + 1; | ||||||
| 			return this.$t('monthAndDay', { | 			return i18n.t('monthAndDay', { | ||||||
| 				month: month.toString(), | 				month: month.toString(), | ||||||
| 				day: date.toString() | 				day: date.toString() | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	render() { | 		if (props.items.length === 0) return; | ||||||
| 		if (this.items.length === 0) return; |  | ||||||
| 
 | 
 | ||||||
| 		const renderChildren = () => this.items.map((item, i) => { | 		const renderChildren = () => props.items.map((item, i) => { | ||||||
| 			const el = this.$slots.default({ | 			if (!slots || !slots.default) return; | ||||||
|  | 
 | ||||||
|  | 			const el = slots.default({ | ||||||
| 				item: item | 				item: item | ||||||
| 			})[0]; | 			})[0]; | ||||||
| 			if (el.key == null && item.id) el.key = item.id; | 			if (el.key == null && item.id) el.key = item.id; | ||||||
| 
 | 
 | ||||||
| 			if ( | 			if ( | ||||||
| 				i != this.items.length - 1 && | 				i != props.items.length - 1 && | ||||||
| 				new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate() | 				new Date(item.createdAt).getDate() != new Date(props.items[i + 1].createdAt).getDate() | ||||||
| 			) { | 			) { | ||||||
| 				const separator = h('div', { | 				const separator = h('div', { | ||||||
| 					class: 'separator', | 					class: 'separator', | ||||||
|  | @ -64,10 +66,10 @@ export default defineComponent({ | ||||||
| 						h('i', { | 						h('i', { | ||||||
| 							class: 'fas fa-angle-up icon', | 							class: 'fas fa-angle-up icon', | ||||||
| 						}), | 						}), | ||||||
| 						this.getDateText(item.createdAt) | 						getDateText(item.createdAt) | ||||||
| 					]), | 					]), | ||||||
| 					h('span', [ | 					h('span', [ | ||||||
| 						this.getDateText(this.items[i + 1].createdAt), | 						getDateText(props.items[i + 1].createdAt), | ||||||
| 						h('i', { | 						h('i', { | ||||||
| 							class: 'fas fa-angle-down icon', | 							class: 'fas fa-angle-down icon', | ||||||
| 						}) | 						}) | ||||||
|  | @ -76,7 +78,7 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| 				return [el, separator]; | 				return [el, separator]; | ||||||
| 			} else { | 			} else { | ||||||
| 				if (this.ad && item._shouldInsertAd_) { | 				if (props.ad && item._shouldInsertAd_) { | ||||||
| 					return [h(MkAd, { | 					return [h(MkAd, { | ||||||
| 						class: 'a', // advertiseの意(ブロッカー対策) | 						class: 'a', // advertiseの意(ブロッカー対策) | ||||||
| 						key: item.id + ':ad', | 						key: item.id + ':ad', | ||||||
|  | @ -88,18 +90,19 @@ export default defineComponent({ | ||||||
| 			} | 			} | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
| 		return h(this.$store.state.animation ? TransitionGroup : 'div', this.$store.state.animation ? { | 		return () => h( | ||||||
| 			class: 'sqadhkmv' + (this.noGap ? ' noGap' : ''), | 			defaultStore.state.animation ? TransitionGroup : 'div', | ||||||
| 			name: 'list', | 			defaultStore.state.animation ? { | ||||||
| 			tag: 'div', | 					class: 'sqadhkmv' + (props.noGap ? ' noGap' : ''), | ||||||
| 			'data-direction': this.direction, | 					name: 'list', | ||||||
| 			'data-reversed': this.reversed ? 'true' : 'false', | 					tag: 'div', | ||||||
| 		} : { | 					'data-direction': props.direction, | ||||||
| 			class: 'sqadhkmv' + (this.noGap ? ' noGap' : ''), | 					'data-reversed': props.reversed ? 'true' : 'false', | ||||||
| 		}, { | 				} : { | ||||||
| 			default: renderChildren | 					class: 'sqadhkmv' + (props.noGap ? ' noGap' : ''), | ||||||
| 		}); | 				}, | ||||||
| 	}, | 			{ default: renderChildren }); | ||||||
|  | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ | ||||||
| 		</div> | 		</div> | ||||||
| 		<header v-if="title"><Mfm :text="title"/></header> | 		<header v-if="title"><Mfm :text="title"/></header> | ||||||
| 		<div v-if="text" class="body"><Mfm :text="text"/></div> | 		<div v-if="text" class="body"><Mfm :text="text"/></div> | ||||||
| 		<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown"> | 		<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" @keydown="onInputKeydown"> | ||||||
| 			<template v-if="input.type === 'password'" #prefix><i class="fas fa-lock"></i></template> | 			<template v-if="input.type === 'password'" #prefix><i class="fas fa-lock"></i></template> | ||||||
| 		</MkInput> | 		</MkInput> | ||||||
| 		<MkSelect v-if="select" v-model="selectedValue" autofocus> | 		<MkSelect v-if="select" v-model="selectedValue" autofocus> | ||||||
|  | @ -38,118 +38,107 @@ | ||||||
| </MkModal> | </MkModal> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts" setup> | ||||||
| import { defineComponent } from 'vue'; | import { onBeforeUnmount, onMounted, ref } from 'vue'; | ||||||
| import MkModal from '@/components/ui/modal.vue'; | import MkModal from '@/components/ui/modal.vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import MkButton from '@/components/ui/button.vue'; | ||||||
| import MkInput from '@/components/form/input.vue'; | import MkInput from '@/components/form/input.vue'; | ||||||
| import MkSelect from '@/components/form/select.vue'; | import MkSelect from '@/components/form/select.vue'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | type Input = { | ||||||
| 	components: { | 	type: HTMLInputElement['type']; | ||||||
| 		MkModal, | 	placeholder?: string | null; | ||||||
| 		MkButton, | 	default: any | null; | ||||||
| 		MkInput, | }; | ||||||
| 		MkSelect, |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	props: { | type Select = { | ||||||
| 		type: { | 	items: { | ||||||
| 			type: String, | 		value: string; | ||||||
| 			required: false, | 		text: string; | ||||||
| 			default: 'info' | 	}[]; | ||||||
| 		}, | 	groupedItems: { | ||||||
| 		title: { | 		label: string; | ||||||
| 			type: String, | 		items: { | ||||||
| 			required: false | 			value: string; | ||||||
| 		}, | 			text: string; | ||||||
| 		text: { | 		}[]; | ||||||
| 			type: String, | 	}[]; | ||||||
| 			required: false | 	default: string | null; | ||||||
| 		}, | }; | ||||||
| 		input: { |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		select: { |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		icon: { |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		actions: { |  | ||||||
| 			required: false |  | ||||||
| 		}, |  | ||||||
| 		showOkButton: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			default: true |  | ||||||
| 		}, |  | ||||||
| 		showCancelButton: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			default: false |  | ||||||
| 		}, |  | ||||||
| 		cancelableByBgClick: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			default: true |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	emits: ['done', 'closed'], | const props = withDefaults(defineProps<{ | ||||||
|  | 	type?: 'success' | 'error' | 'warning' | 'info' | 'question' | 'waiting'; | ||||||
|  | 	title: string; | ||||||
|  | 	text?: string; | ||||||
|  | 	input?: Input; | ||||||
|  | 	select?: Select; | ||||||
|  | 	icon?: string; | ||||||
|  | 	actions?: { | ||||||
|  | 		text: string; | ||||||
|  | 		primary?: boolean, | ||||||
|  | 		callback: (...args: any[]) => void; | ||||||
|  | 	}[]; | ||||||
|  | 	showOkButton?: boolean; | ||||||
|  | 	showCancelButton?: boolean; | ||||||
|  | 	cancelableByBgClick?: boolean; | ||||||
|  | }>(), { | ||||||
|  | 	type: 'info', | ||||||
|  | 	showOkButton: true, | ||||||
|  | 	showCancelButton: false, | ||||||
|  | 	cancelableByBgClick: true, | ||||||
|  | }); | ||||||
| 
 | 
 | ||||||
| 	data() { | const emit = defineEmits<{ | ||||||
| 		return { | 	(e: 'done', v: { canceled: boolean; result: any }): void; | ||||||
| 			inputValue: this.input && this.input.default ? this.input.default : null, | 	(e: 'closed'): void; | ||||||
| 			selectedValue: this.select ? this.select.default ? this.select.default : this.select.items ? this.select.items[0].value : this.select.groupedItems[0].items[0].value : null, | }>(); | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	mounted() { | const modal = ref<InstanceType<typeof MkModal>>(); | ||||||
| 		document.addEventListener('keydown', this.onKeydown); |  | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	beforeUnmount() { | const inputValue = ref(props.input?.default || null); | ||||||
| 		document.removeEventListener('keydown', this.onKeydown); | const selectedValue = ref(props.select?.default || null); | ||||||
| 	}, |  | ||||||
| 
 | 
 | ||||||
| 	methods: { | function done(canceled: boolean, result?) { | ||||||
| 		done(canceled, result?) { | 	emit('done', { canceled, result }); | ||||||
| 			this.$emit('done', { canceled, result }); | 	modal.value?.close(); | ||||||
| 			this.$refs.modal.close(); | } | ||||||
| 		}, |  | ||||||
| 
 | 
 | ||||||
| 		async ok() { | async function ok() { | ||||||
| 			if (!this.showOkButton) return; | 	if (!props.showOkButton) return; | ||||||
| 
 | 
 | ||||||
| 			const result = | 	const result = | ||||||
| 				this.input ? this.inputValue : | 		props.input ? inputValue.value : | ||||||
| 				this.select ? this.selectedValue : | 		props.select ? selectedValue.value : | ||||||
| 				true; | 		true; | ||||||
| 			this.done(false, result); | 	done(false, result); | ||||||
| 		}, | } | ||||||
| 
 | 
 | ||||||
| 		cancel() { | function cancel() { | ||||||
| 			this.done(true); | 	done(true); | ||||||
| 		}, | } | ||||||
|  | /* | ||||||
|  | function onBgClick() { | ||||||
|  | 	if (props.cancelableByBgClick) cancel(); | ||||||
|  | } | ||||||
|  | */ | ||||||
|  | function onKeydown(e: KeyboardEvent) { | ||||||
|  | 	if (e.key === 'Escape') cancel(); | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| 		onBgClick() { | function onInputKeydown(e: KeyboardEvent) { | ||||||
| 			if (this.cancelableByBgClick) { | 	if (e.key === 'Enter') { | ||||||
| 				this.cancel(); | 		e.preventDefault(); | ||||||
| 			} | 		e.stopPropagation(); | ||||||
| 		}, | 		ok(); | ||||||
| 
 |  | ||||||
| 		onKeydown(e) { |  | ||||||
| 			if (e.which === 27) { // ESC |  | ||||||
| 				this.cancel(); |  | ||||||
| 			} |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		onInputKeydown(e) { |  | ||||||
| 			if (e.which === 13) { // Enter |  | ||||||
| 				e.preventDefault(); |  | ||||||
| 				e.stopPropagation(); |  | ||||||
| 				this.ok(); |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | onMounted(() => { | ||||||
|  | 	document.addEventListener('keydown', onKeydown); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | onBeforeUnmount(() => { | ||||||
|  | 	document.removeEventListener('keydown', onKeydown); | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue