From 62bc9db272941be8f2196862552be62f3bc9a0cb Mon Sep 17 00:00:00 2001
From: Takashi Kato <tohosaku@users.osdn.me>
Date: Sun, 24 Nov 2024 11:12:50 +0000
Subject: [PATCH] Replace jQuery UI tooltip to vanilla js tooltip

---
 app/assets/javascripts/application.js         | 12 ---
 app/assets/stylesheets/application.css        | 39 ++++----
 .../controllers/issue_tooltip_controller.js   | 33 +++++++
 app/javascript/main.js                        |  6 ++
 app/javascript/tooltip.js                     | 96 +++++++++++++++++++
 app/views/common/_calendar.html.erb           |  2 +-
 app/views/gantts/show.html.erb                |  2 +-
 config/importmap.rb                           |  1 +
 8 files changed, 160 insertions(+), 31 deletions(-)
 create mode 100644 app/javascript/controllers/issue_tooltip_controller.js
 create mode 100644 app/javascript/tooltip.js

diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 19578bee8f..9f336ae088 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -1160,18 +1160,6 @@ function setupWikiTableSortableHeader() {
   });
 }
 
-$(function () {
-  $("[title]:not(.no-tooltip)").tooltip({
-    show: {
-      delay: 400
-    },
-    position: {
-      my: "center bottom-5",
-      at: "center top"
-    }
-  });
-});
-
 function inlineAutoComplete(element) {
     'use strict';
 
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
index dab8cc4bb0..5c2dc42041 100644
--- a/app/assets/stylesheets/application.css
+++ b/app/assets/stylesheets/application.css
@@ -1348,7 +1348,6 @@ p.cal.legend span {display:flex;}
 /***** Tooltips ******/
 .tooltip{position:relative;z-index:24;}
 .tooltip:hover{z-index:25;color:#000;}
-.tooltip span.tip{display: none; text-align:left;}
 .tooltip span.tip a { color: #169 !important; }
 
 .tooltip span.tip img.gravatar {
@@ -1356,10 +1355,12 @@ p.cal.legend span {display:flex;}
   margin: 0;
 }
 
-div.tooltip:hover span.tip{
+div.tooltip span.tip{
+position:fixed;
+text-align:left;
 display:block;
-position:absolute;
-top:12px; width:270px;
+visibility:hidden;
+width:270px;
 border:1px solid #555;
 border-radius: 3px;
 background-color:#fff;
@@ -1367,10 +1368,25 @@ padding: 4px;
 font-size: 0.75rem;
 color:#505050;
 box-shadow: 0 1px 2px rgba(0,0,0,0.05);
+z-index:26;
 }
 
-table.cal div.tooltip:hover span.tip {
-  top: 25px;
+.action-tooltip {
+  position: fixed;
+  display: inline-block!important;
+  font-weight: normal;
+  background: #000;
+  color: #fff;
+  border-radius: 3px;
+  padding: 10px;
+  visibility: hidden;
+  opacity: 0;
+  transition: 0.3s ease-in;
+  font-size: 0.8rem;
+  border: 0;
+  box-shadow: none;
+  white-space: pre-wrap;
+  pointer-events: none;
 }
 
 img.ui-datepicker-trigger {
@@ -1777,17 +1793,6 @@ td.gantt_selected_column .gantt_hdr,.gantt_selected_column_container {
   background: #EEEEEE;
 }
 
-/***** Tooltips *****/
-.ui-tooltip {
-  background: #000;
-  color: #fff;
-  border-radius: 3px;
-  border: 0;
-  box-shadow: none;
-  white-space: pre-wrap;
-  pointer-events: none;
-}
-
 /***** SVG Icons *****/
 .icon, .icon-only {
   display: inline-flex;
diff --git a/app/javascript/controllers/issue_tooltip_controller.js b/app/javascript/controllers/issue_tooltip_controller.js
new file mode 100644
index 0000000000..e68492e47f
--- /dev/null
+++ b/app/javascript/controllers/issue_tooltip_controller.js
@@ -0,0 +1,33 @@
+/**
+ * Redmine - project management software
+ * Copyright (C) 2006-  Jean-Philippe Lang
+ * This code is released under the GNU General Public License.
+ */
+
+import { Controller } from "@hotwired/stimulus"
+import { Tooltip } from 'tooltip'
+
+// Connects to data-controller="issue--tooltip"
+export default class extends Controller {
+  show(e) {
+    const tooltip = new Tooltip({
+      delay: 0,
+      selector: '.tooltip',
+      createHook: (element) => {
+        element.tooltipElement = element.querySelector('.tip');
+      },
+      positionHook: (element, tooltip) => {
+        const trect = tooltip.getBoundingClientRect();
+        const rect  = element.getBoundingClientRect();
+        const gap = Math.min(12, rect.height / 2)
+        return {
+          top: rect.top + gap,
+          left: rect.left + gap,
+          width: trect.width,
+          height: trect.height
+        }
+      }
+    });
+    tooltip.show(e)
+  }
+}
diff --git a/app/javascript/main.js b/app/javascript/main.js
index 72ef077f84..b1aac929a2 100644
--- a/app/javascript/main.js
+++ b/app/javascript/main.js
@@ -1 +1,7 @@
 import "controllers"
+import {createTooltip} from 'tooltip';
+
+document.addEventListener('mouseover', (e) => {
+  const tooltip = createTooltip()
+  tooltip.show(e)
+});
diff --git a/app/javascript/tooltip.js b/app/javascript/tooltip.js
new file mode 100644
index 0000000000..6c9249451d
--- /dev/null
+++ b/app/javascript/tooltip.js
@@ -0,0 +1,96 @@
+/**
+ * Redmine - project management software
+ * Copyright (C) 2006-  Jean-Philippe Lang
+ * This code is released under the GNU General Public License.
+ */
+
+export function createTooltip() {
+  const tooltip = new Tooltip({
+    selector: '[title]:not(.no-tooltip)',
+    createHook: (element) => {
+      const tooltip = document.createElement('span')
+      tooltip.textContent = element.getAttribute('title');
+      element.setAttribute('title', '');
+      tooltip.classList.add('action-tooltip');
+      element.insertAdjacentElement('afterbegin', tooltip);
+      element.tooltipElement = tooltip;
+    },
+    positionHook: (element, tooltip) => {
+      const trect = tooltip.getBoundingClientRect();
+      const rect  = element.getBoundingClientRect();
+
+      return {
+        top: rect.top - trect.height - 5,
+        left: rect.left + rect.width / 2 - trect.width / 2,
+        width: trect.width,
+        height: trect.height
+      }
+    }
+  });
+  return tooltip;
+}
+
+export class Tooltip {
+  constructor(options) {
+    this.options = Object.assign({
+      delay: 400,
+      selector: undefined,
+    }, options)
+    this.delayedShow = null;
+    this.delayedHide = null;
+  }
+
+  show(e) {
+    const target = e.target.closest(this.options.selector)
+    if (target !== null) {
+
+      if (target.tooltipElement === undefined) {
+        this.options.createHook(target);
+        target.addEventListener('mouseleave', (e) => this.hide(e));
+      }
+
+      this.delayedShow = setTimeout(() => {
+        this.setPosition(target);
+      }, this.options.delay);
+    }
+  }
+
+  hide(e) {
+    if (this.delayedShow !== null) {
+      clearTimeout(this.delayedShow);
+    }
+    this.delayedHide = setTimeout(() => {
+      const tooltip = e.target.tooltipElement;
+      tooltip.style.visibility = 'hidden';
+      tooltip.style.opacity = 0;
+    }, this.options.delay);
+  }
+
+  setPosition(target) {
+    const tooltip = target.tooltipElement;
+    const position = this.options.positionHook(target, tooltip);
+    const cheight = document.documentElement.clientHeight;
+    const cwidth = document.documentElement.clientWidth;
+
+    if (position.top + position.height > cheight) {
+      position.top = cheight - position.height;
+    }
+
+    if (position.left + position.width > cwidth) {
+      position.left = cwidth - position.width;
+    }
+
+    if (position.top < 0) {
+      position.top = 0;
+    }
+
+    if (position.left < 0) {
+      position.left = 0;
+    }
+
+    tooltip.style.top = `${position.top}px`;
+    tooltip.style.left = `${position.left}px`;
+    tooltip.style.visibility = 'visible';
+    tooltip.style.opacity = 1;
+  }
+}
diff --git a/app/views/common/_calendar.html.erb b/app/views/common/_calendar.html.erb
index e1b2023332..1aa10c1520 100644
--- a/app/views/common/_calendar.html.erb
+++ b/app/views/common/_calendar.html.erb
@@ -1,6 +1,6 @@
 <%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do -%>
 <%= hidden_field_tag 'back_url', url_for(:params => request.query_parameters), :id => nil %>
-<ul class="cal">
+<ul class="cal" data-controller="issue_tooltip" data-action="mouseover->issue_tooltip#show">
   <li scope="col" title="<%= l(:label_week) %>" class="calhead week-number"></li>
   <% 7.times do |i| %>
     <li scope="col" class="calhead"><%= day_name((calendar.first_wday + i) % 7) %></li>
diff --git a/app/views/gantts/show.html.erb b/app/views/gantts/show.html.erb
index c43f10fdd8..4076a960d0 100644
--- a/app/views/gantts/show.html.erb
+++ b/app/views/gantts/show.html.erb
@@ -208,7 +208,7 @@
   </td>
 <% end %>
 <td>
-<div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;" id="gantt_area">
+<div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;" id="gantt_area" data-controller="issue_tooltip" data-action="mouseover->issue_tooltip#show">
 <%
   style  = ""
   style += "width: #{g_width - 1}px;"
diff --git a/config/importmap.rb b/config/importmap.rb
index 88274e924d..21ebc4a039 100644
--- a/config/importmap.rb
+++ b/config/importmap.rb
@@ -1,6 +1,7 @@
 # Pin npm packages by running ./bin/importmap
 
 pin "main"
+pin "tooltip"
 pin "@hotwired/stimulus", to: "stimulus.min.js"
 pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
 pin_all_from "app/javascript/controllers", under: "controllers"
-- 
2.39.2

