enhance(client): Chartjsのツールチップを自前に

This commit is contained in:
syuilo 2022-01-31 21:07:33 +09:00
parent a2dcf2fc41
commit 8560e107bc
9 changed files with 138 additions and 29 deletions

View 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>

View file

@ -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,

View file

@ -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');

View file

@ -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');
}); });

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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) {
top = rect.top + window.pageYOffset + props.targetElement.offsetHeight;
el.value.style.transformOrigin = 'center top'; 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>

View file

@ -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, () => {