1
1
import tippy , { followCursor } from 'tippy.js' ;
2
2
import { isDocumentFragmentOrElementNode } from '../utils/dom.ts' ;
3
3
import { formatDatetime } from '../utils/time.ts' ;
4
+ import type { Content , Instance , Props } from 'tippy.js' ;
4
5
5
- const visibleInstances = new Set ( ) ;
6
+ type TippyOpts = {
7
+ role ?: string ,
8
+ theme ?: 'default' | 'tooltip' | 'menu' | 'box-with-header' | 'bare' ,
9
+ } & Partial < Props > ;
10
+
11
+ const visibleInstances = new Set < Instance > ( ) ;
6
12
const arrowSvg = `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>` ;
7
13
8
- export function createTippy ( target , opts = { } ) {
14
+ export function createTippy ( target : Element , opts : TippyOpts = { } ) {
9
15
// the callback functions should be destructured from opts,
10
16
// because we should use our own wrapper functions to handle them, do not let the user override them
11
17
const { onHide, onShow, onDestroy, role, theme, arrow, ...other } = opts ;
12
18
13
- const instance = tippy ( target , {
19
+ const instance : Instance = tippy ( target , {
14
20
appendTo : document . body ,
15
21
animation : false ,
16
22
allowHTML : false ,
17
23
hideOnClick : false ,
18
24
interactiveBorder : 20 ,
19
25
ignoreAttributes : true ,
20
26
maxWidth : 500 , // increase over default 350px
21
- onHide : ( instance ) => {
27
+ onHide : ( instance : Instance ) => {
22
28
visibleInstances . delete ( instance ) ;
23
29
return onHide ?.( instance ) ;
24
30
} ,
25
- onDestroy : ( instance ) => {
31
+ onDestroy : ( instance : Instance ) => {
26
32
visibleInstances . delete ( instance ) ;
27
33
return onDestroy ?.( instance ) ;
28
34
} ,
29
- onShow : ( instance ) => {
35
+ onShow : ( instance : Instance ) => {
30
36
// hide other tooltip instances so only one tooltip shows at a time
31
37
for ( const visibleInstance of visibleInstances ) {
32
38
if ( visibleInstance . props . role === 'tooltip' ) {
@@ -43,7 +49,7 @@ export function createTippy(target, opts = {}) {
43
49
theme : theme || role || 'default' ,
44
50
plugins : [ followCursor ] ,
45
51
...other ,
46
- } ) ;
52
+ } satisfies Partial < Props > ) ;
47
53
48
54
if ( role === 'menu' ) {
49
55
target . setAttribute ( 'aria-haspopup' , 'true' ) ;
@@ -58,12 +64,8 @@ export function createTippy(target, opts = {}) {
58
64
* If the target element has no content, then no tooltip will be attached, and it returns null.
59
65
*
60
66
* Note: "tooltip" doesn't equal to "tippy". "tooltip" means a auto-popup content, it just uses tippy as the implementation.
61
- *
62
- * @param target {HTMLElement}
63
- * @param content {null|string}
64
- * @returns {null|tippy }
65
67
*/
66
- function attachTooltip ( target , content = null ) {
68
+ function attachTooltip ( target : Element , content : Content = null ) {
67
69
switchTitleToTooltip ( target ) ;
68
70
69
71
content = content ?? target . getAttribute ( 'data-tooltip-content' ) ;
@@ -84,7 +86,7 @@ function attachTooltip(target, content = null) {
84
86
placement : target . getAttribute ( 'data-tooltip-placement' ) || 'top-start' ,
85
87
followCursor : target . getAttribute ( 'data-tooltip-follow-cursor' ) || false ,
86
88
...( target . getAttribute ( 'data-tooltip-interactive' ) === 'true' ? { interactive : true , aria : { content : 'describedby' , expanded : false } } : { } ) ,
87
- } ;
89
+ } as TippyOpts ;
88
90
89
91
if ( ! target . _tippy ) {
90
92
createTippy ( target , props ) ;
@@ -94,7 +96,7 @@ function attachTooltip(target, content = null) {
94
96
return target . _tippy ;
95
97
}
96
98
97
- function switchTitleToTooltip ( target ) {
99
+ function switchTitleToTooltip ( target : Element ) {
98
100
let title = target . getAttribute ( 'title' ) ;
99
101
if ( title ) {
100
102
// apply custom formatting to relative-time's tooltips
@@ -118,16 +120,15 @@ function switchTitleToTooltip(target) {
118
120
* According to https://www.w3.org/TR/DOM-Level-3-Events/#events-mouseevent-event-order , mouseover event is fired before mouseenter event
119
121
* Some browsers like PaleMoon don't support "addEventListener('mouseenter', capture)"
120
122
* The tippy by default uses "mouseenter" event to show, so we use "mouseover" event to switch to tippy
121
- * @param e {Event}
122
123
*/
123
- function lazyTooltipOnMouseHover ( e ) {
124
+ function lazyTooltipOnMouseHover ( e : MouseEvent ) {
124
125
e . target . removeEventListener ( 'mouseover' , lazyTooltipOnMouseHover , true ) ;
125
126
attachTooltip ( this ) ;
126
127
}
127
128
128
129
// Activate the tooltip for current element.
129
130
// If the element has no aria-label, use the tooltip content as aria-label.
130
- function attachLazyTooltip ( el ) {
131
+ function attachLazyTooltip ( el : Element ) {
131
132
el . addEventListener ( 'mouseover' , lazyTooltipOnMouseHover , { capture : true } ) ;
132
133
133
134
// meanwhile, if the element has no aria-label, use the tooltip content as aria-label
@@ -140,15 +141,15 @@ function attachLazyTooltip(el) {
140
141
}
141
142
142
143
// Activate the tooltip for all children elements.
143
- function attachChildrenLazyTooltip ( target ) {
144
- for ( const el of target . querySelectorAll ( '[data-tooltip-content]' ) ) {
144
+ function attachChildrenLazyTooltip ( target : Element ) {
145
+ for ( const el of target . querySelectorAll < Element > ( '[data-tooltip-content]' ) ) {
145
146
attachLazyTooltip ( el ) ;
146
147
}
147
148
}
148
149
149
150
export function initGlobalTooltips ( ) {
150
151
// use MutationObserver to detect new "data-tooltip-content" elements added to the DOM, or attributes changed
151
- const observerConnect = ( observer ) => observer . observe ( document , {
152
+ const observerConnect = ( observer : MutationObserver ) => observer . observe ( document , {
152
153
subtree : true ,
153
154
childList : true ,
154
155
attributeFilter : [ 'data-tooltip-content' , 'title' ] ,
@@ -159,15 +160,15 @@ export function initGlobalTooltips() {
159
160
for ( const mutation of [ ...mutationList , ...pending ] ) {
160
161
if ( mutation . type === 'childList' ) {
161
162
// mainly for Vue components and AJAX rendered elements
162
- for ( const el of mutation . addedNodes ) {
163
+ for ( const el of mutation . addedNodes as NodeListOf < Element > ) {
163
164
if ( ! isDocumentFragmentOrElementNode ( el ) ) continue ;
164
165
attachChildrenLazyTooltip ( el ) ;
165
166
if ( el . hasAttribute ( 'data-tooltip-content' ) ) {
166
167
attachLazyTooltip ( el ) ;
167
168
}
168
169
}
169
170
} else if ( mutation . type === 'attributes' ) {
170
- attachTooltip ( mutation . target ) ;
171
+ attachTooltip ( mutation . target as Element ) ;
171
172
}
172
173
}
173
174
observerConnect ( observer ) ;
@@ -177,7 +178,7 @@ export function initGlobalTooltips() {
177
178
attachChildrenLazyTooltip ( document . documentElement ) ;
178
179
}
179
180
180
- export function showTemporaryTooltip ( target , content ) {
181
+ export function showTemporaryTooltip ( target : Element , content : Content ) {
181
182
// if the target is inside a dropdown, don't show the tooltip because when the dropdown
182
183
// closes, the tippy would be pushed unsightly to the top-left of the screen like seen
183
184
// on the issue comment menu.
0 commit comments