1/** 2 * Tooltip â Frontend Runtime 3 * Hover/click tooltips with smart positioning, arrow, animations, full styling control. 4 * Pure vanilla JS, no dependencies. 5 * 6 * @package DNXTE 7 */ 8(function () { 9 'use strict'; 10 11 // Inject base styles once 12 function ensureStyles() { 13 if (document.getElementById('dnxte-tooltip-css')) return; 14 var s = document.createElement('style'); 15 s.id = 'dnxte-tooltip-css'; 16 s.textContent = [ 17 '.dnxte-tooltip{position:fixed;z-index:999999;pointer-events:none;line-height:1.4;white-space:normal;word-wrap:break-word;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:13px;color:#ffffff;box-sizing:border-box}', 18 '.dnxte-tooltip.dnxte-tooltip-clickable{pointer-events:auto}', 19 '.dnxte-tooltip a{color:inherit;text-decoration:underline}', 20 '.dnxte-tooltip strong,.dnxte-tooltip b{font-weight:700}', 21 '.dnxte-tooltip em,.dnxte-tooltip i{font-style:italic}', 22 '.dnxte-tooltip-arrow{position:absolute;width:0;height:0}', 23 // Animations 24 '.dnxte-tooltip[data-anim="fade"]{opacity:0;transition:opacity 0.2s ease-out}', 25 '.dnxte-tooltip[data-anim="fade"].dnxte-tooltip-show{opacity:1}', 26 '.dnxte-tooltip[data-anim="scale"]{opacity:0;transform:scale(0.8);transition:opacity 0.2s ease-out,transform 0.2s ease-out}', 27 '.dnxte-tooltip[data-anim="scale"].dnxte-tooltip-show{opacity:1;transform:scale(1)}', 28 '.dnxte-tooltip[data-anim="slide"]{opacity:0;transition:opacity 0.2s ease-out,transform 0.2s ease-out}', 29 '.dnxte-tooltip[data-anim="slide"].dnxte-tooltip-show{opacity:1;transform:translate(0,0)!important}', 30 '.dnxte-tooltip[data-anim="none"]{transition:none}', 31 ].join('\n'); 32 document.head.appendChild(s); 33 } 34 35 function num(v, def) { 36 var n = parseFloat(v); 37 return isNaN(n) ? def : n; 38 } 39 40 function attr(el, name, def) { 41 var v = el.getAttribute(name); 42 return (v === null || v === '') ? def : v; 43 } 44 45 function DnxteTooltip(el) { 46 this.el = el; 47 this._tooltip = null; 48 this._arrow = null; 49 this._listeners = []; 50 this._docClickHandler = null; 51 this._destroyed = false; 52 53 // âââ Read all data attributes âââ 54 this.text = el.getAttribute('data-dnxte-tooltip') || ''; 55 this.position = attr(el, 'data-tooltip-position', 'top'); 56 this.trigger = attr(el, 'data-tooltip-trigger', 'hover'); 57 this.animation = attr(el, 'data-tooltip-animation', 'fade'); 58 59 // Background 60 this.bg = attr(el, 'data-tooltip-bg', '#1e293b'); 61 62 // Typography â full CSS string from the native divi/font group (built in PHP). 63 // Applied inline because the tooltip lives on <body>, away from the module. 64 this.fontCss = el.getAttribute('data-tooltip-font-css') || ''; 65 66 // Box 67 this.paddingY = num(el.getAttribute('data-tooltip-padding-y'), 6); 68 this.paddingX = num(el.getAttribute('data-tooltip-padding-x'), 12); 69 this.maxWidth = num(el.getAttribute('data-tooltip-max-width'), 250); 70 this.borderRadius = num(el.getAttribute('data-tooltip-border-radius'), 6); 71 72 // Border 73 this.borderWidth = num(el.getAttribute('data-tooltip-border-width'), 0); 74 this.borderColor = attr(el, 'data-tooltip-border-color', '#000000'); 75 76 // Arrow 77 this.showArrow = el.getAttribute('data-tooltip-arrow') !== 'false'; 78 this.arrowSize = num(el.getAttribute('data-tooltip-arrow-size'), 5); 79 80 // Box shadow (from divi/box-shadow group, parsed in PHP into single CSS string) 81 this.shadowCss = el.getAttribute('data-tooltip-shadow') || ''; 82 83 if (!this.text) return; 84 85 this._target = this._findTarget(el); 86 this._init(); 87 } 88 89 var proto = DnxteTooltip.prototype; 90 91 proto._findTarget = function (el) { 92 var btn = el.querySelector('a.et_pb_button, a.dnxte-btn, button, a[href]'); 93 if (btn) return btn; 94 var img = el.querySelector('img, .et_pb_image_wrap'); 95 if (img) return img; 96 var inner = el.querySelector('.et_pb_blurb_content, .et_pb_main_blurb_image'); 97 if (inner) return inner; 98 return el; 99 }; 100 101 proto._gap = function () { 102 return this.showArrow ? this.arrowSize : 6; 103 }; 104 105 proto._init = function () { 106 var self = this; 107 var target = this._target; 108 ensureStyles(); 109 110 if (this.trigger === 'click') { 111 var onClick = function (e) { 112 e.preventDefault(); 113 e.stopPropagation(); 114 if (self._tooltip) self._hide(); 115 else self._show(); 116 }; 117 target.addEventListener('click', onClick); 118 this._listeners.push({ el: target, evt: 'click', fn: onClick }); 119 120 this._docClickHandler = function (e) { 121 if (!self._tooltip) return; 122 if (target.contains(e.target)) return; 123 if (self._tooltip.contains(e.target)) return; 124 self._hide(); 125 }; 126 document.addEventListener('click', this._docClickHandler); 127 128 var onEsc = function (e) { 129 if (e.key === 'Escape' && self._tooltip) self._hide(); 130 }; 131 document.addEventListener('keydown', onEsc); 132 this._listeners.push({ el: document, evt: 'keydown', fn: onEsc }); 133 } else { 134 var onEnter = function () { self._show(); }; 135 var onLeave = function () { self._hide(); }; 136 target.addEventListener('mouseenter', onEnter); 137 target.addEventListener('mouseleave', onLeave); 138 this._listeners.push({ el: target, evt: 'mouseenter', fn: onEnter }); 139 this._listeners.push({ el: target, evt: 'mouseleave', fn: onLeave }); 140 141 target.addEventListener('focus', onEnter); 142 target.addEventListener('blur', onLeave); 143 this._listeners.push({ el: target, evt: 'focus', fn: onEnter }); 144 this._listeners.push({ el: target, evt: 'blur', fn: onLeave }); 145 } 146 }; 147 148 proto._createTooltip = function () { 149 var tip = document.createElement('div'); 150 tip.className = 'dnxte-tooltip'; 151 if (this.trigger === 'click') tip.classList.add('dnxte-tooltip-clickable'); 152 tip.setAttribute('data-anim', this.animation); 153 tip.innerHTML = this.text; 154
155 // Background 156 tip.style.backgroundColor = this.bg; 157 158 // Typography â apply the font group's CSS string inline (font-family, 159 // weight, size, color, letter-spacing, line-height, alignmentâ¦). 160 if (this.fontCss) { 161 tip.style.cssText += ';' + this.fontCss; 162 } 163 164 // Box 165 tip.style.padding = this.paddingY + 'px ' + this.paddingX + 'px'; 166 tip.style.maxWidth = this.maxWidth + 'px'; 167 tip.style.borderRadius = this.borderRadius + 'px'; 168 169 // Border 170 if (this.borderWidth > 0) { 171 tip.style.border = this.borderWidth + 'px solid ' + this.borderColor; 172 } else { 173 tip.style.border = 'none'; 174 } 175 176 // Box Shadow (full CSS string from divi/box-shadow group) 177 if (this.shadowCss && this.shadowCss !== 'none') { 178 tip.style.boxShadow = this.shadowCss; 179 } else { 180 tip.style.boxShadow = 'none'; 181 } 182 183 if (this.showArrow) { 184 var arrow = document.createElement('div'); 185 arrow.className = 'dnxte-tooltip-arrow'; 186 tip.appendChild(arrow); 187 this._arrow = arrow; 188 } 189 190 document.body.appendChild(tip); 191 this._tooltip = tip; 192 return tip; 193 }; 194 195 proto._resolvePosition = function (pos, elRect, tipRect) { 196 var vw = window.innerWidth; 197 var vh = window.innerHeight; 198 var gap = this._gap(); 199 200 var fits = { 201 top: elRect.top - tipRect.height - gap > 0, 202 bottom: elRect.bottom + tipRect.height + gap < vh, 203 left: elRect.left - tipRect.width - gap > 0, 204 right: elRect.right + tipRect.width + gap < vw, 205 }; 206 207 if (pos === 'auto') { 208 if (fits.top) return 'top'; 209 if (fits.bottom) return 'bottom'; 210 if (fits.right) return 'right'; 211 if (fits.left) return 'left'; 212 return 'top'; 213 } 214 215 if (!fits[pos]) { 216 var opposite = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' }; 217 if (fits[opposite[pos]]) return opposite[pos]; 218 } 219 220 return pos; 221 }; 222 223 proto._positionTooltip = function (tip, pos, elRect, tipRect) { 224 var x, y; 225 var vw = window.innerWidth; 226 var gap = this._gap(); 227 228 switch (pos) { 229 case 'top': 230 x = elRect.left + elRect.width / 2 - tipRect.width / 2; 231 y = elRect.top - tipRect.height - gap; 232 break; 233 case 'bottom': 234 x = elRect.left + elRect.width / 2 - tipRect.width / 2; 235 y = elRect.bottom + gap; 236 break; 237 case 'left': 238 x = elRect.left - tipRect.width - gap; 239 y = elRect.top + elRect.height / 2 - tipRect.height / 2; 240 break; 241 case 'right': 242 x = elRect.right + gap; 243 y = elRect.top + elRect.height / 2 - tipRect.height / 2; 244 break; 245 } 246 247 x = Math.max(5, Math.min(x, vw - tipRect.width - 5)); 248 y = Math.max(5, y); 249 250 tip.style.left = x + 'px'; 251 tip.style.top = y + 'px'; 252 253 // âââ Arrow positioning âââ 254 // Arrow box is (arrowSize * 2) tall/wide (border-based). Visible triangle is half. 255 // Overlap 1px with tooltip to kill sub-pixel rendering gap. 256 if (this._arrow) { 257 var arrow = this._arrow; 258 var a = this.arrowSize; 259 var offset = (a * 2) - 1; 260 261 arrow.style.cssText = 'position:absolute;width:0;height:0;border:' + a + 'px solid transparent;'; 262 263 switch (pos) { 264 case 'top': 265 arrow.style.bottom = -offset + 'px'; 266 arrow.style.left = '50%'; 267 arrow.style.transform = 'translateX(-50%)'; 268 arrow.style.borderTopColor = this.bg; 269 break; 270 case 'bottom': 271 arrow.style.top = -offset + 'px'; 272 arrow.style.left = '50%'; 273 arrow.style.transform = 'translateX(-50%)'; 274 arrow.style.borderBottomColor = this.bg; 275 break; 276 case 'left': 277 arrow.style.right = -offset + 'px'; 278 arrow.style.top = '50%'; 279 arrow.style.transform = 'translateY(-50%)'; 280 arrow.style.borderLeftColor = this.bg; 281 break; 282 case 'right': 283 arrow.style.left = -offset + 'px'; 284 arrow.style.top = '50%'; 285 arrow.style.transform = 'translateY(-50%)'; 286 arrow.style.borderRightColor = this.bg; 287 break; 288 } 289 } 290 291 if (this.animation === 'slide') { 292 var sOff = { top: '0,5px', bottom: '0,-5px', left: '5px,0', right: '-5p
292x,0' }; 293 tip.style.transform = 'translate(' + (sOff[pos] || '0,0') + ')'; 294 } 295 }; 296 297 proto._show = function () { 298 var self = this; 299 if (this._tooltip) this._hide(); 300 301 var tip = this._createTooltip(); 302 var elRect = this._target.getBoundingClientRect(); 303 304 requestAnimationFrame(function () { 305 if (self._destroyed || !self._tooltip) return; 306 var tipRect = tip.getBoundingClientRect(); 307 var pos = self._resolvePosition(self.position, elRect, tipRect); 308 self._positionTooltip(tip, pos, elRect, tipRect); 309 310 requestAnimationFrame(function () { 311 if (self._tooltip) self._tooltip.classList.add('dnxte-tooltip-show'); 312 }); 313 }); 314 }; 315 316 proto._hide = function () { 317 var self = this; 318 if (!this._tooltip) return; 319 var tip = this._tooltip; 320 tip.classList.remove('dnxte-tooltip-show'); 321 322 var delay = this.animation === 'none' ? 0 : 250; 323 setTimeout(function () { 324 if (tip.parentNode) tip.parentNode.removeChild(tip); 325 if (self._tooltip === tip) { 326 self._tooltip = null; 327 self._arrow = null; 328 } 329 }, delay); 330 }; 331 332 proto.destroy = function () { 333 this._destroyed = true; 334 this._hide(); 335 this._listeners.forEach(function (l) { 336 try { l.el.removeEventListener(l.evt, l.fn); } catch (e) {} 337 }); 338 this._listeners = []; 339 if (this._docClickHandler) { 340 document.removeEventListener('click', this._docClickHandler); 341 this._docClickHandler = null; 342 } 343 }; 344 345 function initAll() { 346 var els = document.querySelectorAll('[data-dnxte-tooltip]'); 347 els.forEach(function (el) { 348 if (el._dnxteTooltip) return; 349 el._dnxteTooltip = new DnxteTooltip(el); 350 }); 351 } 352 353 function boot() { 354 initAll(); 355 if (document.body) { 356 var observer = new MutationObserver(function (mutations) { 357 var shouldInit = false; 358 mutations.forEach(function (m) { 359 if (m.addedNodes && m.addedNodes.length) shouldInit = true; 360 }); 361 if (shouldInit) initAll(); 362 }); 363 observer.observe(document.body, { childList: true, subtree: true }); 364 } 365 } 366 367 if (document.readyState === 'loading') { 368 document.addEventListener('DOMContentLoaded', boot); 369 } else { 370 boot(); 371 } 372 373 window.DnxteTooltip = DnxteTooltip; 374})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.