diff --git a/packages/client/src/components/chart-tooltip.vue b/packages/client/src/components/chart-tooltip.vue index b080eaf2b45899ed22803289aaafc5d4a2731e70..20e094a5a7e82441146bd47cc2053272689e338a 100644 --- a/packages/client/src/components/chart-tooltip.vue +++ b/packages/client/src/components/chart-tooltip.vue @@ -1,5 +1,5 @@ <template> -<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" @closed="emit('closed')"> +<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" :direction="'left'" :inner-margin="16" @closed="emit('closed')"> <div v-if="title" class="qpcyisrl"> <div class="title">{{ title }}</div> <div v-for="x in series" class="series"> diff --git a/packages/client/src/components/ui/tooltip.vue b/packages/client/src/components/ui/tooltip.vue index 1892877cc106a809c2d33991bccbb10ccff37fee..3ccd1b73169396704eae874808689d4c7b403b94 100644 --- a/packages/client/src/components/ui/tooltip.vue +++ b/packages/client/src/components/ui/tooltip.vue @@ -17,8 +17,12 @@ const props = withDefaults(defineProps<{ y?: number; text?: string; maxWidth?: number; + direction?: 'top' | 'bottom' | 'right' | 'left'; + innerMargin?: number; }>(), { maxWidth: 250, + direction: 'top', + innerMargin: 0, }); const emit = defineEmits<{ @@ -34,39 +38,144 @@ const setPosition = () => { const contentWidth = el.value.offsetWidth; const contentHeight = el.value.offsetHeight; - let left: number; - let top: number; - let rect: DOMRect; if (props.targetElement) { rect = props.targetElement.getBoundingClientRect(); + } + + const calcPosWhenTop = () => { + let left: number; + let top: number; + + if (props.targetElement) { + left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2); + top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin; + } else { + left = props.x; + top = (props.y - contentHeight) - props.innerMargin; + } + + left -= (el.value.offsetWidth / 2); + + if (left + contentWidth - window.pageXOffset > window.innerWidth) { + left = window.innerWidth - contentWidth + window.pageXOffset - 1; + } + + return [left, top]; + } + + const calcPosWhenBottom = () => { + let left: number; + let top: number; + + if (props.targetElement) { + left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2); + top = (rect.top + window.pageYOffset + props.targetElement.offsetHeight) + props.innerMargin; + } else { + left = props.x; + top = (props.y) + props.innerMargin; + } + + left -= (el.value.offsetWidth / 2); - left = rect.left + window.pageXOffset + (props.targetElement.offsetWidth / 2); - top = rect.top + window.pageYOffset - contentHeight; + if (left + contentWidth - window.pageXOffset > window.innerWidth) { + left = window.innerWidth - contentWidth + window.pageXOffset - 1; + } - el.value.style.transformOrigin = 'center bottom'; - } else { - left = props.x; - top = props.y - contentHeight; + return [left, top]; } - left -= (el.value.offsetWidth / 2); + const calcPosWhenLeft = () => { + let left: number; + let top: number; + + if (props.targetElement) { + left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin; + top = rect.top + window.pageYOffset + (props.targetElement.offsetHeight / 2); + } else { + left = (props.x - contentWidth) - props.innerMargin; + top = props.y; + } + + top -= (el.value.offsetHeight / 2); + + if (top + contentHeight - window.pageYOffset > window.innerHeight) { + top = window.innerHeight - contentHeight + window.pageYOffset - 1; + } - if (left + contentWidth - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - contentWidth + window.pageXOffset - 1; + return [left, top]; } - // ツールãƒãƒƒãƒ—を上ã«å‘ã‹ã£ã¦è¡¨ç¤ºã™ã‚‹ã‚¹ãƒšãƒ¼ã‚¹ãŒãªã‘ã‚Œã°ä¸‹ã«å‘ã‹ã£ã¦å‡ºã™ - if (top - window.pageYOffset < 0) { + const calcPosWhenRight = () => { + let left: number; + let top: number; + if (props.targetElement) { - top = rect.top + window.pageYOffset + props.targetElement.offsetHeight; - el.value.style.transformOrigin = 'center top'; + left = (rect.left + window.pageXOffset) + props.innerMargin; + top = rect.top + window.pageYOffset + (props.targetElement.offsetHeight / 2); } else { + left = props.x + props.innerMargin; top = props.y; } + + top -= (el.value.offsetHeight / 2); + + if (top + contentHeight - window.pageYOffset > window.innerHeight) { + top = window.innerHeight - contentHeight + window.pageYOffset - 1; + } + + return [left, top]; + } + + const calc = (): { + left: number; + top: number; + transformOrigin: string; + } => { + switch (props.direction) { + case 'top': { + const [left, top] = calcPosWhenTop(); + + // ツールãƒãƒƒãƒ—を上ã«å‘ã‹ã£ã¦è¡¨ç¤ºã™ã‚‹ã‚¹ãƒšãƒ¼ã‚¹ãŒãªã‘ã‚Œã°ä¸‹ã«å‘ã‹ã£ã¦å‡ºã™ + if (top - window.pageYOffset < 0) { + const [left, top] = calcPosWhenBottom(); + return { left, top, transformOrigin: 'center top' }; + } + + return { left, top, transformOrigin: 'center bottom' }; + } + + case 'bottom': { + const [left, top] = calcPosWhenBottom(); + // TODO: ツールãƒãƒƒãƒ—を下ã«å‘ã‹ã£ã¦è¡¨ç¤ºã™ã‚‹ã‚¹ãƒšãƒ¼ã‚¹ãŒãªã‘ã‚Œã°ä¸Šã«å‘ã‹ã£ã¦å‡ºã™ + return { left, top, transformOrigin: 'center top' }; + } + + case 'left': { + const [left, top] = calcPosWhenLeft(); + + // ツールãƒãƒƒãƒ—ã‚’å·¦ã«å‘ã‹ã£ã¦è¡¨ç¤ºã™ã‚‹ã‚¹ãƒšãƒ¼ã‚¹ãŒãªã‘ã‚Œã°å³ã«å‘ã‹ã£ã¦å‡ºã™ + if (left - window.pageXOffset < 0) { + const [left, top] = calcPosWhenRight(); + return { left, top, transformOrigin: 'left center' }; + } + + return { left, top, transformOrigin: 'right center' }; + } + + case 'right': { + const [left, top] = calcPosWhenRight(); + // TODO: ツールãƒãƒƒãƒ—ã‚’å³ã«å‘ã‹ã£ã¦è¡¨ç¤ºã™ã‚‹ã‚¹ãƒšãƒ¼ã‚¹ãŒãªã‘ã‚Œã°å·¦ã«å‘ã‹ã£ã¦å‡ºã™ + return { left, top, transformOrigin: 'left center' }; + } + } + + return null as never; } + const { left, top, transformOrigin } = calc(); + el.value.style.transformOrigin = transformOrigin; el.value.style.left = left + 'px'; el.value.style.top = top + 'px'; };