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
ep-gr-elementor-control.js
539 lines
| 1 | /** |
| 2 | * Backbone view for the EmbedPress Google Reviews place picker Elementor |
| 3 | * control (type: ep_gr_place_picker). |
| 4 | * |
| 5 | * Stored value shape: { place_id: '', place_name: '' } |
| 6 | * |
| 7 | * Why a custom control vs. the inject-HTML approach we shipped earlier: |
| 8 | * - Elementor manages render / undo / clone properly when the value lives |
| 9 | * in the model behind a real control type. |
| 10 | * - Theme switching (light/dark) works automatically because all styles |
| 11 | * hang off Elementor's CSS variables. |
| 12 | * - No need to maintain mirror text inputs as graceful-degradation fallback. |
| 13 | */ |
| 14 | (function ($) { |
| 15 | 'use strict'; |
| 16 | |
| 17 | if (typeof window.epGoogleReviewsElementor !== 'object') return; |
| 18 | var REST = window.epGoogleReviewsElementor.restUrl; |
| 19 | var NONCE = window.epGoogleReviewsElementor.nonce; |
| 20 | var I18N = window.epGoogleReviewsElementor.i18n || {}; |
| 21 | var PRO_ACTIVE = !!window.epGoogleReviewsElementor.proActive; |
| 22 | var UPGRADE_URL = window.epGoogleReviewsElementor.upgradeUrl || 'https://wpdeveloper.com/in/upgrade-embedpress'; |
| 23 | function t(k, fb) { return I18N[k] || fb; } |
| 24 | // Crown SVG — matches the settings/block "+ Select" upsell badge. |
| 25 | var CROWN = '<svg width="11" height="11" viewBox="0 0 24 24" aria-hidden="true">' + |
| 26 | '<path fill="currentColor" d="M5 16L3 5l5.5 5L12 4l3.5 6L21 5l-2 11H5zm0 2h14v2H5v-2z"/></svg>'; |
| 27 | // Map-pin SVG — same icon + class as the block/admin pickers (parity). |
| 28 | var PIN = '<svg class="ep-gr-suggestion-pin" width="14" height="14" viewBox="0 0 24 24" fill="none" ' + |
| 29 | 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + |
| 30 | '<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>'; |
| 31 | |
| 32 | // Rating + review-count line — mirrors the block/admin SuggestionMeta: |
| 33 | // "� |
| 34 | 4.5 · 1,167 reviews". Returns '' when neither is present. |
| 35 | function metaHtml(rating, count) { |
| 36 | if (rating == null && count == null) { return ''; } |
| 37 | var html = '<span class="ep-gr-suggestion-meta">'; |
| 38 | if (rating != null) { |
| 39 | html += '<span class="ep-gr-suggestion-rating">' + |
| 40 | '<span class="ep-gr-suggestion-star" aria-hidden="true">� |
| 41 | </span>' + |
| 42 | escapeHtml(Number(rating).toFixed(1)) + '</span>'; |
| 43 | } |
| 44 | if (count != null) { |
| 45 | var n = Number(count); |
| 46 | html += '<span class="ep-gr-suggestion-count">' + |
| 47 | escapeHtml(n.toLocaleString()) + ' ' + |
| 48 | escapeHtml(n === 1 ? t('review', 'review') : t('reviews', 'reviews')) + '</span>'; |
| 49 | } |
| 50 | return html + '</span>'; |
| 51 | } |
| 52 | |
| 53 | function escapeHtml(s) { |
| 54 | return String(s == null ? '' : s) |
| 55 | .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') |
| 56 | .replace(/"/g, '"').replace(/'/g, '''); |
| 57 | } |
| 58 | |
| 59 | $(window).on('elementor:init', function () { |
| 60 | if (!window.elementor || !elementor.modules || !elementor.modules.controls) return; |
| 61 | |
| 62 | var ControlBaseDataView = elementor.modules.controls.BaseData; |
| 63 | |
| 64 | var PlacePickerView = ControlBaseDataView.extend({ |
| 65 | |
| 66 | ui: function () { |
| 67 | var base = ControlBaseDataView.prototype.ui.apply(this, arguments); |
| 68 | _.extend(base, { |
| 69 | input: '.ep-gr-picker__input', |
| 70 | spinner: '.ep-gr-picker__spinner', |
| 71 | resultsPanel: '.ep-gr-picker__results-panel', |
| 72 | results: '.ep-gr-picker__results', |
| 73 | resultsNote: '.ep-gr-picker__results-note', |
| 74 | status: '.ep-gr-picker__status', |
| 75 | selected: '.ep-gr-picker__selected', |
| 76 | selectedNm: '.ep-gr-picker__selected-name', |
| 77 | selectedId: '.ep-gr-picker__selected-id', |
| 78 | selectedAddr: '.ep-gr-picker__selected-address', |
| 79 | search: '.ep-gr-picker__search', |
| 80 | clearBtn: '.ep-gr-picker__clear', |
| 81 | lists: '.ep-gr-picker__lists', |
| 82 | listSaved: '.ep-gr-picker__list[data-kind=saved] .ep-gr-picker__list-items', |
| 83 | listRecent: '.ep-gr-picker__list[data-kind=recent] .ep-gr-picker__list-items', |
| 84 | listSavedBox: '.ep-gr-picker__list[data-kind=saved]', |
| 85 | listRecentBox:'.ep-gr-picker__list[data-kind=recent]', |
| 86 | manualToggle: '.ep-gr-picker__manual-toggle', |
| 87 | manualRow: '.ep-gr-picker__manual-row', |
| 88 | manualInput: '.ep-gr-picker__manual-input', |
| 89 | manualApply: '.ep-gr-picker__manual-apply', |
| 90 | }); |
| 91 | return base; |
| 92 | }, |
| 93 | |
| 94 | events: function () { |
| 95 | return _.extend({}, ControlBaseDataView.prototype.events.apply(this, arguments), { |
| 96 | 'input @ui.input': 'onInput', |
| 97 | 'focus @ui.input': 'onInputFocus', |
| 98 | 'keydown @ui.input': 'onInputKey', |
| 99 | 'click @ui.clearBtn': 'onClear', |
| 100 | 'click @ui.results > li': 'onPick', |
| 101 | 'keydown @ui.results > li': 'onResultKey', |
| 102 | 'click @ui.manualToggle': 'onToggleManual', |
| 103 | 'click @ui.manualApply': 'onApplyManual', |
| 104 | 'keydown @ui.manualInput': 'onManualKey', |
| 105 | 'click .ep-gr-picker__list-pick': 'onListPick', |
| 106 | }); |
| 107 | }, |
| 108 | |
| 109 | onReady: function () { |
| 110 | this.renderSelected(); |
| 111 | this.loadLists(); |
| 112 | }, |
| 113 | |
| 114 | getValueOrDefault: function () { |
| 115 | var v = this.readValue(); |
| 116 | if (!v || typeof v !== 'object') v = { place_id: '', place_name: '', place_address: '' }; |
| 117 | return v; |
| 118 | }, |
| 119 | |
| 120 | // Read/write the control value through whichever Elementor API is |
| 121 | // available. Elementor 4.x dropped the BaseData.setControlValue() |
| 122 | // helper on object-typed custom controls; calling it throws |
| 123 | // `this.setControlValue is not a function` and the click silently |
| 124 | // fails. Fall back to the settings model directly. |
| 125 | readValue: function () { |
| 126 | var name = this.model.get('name'); |
| 127 | if (this.container && this.container.settings) { |
| 128 | return this.container.settings.get(name); |
| 129 | } |
| 130 | if (this.elementSettingsModel) { |
| 131 | return this.elementSettingsModel.get(name); |
| 132 | } |
| 133 | if (typeof this.getControlValue === 'function') { |
| 134 | return this.getControlValue(); |
| 135 | } |
| 136 | return null; |
| 137 | }, |
| 138 | |
| 139 | writeValue: function (value) { |
| 140 | var name = this.model.get('name'); |
| 141 | // Preferred: route through Elementor's command pipeline so the |
| 142 | // live preview iframe re-renders and undo history is recorded. |
| 143 | // `container.settings.set()` alone updates the Backbone model |
| 144 | // but skips the render path, so the canvas appears stale until |
| 145 | // the next reload. |
| 146 | if (this.container && window.$e && $e.run) { |
| 147 | try { |
| 148 | $e.run('document/elements/settings', { |
| 149 | container: this.container, |
| 150 | settings: _.object([name], [value]), |
| 151 | }); |
| 152 | return; |
| 153 | } catch (err) { /* fall through to direct model write */ } |
| 154 | } |
| 155 | if (this.container && this.container.settings) { |
| 156 | this.container.settings.set(name, value); |
| 157 | return; |
| 158 | } |
| 159 | if (this.elementSettingsModel) { |
| 160 | this.elementSettingsModel.set(name, value); |
| 161 | return; |
| 162 | } |
| 163 | if (typeof this.setControlValue === 'function') { |
| 164 | this.setControlValue(value); |
| 165 | } |
| 166 | }, |
| 167 | |
| 168 | renderSelected: function () { |
| 169 | var v = this.getValueOrDefault(); |
| 170 | if (v.place_id) { |
| 171 | this.ui.selected.attr('data-state', 'picked'); |
| 172 | // Name line stays clean — when a place was added by raw ID it |
| 173 | // has no name, so show a neutral label rather than dumping the |
| 174 | // ID into the prominent name slot (the ID shows demoted below). |
| 175 | this.ui.selectedNm.text(v.place_name || t('selectedPlace', 'Selected place')); |
| 176 | this.ui.selectedNm.attr('title', v.place_name || v.place_id); |
| 177 | // Show the location details (address) when we have them; fall |
| 178 | // back to the raw Place ID (e.g. added by ID / Maps link). |
| 179 | var addr = v.place_address || ''; |
| 180 | if (addr) { |
| 181 | this.ui.selectedAddr.text(addr).attr('title', addr).prop('hidden', false); |
| 182 | this.ui.selectedId.text('').prop('hidden', true); |
| 183 | } else { |
| 184 | this.ui.selectedAddr.text('').prop('hidden', true); |
| 185 | this.ui.selectedId.text(v.place_id).attr('title', v.place_id).prop('hidden', false); |
| 186 | } |
| 187 | this.ui.search.hide(); |
| 188 | this.hideResults(); |
| 189 | this.ui.lists.hide(); |
| 190 | this.setStatus(''); |
| 191 | } else { |
| 192 | this.ui.selected.attr('data-state', 'empty'); |
| 193 | this.ui.search.show(); |
| 194 | this.renderLists(); |
| 195 | } |
| 196 | }, |
| 197 | |
| 198 | // --- Recent / saved lists ---------------------------------- |
| 199 | |
| 200 | loadLists: function (cb) { |
| 201 | var self = this; |
| 202 | $.ajax({ |
| 203 | url: REST + '/places', |
| 204 | method: 'GET', |
| 205 | beforeSend: function (xhr) { xhr.setRequestHeader('X-WP-Nonce', NONCE); }, |
| 206 | }).done(function (res) { |
| 207 | self._lists = res || { recent: [], saved: [] }; |
| 208 | if (typeof cb === 'function') { cb(); return; } |
| 209 | if (!self.getValueOrDefault().place_id) self.renderLists(); |
| 210 | }); |
| 211 | }, |
| 212 | |
| 213 | renderLists: function () { |
| 214 | if (!this._lists) { this.ui.lists.hide(); return; } |
| 215 | var savedIds = (this._lists.saved || []).reduce(function (acc, p) { acc[p.place_id] = true; return acc; }, {}); |
| 216 | // Free/Pro: SAME treatment as the SEARCH results. When a free user |
| 217 | // is at the 1-place cap, every place EXCEPT the ONE they're allowed |
| 218 | // is LOCKED → pin + "Upgrade to add" (click → upgrade). A free user |
| 219 | // must ALWAYS be able to use at least 1 place: the allowed one is |
| 220 | // the widget's current selection, or — if nothing is selected yet — |
| 221 | // the FIRST saved place. Pro = all pickable. |
| 222 | var limit = this.placeLimitReached(); |
| 223 | var current = this.getValueOrDefault().place_id; |
| 224 | var firstSavedId = ((this._lists && this._lists.saved) || [])[0]; |
| 225 | firstSavedId = firstSavedId ? firstSavedId.place_id : ''; |
| 226 | var allowedId = current || firstSavedId; |
| 227 | // Rows are PICK-ONLY — clicking selects the place. Removing a saved |
| 228 | // place only happens from the settings page, never here. |
| 229 | var renderRow = function (p, kind) { |
| 230 | var locked = limit && p.place_id !== allowedId; |
| 231 | var badge = locked |
| 232 | ? '<em class="ep-gr-suggestion-pro">' + CROWN + ' ' + escapeHtml(t('upgrade', 'Upgrade to add')) + '</em>' |
| 233 | : ''; |
| 234 | return '<li>' + |
| 235 | '<button type="button" class="ep-gr-picker__list-pick' + (locked ? ' is-locked' : '') + '"' + |
| 236 | ' data-id="' + escapeHtml(p.place_id) + '"' + |
| 237 | ' data-name="' + escapeHtml(p.place_name || '') + '"' + |
| 238 | ' data-address="' + escapeHtml(p.address || '') + '"' + |
| 239 | ' data-locked="' + (locked ? '1' : '0') + '"' + |
| 240 | (locked ? ' title="' + escapeHtml(t('limitTitle', 'Showing more than one place needs EmbedPress Pro — click to upgrade.')) + '"' : '') + '>' + |
| 241 | PIN + |
| 242 | '<span class="ep-gr-picker__list-pick-body ep-gr-suggestion-body">' + |
| 243 | '<strong>' + escapeHtml(p.place_name || p.place_id) + '</strong>' + |
| 244 | // Location description (address) — saved with the place. |
| 245 | (p.address ? '<span>' + escapeHtml(p.address) + '</span>' : '') + |
| 246 | badge + |
| 247 | '</span>' + |
| 248 | '</button>' + |
| 249 | '</li>'; |
| 250 | }; |
| 251 | var saved = (this._lists.saved || []).map(function (p) { return renderRow(p, 'saved'); }).join(''); |
| 252 | var recent = (this._lists.recent || []).filter(function (p) { return !savedIds[p.place_id]; }) |
| 253 | .map(function (p) { return renderRow(p, 'recent'); }).join(''); |
| 254 | this.ui.listSaved.html(saved); |
| 255 | this.ui.listRecent.html(recent); |
| 256 | this.ui.listSavedBox.prop('hidden', !saved); |
| 257 | this.ui.listRecentBox.prop('hidden', !recent); |
| 258 | // Upsell note below the lists when at the free cap (mirrors search). |
| 259 | var note = this.ui.lists.find('.ep-gr-picker__lists-note'); |
| 260 | if (limit) { |
| 261 | var noteHtml = escapeHtml(t('limitLead', 'You’ve added your 1 free place.')) + ' ' + |
| 262 | '<a href="' + escapeHtml(UPGRADE_URL) + '" target="_blank" rel="noopener noreferrer">' + |
| 263 | escapeHtml(t('goPro', 'Go Pro')) + '</a> ' + |
| 264 | escapeHtml(t('limitTail', 'to show reviews from multiple businesses at once.')); |
| 265 | if (note.length) { note.html(noteHtml); } |
| 266 | else { this.ui.lists.append('<p class="ep-gr-picker__lists-note ep-gr-inline-note ep-gr-inline-note--muted">' + noteHtml + '</p>'); } |
| 267 | } else if (note.length) { |
| 268 | note.remove(); |
| 269 | } |
| 270 | var empty = !saved && !recent; |
| 271 | // Toggle BOTH the hidden prop and inline display so this plays |
| 272 | // nicely with the .hide()/.show() calls elsewhere. |
| 273 | this.ui.lists.prop('hidden', empty); |
| 274 | if (empty) { this.ui.lists.hide(); } else { this.ui.lists.show(); } |
| 275 | }, |
| 276 | |
| 277 | // Show the saved/recent lists, loading them first if they haven't been |
| 278 | // fetched yet (e.g. focusing the input before onReady's load lands). |
| 279 | showLists: function () { |
| 280 | if (this._lists) { this.renderLists(); return; } |
| 281 | var self = this; |
| 282 | this.loadLists(function () { self.renderLists(); }); |
| 283 | }, |
| 284 | |
| 285 | postPlaces: function (action, place_id, place_name, place_address) { |
| 286 | var self = this; |
| 287 | $.ajax({ |
| 288 | url: REST + '/places', |
| 289 | method: 'POST', |
| 290 | data: { action: action, place_id: place_id, place_name: place_name || '', place_address: place_address || '' }, |
| 291 | beforeSend: function (xhr) { xhr.setRequestHeader('X-WP-Nonce', NONCE); }, |
| 292 | }).done(function (res) { |
| 293 | self._lists = res || { recent: [], saved: [] }; |
| 294 | if (!self.getValueOrDefault().place_id) self.renderLists(); |
| 295 | }); |
| 296 | }, |
| 297 | |
| 298 | commitPick: function (place_id, place_name, place_address) { |
| 299 | this.writeValue({ place_id: place_id, place_name: place_name, place_address: place_address || '' }); |
| 300 | this.postPlaces('recent', place_id, place_name, place_address); |
| 301 | this.ui.input.val(''); |
| 302 | // writeValue() routes through $e.run('document/elements/settings'), |
| 303 | // which re-renders this control view asynchronously — a synchronous |
| 304 | // renderSelected() here would update DOM that the re-render then |
| 305 | // throws away (the picker would snap back to the empty search |
| 306 | // state, so the chosen place "doesn't show"). Render now for the |
| 307 | // case where no re-render fires, AND again on the next frame to win |
| 308 | // against the re-render. |
| 309 | this.renderSelected(); |
| 310 | var self = this; |
| 311 | window.requestAnimationFrame(function () { self.renderSelected(); }); |
| 312 | window.setTimeout(function () { self.renderSelected(); }, 60); |
| 313 | }, |
| 314 | |
| 315 | onListPick: function (e) { |
| 316 | e.preventDefault(); |
| 317 | var $b = $(e.currentTarget); |
| 318 | // Locked (free user at the 1-place cap, a different place) → send |
| 319 | // to the upgrade page instead of selecting. Mirrors search results. |
| 320 | if ($b.attr('data-locked') === '1') { |
| 321 | window.open(UPGRADE_URL, '_blank', 'noopener'); |
| 322 | return; |
| 323 | } |
| 324 | this.commitPick($b.attr('data-id') || '', $b.attr('data-name') || '', $b.attr('data-address') || ''); |
| 325 | }, |
| 326 | |
| 327 | // --- Manual Place ID entry -------------------------------- |
| 328 | |
| 329 | onToggleManual: function (e) { |
| 330 | e.preventDefault(); |
| 331 | var hidden = this.ui.manualRow.prop('hidden'); |
| 332 | this.ui.manualRow.prop('hidden', !hidden); |
| 333 | this.ui.manualToggle.attr('aria-expanded', String(hidden)); |
| 334 | if (hidden) this.ui.manualInput.focus(); |
| 335 | }, |
| 336 | |
| 337 | onApplyManual: function (e) { |
| 338 | e.preventDefault(); |
| 339 | this.applyManual(); |
| 340 | }, |
| 341 | |
| 342 | onManualKey: function (e) { |
| 343 | if (e.key === 'Enter') { |
| 344 | e.preventDefault(); |
| 345 | this.applyManual(); |
| 346 | } |
| 347 | }, |
| 348 | |
| 349 | applyManual: function () { |
| 350 | var raw = (this.ui.manualInput.val() || '').trim(); |
| 351 | // Accept either a bare place_id or a Places API (New) resource name like "places/ChIJ..." |
| 352 | var m = raw.match(/^(?:places\/)?([A-Za-z0-9_\-]+)$/); |
| 353 | if (!m) { |
| 354 | this.setStatus(t('badId', 'That doesn’t look like a valid Place ID.'), 'error'); |
| 355 | return; |
| 356 | } |
| 357 | this.commitPick(m[1], ''); |
| 358 | this.ui.manualInput.val(''); |
| 359 | }, |
| 360 | |
| 361 | setStatus: function (text, tone) { |
| 362 | this.ui.status.text(text || '').attr('data-tone', tone || ''); |
| 363 | }, |
| 364 | |
| 365 | setBusy: function (busy) { |
| 366 | this.ui.spinner.prop('hidden', !busy); |
| 367 | }, |
| 368 | |
| 369 | // Focusing the empty search input reveals the saved + recent places |
| 370 | // below it, so the user can re-pick a place with one click instead of |
| 371 | // retyping. Hidden again as soon as they start typing a query (the |
| 372 | // search results take over). |
| 373 | onInputFocus: function () { |
| 374 | var q = (this.ui.input.val() || '').trim(); |
| 375 | if (q.length < 2) { |
| 376 | this.showLists(); |
| 377 | } |
| 378 | }, |
| 379 | |
| 380 | onInput: function (e) { |
| 381 | var self = this; |
| 382 | var q = (e.target.value || '').trim(); |
| 383 | clearTimeout(this._debounce); |
| 384 | if (q.length < 2) { |
| 385 | this.hideResults(); |
| 386 | this.setStatus(''); |
| 387 | this.setBusy(false); |
| 388 | this.showLists(); // show saved/recent again when cleared |
| 389 | return; |
| 390 | } |
| 391 | this.ui.lists.hide(); // typing a query → results replace lists |
| 392 | this.setBusy(true); |
| 393 | this.setStatus(''); |
| 394 | this._debounce = setTimeout(function () { self.runSearch(q); }, 300); |
| 395 | }, |
| 396 | |
| 397 | onInputKey: function (e) { |
| 398 | if (e.key === 'ArrowDown') { |
| 399 | e.preventDefault(); |
| 400 | var first = this.ui.results.find('li').first(); |
| 401 | if (first.length) first.focus(); |
| 402 | } |
| 403 | }, |
| 404 | |
| 405 | onResultKey: function (e) { |
| 406 | if (e.key === 'Enter' || e.key === ' ') { |
| 407 | e.preventDefault(); |
| 408 | this.onPick(e); |
| 409 | } else if (e.key === 'ArrowDown') { |
| 410 | e.preventDefault(); |
| 411 | var n = $(e.currentTarget).next('li'); |
| 412 | if (n.length) n.focus(); |
| 413 | } else if (e.key === 'ArrowUp') { |
| 414 | e.preventDefault(); |
| 415 | var p = $(e.currentTarget).prev('li'); |
| 416 | if (p.length) p.focus(); else this.ui.input.focus(); |
| 417 | } |
| 418 | }, |
| 419 | |
| 420 | runSearch: function (q) { |
| 421 | var self = this; |
| 422 | // Sequence each request so out-of-order responses can't clobber a |
| 423 | // newer one. Typing "riber road" fires several debounced searches; |
| 424 | // an early uncached query can resolve (or error) AFTER a later one. |
| 425 | // Only the response from the latest-issued query is allowed to |
| 426 | // touch the UI — this is what caused a stale Apify error to flash |
| 427 | // even though the final result came back fine. |
| 428 | var seq = (this._searchSeq = (this._searchSeq || 0) + 1); |
| 429 | var stale = function () { return seq !== self._searchSeq; }; |
| 430 | $.ajax({ |
| 431 | url: REST + '/search?q=' + encodeURIComponent(q), |
| 432 | method: 'GET', |
| 433 | beforeSend: function (xhr) { xhr.setRequestHeader('X-WP-Nonce', NONCE); }, |
| 434 | }).done(function (res) { |
| 435 | if (stale()) { return; } |
| 436 | self.setBusy(false); |
| 437 | var list = (res && res.predictions) || []; |
| 438 | if (!list.length) { |
| 439 | self.hideResults(); |
| 440 | self.setStatus(t('noResults', 'No matches. Try a different spelling or include the city.'), 'muted'); |
| 441 | return; |
| 442 | } |
| 443 | // Unified flow (matches settings + Gutenberg pickers): each row |
| 444 | // shows "+ Select" / "already added" / "Upgrade to add", and an |
| 445 | // upsell note sits BELOW the results when at the free 1-place cap. |
| 446 | var savedIds = (self._lists && self._lists.saved || []).reduce(function (a, p) { a[p.place_id] = true; return a; }, {}); |
| 447 | var limit = self.placeLimitReached(); |
| 448 | var html = list.map(function (p) { |
| 449 | var already = !!savedIds[p.place_id]; |
| 450 | var locked = limit && !already; |
| 451 | var badge = already |
| 452 | ? '<em class="ep-gr-already">' + escapeHtml(t('added', 'already added')) + '</em>' |
| 453 | : locked |
| 454 | ? '<em class="ep-gr-suggestion-pro">' + CROWN + ' ' + escapeHtml(t('upgrade', 'Upgrade to add')) + '</em>' |
| 455 | : '<em class="ep-gr-suggestion-add">+ ' + escapeHtml(t('select', 'Select')) + '</em>'; |
| 456 | return '<li role="option" tabindex="0"' + |
| 457 | ' class="ep-gr-suggestion' + (locked ? ' is-locked' : '') + '"' + |
| 458 | ' data-locked="' + (locked ? '1' : '') + '"' + |
| 459 | ' data-id="' + escapeHtml(p.place_id) + '"' + |
| 460 | ' data-address="' + escapeHtml(p.secondary_text || '') + '"' + |
| 461 | ' data-name="' + escapeHtml(p.main_text || p.description || '') + '">' + |
| 462 | PIN + |
| 463 | '<span class="ep-gr-suggestion-body">' + |
| 464 | '<strong>' + escapeHtml(p.main_text || p.description) + '</strong>' + |
| 465 | (p.secondary_text ? '<span>' + escapeHtml(p.secondary_text) + '</span>' : '') + |
| 466 | metaHtml(p.rating, p.review_count) + |
| 467 | badge + |
| 468 | '</span>' + |
| 469 | '</li>'; |
| 470 | }).join(''); |
| 471 | self.ui.results.html(html); |
| 472 | if (limit) { |
| 473 | // "You’ve added your 1 free place. <a>Go Pro</a> to show |
| 474 | // reviews from multiple businesses at once." |
| 475 | self.ui.resultsNote.html( |
| 476 | escapeHtml(t('limitLead', 'You’ve added your 1 free place.')) + ' ' + |
| 477 | '<a href="' + UPGRADE_URL + '" target="_blank" rel="noopener noreferrer">' + |
| 478 | escapeHtml(t('goPro', 'Go Pro')) + '</a> ' + |
| 479 | escapeHtml(t('limitTail', 'to show reviews from multiple businesses at once.')) |
| 480 | ).prop('hidden', false); |
| 481 | } else { |
| 482 | self.ui.resultsNote.empty().prop('hidden', true); |
| 483 | } |
| 484 | self.ui.resultsPanel.prop('hidden', false).show(); |
| 485 | self.setStatus(''); |
| 486 | }).fail(function (xhr) { |
| 487 | if (stale()) { return; } |
| 488 | self.setBusy(false); |
| 489 | self.hideResults(); |
| 490 | var body = xhr.responseJSON || {}; |
| 491 | var msg = body.message || t('failed', 'Search failed.'); |
| 492 | if (/missing|api[_ ]?key|not configured/i.test(msg)) { |
| 493 | msg += ' ' + t('addKey', 'Add your Google Places API key in EmbedPress → Google Reviews.'); |
| 494 | } |
| 495 | self.setStatus(msg, 'error'); |
| 496 | }); |
| 497 | }, |
| 498 | |
| 499 | onPick: function (e) { |
| 500 | var $li = $(e.currentTarget); |
| 501 | // Locked row (free user at the 1-place cap, net-new place) → send to |
| 502 | // upgrade instead of selecting, mirroring the settings/block pickers. |
| 503 | if ($li.attr('data-locked') === '1') { |
| 504 | window.open(UPGRADE_URL, '_blank', 'noopener'); |
| 505 | return; |
| 506 | } |
| 507 | this.commitPick($li.attr('data-id') || '', $li.attr('data-name') || '', $li.attr('data-address') || ''); |
| 508 | }, |
| 509 | |
| 510 | // A free user is at the 1-place cap once the global library has ≥1 |
| 511 | // saved place. Pro lifts it. Same rule as the settings + block pickers. |
| 512 | placeLimitReached: function () { |
| 513 | if (PRO_ACTIVE) { return false; } |
| 514 | var saved = (this._lists && this._lists.saved) || []; |
| 515 | return saved.length >= 1; |
| 516 | }, |
| 517 | |
| 518 | // Hide + clear the whole results panel (list + note). |
| 519 | hideResults: function () { |
| 520 | this.ui.results.empty(); |
| 521 | this.ui.resultsNote.empty().prop('hidden', true); |
| 522 | this.ui.resultsPanel.prop('hidden', true).hide(); |
| 523 | }, |
| 524 | |
| 525 | onClear: function () { |
| 526 | this.writeValue({ place_id: '', place_name: '', place_address: '' }); |
| 527 | this.renderSelected(); |
| 528 | var self = this; |
| 529 | setTimeout(function () { self.ui.input.focus(); }, 0); |
| 530 | }, |
| 531 | }); |
| 532 | |
| 533 | elementor.addControlView( |
| 534 | 'ep_gr_place_picker', |
| 535 | PlacePickerView |
| 536 | ); |
| 537 | }); |
| 538 | })(jQuery); |
| 539 |