diff --git a/packages/client/src/components/chart-tooltip.vue b/packages/client/src/components/chart-tooltip.vue new file mode 100644 index 0000000000..b080eaf2b4 --- /dev/null +++ b/packages/client/src/components/chart-tooltip.vue @@ -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> diff --git a/packages/client/src/components/chart.vue b/packages/client/src/components/chart.vue index d17c0c9f3e..3e46c51b47 100644 --- a/packages/client/src/components/chart.vue +++ b/packages/client/src/components/chart.vue @@ -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, diff --git a/packages/client/src/components/form/range.vue b/packages/client/src/components/form/range.vue index 3e02cacb9b..a82348d317 100644 --- a/packages/client/src/components/form/range.vue +++ b/packages/client/src/components/form/range.vue @@ -117,7 +117,7 @@ export default defineComponent({ text: computed(() => { return props.textConverter(finalValue.value); }), - source: thumbEl, + targetElement: thumbEl, }, {}, 'closed'); const style = document.createElement('style'); diff --git a/packages/client/src/components/notification.vue b/packages/client/src/components/notification.vue index 5659c899be..d855f81f8a 100644 --- a/packages/client/src/components/notification.vue +++ b/packages/client/src/components/notification.vue @@ -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'); }); diff --git a/packages/client/src/components/reaction-tooltip.vue b/packages/client/src/components/reaction-tooltip.vue index 1b2a024e21..b53061df48 100644 --- a/packages/client/src/components/reaction-tooltip.vue +++ b/packages/client/src/components/reaction-tooltip.vue @@ -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> diff --git a/packages/client/src/components/reactions-viewer.details.vue b/packages/client/src/components/reactions-viewer.details.vue index 8cec8dfa2f..eb889c4888 100644 --- a/packages/client/src/components/reactions-viewer.details.vue +++ b/packages/client/src/components/reactions-viewer.details.vue @@ -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> diff --git a/packages/client/src/components/renote.details.vue b/packages/client/src/components/renote.details.vue index cdbc71bdce..2df19bcd3f 100644 --- a/packages/client/src/components/renote.details.vue +++ b/packages/client/src/components/renote.details.vue @@ -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> diff --git a/packages/client/src/components/ui/tooltip.vue b/packages/client/src/components/ui/tooltip.vue index e2721ed69a..1892877cc1 100644 --- a/packages/client/src/components/ui/tooltip.vue +++ b/packages/client/src/components/ui/tooltip.vue @@ -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; - el.value.style.transformOrigin = 'center top'; + 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> diff --git a/packages/client/src/directives/tooltip.ts b/packages/client/src/directives/tooltip.ts index fffde14874..dd715227a4 100644 --- a/packages/client/src/directives/tooltip.ts +++ b/packages/client/src/directives/tooltip.ts @@ -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, () => {