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 / ep-gr-elementor-control.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
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
56 .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
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