PageSourceSearch

https://growithjane.com/wp-content/themes/generatepress_child/assets/js/blog.js?ver=1.0.1

js growithjane.com collected 2026-10-01 16:30:31 UTC 16,342 bytes, 485 lines download raw bytes

1(function () {
2  const initialSlug = GWJ_BLOG.startTag || '';
3  const initialTagId = (() => {
4    const mapped = initialSlug && GWJ_BLOG.tagMap ? GWJ_BLOG.tagMap[initialSlug] : null;
5    if (mapped) return String(mapped);
6    if (GWJ_BLOG.startTagId) return String(GWJ_BLOG.startTagId);
7    return '';
8  })();
9
10  const S = {
11    state: {
12      tagSlug: initialSlug,
13      tagId: initialTagId,
14      page: 1,
15      search: '',
16      loading: false,
17      done: false,
18      totalPages: null,
19      allowInfinite: true,
20      cache: new Map(), // key: `${tag}|${search}|${page}`
21    },
22    els: {}
23  };
24
25  function qs(s, r=document){return r.querySelector(s);}
26  function qsa(s, r=document){return Array.from(r.querySelectorAll(s));}
27
28  function resolveTagId(slug) {
29    if (!slug || !GWJ_BLOG.tagMap) return '';
30    const id = GWJ_BLOG.tagMap[slug];
31    return id ? String(id) : '';
32  }
33
34  function resolveSlugById(id) {
35    if (!id) return '';
36    if (GWJ_BLOG.tagReverseMap && GWJ_BLOG.tagReverseMap[id]) {
37      return GWJ_BLOG.tagReverseMap[id];
38    }
39    if (GWJ_BLOG.tagMap) {
40      for (const [slug, value] of Object.entries(GWJ_BLOG.tagMap)) {
41        if (String(value) === String(id)) {
42          return slug;
43        }
44      }
45    }
46    return '';
47  }
48
49  function setLoader(active) {
50    if (!S.els.loader) return;
51    S.els.loader.classList.toggle('is-active', active);
52    if (active) {
53      S.els.loader.removeAttribute('hidden');
54    } else {
55      S.els.loader.setAttribute('hidden', '');
56    }
57    S.els.loader.setAttribute('aria-busy', active ? 'true' : 'false');
58  }
59
60  function buildUrl({tagSlug, tagId, page, search}) {
61    const params = new URLSearchParams({
62      per_page: GWJ_BLOG.perPage,
63      page,
64      _embed: '1',
65      order: 'desc',
66      orderby: 'date'
67    });
68    if (GWJ_BLOG.lang) params.set('lang', GWJ_BLOG.lang);
69    if (search) params.set('search', search);
70    if (GWJ_BLOG.category) params.set('categories', GWJ_BLOG.category);
71    const activeTagId = tagId || resolveTagId(tagSlug);
72    if (activeTagId) params.set('tags', activeTagId);
73    return GWJ_BLOG.rest + '?' + params.toString();
74  }
75
76  function stripHtml(html) {
77    if (!html) return '';
78    const tmp = document.createElement('div');
79    tmp.innerHTML = html;
80    return (tmp.textContent || tmp.innerText || '').trim();
81  }
82
83  function truncateWords(text, limit) {
84    const words = text.split(/\s+/).filter(Boolean);
85    if (words.length <= limit) return text;
86    return words.slice(0, limit).join(' ') + '…';
87  }
88
89  function formatDate(dateString) {
90    if (!dateString) return '';
91    const d = new Date(dateString);
92    if (Number.isNaN(d.getTime())) return '';
93    return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
94  }
95
96  function cardHTML(post) {
97    // Featured image (embedded) + author + date
98    const link = post.link;
99    const title = post.title.rendered || '';
100    const titleText = stripHtml(title);
101    const excerpt = truncateWords(stripHtml(post.excerpt?.rendered || ''), 30);
102    const date = formatDate(post.date);
103    const media = post._embedded?.['wp:featuredmedia']?.[0];
104    const src = media?.media_details?.sizes?.medium_large?.source_url || media?.source_url || '';
105    const alt = media?.alt_text || '';
106    const author = post._embedded?.author?.[0]?.name || '';
107    const avatar = post._embedded?.author?.[0]?.avatar_urls?.['48']
108      || post._embedded?.author?.[0]?.avatar_urls?.['96']
109      || '';
110
111    return `
112      <article class="card">
113        <a class="card__thumb" href="${link}" aria-label="${escapeHtml(titleText)}">
114          ${src ? `<img src="${src}" alt="${escapeHtml(alt)}" loading="lazy" decoding="async">` : `<div class="ph"></div>`}
115        </a>
116        <div class="card__body">
117          <div class="card__meta">
118            ${avatar ? `<img class="card__avatar" src="${avatar}" alt="${escapeHtml(author)}" loading="lazy" decoding="async">` : ''}
119            <div class="card__meta-info">
120              <span class="card__author">${escapeHtml(author)}</span>
121              ${date ? `<span class="card__dot" aria-hidden="true">&bull;</span>` : ''}
122              ${date ? `<span class="card__date">${escapeHtml(date)}</span>` : ''}
123            </div>
124          </div>
125          <h3 class="card__title"><a href="${link}">${title}</a></h3>
126          <p class="card__excerpt">${escapeHtml(excerpt)}</p>
127        </div>
128      </article>
129    `;
130  }
131
132  function escapeHtml(s){return (s||'').replace(/[&<>"]/g, c=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}
133
134  function formatString(template, value) {
135    if (!template) return value;
136    return template.replace('%1$s', value).replace('%s', value);
137  }
138
139  function updateActiveChip() {
140    if (!S.els.filterChips) return;
141    S.els.filterChips.forEach(btn=>{
142      const btnSlug = btn.dataset.tag || '';
143      let isActive = false;
144      if (S.state.search) {
145        isActive = false;
146      } else if (S.state.tagSlug) {
147        isActive = btnSlug === S.state.tagSlug;
148      } else {
149        isActive = btnSlug === '';
150      }
151      btn.classList.toggle('is-active', isActive);
152    });
153  }
154
155  function renderEmptyState() {
156    if (!S.els.posts) return;
157    S.els.posts.innerHTML = `<p class="gwj-empty">${escapeHtml(GWJ_BLOG.i18n?.noResults || '')}</p>`;
158  }
159
160  function setSearchButtonLoading(active) {
161    const btn = S.els.searchButton;
162    if (!btn) return;
163    const defaultLabel = btn.dataset.label || btn.textContent.trim();
164    btn.dataset.label = defaultLabel;
165    if (active) {
166      btn.disabled = true;
167      btn.classList.add('is-loading');
168      btn.innerHTML = `<span class="gwj-spinner" aria-hidden="true"></span><span class="screen-reader-text">${escapeHtml(GWJ_BLOG.i18n?.searching || '')}</span>`;
169    } else {
170      btn.disabled = false;
171      btn.classList.remove('is-loading');
172      btn.innerHTML = `<span class="gwj-search-button__label">${escapeHtml(defaultLabel)}</span>`;
173    }
174  }
175
176  async function fetchPage() {
177    if (S.state.totalPages && S.state.page > S.state.totalPages) {
178      S.state.done = true;
179    }
180    if (S.state.loading || S.state.done) return;
181    S.state.loading = true;
182    setLoader(true);
183
184    const cacheTagKey = S.state.tagId || S.state.tagSlug || '';
185    const cacheLangKey = GWJ_BLOG.lang || '';
186    const key = `${cacheLangKey}|${cacheTagKey}|${S.state.search}|${S.state.page}`;
187    if (S.state.cache.has(key)) {
188      const cached = S.state.cache.get(key);
189      const posts = Array.isArray(cached) ? cached : cached.posts;
190      const totalPages = Array.isArray(cached) ? null : cached.totalPages;
191      if (Number.isInteger(totalPages) && !S.state.totalPages) {
192        S.state.totalPages = totalPages;
193      }
194      if (Array.isArray(posts) && posts.length === 0 && S.state.page === 1) {
195        renderEmptyState();
196        S.state.done = true;
197      } else {
198        renderAppend(posts);
199        if (Array.isArray(posts) && posts.length < GWJ_BLOG.perPage) {
200          S.state.done = true;
201        }
202        if (Number.isInteger(totalPages) && S.state.page >= totalPages) {
203          S.state.done = true;
204        }
205      }
206      S.state.loading = false;
207      setLoader(false);
208      return;
209    }
210
211    try {
212      const res = await fetch(buildUrl(S.state), { headers: { 'Accept': 'application/json' }});
213      if (!res.ok) {
214        if (res.status === 400 || res.status === 422) S.state.done = true;
215        S.state.loading = false; return;
216      }
217      const totalPagesHeader = res.headers.get('X-WP-TotalPages');
218      const totalPages = totalPagesHeader ? parseInt(totalPagesHeader, 10) : null;
219      if (Number.isInteger(totalPages)) {
220        S.state.totalPages = totalPages;
221      }
222      const posts = await res.json();
223      S.state.cache.set(key, { posts, totalPages });
224      if (!Array.isArray(posts) || posts.length === 0) {
225        if (S.state.page === 1) renderEmptyState();
226        S.state.done = true;
227      } else {
228        renderAppend(posts);
229        if (posts.length < GWJ_BLOG.perPage) {
230          S.state.done = true;
231        }
232        if (Number.isInteger(totalPages) && S.state.page >= totalPages) {
233          S.state.done = true;
234        }
235      }
236    } catch (e) {
237      console.error(e);
238    } finally {
239      S.state.loading = false;
240      setLoader(false);
241      S.state.allowInfinite = !S.state.done;
242    }
243  }
244
245  function renderAppend(posts) {
246    if (!Array.isArray(posts) || posts.length === 0) return;
247    const html = posts.map(cardHTML).join('');
248    S.els.posts.insertAdjacentHTML('beforeend', html);
249    S.els.posts.dataset.page = String(S.state.page);
250  }
251
252  function clearGrid() {
253    S.els.posts.innerHTML = '';
254    S.state.page = 1;
255    S.state.done = false;
256    S.state.totalPages = null;
257    S.state.allowInfinite = false;
258    setLoader(false);
259  }
260
261  function getTagLabel(slug) {
262    if (!slug) return '';
263    if (GWJ_BLOG.tagLabels && GWJ_BLOG.tagLabels[slug]) {
264      return GWJ_BLOG.tagLabels[slug];
265    }
266    if (S.els.filterChips) {
267      const match = S.els.filterChips.find(btn => (btn.dataset.tag || '') === slug);
268      if (match) {
269        return match.textContent.trim();
270      }
271    }
272    return '';
273  }
274
275  function getTagDescription(slug) {
276    if (!slug || !GWJ_BLOG.tagDescriptions) return '';
277    return GWJ_BLOG.tagDescriptions[slug] || '';
278  }
279
280  function updateTitle() {
281    if (!S.els.title) return;
282    const slug = S.state.tagSlug;
283    const label = getTagLabel(slug);
284    const description = getTagDescription(slug);
285    let titleText = '';
286    let descriptionText = '';
287
288    if (slug) {
289      titleText = label || toTitle(slug);
290      descriptionText = description || '';
291    } else if (S.state.search) {
292      titleText = formatString(GWJ_BLOG.i18n?.resultsFor || 'Results for "%s"', S.state.search);
293    } else {
294      titleText = GWJ_BLOG.i18n?.allGuides || 'All guides';
295    }
296
297    S.els.title.textContent = titleText;
298
299    if (S.els.description) {
300      if (descriptionText) {
301        S.els.description.textContent = descriptionText;
302        S.els.description.hidden = false;
303      } else {
304        S.els.description.textContent = '';
305        S.els.description.hidden = true;
306      }
307    }
308  }
309
310  function toTitle(slug){return slug.replace(/-/g,' ').replace(/\b\w/g, m=>m.toUpperCase());}
311
312  // URL handling (unique URLs per tag/search)
313  function pushUrl() {
314    const url = new URL(window.location.href);
315    url.searchParams.delete('s'); // we use our own
316    if (S.state.tagSlug) {
317      url.searchParams.set('tag', S.state.tagSlug);
318    } else {
319      url.searchParams.delete('tag');
320    }
321    url.searchParams.delete('tag_id'); // legacy param cleanup
322    if (S.state.search) url.searchParams.set('q', S.state.search); else url.searchParams.delete('q');
323    history.pushState(S.state, '', url);
324  }
325
326  function replaceUrlIfNeeded() {
327    const url = new URL(window.location.href);
328    url.searchParams.delete('s');
329    if (S.state.tagSlug) url.searchParams.set('tag', S.state.tagSlug); else url.searchParams.delete('tag');
330    url.searchParams.delete('tag_id');
331    if (S.state.search) url.searchParams.set('q', S.state.search); else url.searchParams.delete('q');
332    history.replaceState(S.state, '', url);
333  }
334
335  function syncFromUrl({allowReplace=false} = {}) {
336    const url = new URL(window.location.href);
337    const tagSlug = url.searchParams.get('tag') || '';
338    const legacyTagId = url.searchParams.get('tag_id') || '';
339    const q = url.searchParams.get('q') || '';
340    S.state.search = q;
341
342    const applyResolvedTag = (slug, id) => {
343      S.state.tagSlug = slug || '';
344      S.state.tagId = id ? String(id) : '';
345    };
346
347    if (tagSlug) {
348      const resolvedId = resolveTagId(tagSlug);
349      if (resolvedId) {
350        applyResolvedTag(tagSlug, resolvedId);
351      } else if (GWJ_BLOG.startTagId) {
352        const localizedSlug = resolveSlugById(String(GWJ_BLOG.startTagId)) || GWJ_BLOG.startTag || '';
353        applyResolvedTag(localizedSlug, GWJ_BLOG.startTagId);
354        if (allowReplace) replaceUrlIfNeeded();
355      } else {
356        applyResolvedTag('', '');
357        if (allowReplace) replaceUrlIfNeeded();
358      }
359    } else if (legacyTagId) {
360      const mappedSlug = resolveSlugById(legacyTagId);
361      if (mappedSlug) {
362        applyResolvedTag(mappedSlug, resolveTagId(mappedSlug));
363        if (allowReplace) replaceUrlIfNeeded();
364      } else if (GWJ_BLOG.startTagId) {
365        const localizedSlug = resolveSlugById(String(GWJ_BLOG.startTagId)) || GWJ_BLOG.startTag || '';
366        applyResolvedTag(localizedSlug, GWJ_BLOG.startTagId);
367        if (allowReplace) replaceUrlIfNeeded();
368      } else {
369        applyResolvedTag('', '');
370        if (allowReplace) replaceUrlIfNeeded();
371      }
372    } else {
373      applyResolvedTag('', '');
374    }
375  }
376
377  // Infinite scroll
378  function initInfinite() {
379    const io = new IntersectionObserver((entries)=>{
380      entries.forEach(e=>{
381        if (e.isIntersecting && S.state.allowInfinite && !S.state.loading && !S.state.done) {
382          S.state.page += 1;
383          fetchPage();
384        }
385      });
386    }, {root: null, rootMargin: '600px 0px 0px 0px', threshold: 0});
387    io.observe(S.els.sentinel);
388    S.io = io;
389  }
390
391  // Filters (chips)
392  function initFilters() {
393    if (!S.els.filterChips) return;
394    S.els.filterChips.forEach(btn=>{
395      btn.addEventListener('click', ()=>{
396        S.state.tagSlug = btn.dataset.tag || '';
397        S.state.tagId = btn.dataset.tagId || resolveTagId(S.state.tagSlug);
398        S.state.search = '';
399        if (S.els.searchInput) {
400          S.els.searchInput.value = '';
401        }
402        updateActiveChip();
403        clearGrid(); updateTitle(); pushUrl(); fetchPage();
404      });
405    });
406  }
407
408  // Search submission only
409  function initSearch() {
410    const input = S.els.searchInput;
411    const form = qs('#gwj-search-form');
412    if (!input || !form) return;
413    form.addEventListener('submit', async (e)=>{
414      e.preventDefault();
415      const v = input.value.trim();
416      S.state.search = v;
417      S.state.tagSlug = '';
418      S.state.tagId = '';
419      updateActiveChip();
420      setSearchButtonLoading(true);
421      clearGrid(); updateTitle(); pushUrl();
422      try {
423        await fetchPage();
424      } finally {
425        setSearchButtonLoading(false);
426      }
427    });
428  }
429
430  // Hydrate initial SSR page and set up everything
431  document.addEventListener('DOMContentLoaded', ()=>{
432    S.els.root     = qs('#gwj-blog');
433    S.els.posts    = qs('#gwj-posts');
434    S.els.title    = qs('#gwj-grid-title');
435    S.els.description = qs('#gwj-grid-description');
436    S.els.sentinel = qs('#gwj-sentinel');
437    S.els.searchInput = qs('#gwj-search-input');
438    S.els.filterChips = qsa('.gwj-grid__filters .chip');
439    S.els.searchButton = qs('#gwj-search-button');
440    S.els.loader   = qs('#gwj-loader');
441
442    if (S.els.searchButton && !S.els.searchButton.dataset.label) {
443      S.els.searchButton.dataset.label = S.els.searchButton.textContent.trim();
444    }
445
446    syncFromUrl({ allowReplace: true });
447    updateTitle();
448    updateActiveChip();
449    initFilters();
450    initSearch();
451    initInfinite();
452    const initialCount = S.els.posts ? S.els.posts.querySelectorAll('.card').length : 0;
453    if (initialCount > 0 && initialCount < GWJ_BLOG.perPage) {
454      S.state.done = true;
455    }
456    S.state.allowInfinite = !S.state.done;
457
458    // If URL indicates a tag/search different from SSR, reload grid
459    const ssrSlug = initialSlug || '';
460    const url = new URL(location.href);
461    const urlTagSlug = url.searchParams.get('tag') || '';
462    const urlTagId = url.searchParams.get('tag_id') || '';
463    const urlQ   = url.searchParams.get('q') || '';
464    const needsInitialFetch = initialCount === 0;
465    const legacySlug = urlTagId ? resolveSlugById(urlTagId) : '';
466    const tagMismatch = urlTagSlug
467      ? urlTagSlug !== ssrSlug
468      : Boolean(legacySlug && legacySlug !== ssrSlug);
469    if (needsInitialFetch || tagMismatch || urlQ) {
470      clearGrid(); fetchPage();
471    } else {
472      // continue with page 2 onward
473      S.state.page = 1; // next fetch increments
474    }
475
476    window.addEventListener('popstate', ()=>{
477      syncFromUrl({ allowReplace: false });
478      if (S.els.searchInput) {
479        S.els.searchInput.value = S.state.search || '';
480      }
481      updateActiveChip();
482      clearGrid(); updateTitle(); fetchPage();
483    });
484  });
485})();

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.