@@ -17,26 +17,33 @@ class TatvaInfotip extends HTMLElement {
1717 ] ;
1818 }
1919
20+ static ICONS = {
21+ info : `<path fill-rule="evenodd" clip-rule="evenodd" d="M5.5 12a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0ZM12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm.75 4v1.5h-1.5V8h1.5Zm0 8v-5h-1.5v5h1.5Z"/>` ,
22+ help : `<path d="M12 4.75a7.25 7.25 0 100 14.5 7.25 7.25 0 000-14.5zM3.25 12a8.75 8.75 0 1117.5 0 8.75 8.75 0 01-17.5 0zM12 8.75a1.5 1.5 0 01.167 2.99c-.465.052-.917.44-.917 1.01V14h1.5v-.845A3 3 0 109 10.25h1.5a1.5 1.5 0 011.5-1.5zM11.25 15v1.5h1.5V15h-1.5z"/>` ,
23+ caution : `<path fill-rule="evenodd" clip-rule="evenodd" d="M5.5 12a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0ZM12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.75 12v-1.5h1.5V16h-1.5Zm0-8v5h1.5V8h-1.5Z"/>` ,
24+ error : `<path fill-rule="evenodd" clip-rule="evenodd" d="M12.218 5.377a.25.25 0 0 0-.436 0l-7.29 12.96a.25.25 0 0 0 .218.373h14.58a.25.25 0 0 0 .218-.372l-7.29-12.96Zm-1.743-.735c.669-1.19 2.381-1.19 3.05 0l7.29 12.96a1.75 1.75 0 0 1-1.525 2.608H4.71a1.75 1.75 0 0 1-1.525-2.608l7.29-12.96ZM12.75 17.46h-1.5v-1.5h1.5v1.5Zm-1.5-3h1.5v-5h-1.5v5Z"/>` ,
25+ notAllowed : `<path fill-rule="evenodd" clip-rule="evenodd" d="M12 18.5A6.5 6.5 0 0 1 6.93 7.931l9.139 9.138A6.473 6.473 0 0 1 12 18.5Zm5.123-2.498a6.5 6.5 0 0 0-9.124-9.124l9.124 9.124ZM4 12a8 8 0 1 1 16 0 8 8 0 0 1-16 0Z"/>` ,
26+ starEmpty : `<path fill-rule="evenodd" d="M9.706 8.646a.25.25 0 01-.188.137l-4.626.672a.25.25 0 00-.139.427l3.348 3.262a.25.25 0 01.072.222l-.79 4.607a.25.25 0 00.362.264l4.138-2.176a.25.25 0 01.233 0l4.137 2.175a.25.25 0 00.363-.263l-.79-4.607a.25.25 0 01.072-.222l3.347-3.262a.25.25 0 00-.139-.427l-4.626-.672a.25.25 0 01-.188-.137l-2.069-4.192a.25.25 0 00-.448 0L9.706 8.646zM12 7.39l-.948 1.921a1.75 1.75 0 01-1.317.957l-2.12.308 1.534 1.495c.412.402.6.982.503 1.55l-.362 2.11 1.896-.997a1.75 1.75 0 011.629 0l1.895.997-.362-2.11a1.75 1.75 0 01.504-1.55l1.533-1.495-2.12-.308a1.75 1.75 0 01-1.317-.957L12 7.39z" clip-rule="evenodd"/>`
27+ } ;
28+
2029 connectedCallback ( ) {
21- this . attachShadow ( { mode : 'open' } ) ;
22- this . shadowRoot . appendChild (
23- this . renderElement ( ) . content . cloneNode ( true )
24- ) ;
25- this . updateIcon ( ) ;
26- requestAnimationFrame ( ( ) => {
30+ this . attachShadow ( { mode : 'open' } ) ;
31+ this . shadowRoot . innerHTML = this . getTemplate ( ) ;
32+
33+ requestAnimationFrame ( ( ) => {
34+ this . updateIcon ( ) ;
2735 this . updatePosition ( ) ;
28- this . initializeEventListeners ( ) ;
36+ this . initEvents ( ) ;
2937 this . hideTooltip ( ) ;
30- } ) ;
38+ } ) ;
3139 }
3240
3341 // Renders the main template for the component
34- renderElement ( ) {
35- const content = this . getAttribute ( 'content' ) ;
36- const template = document . createElement ( 'template' ) ;
37- template . innerHTML = `
42+ getTemplate ( ) {
43+ const content = this . getAttribute ( 'content' ) ;
44+ return `
3845 <style>
39- ${ this . renderStyle ( ) }
46+ ${ this . getStyles ( ) }
4047 </style>
4148 <span class="wrapper">
4249 <span class="text" tabindex="0" role="button" aria-describedby="infotip-popover">
@@ -45,230 +52,198 @@ class TatvaInfotip extends HTMLElement {
4552 </span>
4653 <div class="infotip" id="infotip-popover">
4754 <div class="infotip-popover-content">
48- ${ content }
55+ ${ content }
4956 </div>
5057 <div class="arrow"></div>
5158 </div>
5259 </span>
5360 ` ;
54- return template ;
5561 }
5662
5763 // Updates the icon based on current attributes
5864 updateIcon ( ) {
59- const iconEnabled = this . getAttribute ( 'icon-enabled' ) === 'true' ;
60- const icon = this . shadowRoot . querySelector ( '.icon' ) ;
61- if ( ! icon ) return ;
65+ const iconEnabled = this . getAttribute ( 'icon-enabled' ) === 'true' ;
66+ const iconType = this . getAttribute ( 'icon-type' ) || 'info' ;
67+ const icon = this . shadowRoot . querySelector ( '.icon' ) ;
68+
69+ if ( ! icon ) return ;
70+
6271 icon . innerHTML = iconEnabled
63- ? this . renderIcon ( this . getAttribute ( 'icon-type' ) || 'info' )
72+ ? `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" role="img">
73+ ${ TatvaInfotip . ICONS [ iconType ] || TatvaInfotip . ICONS . info }
74+ </svg>`
6475 : '' ;
6576 }
6677
6778 // Updates the infotip overlay position using FloatingUIDOM
6879 updatePosition ( ) {
69- if ( this . getAttribute ( 'content' ) === '' ) {
80+ const content = this . getAttribute ( 'content' ) ;
81+ if ( ! content ) {
7082 this . hideTooltip ( ) ;
7183 return ;
7284 }
73- const floatingUIDOM = window . FloatingUIDOM ;
74- const anchorText = this . shadowRoot . querySelector ( '.text' ) ;
75- const infotip = this . shadowRoot . querySelector ( '.infotip' ) ;
76- const arrow = infotip . querySelector ( '.arrow' ) ;
77- const overlayPlacement =
78- this . getAttribute ( 'overlay-placement' ) ?? 'top' ;
79- const offset = parseInt ( this . getAttribute ( 'offset' ) ?? 6 , 10 ) ;
8085
81- floatingUIDOM
82- . computePosition ( anchorText , infotip , {
83- placement : overlayPlacement ,
86+ const floatingUI = window . FloatingUIDOM ;
87+ if ( ! floatingUI ) return ;
88+
89+ const anchorText = this . shadowRoot . querySelector ( '.text' ) ;
90+ const infotip = this . shadowRoot . querySelector ( '.infotip' ) ;
91+ const arrow = infotip . querySelector ( '.arrow' ) ;
92+ const placement = this . getAttribute ( 'overlay-placement' ) || 'top' ;
93+ const offset = parseInt ( this . getAttribute ( 'offset' ) || '6' , 10 ) ;
94+
95+ floatingUI
96+ . computePosition ( anchorText , infotip , {
97+ placement,
8498 strategy : 'fixed' ,
8599 middleware : [
86- floatingUIDOM . offset ( offset ) ,
87- floatingUIDOM . flip ( {
100+ floatingUI . offset ( offset ) ,
101+ floatingUI . flip ( {
88102 fallbackPlacements : [
89103 'top' ,
90104 'right' ,
91105 'bottom' ,
92106 'left' ,
93107 ] ,
94- } ) ,
95- floatingUIDOM . shift ( { padding : 5 } ) ,
96- floatingUIDOM . arrow ( { element : arrow , padding : 5 } ) ,
108+ } ) ,
109+ floatingUI . shift ( { padding : 5 } ) ,
110+ floatingUI . arrow ( { element : arrow , padding : 5 } ) ,
97111 ] ,
98- } )
99- . then ( ( { x, y, placement, middlewareData } ) => {
100- Object . assign ( infotip . style , {
101- left : `${ x } px` ,
102- top : `${ y } px` ,
103- } ) ;
104- this . positionArrow ( arrow , placement , middlewareData . arrow ) ;
105- } ) ;
112+ } )
113+ . then ( ( { x, y, placement, middlewareData } ) => {
114+ Object . assign ( infotip . style , {
115+ left : `${ x } px` ,
116+ top : `${ y } px` ,
117+ } ) ;
118+ this . positionArrow ( arrow , placement , middlewareData . arrow ) ;
119+ } ) ;
106120 }
107121
108122 // Positions the arrow based on placement and middleware data
109- positionArrow ( arrow , placement , arrowData ) {
110- if ( ! arrow || ! arrowData ) return ;
111- const basePlacement = placement . split ( '-' ) [ 0 ] ;
112- arrow . style . left = '' ;
113- arrow . style . top = '' ;
114- arrow . style . right = '' ;
115- arrow . style . bottom = '' ;
116- switch ( basePlacement ) {
117- case 'top' :
118- arrow . style . bottom = '-4px' ;
119- arrow . style . left = arrowData . x ? `${ arrowData . x } px` : '' ;
120- break ;
121- case 'bottom' :
122- arrow . style . top = '-4px' ;
123- arrow . style . left = arrowData . x ? `${ arrowData . x } px` : '' ;
124- break ;
125- case 'left' :
126- arrow . style . right = '-4px' ;
127- arrow . style . top = arrowData . y ? `${ arrowData . y } px` : '' ;
128- break ;
129- case 'right' :
130- arrow . style . left = '-4px' ;
131- arrow . style . top = arrowData . y ? `${ arrowData . y } px` : '' ;
132- break ;
123+ positionArrow ( arrow , placement , arrowData ) {
124+ if ( ! arrow || ! arrowData ) return ;
125+
126+ const side = placement . split ( '-' ) [ 0 ] ;
127+ Object . assign ( arrow . style , { left : '' , top : '' , right : '' , bottom : '' } ) ;
128+
129+ const positions = {
130+ top : { bottom : '-4px' , left : `${ arrowData . x } px` } ,
131+ bottom : { top : '-4px' , left : `${ arrowData . x } px` } ,
132+ left : { right : '-4px' , top : `${ arrowData . y } px` } ,
133+ right : { left : '-4px' , top : `${ arrowData . y } px` }
134+ } ;
135+
136+ if ( positions [ side ] ) {
137+ Object . assign ( arrow . style , positions [ side ] ) ;
133138 }
134139 }
135140
136141 showTooltip ( ) {
137- this . shadowRoot . querySelector ( '.infotip' ) . style . display = 'block' ;
142+ this . shadowRoot . querySelector ( '.infotip' ) . style . display = 'block' ;
138143 this . updatePosition ( ) ;
139144 }
140145
141146 hideTooltip ( ) {
142- this . shadowRoot . querySelector ( '.infotip' ) . style . display = 'none' ;
147+ this . shadowRoot . querySelector ( '.infotip' ) . style . display = 'none' ;
143148 }
144149
145150 // Sets up mouse and keyboard event listeners for showing/hiding the tooltip
146- initializeEventListeners ( ) {
151+ initEvents ( ) {
147152 const events = [
148- [ 'mouseenter' , this . showTooltip . bind ( this ) ] ,
149- [ 'mouseleave' , this . hideTooltip . bind ( this ) ] ,
150- [ 'focus' , this . showTooltip . bind ( this ) ] ,
151- [ 'blur' , this . hideTooltip . bind ( this ) ] ,
153+ [ 'mouseenter' , this . showTooltip ] ,
154+ [ 'mouseleave' , this . hideTooltip ] ,
155+ [ 'focus' , this . showTooltip ] ,
156+ [ 'blur' , this . hideTooltip ] ,
152157 ] ;
153- events . forEach ( ( [ event , listener ] ) => {
154- this . addEventListener ( event , listener ) ;
155- } ) ;
158+ events . forEach ( ( [ event , handler ] ) => {
159+ this . addEventListener ( event , handler . bind ( this ) ) ;
160+ } ) ;
156161 }
157162
158163 // Generates the component's CSS based on current attributes
159- renderStyle ( ) {
160- const showUnderline = this . getAttribute ( 'underline' ) !== 'false' ;
161- const iconEnabled = this . getAttribute ( 'icon-enabled' ) === 'true' ;
162- const iconPosition = this . getAttribute ( 'icon-position' ) ?? 'left' ;
163- const iconColor = this . getAttribute ( 'icon-color' ) ?? 'currentColor' ;
164- const overlayTextColor =
165- this . getAttribute ( 'overlay-text-color' ) ?? '#FFFFFF' ;
166- const overlayBackgroundColor =
167- this . getAttribute ( 'overlay-background-color' ) ?? '#222' ;
168-
169- let style = `
164+ getStyles ( ) {
165+ const underline = this . getAttribute ( 'underline' ) ;
166+ const iconEnabled = this . getAttribute ( 'icon-enabled' ) === 'true' ;
167+ const iconPosition = this . getAttribute ( 'icon-position' ) || 'left' ;
168+ const iconColor = this . getAttribute ( 'icon-color' ) || 'currentColor' ;
169+ const textColor = this . getAttribute ( 'overlay-text-color' ) || '#FFFFFF' ;
170+ const bgColor = this . getAttribute ( 'overlay-background-color' ) || '#222' ;
171+
172+ let css = `
170173 .wrapper {
171174 position: relative;
172175 }
173176 .text {
174- text-decoration: ${ showUnderline ? 'dotted underline' : 'none' } ;
177+ text-decoration: ${ underline ? 'dotted underline' : 'none' } ;
175178 cursor: pointer;
176179 display: inline-flex;
177180 vertical-align: bottom;
178181 gap: 2px;
179- flex-direction: ${ iconPosition === 'right' ? 'row-reverse' : 'row' } ;
182+ flex-direction: ${ iconPosition === 'right' ? 'row-reverse' : 'row' } ;
180183 }
181184 .infotip {
182185 display: none;
183186 max-width: 300px;
184187 position: fixed;
185188 top: 0px;
186189 left: 0px;
187- background: ${ overlayBackgroundColor } ;
188- color: ${ overlayTextColor } ;
190+ background: ${ bgColor } ;
191+ color: ${ textColor } ;
189192 padding: 10px;
190193 border-radius: 4px;
191194 font-size: 90%;
192195 }
193196 .infotip .arrow {
194197 position: absolute;
195- background: ${ overlayBackgroundColor } ;
198+ background: ${ bgColor } ;
196199 width: 8px;
197200 height: 8px;
198201 transform: rotate(45deg);
199202 }
200203 ` ;
201204
202- if ( iconEnabled ) {
203- style += `
205+ if ( iconEnabled ) {
206+ css += `
204207 .icon {
205208 display: inline-flex;
206209 align-items: center;
207210 }
208211 .icon svg {
209212 width: 24px;
210213 height: 24px;
211- fill: ${ iconColor } ;
214+ fill: ${ iconColor } ;
212215 }
213216 ` ;
214217 }
215- return style ;
216- }
217-
218- // Returns SVG markup for the given icon type
219- renderIcon ( iconType = 'info' ) {
220- const iconPaths = {
221- info : `<path fill-rule="evenodd" clip-rule="evenodd" d="M5.5 12a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0ZM12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm.75 4v1.5h-1.5V8h1.5Zm0 8v-5h-1.5v5h1.5Z"/>` ,
222-
223- help : `<path d="M12 4.75a7.25 7.25 0 100 14.5 7.25 7.25 0 000-14.5zM3.25 12a8.75 8.75 0 1117.5 0 8.75 8.75 0 01-17.5 0zM12 8.75a1.5 1.5 0 01.167 2.99c-.465.052-.917.44-.917 1.01V14h1.5v-.845A3 3 0 109 10.25h1.5a1.5 1.5 0 011.5-1.5zM11.25 15v1.5h1.5V15h-1.5z"/>` ,
224-
225- caution : `<path fill-rule="evenodd" clip-rule="evenodd" d="M5.5 12a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0ZM12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.75 12v-1.5h1.5V16h-1.5Zm0-8v5h1.5V8h-1.5Z"/>` ,
226-
227- error : `<path fill-rule="evenodd" clip-rule="evenodd" d="M12.218 5.377a.25.25 0 0 0-.436 0l-7.29 12.96a.25.25 0 0 0 .218.373h14.58a.25.25 0 0 0 .218-.372l-7.29-12.96Zm-1.743-.735c.669-1.19 2.381-1.19 3.05 0l7.29 12.96a1.75 1.75 0 0 1-1.525 2.608H4.71a1.75 1.75 0 0 1-1.525-2.608l7.29-12.96ZM12.75 17.46h-1.5v-1.5h1.5v1.5Zm-1.5-3h1.5v-5h-1.5v5Z"/>` ,
228-
229- notAllowed : `<path fill-rule="evenodd" clip-rule="evenodd" d="M12 18.5A6.5 6.5 0 0 1 6.93 7.931l9.139 9.138A6.473 6.473 0 0 1 12 18.5Zm5.123-2.498a6.5 6.5 0 0 0-9.124-9.124l9.124 9.124ZM4 12a8 8 0 1 1 16 0 8 8 0 0 1-16 0Z"/>` ,
230-
231- starEmpty : `<path fill-rule="evenodd" d="M9.706 8.646a.25.25 0 01-.188.137l-4.626.672a.25.25 0 00-.139.427l3.348 3.262a.25.25 0 01.072.222l-.79 4.607a.25.25 0 00.362.264l4.138-2.176a.25.25 0 01.233 0l4.137 2.175a.25.25 0 00.363-.263l-.79-4.607a.25.25 0 01.072-.222l3.347-3.262a.25.25 0 00-.139-.427l-4.626-.672a.25.25 0 01-.188-.137l-2.069-4.192a.25.25 0 00-.448 0L9.706 8.646zM12 7.39l-.948 1.921a1.75 1.75 0 01-1.317.957l-2.12.308 1.534 1.495c.412.402.6.982.503 1.55l-.362 2.11 1.896-.997a1.75 1.75 0 011.629 0l1.895.997-.362-2.11a1.75 1.75 0 01.504-1.55l1.533-1.495-2.12-.308a1.75 1.75 0 01-1.317-.957L12 7.39z" clip-rule="evenodd"/>` ,
232- } ;
233- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" role="img">
234- ${ iconPaths [ iconType ] || iconPaths . info }
235- </svg>` ;
218+ return css ;
236219 }
237220
238221 // Handles attribute changes and updates the component accordingly
239- attributeChangedCallback ( name , oldValue , newValue ) {
240- const shadow = this . shadowRoot ;
241- if ( ! shadow ) return ;
222+ attributeChangedCallback ( name , oldValue , newValue ) {
223+ if ( ! this . shadowRoot || oldValue === newValue ) return ;
242224
243- switch ( name ) {
244- case 'content' :
245- shadow . querySelector ( '.infotip-popover-content' ) . innerHTML =
246- newValue ;
247- this . updatePosition ( ) ;
248- this . showTooltip ( ) ;
249- break ;
250- case 'icon-enabled' :
251- case 'icon-type' :
252- this . updateIcon ( ) ;
253- this . updatePosition ( ) ;
254- break ;
255- case 'overlay-text-color' :
256- case 'overlay-background-color' :
257- this . showTooltip ( ) ;
258- break ;
259- case 'offset' :
260- case 'overlay-placement' :
225+ const updateActions = {
226+ 'content' : ( ) => {
227+ this . shadowRoot . querySelector ( '.infotip-popover-content' ) . innerHTML = newValue ;
261228 this . updatePosition ( ) ;
262- this . showTooltip ( ) ;
263- break ;
229+ } ,
230+ 'icon-enabled' : ( ) => this . updateIcon ( ) ,
231+ 'icon-type' : ( ) => this . updateIcon ( ) ,
232+ 'overlay-placement' : ( ) => this . updatePosition ( ) ,
233+ 'offset' : ( ) => this . updatePosition ( )
234+ } ;
235+
236+ if ( updateActions [ name ] ) {
237+ updateActions [ name ] ( ) ;
264238 }
265- // Always update styles on attribute change
266- shadow . querySelector ( 'style' ) . textContent = this . renderStyle ( ) ;
239+
240+ // Always update styles
241+ this . shadowRoot . querySelector ( 'style' ) . textContent = this . getStyles ( ) ;
267242 }
268243}
269244
270245window . TatvaInfotip = TatvaInfotip ;
271246
272- if ( ! window . customElements . get ( 'tatva-infotip' ) ) {
273- window . customElements . define ( 'tatva-infotip' , TatvaInfotip ) ;
247+ if ( ! window . customElements . get ( 'tatva-infotip' ) ) {
248+ window . customElements . define ( 'tatva-infotip' , TatvaInfotip ) ;
274249}
0 commit comments