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 |