PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents / 4.6.3
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents v4.6.3
4.6.3 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 trunk 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.5.0 1.6.0 1.6.1 1.6.2 1.6.3 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 2.0.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.2.0 2.2.1 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.1.3 3.2.0 3.2.1 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.7.0 3.7.1 3.7.2 3.7.3 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.9.0 3.9.1 3.9.10 3.9.11 3.9.12 3.9.13 3.9.14 3.9.15 3.9.16 3.9.17 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9 4.0.0 4.0.1 4.0.10 4.0.11 4.0.12 4.0.13 4.0.14 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.10 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 4.3.1 4.4.0 4.4.1 4.4.10 4.4.11 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 4.5.0 4.5.1
embedpress / assets / js / google-reviews.js
embedpress / assets / js Last commit date
chunks 3 days ago vendor 10 months ago admin.build.js 1 month ago admin.js 4 months ago analytics-tracker.js 2 months ago analytics.build.js 3 days ago blocks.build.js 3 days ago carousel.js 1 year ago custom-player.build.js 1 month ago documents-viewer-script.js 4 months ago ep-gr-elementor-control.js 1 month ago ep-pdf-lightbox.js 4 months ago ep-view-count.js 1 month ago ep-yt-queue.js 2 months ago feature-notices.js 8 months ago feature-preview-modal.js 1 month ago front.js 1 month ago frontend.build.js 4 months ago gallery-justify.js 1 month ago google-reviews.build.js 3 days ago google-reviews.js 3 days ago gutneberg-script.js 3 months ago index.html 7 years ago initCarousel.js 2 years ago initplyr.js 3 months ago instafeed.js 2 months ago instagram-shortcode-generator.js 2 months ago lazy-load.js 7 months ago license.js 4 months ago meetup-timezone.js 4 months ago onboarding.build.js 1 month ago pdf-gallery-elementor-editor.js 4 months ago pdf-gallery.js 4 months ago preview.js 3 weeks ago settings.js 4 months ago sponsored.js 10 months ago
google-reviews.js
444 lines
1 /**
2 * EmbedPress — Google Reviews (free) frontend behaviour.
3 *
4 * Read-more toggle: the review text is line-clamped via CSS; this reveals the
5 * "Read more" button ONLY when the text actually overflows, and toggles the
6 * expanded state on click. Rating-only / short reviews never get a button.
7 *
8 * Vanilla JS, no dependencies. Idempotent and re-runnable: the block is injected
9 * by ServerSideRender in the editor AFTER load, so a MutationObserver re-inits
10 * newly-added review cards. Uses a distinct init flag from the Pro script so the
11 * two never double-bind the same node.
12 */
13 (function () {
14 'use strict';
15
16 var INIT_FLAG = 'epGrRmInit';
17
18 function initCard(card) {
19 if (card.dataset[INIT_FLAG] === '1') return;
20 var text = card.querySelector('.ep-gr-text');
21 var btn = card.querySelector('.ep-gr-readmore');
22 if (!text || !btn) return;
23 card.dataset[INIT_FLAG] = '1';
24
25 // Show the toggle only when the (clamped) text overflows.
26 var overflows = text.scrollHeight - text.clientHeight > 2;
27 if (!overflows) {
28 btn.hidden = true;
29 return;
30 }
31 btn.hidden = false;
32 btn.addEventListener('click', function () {
33 var expanded = card.classList.toggle('is-expanded');
34 btn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
35 });
36 }
37
38 function init(root) {
39 var scope = root && root.querySelectorAll ? root : document;
40 var cards = scope.querySelectorAll('.ep-gr-body');
41 for (var i = 0; i < cards.length; i++) {
42 initCard(cards[i]);
43 }
44 var carousels = scope.querySelectorAll('.ep-google-reviews--carousel');
45 for (var c = 0; c < carousels.length; c++) {
46 initCarousel(carousels[c]);
47 }
48 var roots = scope.querySelectorAll('.ep-google-reviews[data-ep-gr-loadmore]');
49 for (var m = 0; m < roots.length; m++) {
50 initLoadMore(roots[m]);
51 }
52 var loaders = scope.querySelectorAll('.ep-google-reviews--loading[data-ep-gr-poll]');
53 for (var p = 0; p < loaders.length; p++) {
54 initPoller(loaders[p]);
55 }
56 }
57
58 /**
59 * Frontend "still fetching" self-refresh. A freshly-added place whose reviews
60 * aren't fetched yet renders the .ep-google-reviews--loading placeholder
61 * (see GoogleReviewsRenderer::render_loading) carrying the place_id + the
62 * public status endpoint URL. The moment the background job finishes WITH
63 * reviews, we reload the page once so the server re-renders the real cards
64 * in place — no manual refresh.
65 *
66 * COMPLETION IS NOT CLOCK-BASED. Results are delivered by a push webhook
67 * (api.embedpress.com POSTs straight into the store the instant the scrape
68 * ends), so this poller's real job is just to notice that the data landed
69 * and swap it in — it does NOT have to out-wait the scrape. We therefore
70 * removed the old 10-minute "give up quietly" deadline that surfaced as a
71 * stuck placeholder / perceived failure when a slow scrape outran it.
72 *
73 * Instead: poll with gentle exponential backoff (so a slow job doesn't get
74 * hammered), pause entirely while the tab is hidden (Page Visibility) and
75 * resume on focus — a backgrounded tab burns nothing. A finished-but-empty
76 * or failed job stops the poll (nothing to show); anything still running
77 * keeps checking for as long as the visitor is actually looking at the page.
78 * The endpoint also advances the job server-side as a fallback for sites the
79 * webhook can't reach.
80 */
81 function initPoller(root) {
82 if (root.dataset.epGrPollInit === '1') return;
83 var placeId = root.getAttribute('data-ep-gr-poll');
84 var url = root.getAttribute('data-ep-gr-poll-url');
85 if (!placeId || !url) return;
86 root.dataset.epGrPollInit = '1';
87
88 var base = parseInt(root.getAttribute('data-ep-gr-poll-interval'), 10) || 5000;
89 // Backoff band: start at the configured interval, grow gently up to 30s
90 // so a long scrape is checked patiently rather than every 5s forever.
91 var MAX_INTERVAL = 30000;
92 var interval = base;
93 var stopped = false;
94 var timer = null;
95
96 function schedule() {
97 if (stopped) return;
98 timer = window.setTimeout(tick, interval);
99 // Grow the interval toward the ceiling for the next round.
100 interval = Math.min(MAX_INTERVAL, Math.round(interval * 1.5));
101 }
102
103 function tick() {
104 timer = null;
105 if (stopped) return;
106 // Don't poll a hidden tab — parking here (timer=null) lets the
107 // visibilitychange handler restart the loop when the tab returns.
108 if (document.hidden) { return; }
109 var u = url + (url.indexOf('?') === -1 ? '?' : '&') + 'place_id=' + encodeURIComponent(placeId);
110 fetch(u, { headers: { Accept: 'application/json' } })
111 .then(function (r) { return r.json(); })
112 .then(function (res) {
113 if (stopped) return;
114 if (!res) { schedule(); return; }
115 if (res.ready) {
116 // Terminal. Reload only when reviews actually landed so
117 // the server renders them in place; on failed/0-reviews
118 // stop — there's nothing to show.
119 stopped = true;
120 if ((res.review_count || 0) > 0) {
121 window.location.reload();
122 }
123 return;
124 }
125 schedule(); // still running/queued — keep watching
126 })
127 .catch(function () { if (!stopped) schedule(); });
128 }
129
130 // Resume promptly when the visitor returns to the tab (reset the backoff
131 // so a returning user gets a fast first check).
132 document.addEventListener('visibilitychange', function () {
133 if (!document.hidden && !stopped && timer === null) {
134 interval = base;
135 tick();
136 }
137 });
138
139 schedule();
140 }
141
142 /**
143 * "Load more" (FREE, AJAX): the server renders the first page and stamps a
144 * config blob on data-ep-gr-loadmore. On click we fetch the next page of
145 * rendered cards from the REST endpoint, append them, advance the offset, and
146 * hide the button when the server reports no more.
147 */
148 function initLoadMore(root) {
149 if (root.dataset.epGrLoadmoreInit === '1') return;
150 var cfg;
151 try { cfg = JSON.parse(root.getAttribute('data-ep-gr-loadmore') || '{}'); }
152 catch (e) { return; }
153 if (!cfg || !cfg.rest || !cfg.place_id) return;
154
155 var btn = root.querySelector('.ep-gr-loadmore');
156 var wrap = root.querySelector('.ep-gr-loadmore-wrap');
157 var items = root.querySelector('.ep-gr-items');
158 if (!btn || !items) return;
159 root.dataset.epGrLoadmoreInit = '1';
160
161 var offset = parseInt(cfg.offset, 10) || 0;
162 var loading = false;
163
164 btn.addEventListener('click', function () {
165 if (loading) return;
166 loading = true;
167 btn.classList.add('is-loading');
168 btn.disabled = true;
169
170 var params = ['place_id=' + encodeURIComponent(cfg.place_id),
171 'offset=' + offset,
172 'per_page=' + (parseInt(cfg.per_page, 10) || 5)];
173 var q = cfg.query || {};
174 for (var k in q) {
175 if (!Object.prototype.hasOwnProperty.call(q, k)) continue;
176 var v = q[k];
177 if (Array.isArray(v)) {
178 for (var i = 0; i < v.length; i++) {
179 params.push(encodeURIComponent(k + '[]') + '=' + encodeURIComponent(v[i]));
180 }
181 } else {
182 params.push(encodeURIComponent(k) + '=' + encodeURIComponent(v));
183 }
184 }
185 var url = cfg.rest + (cfg.rest.indexOf('?') === -1 ? '?' : '&') + params.join('&');
186
187 fetch(url, { headers: { Accept: 'application/json' } })
188 .then(function (r) { return r.json(); })
189 .then(function (res) {
190 if (res && res.html) {
191 items.insertAdjacentHTML('beforeend', res.html);
192 init(root); // wire read-more on the new cards
193 }
194 if (res && typeof res.next_offset === 'number') offset = res.next_offset;
195 if (!res || !res.has_more) {
196 if (wrap) { wrap.style.display = 'none'; } else { btn.style.display = 'none'; }
197 }
198 })
199 .catch(function () { /* leave button for retry */ })
200 .then(function () {
201 loading = false;
202 btn.classList.remove('is-loading');
203 btn.disabled = false;
204 });
205 });
206 }
207
208 /**
209 * Real carousel: arrows + dots + autoplay, sliding one "page" at a time.
210 * Page size = how many cards fit the viewport (1 or 2, matching the CSS
211 * flex-basis breakpoint). Idempotent via data flag; rebuilds controls if the
212 * editor re-renders.
213 */
214 function initCarousel(root) {
215 if (root.dataset.epGrCarousel === '1') return;
216 var track = root.querySelector('.ep-gr-items');
217 if (!track) return;
218 var items = track.querySelectorAll('.ep-gr-review');
219 if (items.length < 2) return; // nothing to slide
220 root.dataset.epGrCarousel = '1';
221
222 // The track is the element we translate, so it must NOT be the one that
223 // clips overflow — a clip box on the moving element travels with it, so
224 // after translateX(-100%) the next page lands outside its own clip and
225 // shows blank (the "empty next" bug). Wrap the track in a stationary
226 // viewport that owns overflow:hidden. Idempotent across re-inits.
227 var viewport = track.parentNode;
228 if (!viewport || !viewport.classList || !viewport.classList.contains('ep-gr-viewport')) {
229 viewport = document.createElement('div');
230 viewport.className = 'ep-gr-viewport';
231 track.parentNode.insertBefore(viewport, track);
232 viewport.appendChild(track);
233 }
234
235 function perView() {
236 // Derive from the first card's rendered width vs the viewport width.
237 // Use the viewport (stationary, clips) not the track (sized to its
238 // 17-card content) so the ratio reflects what's actually on screen.
239 var tw = viewport.clientWidth || track.clientWidth || 1;
240 var cw = items[0].getBoundingClientRect().width || tw;
241 return Math.max(1, Math.round(tw / cw));
242 }
243
244 var pv = perView();
245 var realPages = Math.max(1, Math.ceil(items.length / pv));
246
247 // ── Seamless infinite loop ────────────────────────────────────────
248 // Clone the first page's cards and append them as a phantom page. We
249 // animate forward into the phantom, then — once the slide finishes —
250 // silently snap (transition off) back to the real page 0. The viewer
251 // never sees the reset, so it reads as a continuous, never-ending loop.
252 // Seamless infinite loop unless explicitly disabled (data-ep-gr-loop="0").
253 var loopEnabled = root.getAttribute('data-ep-gr-loop') !== '0';
254 var loop = loopEnabled && realPages > 1;
255 if (loop && root.dataset.epGrCloned !== '1') {
256 for (var ci = 0; ci < pv && ci < items.length; ci++) {
257 var clone = items[ci].cloneNode(true);
258 clone.setAttribute('data-ep-gr-clone', '1');
259 clone.setAttribute('aria-hidden', 'true');
260 track.appendChild(clone);
261 }
262 root.dataset.epGrCloned = '1';
263 }
264
265 // FIXED height for the whole carousel — the tallest card across the set,
266 // capped. A fixed box means the cards/dots/everything below never jump
267 // up and down as you slide (the per-page approach caused that). With the
268 // carousel text clamped to 3 lines in CSS, the tallest "normal" card is
269 // modest, so this fixed height is also compact (no giant empty box). The
270 // cap guards against a rare photo-heavy outlier. Computed once.
271 function fitHeight() {
272 var max = 0;
273 for (var i = 0; i < items.length; i++) {
274 var h = items[i].offsetHeight;
275 if (h > max) max = h;
276 }
277 // Cap to a typical clamped-card height. With text limited to 3 lines
278 // the normal tallest card fits well under this; a rare photo-heavy
279 // card is clipped by the viewport rather than ballooning the whole
280 // slider into a tall empty box for every other page.
281 if (max > 340) max = 340;
282 if (max > 0) viewport.style.setProperty('height', max + 'px', 'important');
283 }
284
285 var page = 0; // logical page, 0..realPages-1
286 var slot = 0; // physical slot, 0..realPages (realPages == clone)
287 var animating = false;
288
289 function paint(animate) {
290 var offset = -(slot * 100);
291 track.style.setProperty('transition', animate ? 'transform 0.45s ease' : 'none', 'important');
292 // Use !important so the widget's transform reset (style isolation)
293 // can't cancel the slide.
294 track.style.setProperty('transform', 'translateX(' + offset + '%)', 'important');
295 updateDots();
296 }
297
298 function go(target) {
299 if (animating) return;
300 if (!loop) {
301 page = (target + realPages) % realPages;
302 slot = page;
303 paint(true);
304 return;
305 }
306 // Forward into clone (target == realPages) animates, then resets.
307 if (target >= realPages) {
308 animating = true;
309 slot = realPages; page = 0;
310 paint(true);
311 afterSlide(function () { slot = 0; paint(false); animating = false; });
312 } else if (target < 0) {
313 // Going back from page 0: jump to the clone position first
314 // (no anim), then animate to the last real page.
315 slot = realPages; paint(false);
316 // force reflow so the next transform animates from the clone
317 void track.offsetWidth;
318 animating = true;
319 page = realPages - 1; slot = page;
320 paint(true);
321 afterSlide(function () { animating = false; });
322 } else {
323 page = target; slot = target;
324 paint(true);
325 }
326 }
327
328 // Run cb once after the current transform transition ends (with a
329 // timeout fallback so a missed transitionend never wedges the carousel).
330 function afterSlide(cb) {
331 var done = false;
332 function fire() { if (done) return; done = true; track.removeEventListener('transitionend', onEnd); cb(); }
333 function onEnd(e) { if (e.propertyName === 'transform') fire(); }
334 track.addEventListener('transitionend', onEnd);
335 window.setTimeout(fire, 600);
336 }
337
338 // Controls
339 var prev = document.createElement('button');
340 prev.type = 'button';
341 prev.className = 'ep-gr-carousel-arrow ep-gr-carousel-prev';
342 prev.setAttribute('aria-label', 'Previous');
343 prev.innerHTML = '';
344 var next = document.createElement('button');
345 next.type = 'button';
346 next.className = 'ep-gr-carousel-arrow ep-gr-carousel-next';
347 next.setAttribute('aria-label', 'Next');
348 next.innerHTML = '';
349 prev.addEventListener('click', function () { go(page - 1); stopAuto(); });
350 next.addEventListener('click', function () { go(page + 1); stopAuto(); });
351 // Show arrows unless explicitly disabled (data-ep-gr-arrows="0").
352 var showArrows = root.getAttribute('data-ep-gr-arrows') !== '0';
353 if (showArrows) {
354 root.appendChild(prev);
355 root.appendChild(next);
356 }
357
358 var dotsWrap = document.createElement('div');
359 dotsWrap.className = 'ep-gr-carousel-dots';
360 // Show dots unless explicitly disabled (data-ep-gr-dots="0").
361 var showDots = root.getAttribute('data-ep-gr-dots') !== '0';
362 if (showDots) {
363 root.appendChild(dotsWrap);
364 }
365 function buildDots() {
366 dotsWrap.innerHTML = '';
367 for (var i = 0; i < realPages; i++) {
368 (function (idx) {
369 var d = document.createElement('button');
370 d.type = 'button';
371 d.className = 'ep-gr-carousel-dot';
372 d.setAttribute('aria-label', 'Go to slide ' + (idx + 1));
373 d.addEventListener('click', function () { go(idx); stopAuto(); });
374 dotsWrap.appendChild(d);
375 })(i);
376 }
377 }
378 function updateDots() {
379 var dots = dotsWrap.children;
380 for (var i = 0; i < dots.length; i++) {
381 dots[i].classList.toggle('is-active', i === page);
382 }
383 }
384
385 // Autoplay (opt-in via data-ep-gr-autoplay; pauses on hover). Interval
386 // from data-ep-gr-speed (seconds), clamped to a sane 1–30s band.
387 var timer = null;
388 var autoplay = root.getAttribute('data-ep-gr-autoplay') === '1';
389 var speed = parseFloat(root.getAttribute('data-ep-gr-speed')) || 5;
390 speed = Math.min(30, Math.max(1, speed)) * 1000;
391 function startAuto() {
392 if (!autoplay) return;
393 if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
394 stopAuto();
395 timer = window.setInterval(function () { go(page + 1); }, speed);
396 }
397 function stopAuto() { if (timer) { window.clearInterval(timer); timer = null; } }
398 root.addEventListener('mouseenter', stopAuto);
399 root.addEventListener('mouseleave', startAuto);
400
401 buildDots();
402 fitHeight();
403 paint(false);
404 startAuto();
405
406 // Rebuild on resize (perView may change between 1 and 2). pv/realPages
407 // are captured at init; if perView changes we re-fit height and re-clamp
408 // to a valid page so the box and dots stay correct.
409 var rt;
410 window.addEventListener('resize', function () {
411 window.clearTimeout(rt);
412 rt = window.setTimeout(function () {
413 fitHeight();
414 if (page > realPages - 1) page = realPages - 1;
415 slot = page;
416 paint(false);
417 }, 200);
418 });
419 }
420
421 if (document.readyState === 'loading') {
422 document.addEventListener('DOMContentLoaded', function () { init(document); });
423 } else {
424 init(document);
425 }
426
427 // Editor parity: re-init when SSR injects fresh review markup.
428 if (typeof MutationObserver !== 'undefined') {
429 var obs = new MutationObserver(function (muts) {
430 for (var i = 0; i < muts.length; i++) {
431 if (muts[i].addedNodes && muts[i].addedNodes.length) {
432 init(document);
433 break;
434 }
435 }
436 });
437 var start = function () {
438 if (document.body) obs.observe(document.body, { childList: true, subtree: true });
439 };
440 if (document.body) start();
441 else document.addEventListener('DOMContentLoaded', start);
442 }
443 })();
444