PageSourceSearch

https://livingmaterialslab.org/wp-content/plugins/divi-essential/assets/js/dnxte-tooltip.js?ver=1788643108

js livingmaterialslab.org collected 2026-09-30 10:43:25 UTC 12,530 bytes, 374 lines download raw bytes

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.