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;
|
||||
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>
|
||||
|
|
|
@ -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…
Reference in a new issue