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> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent, onMounted, ref, watch, PropType } from 'vue'; | import { defineComponent, onMounted, ref, watch, PropType, onUnmounted, shallowRef } from 'vue'; | ||||||
| import { | import { | ||||||
| 	Chart, | 	Chart, | ||||||
| 	ArcElement, | 	ArcElement, | ||||||
|  | @ -31,6 +31,7 @@ import { enUS } from 'date-fns/locale'; | ||||||
| import zoomPlugin from 'chartjs-plugin-zoom'; | import zoomPlugin from 'chartjs-plugin-zoom'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { defaultStore } from '@/store'; | import { defaultStore } from '@/store'; | ||||||
|  | import MkChartTooltip from '@/components/chart-tooltip.vue'; | ||||||
| 
 | 
 | ||||||
| Chart.register( | Chart.register( | ||||||
| 	ArcElement, | 	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 = () => { | 		const render = () => { | ||||||
| 			if (chartInstance) { | 			if (chartInstance) { | ||||||
| 				chartInstance.destroy(); | 				chartInstance.destroy(); | ||||||
|  | @ -222,10 +260,12 @@ export default defineComponent({ | ||||||
| 							}, | 							}, | ||||||
| 						}, | 						}, | ||||||
| 						tooltip: { | 						tooltip: { | ||||||
|  | 							enabled: false, | ||||||
| 							mode: 'index', | 							mode: 'index', | ||||||
| 							animation: { | 							animation: { | ||||||
| 								duration: 0, | 								duration: 0, | ||||||
| 							}, | 							}, | ||||||
|  | 							external: externalTooltipHandler, | ||||||
| 						}, | 						}, | ||||||
| 						zoom: { | 						zoom: { | ||||||
| 							pan: { | 							pan: { | ||||||
|  | @ -684,6 +724,10 @@ export default defineComponent({ | ||||||
| 			fetchAndRender(); | 			fetchAndRender(); | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
|  | 		onUnmounted(() => { | ||||||
|  | 			if (disposeTooltipComponent) disposeTooltipComponent(); | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
| 		return { | 		return { | ||||||
| 			chartEl, | 			chartEl, | ||||||
| 			fetching, | 			fetching, | ||||||
|  |  | ||||||
|  | @ -117,7 +117,7 @@ export default defineComponent({ | ||||||
| 				text: computed(() => { | 				text: computed(() => { | ||||||
| 					return props.textConverter(finalValue.value); | 					return props.textConverter(finalValue.value); | ||||||
| 				}), | 				}), | ||||||
| 				source: thumbEl, | 				targetElement: thumbEl, | ||||||
| 			}, {}, 'closed'); | 			}, {}, 'closed'); | ||||||
| 
 | 
 | ||||||
| 			const style = document.createElement('style'); | 			const style = document.createElement('style'); | ||||||
|  |  | ||||||
|  | @ -153,7 +153,7 @@ export default defineComponent({ | ||||||
| 				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, | 				targetElement: reactionRef.value.$el, | ||||||
| 			}, {}, 'closed'); | 			}, {}, 'closed'); | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <template> | <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"> | 	<div class="beeadbfb"> | ||||||
| 		<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/> | 		<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/> | ||||||
| 		<div class="name">{{ reaction.replace('@.', '') }}</div> | 		<div class="name">{{ reaction.replace('@.', '') }}</div> | ||||||
|  | @ -15,11 +15,11 @@ import XReactionIcon from './reaction-icon.vue'; | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	reaction: string; | 	reaction: string; | ||||||
| 	emojis: any[]; // TODO | 	emojis: any[]; // TODO | ||||||
| 	source: any; // TODO | 	targetElement: HTMLElement; | ||||||
| }>(); | }>(); | ||||||
| 
 | 
 | ||||||
| const emit = defineEmits<{ | const emit = defineEmits<{ | ||||||
| 	(e: 'closed'): void; | 	(ev: 'closed'): void; | ||||||
| }>(); | }>(); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <template> | <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="bqxuuuey"> | ||||||
| 		<div class="reaction"> | 		<div class="reaction"> | ||||||
| 			<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/> | 			<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/> | ||||||
|  | @ -26,11 +26,11 @@ const props = defineProps<{ | ||||||
| 	users: any[]; // TODO | 	users: any[]; // TODO | ||||||
| 	count: number; | 	count: number; | ||||||
| 	emojis: any[]; // TODO | 	emojis: any[]; // TODO | ||||||
| 	source: any; // TODO | 	targetElement: HTMLElement; | ||||||
| }>(); | }>(); | ||||||
| 
 | 
 | ||||||
| const emit = defineEmits<{ | const emit = defineEmits<{ | ||||||
| 	(e: 'closed'): void; | 	(ev: 'closed'): void; | ||||||
| }>(); | }>(); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| <template> | <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 class="beaffaef"> | ||||||
| 		<div v-for="u in users" :key="u.id" class="user"> | 		<div v-for="u in users" :key="u.id" class="user"> | ||||||
| 			<MkAvatar class="avatar" :user="u"/> | 			<MkAvatar class="avatar" :user="u"/> | ||||||
|  | @ -17,11 +17,11 @@ import MkTooltip from './ui/tooltip.vue'; | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
| 	users: any[]; // TODO | 	users: any[]; // TODO | ||||||
| 	count: number; | 	count: number; | ||||||
| 	source: any; // TODO | 	targetElement: HTMLElement; | ||||||
| }>(); | }>(); | ||||||
| 
 | 
 | ||||||
| const emit = defineEmits<{ | const emit = defineEmits<{ | ||||||
| 	(e: 'closed'): void; | 	(ev: 'closed'): void; | ||||||
| }>(); | }>(); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -12,9 +12,11 @@ import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| const props = withDefaults(defineProps<{ | const props = withDefaults(defineProps<{ | ||||||
| 	showing: boolean; | 	showing: boolean; | ||||||
| 	source: HTMLElement; | 	targetElement?: HTMLElement; | ||||||
|  | 	x?: number; | ||||||
|  | 	y?: number; | ||||||
| 	text?: string; | 	text?: string; | ||||||
| 	maxWidth?; number; | 	maxWidth?: number; | ||||||
| }>(), { | }>(), { | ||||||
| 	maxWidth: 250, | 	maxWidth: 250, | ||||||
| }); | }); | ||||||
|  | @ -29,13 +31,25 @@ const zIndex = os.claimZIndex('high'); | ||||||
| const setPosition = () => { | const setPosition = () => { | ||||||
| 	if (el.value == null) return; | 	if (el.value == null) return; | ||||||
| 
 | 
 | ||||||
| 	const rect = props.source.getBoundingClientRect(); |  | ||||||
| 
 |  | ||||||
| 	const contentWidth = el.value.offsetWidth; | 	const contentWidth = el.value.offsetWidth; | ||||||
| 	const contentHeight = el.value.offsetHeight; | 	const contentHeight = el.value.offsetHeight; | ||||||
| 
 | 
 | ||||||
| 	let left = rect.left + window.pageXOffset + (props.source.offsetWidth / 2); | 	let left: number; | ||||||
| 	let top = rect.top + window.pageYOffset - contentHeight; | 	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); | 	left -= (el.value.offsetWidth / 2); | ||||||
| 
 | 
 | ||||||
|  | @ -43,9 +57,14 @@ const setPosition = () => { | ||||||
| 		left = window.innerWidth - contentWidth + window.pageXOffset - 1; | 		left = window.innerWidth - contentWidth + window.pageXOffset - 1; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// ツールチップを上に向かって表示するスペースがなければ下に向かって出す | ||||||
| 	if (top - window.pageYOffset < 0) { | 	if (top - window.pageYOffset < 0) { | ||||||
| 		top = rect.top + window.pageYOffset + props.source.offsetHeight; | 		if (props.targetElement) { | ||||||
| 		el.value.style.transformOrigin = 'center top'; | 			top = rect.top + window.pageYOffset + props.targetElement.offsetHeight; | ||||||
|  | 			el.value.style.transformOrigin = 'center top'; | ||||||
|  | 		} else { | ||||||
|  | 			top = props.y; | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	el.value.style.left = left + 'px'; | 	el.value.style.left = left + 'px'; | ||||||
|  | @ -54,11 +73,6 @@ const setPosition = () => { | ||||||
| 
 | 
 | ||||||
| onMounted(() => { | onMounted(() => { | ||||||
| 	nextTick(() => { | 	nextTick(() => { | ||||||
| 		if (props.source == null) { |  | ||||||
| 			emit('closed'); |  | ||||||
| 			return; |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		setPosition(); | 		setPosition(); | ||||||
| 
 | 
 | ||||||
| 		let loopHandler; | 		let loopHandler; | ||||||
|  | @ -101,6 +115,6 @@ onMounted(() => { | ||||||
| 	border-radius: 4px; | 	border-radius: 4px; | ||||||
| 	border: solid 0.5px var(--divider); | 	border: solid 0.5px var(--divider); | ||||||
| 	pointer-events: none; | 	pointer-events: none; | ||||||
| 	transform-origin: center bottom; | 	transform-origin: center center; | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -48,7 +48,7 @@ export default { | ||||||
| 			popup(import('@/components/ui/tooltip.vue'), { | 			popup(import('@/components/ui/tooltip.vue'), { | ||||||
| 				showing, | 				showing, | ||||||
| 				text: self.text, | 				text: self.text, | ||||||
| 				source: el | 				targetElement: el, | ||||||
| 			}, {}, 'closed'); | 			}, {}, 'closed'); | ||||||
| 
 | 
 | ||||||
| 			self._close = () => { | 			self._close = () => { | ||||||
|  | @ -56,8 +56,8 @@ export default { | ||||||
| 			}; | 			}; | ||||||
| 		}; | 		}; | ||||||
| 
 | 
 | ||||||
| 		el.addEventListener('selectstart', e => { | 		el.addEventListener('selectstart', ev => { | ||||||
| 			e.preventDefault(); | 			ev.preventDefault(); | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
| 		el.addEventListener(start, () => { | 		el.addEventListener(start, () => { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue