PluginProbe ʕ •ᴥ•ʔ
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents / 4.6.0
EmbedPress – PDF Embedder, 3D PDF FlipBook, Google Reviews, YouTube Videos, Upload & Embed PDF documents v4.6.0
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 2 weeks ago vendor 9 months ago admin.build.js 2 weeks ago admin.js 3 months ago analytics-tracker.js 1 month ago analytics.build.js 2 weeks ago blocks.build.js 3 days ago carousel.js 1 year ago custom-player.build.js 2 weeks ago documents-viewer-script.js 3 months ago ep-gr-elementor-control.js 3 days ago ep-pdf-lightbox.js 3 months ago ep-view-count.js 2 weeks ago ep-yt-queue.js 1 month ago feature-notices.js 7 months ago feature-preview-modal.js 3 days ago front.js 3 days ago frontend.build.js 3 months ago gallery-justify.js 2 weeks ago google-reviews.build.js 3 days ago google-reviews.js 3 days ago gutneberg-script.js 2 months ago index.html 7 years ago initCarousel.js 2 years ago initplyr.js 2 months ago instafeed.js 1 month ago instagram-shortcode-generator.js 1 month ago lazy-load.js 6 months ago license.js 3 months ago meetup-timezone.js 3 months ago onboarding.build.js 2 weeks ago pdf-gallery-elementor-editor.js 3 months ago pdf-gallery.js 3 months ago preview.js 9 months ago settings.js 3 months ago sponsored.js 9 months ago
google-reviews.js
410 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. We poll that endpoint; the moment the
63 * background job is done WITH reviews, we reload the page once so the server
64 * re-renders the real review cards in place — no manual refresh. On
65 * failed / done-empty we simply stop polling (the placeholder stays as-is;
66 * for visitors a failed fetch renders nothing at all server-side).
67 *
68 * The poll endpoint ALSO advances the job server-side (WP-cron is unreliable
69 * on quiet pages), so polling is what actually drives the fetch to completion
70 * for the first visitor who lands on the page.
71 */
72 function initPoller(root) {
73 if (root.dataset.epGrPollInit === '1') return;
74 var placeId = root.getAttribute('data-ep-gr-poll');
75 var url = root.getAttribute('data-ep-gr-poll-url');
76 if (!placeId || !url) return;
77 root.dataset.epGrPollInit = '1';
78
79 var interval = parseInt(root.getAttribute('data-ep-gr-poll-interval'), 10) || 5000;
80 // Stop after a generous ceiling so a wedged job can't poll forever
81 // (a big place can scrape for a few minutes; 10 min is a safe cap).
82 var deadline = Date.now() + 10 * 60 * 1000;
83
84 function tick() {
85 if (Date.now() > deadline) return; // give up quietly
86 var u = url + (url.indexOf('?') === -1 ? '?' : '&') + 'place_id=' + encodeURIComponent(placeId);
87 fetch(u, { headers: { Accept: 'application/json' } })
88 .then(function (r) { return r.json(); })
89 .then(function (res) {
90 if (!res) { window.setTimeout(tick, interval); return; }
91 if (res.ready) {
92 // Done (or failed). Reload only when reviews actually
93 // landed, so the server renders them in place. On
94 // failed / 0-reviews, stop — nothing to show.
95 if ((res.review_count || 0) > 0) {
96 window.location.reload();
97 }
98 return; // stop polling either way
99 }
100 window.setTimeout(tick, interval); // still running/queued
101 })
102 .catch(function () { window.setTimeout(tick, interval); });
103 }
104
105 window.setTimeout(tick, interval);
106 }
107
108 /**
109 * "Load more" (FREE, AJAX): the server renders the first page and stamps a
110 * config blob on data-ep-gr-loadmore. On click we fetch the next page of
111 * rendered cards from the REST endpoint, append them, advance the offset, and
112 * hide the button when the server reports no more.
113 */
114 function initLoadMore(root) {
115 if (root.dataset.epGrLoadmoreInit === '1') return;
116 var cfg;
117 try { cfg = JSON.parse(root.getAttribute('data-ep-gr-loadmore') || '{}'); }
118 catch (e) { return; }
119 if (!cfg || !cfg.rest || !cfg.place_id) return;
120
121 var btn = root.querySelector('.ep-gr-loadmore');
122 var wrap = root.querySelector('.ep-gr-loadmore-wrap');
123 var items = root.querySelector('.ep-gr-items');
124 if (!btn || !items) return;
125 root.dataset.epGrLoadmoreInit = '1';
126
127 var offset = parseInt(cfg.offset, 10) || 0;
128 var loading = false;
129
130 btn.addEventListener('click', function () {
131 if (loading) return;
132 loading = true;
133 btn.classList.add('is-loading');
134 btn.disabled = true;
135
136 var params = ['place_id=' + encodeURIComponent(cfg.place_id),
137 'offset=' + offset,
138 'per_page=' + (parseInt(cfg.per_page, 10) || 5)];
139 var q = cfg.query || {};
140 for (var k in q) {
141 if (!Object.prototype.hasOwnProperty.call(q, k)) continue;
142 var v = q[k];
143 if (Array.isArray(v)) {
144 for (var i = 0; i < v.length; i++) {
145 params.push(encodeURIComponent(k + '[]') + '=' + encodeURIComponent(v[i]));
146 }
147 } else {
148 params.push(encodeURIComponent(k) + '=' + encodeURIComponent(v));
149 }
150 }
151 var url = cfg.rest + (cfg.rest.indexOf('?') === -1 ? '?' : '&') + params.join('&');
152
153 fetch(url, { headers: { Accept: 'application/json' } })
154 .then(function (r) { return r.json(); })
155 .then(function (res) {
156 if (res && res.html) {
157 items.insertAdjacentHTML('beforeend', res.html);
158 init(root); // wire read-more on the new cards
159 }
160 if (res && typeof res.next_offset === 'number') offset = res.next_offset;
161 if (!res || !res.has_more) {
162 if (wrap) { wrap.style.display = 'none'; } else { btn.style.display = 'none'; }
163 }
164 })
165 .catch(function () { /* leave button for retry */ })
166 .then(function () {
167 loading = false;
168 btn.classList.remove('is-loading');
169 btn.disabled = false;
170 });
171 });
172 }
173
174 /**
175 * Real carousel: arrows + dots + autoplay, sliding one "page" at a time.
176 * Page size = how many cards fit the viewport (1 or 2, matching the CSS
177 * flex-basis breakpoint). Idempotent via data flag; rebuilds controls if the
178 * editor re-renders.
179 */
180 function initCarousel(root) {
181 if (root.dataset.epGrCarousel === '1') return;
182 var track = root.querySelector('.ep-gr-items');
183 if (!track) return;
184 var items = track.querySelectorAll('.ep-gr-review');
185 if (items.length < 2) return; // nothing to slide
186 root.dataset.epGrCarousel = '1';
187
188 // The track is the element we translate, so it must NOT be the one that
189 // clips overflow — a clip box on the moving element travels with it, so
190 // after translateX(-100%) the next page lands outside its own clip and
191 // shows blank (the "empty next" bug). Wrap the track in a stationary
192 // viewport that owns overflow:hidden. Idempotent across re-inits.
193 var viewport = track.parentNode;
194 if (!viewport || !viewport.classList || !viewport.classList.contains('ep-gr-viewport')) {
195 viewport = document.createElement('div');
196 viewport.className = 'ep-gr-viewport';
197 track.parentNode.insertBefore(viewport, track);
198 viewport.appendChild(track);
199 }
200
201 function perView() {
202 // Derive from the first card's rendered width vs the viewport width.
203 // Use the viewport (stationary, clips) not the track (sized to its
204 // 17-card content) so the ratio reflects what's actually on screen.
205 var tw = viewport.clientWidth || track.clientWidth || 1;
206 var cw = items[0].getBoundingClientRect().width || tw;
207 return Math.max(1, Math.round(tw / cw));
208 }
209
210 var pv = perView();
211 var realPages = Math.max(1, Math.ceil(items.length / pv));
212
213 // ── Seamless infinite loop ────────────────────────────────────────
214 // Clone the first page's cards and append them as a phantom page. We
215 // animate forward into the phantom, then — once the slide finishes —
216 // silently snap (transition off) back to the real page 0. The viewer
217 // never sees the reset, so it reads as a continuous, never-ending loop.
218 // Seamless infinite loop unless explicitly disabled (data-ep-gr-loop="0").
219 var loopEnabled = root.getAttribute('data-ep-gr-loop') !== '0';
220 var loop = loopEnabled && realPages > 1;
221 if (loop && root.dataset.epGrCloned !== '1') {
222 for (var ci = 0; ci < pv && ci < items.length; ci++) {
223 var clone = items[ci].cloneNode(true);
224 clone.setAttribute('data-ep-gr-clone', '1');
225 clone.setAttribute('aria-hidden', 'true');
226 track.appendChild(clone);
227 }
228 root.dataset.epGrCloned = '1';
229 }
230
231 // FIXED height for the whole carousel — the tallest card across the set,
232 // capped. A fixed box means the cards/dots/everything below never jump
233 // up and down as you slide (the per-page approach caused that). With the
234 // carousel text clamped to 3 lines in CSS, the tallest "normal" card is
235 // modest, so this fixed height is also compact (no giant empty box). The
236 // cap guards against a rare photo-heavy outlier. Computed once.
237 function fitHeight() {
238 var max = 0;
239 for (var i = 0; i < items.length; i++) {
240 var h = items[i].offsetHeight;
241 if (h > max) max = h;
242 }
243 // Cap to a typical clamped-card height. With text limited to 3 lines
244 // the normal tallest card fits well under this; a rare photo-heavy
245 // card is clipped by the viewport rather than ballooning the whole
246 // slider into a tall empty box for every other page.
247 if (max > 340) max = 340;
248 if (max > 0) viewport.style.setProperty('height', max + 'px', 'important');
249 }
250
251 var page = 0; // logical page, 0..realPages-1
252 var slot = 0; // physical slot, 0..realPages (realPages == clone)
253 var animating = false;
254
255 function paint(animate) {
256 var offset = -(slot * 100);
257 track.style.setProperty('transition', animate ? 'transform 0.45s ease' : 'none', 'important');
258 // Use !important so the widget's transform reset (style isolation)
259 // can't cancel the slide.
260 track.style.setProperty('transform', 'translateX(' + offset + '%)', 'important');
261 updateDots();
262 }
263
264 function go(target) {
265 if (animating) return;
266 if (!loop) {
267 page = (target + realPages) % realPages;
268 slot = page;
269 paint(true);
270 return;
271 }
272 // Forward into clone (target == realPages) animates, then resets.
273 if (target >= realPages) {
274 animating = true;
275 slot = realPages; page = 0;
276 paint(true);
277 afterSlide(function () { slot = 0; paint(false); animating = false; });
278 } else if (target < 0) {
279 // Going back from page 0: jump to the clone position first
280 // (no anim), then animate to the last real page.
281 slot = realPages; paint(false);
282 // force reflow so the next transform animates from the clone
283 void track.offsetWidth;
284 animating = true;
285 page = realPages - 1; slot = page;
286 paint(true);
287 afterSlide(function () { animating = false; });
288 } else {
289 page = target; slot = target;
290 paint(true);
291 }
292 }
293
294 // Run cb once after the current transform transition ends (with a
295 // timeout fallback so a missed transitionend never wedges the carousel).
296 function afterSlide(cb) {
297 var done = false;
298 function fire() { if (done) return; done = true; track.removeEventListener('transitionend', onEnd); cb(); }
299 function onEnd(e) { if (e.propertyName === 'transform') fire(); }
300 track.addEventListener('transitionend', onEnd);
301 window.setTimeout(fire, 600);
302 }
303
304 // Controls
305 var prev = document.createElement('button');
306 prev.type = 'button';
307 prev.className = 'ep-gr-carousel-arrow ep-gr-carousel-prev';
308 prev.setAttribute('aria-label', 'Previous');
309 prev.innerHTML = '';
310 var next = document.createElement('button');
311 next.type = 'button';
312 next.className = 'ep-gr-carousel-arrow ep-gr-carousel-next';
313 next.setAttribute('aria-label', 'Next');
314 next.innerHTML = '';
315 prev.addEventListener('click', function () { go(page - 1); stopAuto(); });
316 next.addEventListener('click', function () { go(page + 1); stopAuto(); });
317 // Show arrows unless explicitly disabled (data-ep-gr-arrows="0").
318 var showArrows = root.getAttribute('data-ep-gr-arrows') !== '0';
319 if (showArrows) {
320 root.appendChild(prev);
321 root.appendChild(next);
322 }
323
324 var dotsWrap = document.createElement('div');
325 dotsWrap.className = 'ep-gr-carousel-dots';
326 // Show dots unless explicitly disabled (data-ep-gr-dots="0").
327 var showDots = root.getAttribute('data-ep-gr-dots') !== '0';
328 if (showDots) {
329 root.appendChild(dotsWrap);
330 }
331 function buildDots() {
332 dotsWrap.innerHTML = '';
333 for (var i = 0; i < realPages; i++) {
334 (function (idx) {
335 var d = document.createElement('button');
336 d.type = 'button';
337 d.className = 'ep-gr-carousel-dot';
338 d.setAttribute('aria-label', 'Go to slide ' + (idx + 1));
339 d.addEventListener('click', function () { go(idx); stopAuto(); });
340 dotsWrap.appendChild(d);
341 })(i);
342 }
343 }
344 function updateDots() {
345 var dots = dotsWrap.children;
346 for (var i = 0; i < dots.length; i++) {
347 dots[i].classList.toggle('is-active', i === page);
348 }
349 }
350
351 // Autoplay (opt-in via data-ep-gr-autoplay; pauses on hover). Interval
352 // from data-ep-gr-speed (seconds), clamped to a sane 1–30s band.
353 var timer = null;
354 var autoplay = root.getAttribute('data-ep-gr-autoplay') === '1';
355 var speed = parseFloat(root.getAttribute('data-ep-gr-speed')) || 5;
356 speed = Math.min(30, Math.max(1, speed)) * 1000;
357 function startAuto() {
358 if (!autoplay) return;
359 if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
360 stopAuto();
361 timer = window.setInterval(function () { go(page + 1); }, speed);
362 }
363 function stopAuto() { if (timer) { window.clearInterval(timer); timer = null; } }
364 root.addEventListener('mouseenter', stopAuto);
365 root.addEventListener('mouseleave', startAuto);
366
367 buildDots();
368 fitHeight();
369 paint(false);
370 startAuto();
371
372 // Rebuild on resize (perView may change between 1 and 2). pv/realPages
373 // are captured at init; if perView changes we re-fit height and re-clamp
374 // to a valid page so the box and dots stay correct.
375 var rt;
376 window.addEventListener('resize', function () {
377 window.clearTimeout(rt);
378 rt = window.setTimeout(function () {
379 fitHeight();
380 if (page > realPages - 1) page = realPages - 1;
381 slot = page;
382 paint(false);
383 }, 200);
384 });
385 }
386
387 if (document.readyState === 'loading') {
388 document.addEventListener('DOMContentLoaded', function () { init(document); });
389 } else {
390 init(document);
391 }
392
393 // Editor parity: re-init when SSR injects fresh review markup.
394 if (typeof MutationObserver !== 'undefined') {
395 var obs = new MutationObserver(function (muts) {
396 for (var i = 0; i < muts.length; i++) {
397 if (muts[i].addedNodes && muts[i].addedNodes.length) {
398 init(document);
399 break;
400 }
401 }
402 });
403 var start = function () {
404 if (document.body) obs.observe(document.body, { childList: true, subtree: true });
405 };
406 if (document.body) start();
407 else document.addEventListener('DOMContentLoaded', start);
408 }
409 })();
410