PageSourceSearch

https://monkeypuzzledaynurseries.com/wp-content/plugins/mp-plugins/assets/finder/js/finder.js?ver=1.14.1

js monkeypuzzledaynurseries.com collected 2026-10-09 02:48:10 UTC 19,218 bytes, 340 lines download raw bytes

1/* MP Suite — Nursery Finder engine. Card-based, no map.
2   Home page: set data-redirect so a search sends the visitor to the finder page with the
3   query, where results render inline. Status shows as a floating tip, never shifting layout. */
4(function () {
5  'use strict';
6
7  var cfg = window.mpSuiteFinder || {};
8  var root = document.getElementById('mp-finder');
9  if (!root) { return; }
10
11  var redirectUrl = root.getAttribute('data-redirect') || '';
12  var elFeatured = document.getElementById('mp-nf-featured');
13  var elOthers = document.getElementById('mp-nf-others');
14  var elOthersTitle = document.getElementById('mp-nf-others-head');
15  var elAllHead = document.getElementById('mp-nf-all-head');
16  var elTip = document.getElementById('mp-nf-status');
17  var elEnquiry = document.getElementById('mp-nf-enquiry');
18  var elEnqEyebrow = document.getElementById('mp-nf-enq-eyebrow');
19  var form = document.getElementById('mp-nf-search');
20  var dataLayer = (window.dataLayer = window.dataLayer || []);
21  var jq = window.jQuery || null;
22  if (jq && cfg.enquiryFormId) {
23    jq(document).on('gform_confirmation_loaded', function (e, formId) {
24      if (parseInt(formId, 10) === parseInt(cfg.enquiryFormId, 10)) {
25        dataLayer.push({ event: 'gf_find_nursery_submission', nursery_id: state.featuredId || null });
26      }
27    });
28  }
29  var tipTimer = null;
30
31  // searched: a real search has run, so the landing list must not overwrite its results.
32  var state = { results: [], featuredId: null, nearestId: null, searched: false, search: { start: '', dob: '', postcode: '' } };
33
34  var fpStart = null, fpDob = null;
35  if (window.flatpickr) {
36    var common = { dateFormat: 'd/m/Y', disableMobile: true, allowInput: true };
37    fpStart = window.flatpickr($('mp-nf-start'), Object.assign({ minDate: 'today' }, common));
38    // DOB allows up to a year ahead: parents enquire before the baby is born.
39    fpDob = window.flatpickr($('mp-nf-dob'), Object.assign({ maxDate: new Date().fp_incr(365) }, common));
40  }
41
42  function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
43    return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]; }); }
44  function $(id) { return document.getElementById(id); }
45  function miles(d) { return (Math.round(parseFloat(d) * 10) / 10) + ' mi'; }
46  // "X miles away", matching the quiz results cards.
47  function milesAway(d) { var m = Math.round((parseFloat(d) || 0) * 10) / 10; return m + (m === 1 ? ' mile away' : ' miles away'); }
48  // The bare number for the CRM ("2.3", not "2.3 miles away"), so the Monday column can be a
49  // Numbers one and the board can sort and filter on it. Returned as a string so a legitimate
50  // 0 survives the truthiness guard in setEnqField.
51  function distValue(d) { return (d == null || d === '') ? '' : String(Math.round((parseFloat(d) || 0) * 10) / 10); }
52  function byId(id) { return state.results.filter(function (n) { return String(n.id) === String(id); })[0] || null; }
53  function nameHtml(t) { return String(t == null ? '' : t).split(/<br\s*\/?>/i).map(function (s) { return esc(s.trim()); }).filter(Boolean).join('<br>'); }
54  function nudge(el) { if (!el) { return; } var r = el.getBoundingClientRect(); var vh = window.innerHeight || document.documentElement.clientHeight; if (r.top < 0 || r.top > vh) { el.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }
55  // Scroll so the top of the featured card sits just below the site's sticky header.
56  var HEADER_OFFSET = 110;
57  function scrollToFeatured() {
58    var r = elFeatured.getBoundingClientRect();
59    var y = r.top + (window.pageYOffset || document.documentElement.scrollTop || 0) - HEADER_OFFSET;
60    window.scrollTo({ top: Math.max(0, y), behavior: 'smooth' });
61  }
62
63  function tip(msg, sticky) {
64    if (tipTimer) { clearTimeout(tipTimer); tipTimer = null; }
65    if (!msg) { elTip.hidden = true; elTip.textContent = ''; return; }
66    elTip.textContent = msg; elTip.hidden = false;
67    if (!sticky) { tipTimer = setTimeout(function () { elTip.hidden = true; }, 4000); }
68  }
69
70  function buildRedirect() {
71    var p = [];
72    if (state.search.postcode) { p.push('postcode=' + encodeURIComponent(state.search.postcode)); }
73    if (state.search.start) { p.push('start=' + encodeURIComponent(state.search.start)); }
74    if (state.search.dob) { p.push('dob=' + encodeURIComponent(state.search.dob)); }
75    return redirectUrl + (redirectUrl.indexOf('?') > -1 ? '&' : '?') + p.join('&');
76  }
77
78  /* ---------- search ---------- */
79  function lookup(params) {
80    var url = new URL(cfg.restUrl, window.location.origin);
81    if (params.lat != null) { url.searchParams.set('lat', params.lat); url.searchParams.set('lng', params.lng); }
82    else { url.searchParams.set('postcode', params.postcode); }
83    url.searchParams.set('radius', cfg.radius || 10);
84    url.searchParams.set('limit', cfg.limit || 6);
85    state.searched = true;
86    tip('Searching\u2026', true);
87    fetch(url.toString(), { headers: { 'Accept': 'application/json' } })
88      .then(function (r) { return r.json(); })
89      .then(function (b) {
90        if (b && b.error === 'location_unresolved') { tip('We couldn\u2019t find that location. Try a postcode or a nearby town.'); clearResults(); return; }
91        state.results = (b && b.results) || [];
92        dataLayer.push({ event: 'nursery_search', postcode: state.search.postcode, results: state.results.length, in_range: !!(b && b.in_range) });
93        if (!state.results.length) { tip(cfg.noResults || 'No nurseries found near there yet.'); clearResults(); return; }
94        state.nearestId = state.results[0].id;
95        state.featuredId = state.results[0].id;
96        tip((b && b.in_range) ? '' : 'Nothing within range, here\u2019s your closest nursery.');
97        render();
98      })
99      .catch(function (e) { console.error('[mp-suite] finder', e); tip('Something went wrong with the search.'); });
100  }
101
102  function clearResults() {
103    elFeatured.innerHTML = ''; elOthers.innerHTML = '';
104    elOthersTitle.hidden = true; elAllHead.hidden = true;
105  }
106
107  // Landing state: every nursery A-Z in the same 3-column cards a search renders, minus the
108  // distances. Skipped if a search has already run (a fast typist beats the fetch).
109  function loadAll() {
110    if (!cfg.listUrl) { return; }
111    fetch(cfg.listUrl, { headers: { 'Accept': 'application/json' } })
112      .then(function (r) { return r.json(); })
113      .then(function (b) {
114        if (state.searched) { return; }
115        state.results = (b && b.results) || [];
116        state.featuredId = null;
117        state.nearestId = null;
118        render();
119      })
120      .catch(function (e) { console.error('[mp-suite] finder list', e); });
121  }
122
123  /* ---------- render ---------- */
124  var ICON_PIN = '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21s7-6.3 7-11a7 7 0 1 0-14 0c0 4.7 7 11 7 11Z"/><circle cx="12" cy="10" r="2.5"/></svg>';
125  var ICON_PHONE = '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2Z"/></svg>';
126  var ICON_CLOCK = '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>';
127  var ICON_ARROW = '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg>';
128
129  // Slides plus arrows and dots for a nursery's gallery; shared by the featured and list cards.
130  function slidesHtml(n, imgs) {
131    var label = plainName(n.title) || 'Nursery';
132    var slides = imgs.map(function (u, i) { return '<div class="mp-nf__slide" role="img" aria-label="' + esc(label) + ' photo ' + (i + 1) + '" style="background-image:url(\'' + encodeURI(u) + '\')"></div>'; }).join('');
133    var dots = imgs.length > 1 ? '<div class="mp-nf__dots">' + imgs.map(function (_, i) { return '<button type="button" class="mp-nf__dot' + (i === 0 ? ' is-on' : '') + '" data-slide="' + i + '" aria-label="Image ' + (i + 1) + '"></button>'; }).join('') + '</div>' : '';
134    var arrows = imgs.length > 1 ? '<button type="button" class="mp-nf__sl-arrow mp-nf__sl-prev" data-slide-dir="-1" aria-label="Previous">\u2039</button><button type="button" class="mp-nf__sl-arrow mp-nf__sl-next" data-slide-dir="1" aria-label="Next">\u203a</button>' : '';
135    return '<div class="mp-nf__slides">' + slides + '</div>' + arrows + dots;
136  }
137
138  function illus(n) {
139    var imgs = (n.images && n.images.length) ? n.images : [];
140    if (!imgs.length) {
141      if (cfg.placeholder) {
142        return '<div class="mp-nf__card-illus" style="background-image:url(\'' + encodeURI(cfg.placeholder) + '\')">{BADGE}</div>';
143      }
144      return '<div class="mp-nf__card-illus">{BADGE}</div>';
145    }
146    return '<div class="mp-nf__card-illus mp-nf__slider" data-index="0">' + slidesHtml(n, imgs) + '{BADGE}</div>';
147  }
148
149  // List-card image: the same slider at list height, with the distance badge (if any) on top.
150  function listIllus(n, badge) {
151    var imgs = (n.images && n.images.length) ? n.images : [];
152    if (!imgs.length) {
153      if (cfg.placeholder) {
154        return '<div class="mp-nf__list-illus" style="background-image:url(\'' + encodeURI(cfg.placeholder) + '\')">' + badge + '</div>';
155      }
156      return '<div class="mp-nf__list-illus mp-nf__list-illus--ph">' + badge + '</div>';
157    }
158    return '<div class="mp-nf__list-illus mp-nf__slider mp-nf__slider--list" data-index="0">' + slidesHtml(n, imgs) + badge + '</div>';
159  }
160
161  function initSliders(scope) {
162    scope.querySelectorAll('.mp-nf__slider').forEach(function (slider) {
163      var track = slider.querySelector('.mp-nf__slides');
164      var count = track ? track.children.length : 0;
165      if (count < 2) { return; }
166      function go(i) {
167        var idx = ((i % count) + count) % count;
168        slider.setAttribute('data-index', idx);
169        track.style.transform = 'translateX(' + (-idx * 100) + '%)';
170        var dots = slider.querySelectorAll('.mp-nf__dot');
171        dots.forEach(function (d, di) { d.classList.toggle('is-on', di === idx); });
172      }
173      slider.addEventListener('click', function (e) {
174        var dir = e.target.closest && e.target.closest('[data-slide-dir]');
175        var dot = e.target.closest && e.target.closest('[data-slide]');
176        if (dir) { e.stopPropagation(); go(parseInt(slider.getAttribute('data-index'), 10) + parseInt(dir.getAttribute('data-slide-dir'), 10)); }
177        else if (dot) { e.stopPropagation(); go(parseInt(dot.getAttribute('data-slide'), 10)); }
178      });
179    });
180  }
181
182  function listCard(n) {
183    var badge = n.distance != null ? '<span class="mp-nf__list-dist">' + esc(milesAway(n.distance)) + '</span>' : '';
184    var addr = [n.address, n.postcode].filter(Boolean).join(', ');
185    return '<div class="mp-nf__list-card mp-card">' +
186      listIllus(n, badge) +
187      '<div class="mp-nf__list-body">' +
188        '<div class="mp-nf__list-head">' +
189          '<h3 class="mp-nf__name">' + nameHtml(n.title) + '</h3>' +
190          (n.review ? '<span class="mp-nf__score">' + esc(n.review) + '</span>' : '') +
191        '</div>' +
192        (addr ? '<p class="mp-nf__meta"><span class="mp-nf__meta-ic">' + ICON_PIN + '</span>' + esc(addr) + '</p>' : '') +
193        '<div class="mp-nf__list-actions">' +
194          '<button type="button" class="mp-nf__btn mp-nf__btn--primary" data-enquire-id="' + esc(n.id) + '">Enquire now</button>' +
195          '<a class="mp-nf__btn mp-nf__btn--ghost" href="' + nurseryHref(n) + '"' + nurseryTarget(n) + '>View nursery</a>' +
196        '</div>' +
197      '</div>' +
198    '</div>';
199  }
200
201  function render() {
202    // Search results carry a distance; the landing list (every nursery, A-Z) does not, so it
203    // gets no distance badges, no "nearest" eyebrow, no featured card until one is chosen,
204    // and its own heading.
205    var located = state.results.length > 0 && state.results[0].distance != null;
206    var featured = byId(state.featuredId) || (located ? state.results[0] : null);
207
208    if (featured) {
209      var badge = featured.distance != null ? '<span class="mp-nf__badge">' + esc(milesAway(featured.distance)) + '</span>' : '';
210      var addr = [featured.address, featured.postcode].filter(Boolean).join(', ');
211      elFeatured.innerHTML =
212        '<div class="mp-nf__card mp-card">' +
213          illus(featured).replace('{BADGE}', badge) +
214          '<div class="mp-nf__card-body">' +
215            (featured.review ? '<div class="mp-nf__score-wrap"><span class="mp-nf__score">' + esc(featured.review) + '</span><div class="mp-nf__score-label">review score</div></div>' : '') +
216            (located ? '<p class="mp-nf__eyebrow">Your nearest Monkey Puzzle</p>' : '') +
217            '<h3 class="mp-nf__name">' + nameHtml(featured.title) + '</h3>' +
218            (featured.desc ? '<p class="mp-nf__desc">' + esc(featured.desc) + '</p>' : '') +
219            (addr ? '<p class="mp-nf__meta"><span class="mp-nf__meta-ic">' + ICON_PIN + '</span>' + esc(addr) + '</p>' : '') +
220            (featured.hours ? '<p class="mp-nf__meta"><span class="mp-nf__meta-ic">' + ICON_CLOCK + '</span>' + esc(featured.hours) + '</p>' : '') +
221            (featured.phone ? '<p class="mp-nf__meta"><span class="mp-nf__meta-ic">' + ICON_PHONE + '</span><a href="tel:' + esc(featured.phone.replace(/\s+/g, '')) + '">' + esc(featured.phone) + '</a></p>' : '') +
222            '<div class="mp-nf__actions">' +
223              '<button type="button" class="mp-nf__btn mp-nf__btn--primary" data-enquire>ENQUIRE NOW</button>' +
224              '<a class="mp-nf__btn mp-nf__btn--ghost" href="' + nurseryHref(featured) + '"' + nurseryTarget(featured) + '>VIEW NURSERY</a>' +
225            '</div>' +
226          '</div>' +
227        '</div>';
228      initSliders(elFeatured);
229    } else {
230      elFeatured.innerHTML = '';
231    }
232
233    var others = featured ? state.results.filter(function (n) { return String(n.id) !== String(featured.id); }) : state.results;
234    elOthersTitle.hidden = !located || others.length === 0;
235    elAllHead.hidden = located || others.length === 0;
236    elOthers.innerHTML = others.map(listCard).join('');
237    initSliders(elOthers);
238
239    elEnquiry.hidden = true;
240  }
241
242  /* ---------- inline enquiry ---------- */
243  function setEnqField(cls, value, tint) {
244    if (!value) { return; }
245    var wrap = elEnquiry.querySelector('.' + cls);
246    if (!wrap) { return; }
247    var input = wrap.querySelector('input, select, textarea');
248    if (input) { input.value = value; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); }
249    if (tint) { wrap.classList.add('mp-prefilled'); }
250  }
251  function plainName(t) { return String(t == null ? '' : t).replace(/<br\s*\/?>/ig, ' ').replace(/\s+/g, ' ').trim(); }
252  // "View nursery" links to the nursery's own website (ACF), in a new tab; falls back to the
253  // CPT permalink when no website is set.
254  function nurseryHref(n) { return esc((n && n.website) || (n && n.permalink) || '#'); }
255  function nurseryTarget(n) { return (n && n.website) ? ' target="_blank" rel="noopener"' : ''; }
256
257  function openEnquiry() {
258    var featured = byId(state.featuredId);
259    if (!featured) { return; }
260    elEnqEyebrow.textContent = 'ENQUIRY \u00b7 ' + plainName(featured.title).toUpperCase();
261    setEnqField('nursery_id', featured.id, false);
262    setEnqField('mp-nursery-name', plainName(featured.title), false);
263    setEnqField('mp-distance', distValue(featured.distance), false);
264    setEnqField('mp-start-date', state.search.start, true);
265    setEnqField('mp-child-dob', state.search.dob, true);
266    setEnqField('mp-postcode', state.search.postcode, true);
267    elEnquiry.hidden = false;
268    nudge(elEnquiry);
269  }
270  function viewNursery(id) {
271    state.featuredId = id;
272    dataLayer.push({ event: 'nursery_result_click', nursery_id: id });
273    render();
274    nudge(elFeatured);
275  }
276
277  /* ---------- events ---------- */
278  function readPills() {
279    state.search = { start: $('mp-nf-start').value.trim(), dob: $('mp-nf-dob').value.trim(), postcode: $('mp-nf-postcode').value.trim() };
280  }
281
282  form.addEventListener('submit', function (e) {
283    e.preventDefault();
284    readPills();
285    if (!state.search.postcode) { tip('Enter a postcode or location to search.'); return; }
286    if (redirectUrl) { window.location.href = buildRedirect(); return; }
287    lookup({ postcode: state.search.postcode });
288  });
289
290  root.addEventListener('click', function (e) {
291    var enq = e.target.closest ? e.target.closest('[data-enquire]') : null;
292    if (enq) { openEnquiry(); return; }
293    // "Enquire now" on a grid card: promote it to the featured (top) card, re-render so it
294    // moves up and out of the list, open the enquiry pointed at it, and scroll to the card
295    // itself (not the form beneath it) so the nursery's name and photos are what you see.
296    var enqId = e.target.closest ? e.target.closest('[data-enquire-id]') : null;
297    if (enqId) {
298      state.featuredId = enqId.getAttribute('data-enquire-id');
299      dataLayer.push({ event: 'nursery_result_click', nursery_id: state.featuredId });
300      render();
301      openEnquiry();
302      scrollToFeatured();
303      return;
304    }
305    var view = e.target.closest ? e.target.closest('[data-view]') : null;
306    if (view) { viewNursery(view.getAttribute('data-view')); return; }
307    if (e.target.id === 'mp-nf-enq-close') { elEnquiry.hidden = true; }
308  });
309
310  /* optional Places Autocomplete on the location pill */
311  if (cfg.hasMap && window.mpSuiteMaps) {
312    window.mpSuiteMaps.onReady(function () {
313      try {
314        var pc = $('mp-nf-postcode');
315        var ac = new google.maps.places.Autocomplete(pc, { componentRestrictions: { country: 'gb' }, fields: ['geometry'], types: ['geocode'] });
316        ac.addListener('place_changed', function () {
317          readPills();
318          var p = ac.getPlace();
319          if (redirectUrl) { window.location.href = buildRedirect(); return; }
320          if (p && p.geometry && p.geometry.location) { lookup({ lat: p.geometry.location.lat(), lng: p.geometry.location.lng() }); }
321          else if (state.search.postcode) { lookup({ postcode: state.search.postcode }); }
322        });
323      } catch (e) { /* server geocode fallback on submit */ }
324    });
325  }
326
327  /* arriving with a query (redirected from the home page): prefill and search inline.
328     Arriving without one: show every nursery. */
329  (function fromQuery() {
330    if (redirectUrl) { return; }
331    var q = new URLSearchParams(window.location.search);
332    var pc = q.get('postcode');
333    if (!pc) { loadAll(); return; }
334    $('mp-nf-postcode').value = pc;
335    if (q.get('start')) { fpStart ? fpStart.setDate(q.get('start'), false, 'd/m/Y') : ($('mp-nf-start').value = q.get('start')); }
336    if (q.get('dob')) { fpDob ? fpDob.setDate(q.get('dob'), false, 'd/m/Y') : ($('mp-nf-dob').value = q.get('dob')); }
337    readPills();
338    lookup({ postcode: pc });
339  })();
340})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.