Skip to content

Commit 2a98604

Browse files
committed
refactor: TatvaInfotip and TatvaMarker web components
1 parent 64e942e commit 2a98604

File tree

2 files changed

+164
-267
lines changed

2 files changed

+164
-267
lines changed

assets/web-components/infotip.js

Lines changed: 119 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -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

270245
window.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

Comments
 (0)