enhance(client): Chartjsのツールチップを自前に
This commit is contained in:
		
							parent
							
								
									a2dcf2fc41
								
							
						
					
					
						commit
						8560e107bc
					
				
					 9 changed files with 138 additions and 29 deletions
				
			
		
							
								
								
									
										51
									
								
								packages/client/src/components/chart-tooltip.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								packages/client/src/components/chart-tooltip.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,51 @@ | |||
| <template> | ||||
| <MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" @closed="emit('closed')"> | ||||
| 	<div v-if="title" class="qpcyisrl"> | ||||
| 		<div class="title">{{ title }}</div> | ||||
| 		<div v-for="x in series" class="series"> | ||||
| 			<span class="color" :style="{ background: x.backgroundColor, borderColor: x.borderColor }"></span> | ||||
| 			<span>{{ x.text }}</span> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </MkTooltip> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts" setup> | ||||
| import {  } from 'vue'; | ||||
| import MkTooltip from './ui/tooltip.vue'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
| 	showing: boolean; | ||||
| 	x: number; | ||||
| 	y: number; | ||||
| 	title: string; | ||||
| 	series: { | ||||
| 		backgroundColor: string; | ||||
| 		borderColor: string; | ||||
| 		text: string; | ||||
| 	}[]; | ||||
| }>(); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
| 	(ev: 'closed'): void; | ||||
| }>(); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .qpcyisrl { | ||||
| 	> .title { | ||||
| 		margin-bottom: 4px; | ||||
| 	} | ||||
| 
 | ||||
| 	> .series { | ||||
| 		> .color { | ||||
| 			display: inline-block; | ||||
| 			width: 8px; | ||||
| 			height: 8px; | ||||
| 			border-width: 1px; | ||||
| 			border-style: solid; | ||||
| 			margin-right: 8px; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -8,7 +8,7 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, onMounted, ref, watch, PropType } from 'vue'; | ||||
| import { defineComponent, onMounted, ref, watch, PropType, onUnmounted, shallowRef } from 'vue'; | ||||
| import { | ||||
| 	Chart, | ||||
| 	ArcElement, | ||||
|  | @ -31,6 +31,7 @@ import { enUS } from 'date-fns/locale'; | |||
| import zoomPlugin from 'chartjs-plugin-zoom'; | ||||
| import * as os from '@/os'; | ||||
| import { defaultStore } from '@/store'; | ||||
| import MkChartTooltip from '@/components/chart-tooltip.vue'; | ||||
| 
 | ||||
| Chart.register( | ||||
| 	ArcElement, | ||||
|  | @ -137,6 +138,43 @@ export default defineComponent({ | |||
| 			})); | ||||
| 		}; | ||||
| 
 | ||||
| 		const tooltipShowing = ref(false); | ||||
| 		const tooltipX = ref(0); | ||||
| 		const tooltipY = ref(0); | ||||
| 		const tooltipTitle = ref(null); | ||||
| 		const tooltipSeries = ref(null); | ||||
| 		let disposeTooltipComponent; | ||||
| 
 | ||||
| 		os.popup(MkChartTooltip, { | ||||
| 			showing: tooltipShowing, | ||||
| 			x: tooltipX, | ||||
| 			y: tooltipY, | ||||
| 			title: tooltipTitle, | ||||
| 			series: tooltipSeries, | ||||
| 		}, {}).then(({ dispose }) => { | ||||
| 			disposeTooltipComponent = dispose; | ||||
| 		}); | ||||
| 
 | ||||
| 		function externalTooltipHandler(context) { | ||||
| 			if (context.tooltip.opacity === 0) { | ||||
| 				tooltipShowing.value = false; | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			tooltipTitle.value = context.tooltip.title[0]; | ||||
| 			tooltipSeries.value = context.tooltip.body.map((b, i) => ({ | ||||
| 				backgroundColor: context.tooltip.labelColors[i].backgroundColor, | ||||
| 				borderColor: context.tooltip.labelColors[i].borderColor, | ||||
| 				text: b.lines[0], | ||||
| 			})); | ||||
| 
 | ||||
| 			const rect = context.chart.canvas.getBoundingClientRect(); | ||||
| 
 | ||||
| 			tooltipShowing.value = true; | ||||
| 			tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX; | ||||
| 			tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY; | ||||
| 		} | ||||
| 
 | ||||
| 		const render = () => { | ||||
| 			if (chartInstance) { | ||||
| 				chartInstance.destroy(); | ||||
|  | @ -222,10 +260,12 @@ export default defineComponent({ | |||
| 							}, | ||||
| 						}, | ||||
| 						tooltip: { | ||||
| 							enabled: false, | ||||
| 							mode: 'index', | ||||
| 							animation: { | ||||
| 								duration: 0, | ||||
| 							}, | ||||
| 							external: externalTooltipHandler, | ||||
| 						}, | ||||
| 						zoom: { | ||||
| 							pan: { | ||||
|  | @ -684,6 +724,10 @@ export default defineComponent({ | |||
| 			fetchAndRender(); | ||||
| 		}); | ||||
| 
 | ||||
| 		onUnmounted(() => { | ||||
| 			if (disposeTooltipComponent) disposeTooltipComponent(); | ||||
| 		}); | ||||
| 
 | ||||
| 		return { | ||||
| 			chartEl, | ||||
| 			fetching, | ||||
|  |  | |||
|  | @ -117,7 +117,7 @@ export default defineComponent({ | |||
| 				text: computed(() => { | ||||
| 					return props.textConverter(finalValue.value); | ||||
| 				}), | ||||
| 				source: thumbEl, | ||||
| 				targetElement: thumbEl, | ||||
| 			}, {}, 'closed'); | ||||
| 
 | ||||
| 			const style = document.createElement('style'); | ||||
|  |  | |||
|  | @ -153,7 +153,7 @@ export default defineComponent({ | |||
| 				showing, | ||||
| 				reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction, | ||||
| 				emojis: props.notification.note.emojis, | ||||
| 				source: reactionRef.value.$el, | ||||
| 				targetElement: reactionRef.value.$el, | ||||
| 			}, {}, 'closed'); | ||||
| 		}); | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')"> | ||||
| <MkTooltip ref="tooltip" :target-element="targetElement" :max-width="340" @closed="emit('closed')"> | ||||
| 	<div class="beeadbfb"> | ||||
| 		<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/> | ||||
| 		<div class="name">{{ reaction.replace('@.', '') }}</div> | ||||
|  | @ -15,11 +15,11 @@ import XReactionIcon from './reaction-icon.vue'; | |||
| const props = defineProps<{ | ||||
| 	reaction: string; | ||||
| 	emojis: any[]; // TODO | ||||
| 	source: any; // TODO | ||||
| 	targetElement: HTMLElement; | ||||
| }>(); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
| 	(e: 'closed'): void; | ||||
| 	(ev: 'closed'): void; | ||||
| }>(); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')"> | ||||
| <MkTooltip ref="tooltip" :target-element="targetElement" :max-width="340" @closed="emit('closed')"> | ||||
| 	<div class="bqxuuuey"> | ||||
| 		<div class="reaction"> | ||||
| 			<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/> | ||||
|  | @ -26,11 +26,11 @@ const props = defineProps<{ | |||
| 	users: any[]; // TODO | ||||
| 	count: number; | ||||
| 	emojis: any[]; // TODO | ||||
| 	source: any; // TODO | ||||
| 	targetElement: HTMLElement; | ||||
| }>(); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
| 	(e: 'closed'): void; | ||||
| 	(ev: 'closed'): void; | ||||
| }>(); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <MkTooltip ref="tooltip" :source="source" :max-width="250" @closed="emit('closed')"> | ||||
| <MkTooltip ref="tooltip" :target-element="targetElement" :max-width="250" @closed="emit('closed')"> | ||||
| 	<div class="beaffaef"> | ||||
| 		<div v-for="u in users" :key="u.id" class="user"> | ||||
| 			<MkAvatar class="avatar" :user="u"/> | ||||
|  | @ -17,11 +17,11 @@ import MkTooltip from './ui/tooltip.vue'; | |||
| const props = defineProps<{ | ||||
| 	users: any[]; // TODO | ||||
| 	count: number; | ||||
| 	source: any; // TODO | ||||
| 	targetElement: HTMLElement; | ||||
| }>(); | ||||
| 
 | ||||
| const emit = defineEmits<{ | ||||
| 	(e: 'closed'): void; | ||||
| 	(ev: 'closed'): void; | ||||
| }>(); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -12,9 +12,11 @@ import * as os from '@/os'; | |||
| 
 | ||||
| const props = withDefaults(defineProps<{ | ||||
| 	showing: boolean; | ||||
| 	source: HTMLElement; | ||||
| 	targetElement?: HTMLElement; | ||||
| 	x?: number; | ||||
| 	y?: number; | ||||
| 	text?: string; | ||||
| 	maxWidth?; number; | ||||
| 	maxWidth?: number; | ||||
| }>(), { | ||||
| 	maxWidth: 250, | ||||
| }); | ||||
|  | @ -29,13 +31,25 @@ const zIndex = os.claimZIndex('high'); | |||
| const setPosition = () => { | ||||
| 	if (el.value == null) return; | ||||
| 
 | ||||
| 	const rect = props.source.getBoundingClientRect(); | ||||
| 
 | ||||
| 	const contentWidth = el.value.offsetWidth; | ||||
| 	const contentHeight = el.value.offsetHeight; | ||||
| 
 | ||||
| 	let left = rect.left + window.pageXOffset + (props.source.offsetWidth / 2); | ||||
| 	let top = rect.top + window.pageYOffset - contentHeight; | ||||
| 	let left: number; | ||||
| 	let top: number; | ||||
| 
 | ||||
| 	let rect: DOMRect; | ||||
| 
 | ||||
| 	if (props.targetElement) { | ||||
| 		rect = props.targetElement.getBoundingClientRect(); | ||||
| 
 | ||||
| 		left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2); | ||||
| 		top = rect.top + window.pageYOffset - contentHeight; | ||||
| 
 | ||||
| 		el.value.style.transformOrigin = 'center bottom'; | ||||
| 	} else { | ||||
| 		left = props.x; | ||||
| 		top = props.y - contentHeight; | ||||
| 	} | ||||
| 
 | ||||
| 	left -= (el.value.offsetWidth / 2); | ||||
| 
 | ||||
|  | @ -43,9 +57,14 @@ const setPosition = () => { | |||
| 		left = window.innerWidth - contentWidth + window.pageXOffset - 1; | ||||
| 	} | ||||
| 
 | ||||
| 	// ツールチップを上に向かって表示するスペースがなければ下に向かって出す | ||||
| 	if (top - window.pageYOffset < 0) { | ||||
| 		top = rect.top + window.pageYOffset + props.source.offsetHeight; | ||||
| 		if (props.targetElement) { | ||||
| 			top = rect.top + window.pageYOffset + props.targetElement.offsetHeight; | ||||
| 			el.value.style.transformOrigin = 'center top'; | ||||
| 		} else { | ||||
| 			top = props.y; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	el.value.style.left = left + 'px'; | ||||
|  | @ -54,11 +73,6 @@ const setPosition = () => { | |||
| 
 | ||||
| onMounted(() => { | ||||
| 	nextTick(() => { | ||||
| 		if (props.source == null) { | ||||
| 			emit('closed'); | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		setPosition(); | ||||
| 
 | ||||
| 		let loopHandler; | ||||
|  | @ -101,6 +115,6 @@ onMounted(() => { | |||
| 	border-radius: 4px; | ||||
| 	border: solid 0.5px var(--divider); | ||||
| 	pointer-events: none; | ||||
| 	transform-origin: center bottom; | ||||
| 	transform-origin: center center; | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -48,7 +48,7 @@ export default { | |||
| 			popup(import('@/components/ui/tooltip.vue'), { | ||||
| 				showing, | ||||
| 				text: self.text, | ||||
| 				source: el | ||||
| 				targetElement: el, | ||||
| 			}, {}, 'closed'); | ||||
| 
 | ||||
| 			self._close = () => { | ||||
|  | @ -56,8 +56,8 @@ export default { | |||
| 			}; | ||||
| 		}; | ||||
| 
 | ||||
| 		el.addEventListener('selectstart', e => { | ||||
| 			e.preventDefault(); | ||||
| 		el.addEventListener('selectstart', ev => { | ||||
| 			ev.preventDefault(); | ||||
| 		}); | ||||
| 
 | ||||
| 		el.addEventListener(start, () => { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue