refactor(client): use composition api for tooltip logic
This commit is contained in:
		
							parent
							
								
									0e3213ff6d
								
							
						
					
					
						commit
						4b7b51d5cc
					
				
					 6 changed files with 187 additions and 219 deletions
				
			
		|  | @ -94,7 +94,7 @@ | ||||||
| 					<template v-else><i class="fas fa-reply"></i></template> | 					<template v-else><i class="fas fa-reply"></i></template> | ||||||
| 					<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> | 					<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> | ||||||
| 				</button> | 				</button> | ||||||
| 				<XRenoteButton :note="appearNote" :count="appearNote.renoteCount" ref="renoteButton"/> | 				<XRenoteButton class="button" :note="appearNote" :count="appearNote.renoteCount" ref="renoteButton"/> | ||||||
| 				<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton"> | 				<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton"> | ||||||
| 					<i class="fas fa-plus"></i> | 					<i class="fas fa-plus"></i> | ||||||
| 				</button> | 				</button> | ||||||
|  | @ -132,16 +132,16 @@ import XMediaList from './media-list.vue'; | ||||||
| import XCwButton from './cw-button.vue'; | import XCwButton from './cw-button.vue'; | ||||||
| import XPoll from './poll.vue'; | import XPoll from './poll.vue'; | ||||||
| import XRenoteButton from './renote-button.vue'; | import XRenoteButton from './renote-button.vue'; | ||||||
| import { pleaseLogin } from '@client/scripts/please-login'; | import { pleaseLogin } from '@/scripts/please-login'; | ||||||
| import { focusPrev, focusNext } from '@client/scripts/focus'; | import { focusPrev, focusNext } from '@/scripts/focus'; | ||||||
| import { url } from '@client/config'; | import { url } from '@/config'; | ||||||
| import copyToClipboard from '@client/scripts/copy-to-clipboard'; | import copyToClipboard from '@/scripts/copy-to-clipboard'; | ||||||
| import { checkWordMute } from '@client/scripts/check-word-mute'; | import { checkWordMute } from '@/scripts/check-word-mute'; | ||||||
| import { userPage } from '@client/filters/user'; | import { userPage } from '@/filters/user'; | ||||||
| import * as os from '@client/os'; | import * as os from '@/os'; | ||||||
| import { noteActions, noteViewInterruptors } from '@client/store'; | import { noteActions, noteViewInterruptors } from '@/store'; | ||||||
| import { reactionPicker } from '@client/scripts/reaction-picker'; | import { reactionPicker } from '@/scripts/reaction-picker'; | ||||||
| import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm'; | import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm'; | ||||||
| 
 | 
 | ||||||
