PageSourceSearch

https://avarcacastell.com/modules/newsletter_popup/views/js/newsletter_popup.js?v=1790327168

js avarcacastell.com collected 2026-10-07 02:59:04 UTC 9,469 bytes, 263 lines download raw bytes

1/**
2 * Newsletter Popup — vanilla JS, no jQuery
3 * @author Ecoeureka
4 */
5(function () {
6  'use strict';
7
8  var cfg = window.__npp;
9  if (!cfg) return;
10
11  /* El nombre incluye la versión del popup: al subirla desde el BO, las cookies anteriores dejan de contar.
12     El PHP lee la misma cookie y, si existe, no pinta el popup. */
13  var COOKIE_NAME     = cfg.cookieName || 'npp_v1';
14  var HIDE_DAYS       = typeof cfg.hideDays === 'number' ? cfg.hideDays : 30;
15  var SUBSCRIBED_DAYS = cfg.subscribedDays || 365;
16
17  /* ── Helpers ─────────────────────────────────────────────────────────── */
18
19  function isMobile() {
20    return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
21      navigator.userAgent
22    );
23  }
24
25  function hasCookie() {
26    return document.cookie.split('; ').some(function (c) {
27      return c.indexOf(COOKIE_NAME + '=') === 0;
28    });
29  }
30
31  /* days = 0 → cookie de sesión */
32  function setCookie(value, days) {
33    var cookie = COOKIE_NAME + '=' + value + '; path=/; SameSite=Lax';
34    if (days > 0) cookie += '; max-age=' + Math.round(days * 86400);
35    if (location.protocol === 'https:') cookie += '; Secure';
36    document.cookie = cookie;
37  }
38
39  /* La comprobación en JS cubre páginas servidas desde caché, donde el PHP no ha visto la cookie */
40  function shouldShow() {
41    return !hasCookie();
42  }
43
44  /**
45   * @param {boolean} subscribed  true → suscrito (SUBSCRIBED_DAYS), false → cerrado con la X (HIDE_DAYS)
46   */
47  var isSubscribed = false;
48
49  function markShown(subscribed) {
50    if (isSubscribed) return; /* no sustituir la cookie de suscripción por la de cierre */
51
52    if (subscribed) {
53      isSubscribed = true;
54      setCookie('subscribed', SUBSCRIBED_DAYS);
55    } else {
56      setCookie('closed', HIDE_DAYS);
57    }
58  }
59
60  function esc(str) {
61    var d = document.createElement('div');
62    d.appendChild(document.createTextNode(String(str)));
63    return d.innerHTML;
64  }
65
66  /* Posición efectiva: bottom_right solo en desktop */
67  function effectivePosition() {
68    return isMobile() ? 'center' : (cfg.position || 'center');
69  }
70
71  /* ── DOM builder ─────────────────────────────────────────────────────── */
72
73  function buildPopup() {
74    var i18n = cfg.i18n || {};
75    var pos  = effectivePosition();
76
77    var overlay = document.createElement('div');
78    overlay.id = 'npp-overlay';
79    overlay.classList.add('npp-pos-' + pos.replace('_', '-'));
80    overlay.setAttribute('role', 'dialog');
81    overlay.setAttribute('aria-modal', 'true');
82
83    var boxStyle = 'background-color:' + esc(cfg.bgColor || '#ffffff') + ';';
84    if (cfg.maxWidth) boxStyle += 'max-width:' + parseInt(cfg.maxWidth, 10) + 'px;';
85
86    var headerHtml = cfg.headerImage
87      ? '<div class="npp-header-img"><img src="' + esc(cfg.headerImage) + '" alt="" role="presentation"></div>'
88      : '';
89
90    /* El iframe se crea al abrir el popup: no se hace ninguna petición a Brevo hasta entonces */
91    var iframeHtml = cfg.iframeUrl
92      ? '<div class="npp-iframe">' +
93          '<iframe src="' + esc(cfg.iframeUrl) + '"' +
94          ' height="' + (parseInt(cfg.iframeHeight, 10) || 400) + '"' +
95          ' frameborder="0" scrolling="auto" allowfullscreen></iframe>' +
96        '</div>'
97      : '';
98
99    overlay.innerHTML =
100      '<div id="npp-box" style="' + boxStyle + '">' +
101        '<button type="button" id="npp-close" aria-label="' + esc(i18n.close || 'Cerrar') + '">' +
102          '<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" focusable="false">' +
103            '<path d="M5 5l14 14M19 5L5 19" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" fill="none"/>' +
104          '</svg>' +
105        '</button>' +
106        headerHtml +
107        (cfg.content ? '<div id="npp-content">' + cfg.content + '</div>' : '') +
108        (cfg.formHtml ? '<div class="npp-form"></div>' : '') +
109        iframeHtml +
110      '</div>';
111
112    return overlay;
113  }
114
115  /* ── Formulario HTML (código de Brevo) ──────────────────────────────── */
116
117  var SUCCESS_SELECTOR  = '#success-message.sib-form-message-panel--active';
118  var SUCCESS_CLOSE_MS  = 5000;
119
120  /* Los <script> metidos con innerHTML no se ejecutan: se recrean uno a uno y en orden,
121     esperando a que cargue cada script externo antes de seguir (main.js necesita las
122     variables window.* del script inline y el formulario ya en el DOM). */
123  function runScripts(scripts, i) {
124    if (i >= scripts.length) return;
125
126    var old = scripts[i];
127    var el  = document.createElement('script');
128
129    for (var a = 0; a < old.attributes.length; a++) {
130      el.setAttribute(old.attributes[a].name, old.attributes[a].value);
131    }
132
133    if (old.src) {
134      el.onload = el.onerror = function () { runScripts(scripts, i + 1); };
135      document.body.appendChild(el);
136    } else {
137      el.textContent = old.textContent;
138      document.body.appendChild(el);
139      runScripts(scripts, i + 1);
140    }
141  }
142
143  function mountForm(container) {
144    var tpl = document.createElement('template');
145    tpl.innerHTML = cfg.formHtml;
146
147    var scripts = Array.prototype.slice.call(tpl.content.querySelectorAll('script'));
148    scripts.forEach(function (sc) { sc.parentNode.removeChild(sc); });
149
150    container.appendChild(tpl.content);
151    watchSuccess(container);
152    runScripts(scripts, 0);
153  }
154
155  /* Brevo marca el éxito añadiendo una clase a #success-message */
156  function watchSuccess(container) {
157    if (!window.MutationObserver) return;
158
159    var observer = new MutationObserver(function () {
160      if (!container.querySelector(SUCCESS_SELECTOR)) return;
161      observer.disconnect();
162      markShown(true);
163      setTimeout(function () { closePopup(false); }, SUCCESS_CLOSE_MS);
164    });
165
166    observer.observe(container, { attributes: true, attributeFilter: ['class'], subtree: true });
167  }
168
169  /* ── Lifecycle ───────────────────────────────────────────────────────── */
170
171  /* El popup se crea la primera vez que se abre y después solo se oculta:
172     así el formulario de Brevo no se reinicializa si se vuelve a abrir con nppOpen() */
173  var popupEl = null;
174  var isOpen  = false;
175
176  function createPopup() {
177    popupEl = buildPopup();
178    document.body.appendChild(popupEl);
179
180    document.getElementById('npp-close').addEventListener('click', function () {
181      closePopup(true); /* X → marcar y no volver a mostrar */
182    });
183
184    /* Clic en overlay: cierra visualmente pero NO marca → el popup vuelve en la siguiente página */
185    popupEl.addEventListener('click', function (e) {
186      if (e.target === popupEl) closePopup(false);
187    });
188
189    var formContainer = popupEl.querySelector('.npp-form');
190    if (formContainer) mountForm(formContainer);
191  }
192
193  function openPopup() {
194    if (isOpen) return; /* ya visible */
195    isOpen = true;
196
197    if (!popupEl) createPopup();
198
199    /* Doble rAF para que la transición CSS dispare desde el estado inicial */
200    requestAnimationFrame(function () {
201      requestAnimationFrame(function () {
202        popupEl.classList.add('npp-visible');
203      });
204    });
205
206    /* ESC: cierre sin marcar */
207    document.addEventListener('keydown', onKeyDown);
208  }
209
210  function showPopup() {
211    if (shouldShow()) openPopup();
212  }
213
214  /**
215   * @param {boolean} permanent  true → guardar en storage (no volver a mostrar)
216   */
217  function closePopup(permanent) {
218    if (!isOpen) return;
219    isOpen = false;
220
221    if (permanent) markShown(false);
222
223    document.removeEventListener('keydown', onKeyDown);
224    popupEl.classList.remove('npp-visible');
225  }
226
227  function onKeyDown(e) {
228    if (e.key === 'Escape') closePopup(false);
229  }
230
231  /* ── Triggers ────────────────────────────────────────────────────────── */
232
233  function setupTriggers() {
234    if (isMobile()) {
235      var target = (cfg.triggerMobile || 50) / 100;
236
237      function onScroll() {
238        var scrollable = document.body.scrollHeight - window.innerHeight;
239        if (scrollable <= 0) return;
240        if (window.scrollY / scrollable >= target) {
241          window.removeEventListener('scroll', onScroll);
242          showPopup();
243        }
244      }
245      window.addEventListener('scroll', onScroll, { passive: true });
246    } else {
247      setTimeout(showPopup, (cfg.triggerDesktop || 0) * 1000);
248    }
249  }
250
251  /* ── Entry ───────────────────────────────────────────────────────────── */
252
253  if (document.readyState === 'loading') {
254    document.addEventListener('DOMContentLoaded', setupTriggers);
255  } else {
256    setupTriggers();
257  }
258
259  /* ── API pública ─────────────────────────────────────────────────────── */
260  /* Permite abrir el popup desde cualquier botón/enlace de la página:
261     <button onclick="nppOpen()">Suscribirse</button>                       */
262  window.nppOpen = openPopup;
263})();

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.