From e88c1fa32979bbfa45f98a8135394674dd2bc2fd Mon Sep 17 00:00:00 2001
From: advplyr <advplyr@protonmail.com>
Date: Sat, 6 Jan 2024 15:54:48 -0600
Subject: [PATCH] Update:Show tooltip for library item card titles that are
 truncated #2451 - Refactored tooltip so that they dont overflow the window

---
 client/components/cards/LazyBookCard.vue | 13 +++-
 client/components/ui/Tooltip.vue         | 77 ++++++++++++++++++------
 2 files changed, 69 insertions(+), 21 deletions(-)

diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue
index c4d1345d..04b3ce59 100644
--- a/client/components/cards/LazyBookCard.vue
+++ b/client/components/cards/LazyBookCard.vue
@@ -8,10 +8,10 @@
     <!-- Alternative bookshelf title/author/sort -->
     <div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
       <div :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
-        <div class="flex items-center">
-          <span class="truncate">{{ displayTitle }}</span>
+        <ui-tooltip :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center">
+          <p ref="displayTitle" class="truncate">{{ displayTitle }}</p>
           <widgets-explicit-indicator :explicit="isExplicit" />
-        </div>
+        </ui-tooltip>
       </div>
       <p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || '&nbsp;' }}</p>
       <p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
@@ -164,6 +164,7 @@ export default {
       imageReady: false,
       selected: false,
       isSelectionMode: false,
+      displayTitleTruncated: false,
       showCoverBg: false
     }
   },
@@ -642,6 +643,12 @@ export default {
       }
 
       this.libraryItem = libraryItem
+
+      this.$nextTick(() => {
+        if (this.$refs.displayTitle) {
+          this.displayTitleTruncated = this.$refs.displayTitle.scrollWidth > this.$refs.displayTitle.clientWidth
+        }
+      })
     },
     clickCard(e) {
       if (this.processing) return
diff --git a/client/components/ui/Tooltip.vue b/client/components/ui/Tooltip.vue
index c1eabfc6..77245537 100644
--- a/client/components/ui/Tooltip.vue
+++ b/client/components/ui/Tooltip.vue
@@ -15,6 +15,13 @@ export default {
       type: String,
       default: 'right'
     },
+    /**
+     * Delay showing the tooltip after X milliseconds of hovering
+     */
+    delayOnShow: {
+      type: Number,
+      default: 0
+    },
     disabled: Boolean
   },
   data() {
@@ -22,7 +29,8 @@ export default {
       tooltip: null,
       tooltipId: null,
       isShowing: false,
-      hideTimeout: null
+      hideTimeout: null,
+      delayOnShowTimeout: null
     }
   },
   watch: {
@@ -59,29 +67,44 @@ export default {
       this.tooltip = tooltip
     },
     setTooltipPosition(tooltip) {
-      var boxChow = this.$refs.box.getBoundingClientRect()
+      const boxRect = this.$refs.box.getBoundingClientRect()
+
+      const shouldMount = !tooltip.isConnected
 
-      var shouldMount = !tooltip.isConnected
       // Calculate size of tooltip
       if (shouldMount) document.body.appendChild(tooltip)
-      var { width, height } = tooltip.getBoundingClientRect()
+      const tooltipRect = tooltip.getBoundingClientRect()
       if (shouldMount) tooltip.remove()
 
-      var top = 0
-      var left = 0
+      // Subtracting scrollbar size
+      const windowHeight = window.innerHeight - 8
+      const windowWidth = window.innerWidth - 8
+
+      let top = 0
+      let left = 0
       if (this.direction === 'right') {
-        top = boxChow.top - height / 2 + boxChow.height / 2
-        left = boxChow.left + boxChow.width + 4
+        top = Math.max(0, boxRect.top - tooltipRect.height / 2 + boxRect.height / 2)
+        left = Math.max(0, boxRect.left + boxRect.width + 4)
       } else if (this.direction === 'bottom') {
-        top = boxChow.top + boxChow.height + 4
-        left = boxChow.left - width / 2 + boxChow.width / 2
+        top = Math.max(0, boxRect.top + boxRect.height + 4)
+        left = Math.max(0, boxRect.left - tooltipRect.width / 2 + boxRect.width / 2)
       } else if (this.direction === 'top') {
-        top = boxChow.top - height - 4
-        left = boxChow.left - width / 2 + boxChow.width / 2
+        top = Math.max(0, boxRect.top - tooltipRect.height - 4)
+        left = Math.max(0, boxRect.left - tooltipRect.width / 2 + boxRect.width / 2)
       } else if (this.direction === 'left') {
-        top = boxChow.top - height / 2 + boxChow.height / 2
-        left = boxChow.left - width - 4
+        top = Math.max(0, boxRect.top - tooltipRect.height / 2 + boxRect.height / 2)
+        left = Math.max(0, boxRect.left - tooltipRect.width - 4)
       }
+
+      // Shift left if tooltip would overflow the window on the right
+      if (left + tooltipRect.width > windowWidth) {
+        left -= left + tooltipRect.width - windowWidth
+      }
+      // Shift up if tooltip would overflow the window on the bottom
+      if (top + tooltipRect.height > windowHeight) {
+        top -= top + tooltipRect.height - windowHeight
+      }
+
       tooltip.style.top = top + 'px'
       tooltip.style.left = left + 'px'
     },
@@ -107,15 +130,33 @@ export default {
       this.isShowing = false
     },
     cancelHide() {
-      if (this.hideTimeout) clearTimeout(this.hideTimeout)
+      clearTimeout(this.hideTimeout)
     },
     mouseover() {
-      if (!this.isShowing) this.showTooltip()
+      if (this.isShowing || this.disabled) return
+
+      if (this.delayOnShow) {
+        if (this.delayOnShowTimeout) {
+          // Delay already running
+          return
+        }
+
+        this.delayOnShowTimeout = setTimeout(() => {
+          this.showTooltip()
+          this.delayOnShowTimeout = null
+        }, this.delayOnShow)
+      } else {
+        this.showTooltip()
+      }
     },
     mouseleave() {
-      if (this.isShowing) {
-        this.hideTimeout = setTimeout(this.hideTooltip, 100)
+      if (!this.isShowing) {
+        clearTimeout(this.delayOnShowTimeout)
+        this.delayOnShowTimeout = null
+        return
       }
+
+      this.hideTimeout = setTimeout(this.hideTooltip, 100)
     }
   },
   beforeDestroy() {