| // TODO: note.vueとほぼ同じなので共通化したい | // TODO: note.vueとほぼ同じなので共通化したい | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|  | @ -154,8 +154,8 @@ export default defineComponent({ | ||||||
| 		XCwButton, | 		XCwButton, | ||||||
| 		XPoll, | 		XPoll, | ||||||
| 		XRenoteButton, | 		XRenoteButton, | ||||||
| 		MkUrlPreview: defineAsyncComponent(() => import('@client/components/url-preview.vue')), | 		MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')), | ||||||
| 		MkInstanceTicker: defineAsyncComponent(() => import('@client/components/instance-ticker.vue')), | 		MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')), | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	inject: { | 	inject: { | ||||||
|  |  | ||||||
|  | @ -78,7 +78,7 @@ | ||||||
| 					<template v-else><i class="fas fa-reply"></i></template> | 					<template v-else><i class="fas fa-reply"></i></template> | ||||||
| 					<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> | 					<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> | ||||||
| 				</button> | 				</button> | ||||||
| 				<XRenoteButton :note="appearNote" :count="appearNote.renoteCount" ref="renoteButton"/> | 				<XRenoteButton class="button" :note="appearNote" :count="appearNote.renoteCount" ref="renoteButton"/> | ||||||
| 				<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton"> | 				<button v-if="appearNote.myReaction == null" class="button _button" @click="react()" ref="reactButton"> | ||||||
| 					<i class="fas fa-plus"></i> | 					<i class="fas fa-plus"></i> | ||||||
| 				</button> | 				</button> | ||||||
|  | @ -115,16 +115,16 @@ import XMediaList from './media-list.vue'; | ||||||
| import XCwButton from './cw-button.vue'; | import XCwButton from './cw-button.vue'; | ||||||
| import XPoll from './poll.vue'; | import XPoll from './poll.vue'; | ||||||
| import XRenoteButton from './renote-button.vue'; | import XRenoteButton from './renote-button.vue'; | ||||||
| import { pleaseLogin } from '@client/scripts/please-login'; | import { pleaseLogin } from '@/scripts/please-login'; | ||||||
| import { focusPrev, focusNext } from '@client/scripts/focus'; | import { focusPrev, focusNext } from '@/scripts/focus'; | ||||||
| import { url } from '@client/config'; | import { url } from '@/config'; | ||||||
| import copyToClipboard from '@client/scripts/copy-to-clipboard'; | import copyToClipboard from '@/scripts/copy-to-clipboard'; | ||||||
| import { checkWordMute } from '@client/scripts/check-word-mute'; | import { checkWordMute } from '@/scripts/check-word-mute'; | ||||||
| import { userPage } from '@client/filters/user'; | import { userPage } from '@/filters/user'; | ||||||
| import * as os from '@client/os'; | import * as os from '@/os'; | ||||||
| import { noteActions, noteViewInterruptors } from '@client/store'; | import { noteActions, noteViewInterruptors } from '@/store'; | ||||||
| import { reactionPicker } from '@client/scripts/reaction-picker'; | import { reactionPicker } from '@/scripts/reaction-picker'; | ||||||
| import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm'; | import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
|  | @ -136,8 +136,8 @@ export default defineComponent({ | ||||||
| 		XCwButton, | 		XCwButton, | ||||||
| 		XPoll, | 		XPoll, | ||||||
| 		XRenoteButton, | 		XRenoteButton, | ||||||
| 		MkUrlPreview: defineAsyncComponent(() => import('@client/components/url-preview.vue')), | 		MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')), | ||||||
| 		MkInstanceTicker: defineAsyncComponent(() => import('@client/components/instance-ticker.vue')), | 		MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')), | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	inject: { | 	inject: { | ||||||
|  |  | ||||||
|  | @ -78,6 +78,7 @@ import notePage from '@/filters/note'; | ||||||
| import { userPage } from '@/filters/user'; | import { userPage } from '@/filters/user'; | ||||||
| import { i18n } from '@/i18n'; | import { i18n } from '@/i18n'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
|  | import { useTooltip } from '@/scripts/use-tooltip'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
|  | @ -153,47 +154,14 @@ export default defineComponent({ | ||||||
| 			os.api('users/groups/invitations/reject', { invitationId: props.notification.invitation.id }); | 			os.api('users/groups/invitations/reject', { invitationId: props.notification.invitation.id }); | ||||||
| 		}; | 		}; | ||||||
| 
 | 
 | ||||||
| 		let isReactionHovering = false; | 		const { onMouseover: onReactionMouseover, onMouseleave: onReactionMouseleave } = useTooltip((showing) => { | ||||||
| 		let reactionTooltipTimeoutId; |  | ||||||
| 
 |  | ||||||
| 		const onReactionMouseover = () => { |  | ||||||
| 			if (isReactionHovering) return; |  | ||||||
| 			isReactionHovering = true; |  | ||||||
| 			reactionTooltipTimeoutId = setTimeout(openReactionTooltip, 300); |  | ||||||
| 		}; |  | ||||||
| 
 |  | ||||||
| 		const onReactionMouseleave = () => { |  | ||||||
| 			if (!isReactionHovering) return; |  | ||||||
| 			isReactionHovering = false; |  | ||||||
| 			clearTimeout(reactionTooltipTimeoutId); |  | ||||||
| 			closeReactionTooltip(); |  | ||||||
| 		}; |  | ||||||
| 
 |  | ||||||
| 		let changeReactionTooltipShowingState: (() => void) | null; |  | ||||||
| 
 |  | ||||||
| 		const openReactionTooltip = () => { |  | ||||||
| 			closeReactionTooltip(); |  | ||||||
| 			if (!isReactionHovering) return; |  | ||||||
| 
 |  | ||||||
| 			const showing = ref(true); |  | ||||||
| 			os.popup(XReactionTooltip, { | 			os.popup(XReactionTooltip, { | ||||||
| 				showing, | 				showing, | ||||||
| 				reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction, | 				reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction, | ||||||
| 				emojis: props.notification.note.emojis, | 				emojis: props.notification.note.emojis, | ||||||
| 				source: reactionRef.value.$el, | 				source: reactionRef.value.$el, | ||||||
| 			}, {}, 'closed'); | 			}, {}, 'closed'); | ||||||
| 
 | 		}); | ||||||
| 			changeReactionTooltipShowingState = () => { |  | ||||||
| 				showing.value = false; |  | ||||||
| 			}; |  | ||||||
| 		}; |  | ||||||
| 
 |  | ||||||
| 		const closeReactionTooltip = () => { |  | ||||||
| 			if (changeReactionTooltipShowingState != null) { |  | ||||||
| 				changeReactionTooltipShowingState(); |  | ||||||
| 				changeReactionTooltipShowingState = null; |  | ||||||
| 			} |  | ||||||
| 		}; |  | ||||||
| 
 | 
 | ||||||
| 		return { | 		return { | ||||||
| 			getNoteSummary: (note: misskey.entities.Note) => getNoteSummary(note), | 			getNoteSummary: (note: misskey.entities.Note) => getNoteSummary(note), | ||||||
|  |  | ||||||
|  | @ -2,13 +2,13 @@ | ||||||
| <button | <button | ||||||
| 	class="hkzvhatu _button" | 	class="hkzvhatu _button" | ||||||
| 	:class="{ reacted: note.myReaction == reaction, canToggle }" | 	:class="{ reacted: note.myReaction == reaction, canToggle }" | ||||||
| 	@click="toggleReaction(reaction)" | 	@click="toggleReaction()" | ||||||
| 	v-if="count > 0" | 	v-if="count > 0" | ||||||
| 	@touchstart.passive="onMouseover" | 	@touchstart.passive="onMouseover" | ||||||
| 	@mouseover="onMouseover" | 	@mouseover="onMouseover" | ||||||
| 	@mouseleave="onMouseleave" | 	@mouseleave="onMouseleave" | ||||||
| 	@touchend="onMouseleave" | 	@touchend="onMouseleave" | ||||||
| 	ref="reaction" | 	ref="buttonRef" | ||||||
| 	v-particle="canToggle" | 	v-particle="canToggle" | ||||||
| > | > | ||||||
| 	<XReactionIcon :reaction="reaction" :custom-emojis="note.emojis"/> | 	<XReactionIcon :reaction="reaction" :custom-emojis="note.emojis"/> | ||||||
|  | @ -17,15 +17,18 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent, ref } from 'vue'; | import { computed, defineComponent, onMounted, ref, watch } from 'vue'; | ||||||
| import XDetails from '@/components/reactions-viewer.details.vue'; | import XDetails from '@/components/reactions-viewer.details.vue'; | ||||||
| import XReactionIcon from '@/components/reaction-icon.vue'; | import XReactionIcon from '@/components/reaction-icon.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
|  | import { useTooltip } from '@/scripts/use-tooltip'; | ||||||
|  | import { $i } from '@/account'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XReactionIcon | 		XReactionIcon | ||||||
| 	}, | 	}, | ||||||
|  | 
 | ||||||
| 	props: { | 	props: { | ||||||
| 		reaction: { | 		reaction: { | ||||||
| 			type: String, | 			type: String, | ||||||
|  | @ -44,101 +47,78 @@ export default defineComponent({ | ||||||
| 			required: true, | 			required: true, | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			close: null, |  | ||||||
| 			detailsTimeoutId: null, |  | ||||||
| 			isHovering: false |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 	computed: { |  | ||||||
| 		canToggle(): boolean { |  | ||||||
| 			return !this.reaction.match(/@\w/) && this.$i; |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	watch: { |  | ||||||
| 		count(newCount, oldCount) { |  | ||||||
| 			if (oldCount < newCount) this.anime(); |  | ||||||
| 			if (this.close != null) this.openDetails(); |  | ||||||
| 		}, |  | ||||||
| 	}, |  | ||||||
| 	mounted() { |  | ||||||
| 		if (!this.isInitial) this.anime(); |  | ||||||
| 	}, |  | ||||||
| 	methods: { |  | ||||||
| 		toggleReaction() { |  | ||||||
| 			if (!this.canToggle) return; |  | ||||||
| 
 | 
 | ||||||
| 			const oldReaction = this.note.myReaction; | 	setup(props) { | ||||||
|  | 		const buttonRef = ref<HTMLElement>(); | ||||||
|  | 
 | ||||||
|  | 		const canToggle = computed(() => !props.reaction.match(/@\w/) && $i); | ||||||
|  | 
 | ||||||
|  | 		const toggleReaction = () => { | ||||||
|  | 			if (!canToggle.value) return; | ||||||
|  | 
 | ||||||
|  | 			const oldReaction = props.note.myReaction; | ||||||
| 			if (oldReaction) { | 			if (oldReaction) { | ||||||
| 				os.api('notes/reactions/delete', { | 				os.api('notes/reactions/delete', { | ||||||
| 					noteId: this.note.id | 					noteId: props.note.id | ||||||
| 				}).then(() => { | 				}).then(() => { | ||||||
| 					if (oldReaction !== this.reaction) { | 					if (oldReaction !== props.reaction) { | ||||||
| 						os.api('notes/reactions/create', { | 						os.api('notes/reactions/create', { | ||||||
| 							noteId: this.note.id, | 							noteId: props.note.id, | ||||||
| 							reaction: this.reaction | 							reaction: props.reaction | ||||||
| 						}); | 						}); | ||||||
| 					} | 					} | ||||||
| 				}); | 				}); | ||||||
| 			} else { | 			} else { | ||||||
| 				os.api('notes/reactions/create', { | 				os.api('notes/reactions/create', { | ||||||
| 					noteId: this.note.id, | 					noteId: props.note.id, | ||||||
| 					reaction: this.reaction | 					reaction: props.reaction | ||||||
| 				}); | 				}); | ||||||
| 			} | 			} | ||||||
| 		}, | 		}; | ||||||
| 		onMouseover() { |  | ||||||
| 			if (this.isHovering) return; |  | ||||||
| 			this.isHovering = true; |  | ||||||
| 			this.detailsTimeoutId = setTimeout(this.openDetails, 300); |  | ||||||
| 		}, |  | ||||||
| 		onMouseleave() { |  | ||||||
| 			if (!this.isHovering) return; |  | ||||||
| 			this.isHovering = false; |  | ||||||
| 			clearTimeout(this.detailsTimeoutId); |  | ||||||
| 			this.closeDetails(); |  | ||||||
| 		}, |  | ||||||
| 		openDetails() { |  | ||||||
| 			os.api('notes/reactions', { |  | ||||||
| 				noteId: this.note.id, |  | ||||||
| 				type: this.reaction, |  | ||||||
| 				limit: 11 |  | ||||||
| 			}).then((reactions: any[]) => { |  | ||||||
| 				const users = reactions |  | ||||||
| 					.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) |  | ||||||
| 					.map(x => x.user); |  | ||||||
| 
 | 
 | ||||||
| 				this.closeDetails(); | 		const anime = () => { | ||||||
| 				if (!this.isHovering) return; |  | ||||||
| 
 |  | ||||||
| 				const showing = ref(true); |  | ||||||
| 				os.popup(XDetails, { |  | ||||||
| 					showing, |  | ||||||
| 					reaction: this.reaction, |  | ||||||
| 					emojis: this.note.emojis, |  | ||||||
| 					users, |  | ||||||
| 					count: this.count, |  | ||||||
| 					source: this.$refs.reaction |  | ||||||
| 				}, {}, 'closed'); |  | ||||||
| 
 |  | ||||||
| 				this.close = () => { |  | ||||||
| 					showing.value = false; |  | ||||||
| 				}; |  | ||||||
| 			}); |  | ||||||
| 		}, |  | ||||||
| 		closeDetails() { |  | ||||||
| 			if (this.close != null) { |  | ||||||
| 				this.close(); |  | ||||||
| 				this.close = null; |  | ||||||
| 			} |  | ||||||
| 		}, |  | ||||||
| 		anime() { |  | ||||||
| 			if (document.hidden) return; | 			if (document.hidden) return; | ||||||
| 
 | 
 | ||||||
| 			// TODO | 			// TODO: 新しくリアクションが付いたことが視覚的に分かりやすいアニメーション | ||||||
| 		}, | 		}; | ||||||
| 	} | 
 | ||||||
|  | 		watch(() => props.count, (newCount, oldCount) => { | ||||||
|  | 			if (oldCount < newCount) anime(); | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		onMounted(() => { | ||||||
|  | 			if (!props.isInitial) anime(); | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		const { onMouseover, onMouseleave } = useTooltip(async (showing) => { | ||||||
|  | 			const reactions = await os.api('notes/reactions', { | ||||||
|  | 				noteId: props.note.id, | ||||||
|  | 				type: props.reaction, | ||||||
|  | 				limit: 11 | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			const users = reactions | ||||||
|  | 				.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) | ||||||
|  | 				.map(x => x.user); | ||||||
|  | 
 | ||||||
|  | 			os.popup(XDetails, { | ||||||
|  | 				showing, | ||||||
|  | 				reaction: props.reaction, | ||||||
|  | 				emojis: props.note.emojis, | ||||||
|  | 				users, | ||||||
|  | 				count: props.count, | ||||||
|  | 				source: buttonRef.value | ||||||
|  | 			}, {}, 'closed'); | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		return { | ||||||
|  | 			buttonRef, | ||||||
|  | 			canToggle, | ||||||
|  | 			toggleReaction, | ||||||
|  | 			onMouseover, | ||||||
|  | 			onMouseleave, | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,13 +1,13 @@ | ||||||
| <template> | <template> | ||||||
| <button | <button | ||||||
| 	class="button _button canRenote" | 	class="eddddedb _button canRenote" | ||||||
| 	@click="renote()" | 	@click="renote()" | ||||||
| 	v-if="canRenote" | 	v-if="canRenote" | ||||||
| 	@touchstart.passive="onMouseover" | 	@touchstart.passive="onMouseover" | ||||||
| 	@mouseover="onMouseover" | 	@mouseover="onMouseover" | ||||||
| 	@mouseleave="onMouseleave" | 	@mouseleave="onMouseleave" | ||||||
| 	@touchend="onMouseleave" | 	@touchend="onMouseleave" | ||||||
| 	ref="renoteButton" | 	ref="buttonRef" | ||||||
| > | > | ||||||
| 	<i class="fas fa-retweet"></i> | 	<i class="fas fa-retweet"></i> | ||||||
| 	<p class="count" v-if="count > 0">{{ count }}</p> | 	<p class="count" v-if="count > 0">{{ count }}</p> | ||||||
|  | @ -21,10 +21,13 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent, ref } from 'vue'; | import { computed, defineComponent, ref } from 'vue'; | ||||||
| import XDetails from '@client/components/renote.details.vue'; | import XDetails from '@/components/renote.details.vue'; | ||||||
| import { pleaseLogin } from '@client/scripts/please-login'; | import { pleaseLogin } from '@/scripts/please-login'; | ||||||
| import * as os from '@client/os'; | import * as os from '@/os'; | ||||||
|  | import { $i } from '@/account'; | ||||||
|  | import { useTooltip } from '@/scripts/use-tooltip'; | ||||||
|  | import { i18n } from '@/i18n'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	props: { | 	props: { | ||||||
|  | @ -37,95 +40,68 @@ export default defineComponent({ | ||||||
| 			required: true, | 			required: true, | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
| 	data() { | 
 | ||||||
| 		return { | 	setup(props) { | ||||||
| 			close: null, | 		const buttonRef = ref<HTMLElement>(); | ||||||
| 			detailsTimeoutId: null, | 
 | ||||||
| 			isHovering: false | 		const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id); | ||||||
| 		}; | 
 | ||||||
| 	}, | 		const { onMouseover, onMouseleave } = useTooltip(async (showing) => { | ||||||
| 	computed: { | 			const renotes = await os.api('notes/renotes', { | ||||||
| 		canRenote(): boolean { | 				noteId: props.note.id, | ||||||
| 			return ['public', 'home'].includes(this.note.visibility) || this.note.userId === this.$i.id; | 				limit: 11 | ||||||
| 		}, | 			}); | ||||||
| 	}, | 
 | ||||||
| 	watch: { | 			const users = renotes | ||||||
| 		count(newCount, oldCount) { | 				.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) | ||||||
| 			if (oldCount < newCount) this.anime(); | 				.map(x => x.user); | ||||||
| 			if (this.close != null) this.openDetails(); | 
 | ||||||
| 		}, | 			if (users.length < 1) return; | ||||||
| 	}, | 
 | ||||||
| 	methods: { | 			os.popup(XDetails, { | ||||||
| 		renote(viaKeyboard = false) { | 				showing, | ||||||
|  | 				users, | ||||||
|  | 				count: props.count, | ||||||
|  | 				source: buttonRef.value | ||||||
|  | 			}, {}, 'closed'); | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		const renote = (viaKeyboard = false) => { | ||||||
| 			pleaseLogin(); | 			pleaseLogin(); | ||||||
| 			os.popupMenu([{ | 			os.popupMenu([{ | ||||||
| 				text: this.$ts.renote, | 				text: i18n.locale.renote, | ||||||
| 				icon: 'fas fa-retweet', | 				icon: 'fas fa-retweet', | ||||||
| 				action: () => { | 				action: () => { | ||||||
| 					os.api('notes/create', { | 					os.api('notes/create', { | ||||||
| 						renoteId: this.note.id | 						renoteId: props.note.id | ||||||
| 					}); | 					}); | ||||||
| 				} | 				} | ||||||
| 			}, { | 			}, { | ||||||
| 				text: this.$ts.quote, | 				text: i18n.locale.quote, | ||||||
| 				icon: 'fas fa-quote-right', | 				icon: 'fas fa-quote-right', | ||||||
| 				action: () => { | 				action: () => { | ||||||
| 					os.post({ | 					os.post({ | ||||||
| 						renote: this.note, | 						renote: props.note, | ||||||
| 					}); | 					}); | ||||||
| 				} | 				} | ||||||
| 			}], this.$refs.renoteButton, { | 			}], buttonRef.value, { | ||||||
| 				viaKeyboard | 				viaKeyboard | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}; | ||||||
| 		onMouseover() { |  | ||||||
| 			if (this.isHovering) return; |  | ||||||
| 			this.isHovering = true; |  | ||||||
| 			this.detailsTimeoutId = setTimeout(this.openDetails, 300); |  | ||||||
| 		}, |  | ||||||
| 		onMouseleave() { |  | ||||||
| 			if (!this.isHovering) return; |  | ||||||
| 			this.isHovering = false; |  | ||||||
| 			clearTimeout(this.detailsTimeoutId); |  | ||||||
| 			this.closeDetails(); |  | ||||||
| 		}, |  | ||||||
| 		openDetails() { |  | ||||||
| 			os.api('notes/renotes', { |  | ||||||
| 				noteId: this.note.id, |  | ||||||
| 				limit: 11 |  | ||||||
| 			}).then((renotes: any[]) => { |  | ||||||
| 				const users = renotes |  | ||||||
| 					.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) |  | ||||||
| 					.map(x => x.user); |  | ||||||
| 
 | 
 | ||||||
| 				this.closeDetails(); | 		return { | ||||||
| 				if (!this.isHovering || users.length < 1) return; | 			buttonRef, | ||||||
| 
 | 			canRenote, | ||||||
| 				const showing = ref(true); | 			renote, | ||||||
| 				os.popup(XDetails, { | 			onMouseover, | ||||||
| 					showing, | 			onMouseleave, | ||||||
| 					users, | 		}; | ||||||
| 					count: this.count, | 	}, | ||||||
| 					source: this.$refs.renoteButton |  | ||||||
| 				}, {}, 'closed'); |  | ||||||
| 
 |  | ||||||
| 				this.close = () => { |  | ||||||
| 					showing.value = false; |  | ||||||
| 				}; |  | ||||||
| 			}); |  | ||||||
| 		}, |  | ||||||
| 		closeDetails() { |  | ||||||
| 			if (this.close != null) { |  | ||||||
| 				this.close(); |  | ||||||
| 				this.close = null; |  | ||||||
| 			} |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .button { | .eddddedb { | ||||||
| 	display: inline-block; | 	display: inline-block; | ||||||
| 	height: 32px; | 	height: 32px; | ||||||
| 	margin: 2px; | 	margin: 2px; | ||||||
|  |  | ||||||
							
								
								
									
										44
									
								
								packages/client/src/scripts/use-tooltip.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								packages/client/src/scripts/use-tooltip.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,44 @@ | ||||||
|  | import { Ref, ref } from 'vue'; | ||||||
|  | 
 | ||||||
|  | export function useTooltip(onShow: (showing: Ref<boolean>) => void) { | ||||||
|  | 	let isHovering = false; | ||||||
|  | 	let timeoutId: number; | ||||||
|  | 
 | ||||||
|  | 	let changeShowingState: (() => void) | null; | ||||||
|  | 
 | ||||||
|  | 	const open = () => { | ||||||
|  | 		close(); | ||||||
|  | 		if (!isHovering) return; | ||||||
|  | 
 | ||||||
|  | 		const showing = ref(true); | ||||||
|  | 		onShow(showing); | ||||||
|  | 		changeShowingState = () => { | ||||||
|  | 			showing.value = false; | ||||||
|  | 		}; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	const close = () => { | ||||||
|  | 		if (changeShowingState != null) { | ||||||
|  | 			changeShowingState(); | ||||||
|  | 			changeShowingState = null; | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	const onMouseover = () => { | ||||||
|  | 		if (isHovering) return; | ||||||
|  | 		isHovering = true; | ||||||
|  | 		timeoutId = window.setTimeout(open, 300); | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	const onMouseleave = () => { | ||||||
|  | 		if (!isHovering) return; | ||||||
|  | 		isHovering = false; | ||||||
|  | 		window.clearTimeout(timeoutId); | ||||||
|  | 		close(); | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	return { | ||||||
|  | 		onMouseover, | ||||||
|  | 		onMouseleave, | ||||||
|  | 	}; | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue