PageSourceSearch

https://spiraclethemes.com/wp-content/themes/spiraclethemes/assets/js/offer-popup.js?ver=1789739742

js spiraclethemes.com collected 2026-10-03 19:13:44 UTC 5,909 bytes, 240 lines download raw bytes

1/**
2 * Web Development Offer Popup Script
3 */
4(function () {
5	'use strict';
6
7	var STORAGE_KEY = 'stOfferPopupMinimized';
8	var SHOWN_KEY = 'stOfferPopupShownAt';
9	var SHOW_DELAY_MS = 2500;
10	var HIDE_TRANSITION_MS = 450;
11	var RESURFACE_TTL_MS = 24 * 60 * 60 * 1000;
12
13	var popup = document.getElementById('stOfferPopup');
14	var bubble = document.getElementById('stOfferBubble');
15	
16	var timerEls = document.querySelectorAll('.st-offer-timer-value, .st-theme-coupon-timer-value');
17
18	if (timerEls.length) {
19		var pad = function (num) {
20			return (num < 10 ? '0' : '') + num;
21		};
22
23		var cell = function (val, unit) {
24			return '<span class="st-offer-timer-cell"><b>' + val + '</b><i>' + unit + '</i></span>';
25		};
26
27		var tickTimer = function () {
28			var now = Date.now();
29
30			Array.prototype.forEach.call(timerEls, function (el) {
31				var deadline = parseInt(el.getAttribute('data-deadline'), 10) * 1000;
32				var diff = deadline - now;
33				var wrap = el.closest('.st-offer-popup-timer, .st-theme-coupon-timer');
34
35				if (isNaN(deadline) || diff <= 0) {
36					if (wrap) {
37						wrap.hidden = true;
38					}
39					return;
40				}
41
42				var days = Math.floor(diff / 86400000);
43				var hours = Math.floor((diff % 86400000) / 3600000);
44				var mins = Math.floor((diff % 3600000) / 60000);
45				var secs = Math.floor((diff % 60000) / 1000);
46
47				el.innerHTML =
48					cell(pad(days), 'd') +
49					'<span class="st-offer-timer-sep">:</span>' +
50					cell(pad(hours), 'h') +
51					'<span class="st-offer-timer-sep">:</span>' +
52					cell(pad(mins), 'm') +
53					'<span class="st-offer-timer-sep">:</span>' +
54					cell(pad(secs), 's');
55			});
56		};
57
58		tickTimer();
59		window.setInterval(tickTimer, 1000);
60	}
61
62	if (!popup || !bubble) {
63		return;
64	}
65
66	function readStore(key) {
67		try {
68			return window.localStorage.getItem(key);
69		} catch (e) {
70			return null;
71		}
72	}
73
74	function writeStore(key, value) {
75		try {
76			window.localStorage.setItem(key, value);
77		} catch (e) {
78			/* localStorage unavailable — state just won't persist */
79		}
80	}
81
82	function removeStore(key) {
83		try {
84			window.localStorage.removeItem(key);
85		} catch (e) {
86			/* localStorage unavailable — state just won't persist */
87		}
88	}
89
90	function isMinimized() {
91		return readStore(STORAGE_KEY) === '1';
92	}
93
94	function setMinimized(state) {
95		if (state) {
96			writeStore(STORAGE_KEY, '1');
97		} else {
98			removeStore(STORAGE_KEY);
99		}
100	}
101
102	function hasSeenOffer() {
103		var shownAt = parseInt(readStore(SHOWN_KEY), 10);
104		return !isNaN(shownAt) && (Date.now() - shownAt) < RESURFACE_TTL_MS;
105	}
106
107	function markOfferShown() {
108		writeStore(SHOWN_KEY, String(Date.now()));
109	}
110
111	function showBubble() {
112		bubble.hidden = false;
113		void bubble.offsetWidth;
114		bubble.classList.add('is-visible');
115	}
116
117	function showPopup() {
118		setMinimized(false);
119		markOfferShown();
120
121		bubble.classList.remove('is-visible');
122		window.setTimeout(function () {
123			bubble.hidden = true;
124		}, 400);
125
126		popup.hidden = false;
127		// Force reflow so the transition runs after unhide.
128		void popup.offsetWidth;
129		popup.classList.add('is-visible');
130	}
131
132	function minimizePopup() {
133		setMinimized(true);
134
135		popup.classList.remove('is-visible');
136
137		window.setTimeout(function () {
138			popup.hidden = true;
139			showBubble();
140			bubble.focus();
141		}, HIDE_TRANSITION_MS);
142	}
143
144	var closeBtn = document.getElementById('stOfferPopupClose');
145
146	if (closeBtn) {
147		closeBtn.addEventListener('click', minimizePopup);
148	}
149
150	bubble.addEventListener('click', showPopup);
151
152	// Allow Escape to minimize while focus is inside the popup.
153	popup.addEventListener('keydown', function (event) {
154		if (event.key === 'Escape' || event.keyCode === 27) {
155			minimizePopup();
156		}
157	});
158
159	// Info icon: click copies the email to the clipboard and shows feedback.
160	var infoWrap = popup.querySelector('.st-offer-info');
161
162	if (infoWrap) {
163		var infoBtn = infoWrap.querySelector('.st-offer-info-btn');
164		var infoTip = infoWrap.querySelector('.st-offer-info-tip');
165		var copiedTimer = null;
166
167		function fallbackCopy(text, done) {
168			var ta = document.createElement('textarea');
169			ta.value = text;
170			ta.setAttribute('readonly', '');
171			ta.style.position = 'fixed';
172			ta.style.top = '-9999px';
173			document.body.appendChild(ta);
174			ta.select();
175			try {
176				document.execCommand('copy');
177				done();
178			} catch (e) {
179				/* copy failed — keep default tooltip */
180			}
181			document.body.removeChild(ta);
182		}
183
184		function showCopied() {
185			infoWrap.classList.add('is-open');
186			if (infoTip) {
187				infoTip.classList.add('is-copied');
188			}
189			window.clearTimeout(copiedTimer);
190			copiedTimer = window.setTimeout(function () {
191				if (infoTip) {
192					infoTip.classList.remove('is-copied');
193				}
194				infoWrap.classList.remove('is-open');
195			}, 2000);
196		}
197
198		if (infoBtn) {
199			infoBtn.addEventListener('click', function () {
200				infoBtn.setAttribute('aria-expanded', 'true');
201
202				var email = infoBtn.getAttribute('data-email') || '';
203				if (!email) {
204					return;
205				}
206
207				if (navigator.clipboard && navigator.clipboard.writeText) {
208					navigator.clipboard.writeText(email).then(showCopied, function () {
209						fallbackCopy(email, showCopied);
210					});
211				} else {
212					fallbackCopy(email, showCopied);
213				}
214			});
215		}
216
217		// Reset state when clicking anywhere outside it.
218		document.addEventListener('click', function (event) {
219			if (infoWrap.classList.contains('is-open') && !infoWrap.contains(event.target)) {
220				infoWrap.classList.remove('is-open');
221				if (infoTip) {
222					infoTip.classList.remove('is-copied');
223				}
224				if (infoBtn) {
225					infoBtn.setAttribute('aria-expanded', 'false');
226				}
227			}
228		});
229	}
230
231	// On each page load, restore the state from the previous page:
232	// the popup auto-opens only on the first view within 24h — after
233	// that (or once minimized) only the bubble is shown.
234	if (isMinimized() || hasSeenOffer()) {
235		popup.hidden = true;
236		showBubble();
237	} else {
238		window.setTimeout(showPopup, SHOW_DELAY_MS);
239	}
240})();

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.