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