@@ -3,7 +3,7 @@ import tippy from 'tippy.js';
33export function createTippy ( target , opts = { } ) {
44 const instance = tippy ( target , {
55 appendTo : document . body ,
6- placement : target . getAttribute ( 'data-placement' ) || 'top-start' ,
6+ placement : 'top-start' ,
77 animation : false ,
88 allowHTML : false ,
99 hideOnClick : false ,
@@ -25,38 +25,108 @@ export function createTippy(target, opts = {}) {
2525 return instance ;
2626}
2727
28- export function initTooltip ( el , props = { } ) {
29- const content = el . getAttribute ( 'data-content' ) || props . content ;
28+ function getTippyTooltipContent ( target ) {
29+ // prefer to always use the "[data-tooltip-content]" attribute
30+ // for backward compatibility, we also support the ".tooltip[data-content]" attribute
31+ let content = target . getAttribute ( 'data-tooltip-content' ) ;
32+ if ( ! content && target . classList . contains ( 'tooltip' ) ) {
33+ content = target . getAttribute ( 'data-content' ) ;
34+ }
35+ return content ;
36+ }
37+
38+ /**
39+ * Attach a tippy tooltip to the given target element.
40+ * If the target element already has a tippy tooltip attached, the tooltip will be updated with the new content.
41+ * If the target element has no content, then no tooltip will be attached, and it returns null.
42+ * @param target {HTMLElement}
43+ * @param content {null|string}
44+ * @returns {null|tippy }
45+ */
46+ function attachTippyTooltip ( target , content = null ) {
47+ content = content ?? getTippyTooltipContent ( target ) ;
3048 if ( ! content ) return null ;
31- if ( ! el . hasAttribute ( 'aria-label' ) ) el . setAttribute ( 'aria-label' , content ) ;
32- return createTippy ( el , {
49+
50+ const props = {
3351 content,
3452 delay : 100 ,
3553 role : 'tooltip' ,
36- ...( el . getAttribute ( 'data-tooltip-interactive' ) === 'true' ? { interactive : true } : { } ) ,
37- ...props ,
38- } ) ;
39- }
54+ placement : target . getAttribute ( 'data-tooltip-placement' ) || 'top-start' ,
55+ ...( target . getAttribute ( 'data-tooltip-interactive' ) === 'true' ? { interactive : true } : { } ) ,
56+ } ;
4057
41- export function showTemporaryTooltip ( target , content ) {
42- let tippy , oldContent ;
43- if ( target . _tippy ) {
44- tippy = target . _tippy ;
45- oldContent = tippy . props . content ;
58+ if ( ! target . _tippy ) {
59+ createTippy ( target , props ) ;
4660 } else {
47- tippy = initTooltip ( target , { content} ) ;
61+ target . _tippy . setProps ( props ) ;
62+ }
63+ return target . _tippy ;
64+ }
65+
66+ /**
67+ * creating tippy instance is expensive, so we only create it when the user hovers over the element
68+ * @param e {Event}
69+ */
70+ function lazyTippyOnMouseEnter ( e ) {
71+ e . target . removeEventListener ( 'mouseenter' , lazyTippyOnMouseEnter , true ) ;
72+ attachTippyTooltip ( this ) ;
73+ }
74+
75+ /**
76+ * Activate the tippy tooltip for all children elements
77+ * And if the element has no aria-label, use the tooltip content as aria-label
78+ * @param target {HTMLElement}
79+ */
80+ function attachChildrenLazyTippyTooltip ( target ) {
81+ // the selector must match the logic in getTippyTooltipContent
82+ for ( const el of target . querySelectorAll ( '[data-tooltip-content], .tooltip[data-content]' ) ) {
83+ el . addEventListener ( 'mouseenter' , lazyTippyOnMouseEnter , true ) ;
84+
85+ // meanwhile, if the element has no aria-label, use the tooltip content as aria-label
86+ if ( ! el . hasAttribute ( 'aria-label' ) ) {
87+ const content = getTippyTooltipContent ( el ) ;
88+ if ( content ) {
89+ el . setAttribute ( 'aria-label' , content ) ;
90+ }
91+ }
4892 }
93+ }
4994
95+ export function initGlobalTooltips ( ) {
96+ // use MutationObserver to detect new elements added to the DOM, or attributes changed
97+ const observer = new MutationObserver ( ( mutationList ) => {
98+ for ( const mutation of mutationList ) {
99+ if ( mutation . type === 'childList' ) {
100+ for ( const el of mutation . addedNodes ) {
101+ // handle all "tooltip" elements in newly added nodes, skip non-related nodes (eg: "#text")
102+ if ( el . querySelectorAll ) {
103+ attachChildrenLazyTippyTooltip ( el ) ;
104+ }
105+ }
106+ } else if ( mutation . type === 'attributes' ) {
107+ // sync the tooltip content if the attributes change
108+ attachTippyTooltip ( mutation . target ) ;
109+ }
110+ }
111+ } ) ;
112+ observer . observe ( document , {
113+ subtree : true ,
114+ childList : true ,
115+ attributeFilter : [ 'data-tooltip-content' , 'data-content' ] ,
116+ } ) ;
117+
118+ attachChildrenLazyTippyTooltip ( document . documentElement ) ;
119+ }
120+
121+ export function showTemporaryTooltip ( target , content ) {
122+ const tippy = target . _tippy ?? attachTippyTooltip ( target , content ) ;
50123 tippy . setContent ( content ) ;
51124 if ( ! tippy . state . isShown ) tippy . show ( ) ;
52125 tippy . setProps ( {
53126 onHidden : ( tippy ) => {
54- if ( oldContent ) {
55- tippy . setContent ( oldContent ) ;
56- tippy . setProps ( { onHidden : undefined } ) ;
57- } else {
127+ // reset the default tooltip content, if no default, then this temporary tooltip could be destroyed
128+ if ( ! attachTippyTooltip ( target ) ) {
58129 tippy . destroy ( ) ;
59- // after destroy, the `_tippy` is detached, it can't do "setProps (etc...)" anymore
60130 }
61131 } ,
62132 } ) ;
0 commit comments