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 |