11import styles from '../css/tooltip.module.css'
22
3- console . log ( styles )
4-
53interface SmartTooltipOptions {
64 triggerName : string
75}
86
7+ interface Position {
8+ name : 'top' | 'bottom' | 'left' | 'right'
9+ x : number
10+ y : number
11+ }
12+
913const defaultOptions = {
1014 triggerName : 'tooltip' ,
1115}
1216
1317export class SmartTooltip {
14- private readonly options : SmartTooltipOptions
18+ readonly triggerName : string
1519 private readonly tooltip : HTMLDivElement
1620 private activeTriggerType : string | null = null
21+ private readonly spacing = 12
1722
1823 constructor ( options : SmartTooltipOptions = defaultOptions ) {
19- this . options = options
24+ this . triggerName = `data- ${ options . triggerName } `
2025 this . tooltip = document . createElement ( 'div' )
2126 this . tooltip . className = styles . tooltip
2227 document . body . appendChild ( this . tooltip )
@@ -25,89 +30,102 @@ export class SmartTooltip {
2530 }
2631
2732 private setupEventListeners ( ) {
28- const triggerName = `data-${ this . options . triggerName } `
29- // Handle hover-based tooltips
30- document . addEventListener ( 'mouseover' , e => {
31- const trigger = ( e . target as Element ) . closest ( `[${ triggerName } ]` )
32- if ( this . activeTriggerType !== 'click' && trigger ?. getAttribute ( `${ triggerName } -type` ) !== 'click' ) {
33- const content = trigger ?. getAttribute ( `${ triggerName } ` )
34- if ( content ) {
35- this . show ( trigger as HTMLElement , content )
36- this . activeTriggerType = 'hover'
37- }
38- }
39- } )
33+ document . addEventListener ( 'mouseover' , this . handleMouseOver )
34+ document . addEventListener ( 'mouseout' , this . handleMouseOut )
35+ document . addEventListener ( 'click' , this . handleClick )
36+ window . addEventListener ( 'resize' , this . handleResize )
37+ window . addEventListener ( 'scroll' , this . handleScroll , true )
38+ }
4039
41- document . addEventListener ( 'mouseout' , e => {
42- const trigger = ( e . target as Element ) . closest ( `[${ triggerName } ]` )
43- if ( this . activeTriggerType !== 'click' && trigger ?. getAttribute ( `${ triggerName } -type` ) !== 'click' ) {
40+ private readonly handleClick = ( e : Event ) : void => {
41+ const triggerName = this . triggerName
42+ const trigger = ( e . target as Element ) . closest ( `[${ triggerName } ][${ triggerName } -type="click"]` )
43+ if ( trigger ) {
44+ if ( this . isVisible ( ) ) {
4445 this . hide ( )
45- }
46- } )
47-
48- // Handle click-based tooltips
49- document . addEventListener ( 'click' , e => {
50- const trigger = ( e . target as Element ) . closest ( `[${ triggerName } ][${ triggerName } -type="click"]` )
51- if ( trigger ) {
52- if ( this . isVisible ( ) ) {
53- this . hide ( )
54- } else {
55- const content = trigger . getAttribute ( `${ triggerName } ` )
56- this . show ( trigger as HTMLElement , content )
57- this . activeTriggerType = 'click'
58- }
5946 } else {
60- this . hide ( )
47+ const content = trigger . getAttribute ( `${ triggerName } ` )
48+ this . show ( trigger as HTMLElement , content )
49+ this . activeTriggerType = 'click'
50+ }
51+ } else {
52+ this . hide ( )
53+ }
54+ }
55+
56+ private readonly handleMouseOver = ( e : Event ) : void => {
57+ const triggerName = this . triggerName
58+ const trigger = ( e . target as Element ) . closest ( `[${ triggerName } ]` )
59+ if ( this . activeTriggerType !== 'click' && trigger ?. getAttribute ( `${ triggerName } -type` ) !== 'click' ) {
60+ const content = trigger ?. getAttribute ( `${ triggerName } ` )
61+ if ( content ) {
62+ this . show ( trigger as HTMLElement , content )
63+ this . activeTriggerType = 'hover'
6164 }
62- } )
65+ }
66+ }
67+
68+ private readonly handleMouseOut = ( e : Event ) : void => {
69+ const triggerName = this . triggerName
70+ const trigger = ( e . target as Element ) . closest ( `[${ triggerName } ]` )
71+ if ( this . activeTriggerType !== 'click' && trigger ?. getAttribute ( `${ triggerName } -type` ) !== 'click' ) {
72+ this . hide ( )
73+ }
74+ }
75+
76+ private readonly handleResize = ( ) : void => {
77+ if ( this . isVisible ( ) ) {
78+ this . hide ( )
79+ }
80+ }
81+
82+ private readonly handleScroll = ( ) : void => {
83+ if ( this . isVisible ( ) ) {
84+ this . hide ( )
85+ }
6386 }
6487
6588 private isVisible ( ) : boolean {
6689 return this . tooltip . classList . contains ( styles . visible )
6790 }
6891
69- calculatePosition ( trigger : Element ) {
92+ private calculatePosition ( trigger : HTMLElement ) : Position {
7093 const triggerRect = trigger . getBoundingClientRect ( )
7194 const tooltipRect = this . tooltip . getBoundingClientRect ( )
72- const spacing = 12 // Space between tooltip and trigger
7395
74- // Try positions in order of preference
75- const positions = [
96+ const positions : Position [ ] = [
7697 {
7798 name : 'top' ,
7899 x : triggerRect . left + ( triggerRect . width - tooltipRect . width ) / 2 ,
79- y : triggerRect . top - tooltipRect . height - spacing ,
100+ y : triggerRect . top - tooltipRect . height - this . spacing ,
80101 } ,
81102 {
82103 name : 'bottom' ,
83104 x : triggerRect . left + ( triggerRect . width - tooltipRect . width ) / 2 ,
84- y : triggerRect . bottom + spacing ,
105+ y : triggerRect . bottom + this . spacing ,
85106 } ,
86107 {
87- name : 'right ' ,
88- x : triggerRect . right + spacing ,
108+ name : 'left ' ,
109+ x : triggerRect . left - tooltipRect . width - this . spacing ,
89110 y : triggerRect . top + ( triggerRect . height - tooltipRect . height ) / 2 ,
90111 } ,
91112 {
92- name : 'left ' ,
93- x : triggerRect . left - tooltipRect . width - spacing ,
113+ name : 'right ' ,
114+ x : triggerRect . right + this . spacing ,
94115 y : triggerRect . top + ( triggerRect . height - tooltipRect . height ) / 2 ,
95116 } ,
96117 ]
97118
98- // Find first position that fits in viewport
99- for ( const pos of positions ) {
100- if ( this . fitsInViewport ( pos . x , pos . y , tooltipRect . width , tooltipRect . height ) ) {
101- return pos
102- }
103- }
104-
105- // If no position fits perfectly, default to top
106- return positions [ 0 ]
119+ return positions . find ( pos => this . fitsInViewport ( pos , tooltipRect ) ) || positions [ 0 ]
107120 }
108121
109- fitsInViewport ( x : number , y : number , width : number , height : number ) : boolean {
110- return x >= 0 && y >= 0 && x + width <= window . innerWidth && y + height <= window . innerHeight
122+ private fitsInViewport ( pos : Position , tooltipRect : DOMRect ) : boolean {
123+ return (
124+ pos . x >= 0 &&
125+ pos . y >= 0 &&
126+ pos . x + tooltipRect . width <= window . innerWidth &&
127+ pos . y + tooltipRect . height <= window . innerHeight
128+ )
111129 }
112130
113131 private show ( trigger : HTMLElement , content : string | null ) {
@@ -124,6 +142,25 @@ export class SmartTooltip {
124142 this . tooltip . classList . remove ( styles . visible )
125143 this . activeTriggerType = null
126144 }
145+
146+ public destroy ( ) : void {
147+ document . removeEventListener ( 'mouseover' , this . handleMouseOver )
148+ document . removeEventListener ( 'mouseout' , this . handleMouseOut )
149+ document . removeEventListener ( 'click' , this . handleClick )
150+ window . removeEventListener ( 'resize' , this . handleResize )
151+ window . removeEventListener ( 'scroll' , this . handleScroll , true )
152+ this . tooltip . remove ( )
153+ }
154+ }
155+
156+ declare global {
157+ interface Window {
158+ SmartTooltip : typeof SmartTooltip
159+ }
160+ }
161+
162+ if ( window !== undefined ) {
163+ window . SmartTooltip = SmartTooltip
127164}
128165
129166export default SmartTooltip
0 commit comments