Project

General

Profile

Defect #42517 ยป 0001-Replace-jQuery-UI-tooltip-to-vanilla-js-tooltip.patch

Takashi Kato, 2025-04-05 02:46

View differences:

app/assets/javascripts/application.js
1160 1160
  });
1161 1161
}
1162 1162

  
1163
$(function () {
1164
  $("[title]:not(.no-tooltip)").tooltip({
1165
    show: {
1166
      delay: 400
1167
    },
1168
    position: {
1169
      my: "center bottom-5",
1170
      at: "center top"
1171
    }
1172
  });
1173
});
1174

  
1175 1163
function inlineAutoComplete(element) {
1176 1164
    'use strict';
1177 1165

  
app/assets/stylesheets/application.css
1348 1348
/***** Tooltips ******/
1349 1349
.tooltip{position:relative;z-index:24;}
1350 1350
.tooltip:hover{z-index:25;color:#000;}
1351
.tooltip span.tip{display: none; text-align:left;}
1352 1351
.tooltip span.tip a { color: #169 !important; }
1353 1352

  
1354 1353
.tooltip span.tip img.gravatar {
......
1356 1355
  margin: 0;
1357 1356
}
1358 1357

  
1359
div.tooltip:hover span.tip{
1358
div.tooltip span.tip{
1359
position:fixed;
1360
text-align:left;
1360 1361
display:block;
1361
position:absolute;
1362
top:12px; width:270px;
1362
visibility:hidden;
1363
width:270px;
1363 1364
border:1px solid #555;
1364 1365
border-radius: 3px;
1365 1366
background-color:#fff;
......
1367 1368
font-size: 0.75rem;
1368 1369
color:#505050;
1369 1370
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
1371
z-index:26;
1370 1372
}
1371 1373

  
1372
table.cal div.tooltip:hover span.tip {
1373
  top: 25px;
1374
.action-tooltip {
1375
  position: fixed;
1376
  display: inline-block!important;
1377
  font-weight: normal;
1378
  background: #000;
1379
  color: #fff;
1380
  border-radius: 3px;
1381
  padding: 10px;
1382
  visibility: hidden;
1383
  opacity: 0;
1384
  transition: 0.3s ease-in;
1385
  font-size: 0.8rem;
1386
  border: 0;
1387
  box-shadow: none;
1388
  white-space: pre-wrap;
1389
  pointer-events: none;
1374 1390
}
1375 1391

  
1376 1392
img.ui-datepicker-trigger {
......
1777 1793
  background: #EEEEEE;
1778 1794
}
1779 1795

  
1780
/***** Tooltips *****/
1781
.ui-tooltip {
1782
  background: #000;
1783
  color: #fff;
1784
  border-radius: 3px;
1785
  border: 0;
1786
  box-shadow: none;
1787
  white-space: pre-wrap;
1788
  pointer-events: none;
1789
}
1790

  
1791 1796
/***** SVG Icons *****/
1792 1797
.icon, .icon-only {
1793 1798
  display: inline-flex;
app/javascript/controllers/issue_tooltip_controller.js
1
/**
2
 * Redmine - project management software
3
 * Copyright (C) 2006-  Jean-Philippe Lang
4
 * This code is released under the GNU General Public License.
5
 */
6

  
7
import { Controller } from "@hotwired/stimulus"
8
import { Tooltip } from 'tooltip'
9

  
10
// Connects to data-controller="issue--tooltip"
11
export default class extends Controller {
12
  show(e) {
13
    const tooltip = new Tooltip({
14
      delay: 0,
15
      selector: '.tooltip',
16
      createHook: (element) => {
17
        element.tooltipElement = element.querySelector('.tip');
18
      },
19
      positionHook: (element, tooltip) => {
20
        const trect = tooltip.getBoundingClientRect();
21
        const rect  = element.getBoundingClientRect();
22
        const gap = Math.min(12, rect.height / 2)
23
        return {
24
          top: rect.top + gap,
25
          left: rect.left + gap,
26
          width: trect.width,
27
          height: trect.height
28
        }
29
      }
30
    });
31
    tooltip.show(e)
32
  }
33
}
app/javascript/main.js
1 1
import "controllers"
2
import {createTooltip} from 'tooltip';
3

  
4
document.addEventListener('mouseover', (e) => {
5
  const tooltip = createTooltip()
6
  tooltip.show(e)
7
});
app/javascript/tooltip.js
1
/**
2
 * Redmine - project management software
3
 * Copyright (C) 2006-  Jean-Philippe Lang
4
 * This code is released under the GNU General Public License.
5
 */
6

  
7
export function createTooltip() {
8
  const tooltip = new Tooltip({
9
    selector: '[title]:not(.no-tooltip)',
10
    createHook: (element) => {
11
      const tooltip = document.createElement('span')
12
      tooltip.textContent = element.getAttribute('title');
13
      element.setAttribute('title', '');
14
      tooltip.classList.add('action-tooltip');
15
      element.insertAdjacentElement('afterbegin', tooltip);
16
      element.tooltipElement = tooltip;
17
    },
18
    positionHook: (element, tooltip) => {
19
      const trect = tooltip.getBoundingClientRect();
20
      const rect  = element.getBoundingClientRect();
21

  
22
      return {
23
        top: rect.top - trect.height - 5,
24
        left: rect.left + rect.width / 2 - trect.width / 2,
25
        width: trect.width,
26
        height: trect.height
27
      }
28
    }
29
  });
30
  return tooltip;
31
}
32

  
33
export class Tooltip {
34
  constructor(options) {
35
    this.options = Object.assign({
36
      delay: 400,
37
      selector: undefined,
38
    }, options)
39
    this.delayedShow = null;
40
    this.delayedHide = null;
41
  }
42

  
43
  show(e) {
44
    const target = e.target.closest(this.options.selector)
45
    if (target !== null) {
46

  
47
      if (target.tooltipElement === undefined) {
48
        this.options.createHook(target);
49
        target.addEventListener('mouseleave', (e) => this.hide(e));
50
      }
51

  
52
      this.delayedShow = setTimeout(() => {
53
        this.setPosition(target);
54
      }, this.options.delay);
55
    }
56
  }
57

  
58
  hide(e) {
59
    if (this.delayedShow !== null) {
60
      clearTimeout(this.delayedShow);
61
    }
62
    this.delayedHide = setTimeout(() => {
63
      const tooltip = e.target.tooltipElement;
64
      tooltip.style.visibility = 'hidden';
65
      tooltip.style.opacity = 0;
66
    }, this.options.delay);
67
  }
68

  
69
  setPosition(target) {
70
    const tooltip = target.tooltipElement;
71
    const position = this.options.positionHook(target, tooltip);
72
    const cheight = document.documentElement.clientHeight;
73
    const cwidth = document.documentElement.clientWidth;
74

  
75
    if (position.top + position.height > cheight) {
76
      position.top = cheight - position.height;
77
    }
78

  
79
    if (position.left + position.width > cwidth) {
80
      position.left = cwidth - position.width;
81
    }
82

  
83
    if (position.top < 0) {
84
      position.top = 0;
85
    }
86

  
87
    if (position.left < 0) {
88
      position.left = 0;
89
    }
90

  
91
    tooltip.style.top = `${position.top}px`;
92
    tooltip.style.left = `${position.left}px`;
93
    tooltip.style.visibility = 'visible';
94
    tooltip.style.opacity = 1;
95
  }
96
}
app/views/common/_calendar.html.erb
1 1
<%= form_tag({}, :data => {:cm_url => issues_context_menu_path}) do -%>
2 2
<%= hidden_field_tag 'back_url', url_for(:params => request.query_parameters), :id => nil %>
3
<ul class="cal">
3
<ul class="cal" data-controller="issue_tooltip" data-action="mouseover->issue_tooltip#show">
4 4
  <li scope="col" title="<%= l(:label_week) %>" class="calhead week-number"></li>
5 5
  <% 7.times do |i| %>
6 6
    <li scope="col" class="calhead"><%= day_name((calendar.first_wday + i) % 7) %></li>
app/views/gantts/show.html.erb
208 208
  </td>
209 209
<% end %>
210 210
<td>
211
<div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;" id="gantt_area">
211
<div style="position:relative;height:<%= t_height + 24 %>px;overflow:auto;" id="gantt_area" data-controller="issue_tooltip" data-action="mouseover->issue_tooltip#show">
212 212
<%
213 213
  style  = ""
214 214
  style += "width: #{g_width - 1}px;"
config/importmap.rb
1 1
# Pin npm packages by running ./bin/importmap
2 2

  
3 3
pin "main"
4
pin "tooltip"
4 5
pin "@hotwired/stimulus", to: "stimulus.min.js"
5 6
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
6 7
pin_all_from "app/javascript/controllers", under: "controllers"
    (1-1/1)