PageSourceSearch

https://kanazawaiemoto.jp/wp-content/themes/iemoto-blocktheme/blocks/works-filter/view.js?ver=7.1.2

js kanazawaiemoto.jp collected 2026-10-06 12:25:09 UTC 17,089 bytes, 442 lines download raw bytes

1// blocks/works-filter/view.js
2(function () {
3  // ---------- utilities ----------
4  function qs(a, b) { return (a || document).querySelector(b); }
5  function qsa(a, b) { return Array.prototype.slice.call((a || document).querySelectorAll(b)); }
6
7  // Detect post type (projects or works)
8  function detectPostType() {
9    if (document.querySelector('.js-projects-list') || window.projectsArchiveBase) {
10      return { type: 'projects', tag: 'projects_tag', archiveBase: window.projectsArchiveBase || window.location.pathname };
11    }
12    return { type: 'works', tag: 'works_tag', archiveBase: window.worksArchiveBase || window.location.pathname };
13  }
14
15  var POST_TYPE = detectPostType();
16  var TAG_PARAM = POST_TYPE.tag;
17  var TAG_PARAM_ARRAY = POST_TYPE.tag + '[]';
18  var TAG_REGEX = new RegExp('^' + POST_TYPE.tag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\[\\d*\\]$');
19  var VISIBLE_TAGS = 8;
20
21  // ---------- shared URL / fetch helpers ----------
22  function localBuildUrl(setObj, removeKeys) {
23    var base = window.location.origin + POST_TYPE.archiveBase;
24    var ALLOWED_BASE = new Set(['category_name', 'paged', TAG_PARAM, TAG_PARAM_ARRAY]);
25    var isAllowedKey = function (key) { return ALLOWED_BASE.has(key) || TAG_REGEX.test(key); };
26    var src = new URLSearchParams(window.location.search);
27
28    (removeKeys || []).forEach(function (k) { src.delete(k); });
29    Object.entries(setObj || {}).forEach(function (pair) {
30      var k = pair[0], v = pair[1];
31      if (v === null || v === '') { src.delete(k); return; }
32      if (Array.isArray(v)) {
33        if (k === TAG_PARAM_ARRAY) {
34          Array.from(src.keys()).forEach(function (key) {
35            if (key === TAG_PARAM_ARRAY || key === TAG_PARAM || TAG_REGEX.test(key)) src.delete(key);
36          });
37        } else { src.delete(k); }
38        v.forEach(function (val) { src.append(k, val); });
39      } else { src.set(k, v); }
40    });
41    Array.from(src.keys()).forEach(function (k) { if (!isAllowedKey(k)) src.delete(k); });
42    var q = src.toString();
43    return q ? base + '?' + q : base;
44  }
45
46  function parseSearch() {
47    var params = new URLSearchParams(window.location.search);
48    var tags = [];
49    params.forEach(function (v, k) {
50      if (k === TAG_PARAM_ARRAY || TAG_REGEX.test(k) || k === TAG_PARAM) tags.push(v);
51    });
52    return { params: params, tags: tags };
53  }
54
55  function fetchAllTerms(endpoint) {
56    var url = (window.wpApiSettings && window.wpApiSettings.root || '/wp-json/') + 'wp/v2/' + endpoint
57      + '?per_page=100&hide_empty=true&orderby=count&order=desc';
58    return fetch(url, { credentials: 'same-origin' }).then(function (r) {
59      if (!r.ok) throw new Error('HTTP ' + r.status);
60      return r.json();
61    });
62  }
63
64  // ============================================================
65  // PART A: Existing modal filter (kept intact)
66  // ============================================================
67  function trapFocus(modal) {
68    var selectors = [
69      'a[href]', 'button:not([disabled])', 'input:not([disabled])',
70      'select:not([disabled])', 'textarea:not([disabled])', '[tabindex]:not([tabindex="-1"])'
71    ];
72    function qsf() { return qsa(modal, selectors.join(',')); }
73    function onKeyFactory(first, last) {
74      return function onKey(e) {
75        if (e.key === 'Escape') { close(); }
76        if (e.key !== 'Tab') return;
77        if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
78        else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
79      };
80    }
81    var onKey = null;
82
83    function open() {
84      modal.removeAttribute('hidden');
85      modal.setAttribute('aria-hidden', 'false');
86      var f = qsf(); var first = f[0] || modal; var last = f[f.length - 1] || modal;
87      onKey = onKeyFactory(first, last);
88      document.addEventListener('keydown', onKey);
89      document.documentElement.style.overflow = 'hidden';
90      first.focus();
91      var opener = document.querySelector('[data-works-filter-open]');
92      if (opener) { opener.setAttribute('aria-expanded', 'true'); }
93    }
94
95    function close() {
96      modal.setAttribute('hidden', '');
97      modal.setAttribute('aria-hidden', 'true');
98      if (onKey) document.removeEventListener('keydown', onKey);
99      document.documentElement.style.overflow = '';
100      var opener = document.querySelector('[data-works-filter-open]');
101      if (opener) { opener.setAttribute('aria-expanded', 'false');
101 opener.focus(); }
102    }
103    return { open: open, close: close };
104  }
105
106  function renderOpenButton(root) {
107    if (root.__wf_inited) return root.__wf_api;
108    root.__wf_inited = true;
109
110    var openBtn = document.createElement('button');
111    openBtn.type = 'button';
112    openBtn.className = 'works-filter__open';
113    openBtn.setAttribute('data-works-filter-open', '');
114    openBtn.setAttribute('aria-haspopup', 'dialog');
115    openBtn.setAttribute('aria-expanded', 'false');
116    openBtn.innerHTML = '<span class="label">特徴で選ぶ</span>';
117    root.innerHTML = '';
118    root.appendChild(openBtn);
119
120    var modal = document.createElement('div');
121    modal.className = 'works-filter__dialog';
122    modal.id = 'works-filter-modal';
123    modal.setAttribute('role', 'dialog');
124    modal.setAttribute('aria-modal', 'true');
125    modal.setAttribute('aria-labelledby', 'works-filter-title');
126    modal.setAttribute('data-works-filter-modal', '');
127    modal.setAttribute('hidden', '');
128    root.appendChild(modal);
129
130    var TF = trapFocus(modal);
131    openBtn.addEventListener('click', TF.open);
132    modal.addEventListener('click', function (e) { if (e.target === modal) TF.close(); });
133
134    var api = {
135      mountContent: function (terms, preselected) {
136        var panel = document.createElement('div');
137        panel.className = 'works-filter__panel';
138        panel.setAttribute('role', 'document');
139
140        var header = document.createElement('header');
141        header.className = 'works-filter__header';
142        header.innerHTML =
143          '<button type="button" class="works-filter__close" data-works-filter-close aria-label="閉じる">×</button>' +
144          '<h3 id="works-filter-title">絞り込み</h3>' +
145          '<button type="button" class="works-filter__clear-header" data-clear-header aria-label="クリア">クリア</button>';
146
147        var body = document.createElement('div');
148        body.className = 'works-filter__body';
149
150        var form = document.createElement('form');
151        form.className = 'works-filter__form';
152        form.setAttribute('data-filter-form', '');
153
154        var fs = document.createElement('fieldset');
155        var legend = document.createElement('legend');
156        legend.textContent = '全てのタグ';
157
158        var ul = document.createElement('ul');
159        ul.className = 'works-filter__list';
160
161        if (!terms || !terms.length) {
162          var p = document.createElement('p');
163          p.style.margin = '0';
164          p.textContent = 'タグがありません。';
165          body.appendChild(p);
166        } else {
167          terms.forEach(function (t) {
168            var li = document.createElement('li');
169            var label = document.createElement('label');
170            var input = document.createElement('input');
171            input.type = 'checkbox';
172            input.name = TAG_PARAM_ARRAY;
173            input.value = t.slug;
174            if (preselected.has(t.slug)) input.checked = true;
175            var span = document.createElement('span');
176            span.textContent = t.name;
177            label.appendChild(input); label.appendChild(span);
178            li.appendChild(label); ul.appendChild(li);
179          });
180
181          fs.appendChild(legend);
182          fs.appendChild(ul);
183          form.appendChild(fs);
184
185          var actions = document.createElement('div');
186          actions.className = 'works-filter__actions';
187          actions.innerHTML =
188            '<button type="submit" class="button button-primary">SEARCH</button>' +
189            '<button type="button" class="button works-filter__clear">クリア</button>';
190          form.appendChild(actions);
191          body.appendChild(form);
192        }
193
194        panel.appendChild(header);
195        panel.appendChild(body);
196
197        modal.innerHTML = '';
198        modal.appendChild(panel);
199
200        var closeEl = qs(modal, '[data-works-filter-close]');
201        if (closeEl) closeEl.addEventListener('click', TF.close);
202
203        var clearBtn = qs(modal, '.works-filter__clear');
204        if (clearBtn) clearBtn.addEventListener('click', function () {
205          qsa(modal, 'input[name="' + TAG_PARAM_ARRAY + '"]').forEach(function (i) { i.checked = false; });
206        });
207
208        var clearHeaderBtn = qs(modal, '[data-clear-header]');
209        if (clearHeaderBtn) clearHeaderBtn.addEventListener('click', function () {
210          qsa(modal, 'input[name="' + TAG_PARAM_ARRAY + '"]').forEach(function (i) { i.checked = false; });
211        });
212
213        if (form) {
214          form.addEventListener('submit', function (e) {
215            e.preventDefault();
216            var selected = qsa(modal, 'input[name="' + TAG_PARAM_ARRAY + '"]:checked').map(function (i) { return i.value; });
217
218            var WA = (window.worksArchive || window.projectsArchive || {});
219            var build = (typeof WA.buildUrl === 'function') ? WA.buildUrl : localBuildUrl;
220            var url = selected.length
221              ? build({ [TAG_PARAM_ARRAY]: selected }, ['paged'])
222              : build({}, [TAG_PARAM_ARRAY, TAG_PARAM, 'paged']);
223
224            if (typeof WA.fetchAndSwap === 'function') {
225              WA.fetchAndSwap(url);
226              TF.close();
227            } else {
228              window.location.assign(url);
229            }
230          });
231        }
232      }
233    };
234
235    root.__wf_api = api;
236    return api;
237  }
238
239  // ============================================================
240  // PART B: Inline tags (neie.jp-inspired) — added alongside modal
241  // Rendered with display:none — toggle in devtools to preview
242  // ============================================================
243  function buildTagUrl(slug) {
244    var WA = window.worksArchive || window.projectsArchive || {};
245    var build = typeof WA.buildUrl === 'function' ? WA.buildUrl : localBuildUrl;
246    if (!slug) return build({}, [TAG_PARAM_ARRAY, TAG_PARAM, 'paged']);
247    return build({ [TAG_PARAM_ARRAY]: [slug] }, ['paged']);
248  }
249
250  function getActiveTags() {
251    var params = new URLSearchParams(window.location.search);
252    var tags = [];
253    params.forEach(function (v, k) {
254      if (k === TAG_PARAM_ARRAY || TAG_REGEX.test(k) || k === TAG_PARAM) tags.push(v);
255    });
256    var pattern = POST_TYPE.type === 'projects' ? /\/projects-tag\/([^\/]+)/ : /\/works-tag\/([^\/]+)/;
257    var match = window.location.pathname.match(pattern);
258    if (match && match[1]) tags.push(match[1]);
259    var globalTag = window.currentWorksTag || window.currentProjectsTag;
260    if (globalTag && tags.indexOf(globalTag) === -1) tags.push(globalTag);
261    return tags;
262  }
263
264  function navigateToTag(url) {
265    var WA = window.worksArchive || window.projectsArchive || {};
266    if (typeof WA.fetchAndSwap === 'function') {
267      WA.fetchAndSwap(url);
268    } else {
269      window.location.assign(url);
270    }
271  }
272
273  function createTagItem(name, slug, isActive) {
274    var li = document.createElement('li');
275    li.className = 'works-tags__item';
276
277    var a = document.createElement('a');
278    a.className = 'works-tags__link';
279    a.href = buildTagUrl(slug);
280    if (isActive) a.classList.add('is-current');
281
282    var outer = document.createElement('span');
283    outer.setAttribute('data-text', name);
284    var inner = document.createElement('span');
285    inner.textContent = name;
286    outer.appendChild(inner);
287    a.appendChild(outer);
288
289    a.addEventListener('click', function (e) {
290      e.preventDefault();
291      qsa(document, '.works-tags__link').forEach(function (l) { l.classList.remove('is-current'); });
292      a.classList.add('is-current');
293      navigateToTag(a.href);
294    });
295
296    li.appendChild(a);
297    return li;
298  }
299
300  function renderInlineTags(filterBar, terms, activeSlugs) {
301    // Prevent double-init
302    if (filterBar.querySelector('.works-tags')) return;
303
304    var container = document.createElement('div');
305    container.className = 'works-tags';
306    // container.style.display = 'none'; // Enabled — client approved
307
308    var hasExtras = terms.length > VISIBLE_TAGS;
309
310    // Header: "タグ" + SP trigger (もっと見る with chevron)
311    var header = document.createElement('span');
312    header.className = 'works-tags__header';
313    header.textContent = 'タグ';
314
315    // SP trigger — same text/icon as PC "もっと見る"
316    var trigger, triggerText, triggerIcon;
317    if (hasExtras) {
318      trigger = document.createElement('button');
319      trigger.type = 'button';
320      trigger.className = 'works-tags__trigger';
321      triggerText = document.createElement('span');
322      triggerText.className = 'works-tags__triggerText';
323      triggerText.textContent = 'もっと見る';
324      triggerIcon = document.createElement('span');
325      triggerIcon.className = 'works-tags__triggerChevron';
326      trigger.appendChild(triggerText);
327      trigger.appendChild(triggerIcon);
328      header.appendChild(trigger);
329    }
330
331    // Tag list
332    var ul = document.createElement('ul');
333    ul.className = 'works-tags__list';
334
335    var allActive = activeSlugs.length === 0;
336    ul.appendChild(createTagItem('すべて', '', allActive));
337
338    terms.forEach(function (t, i) {
339      if (i < VISIBLE_TAGS) {
340        ul.appendChild(createTagItem(t.name, t.slug, activeSlugs.indexOf(t.slug) >= 0));
341      }
342    });
343
344    // "もっと見る" button (PC inline) + extras wrapper
345    var extrasUl = null;
346    if (hasExtras) {
347      // PC inline button
348      var moreLi = document.createElement('li');
349      moreLi.className = 'works-tags__item works-tags__more';
350      var moreBtn = document.createElement('button');
351      moreBtn.type = 'button';
352      moreBtn.className = 'works-tags__moreBtn';
353      moreBtn.textContent = 'もっと見る';
354      moreLi.appendChild(moreBtn);
355      ul.appendChild(moreLi);
356
357      // Extras wrapper
358      var extrasWrapLi = document.createElement('li');
359      extrasWrapLi.className = 'works-tags__extras-wrap';
360      extrasUl = document.createElement('ul');
361      extrasUl.className = 'works-tags__extras';
362
363      terms.forEach(function (t, i) {
364        if (i >= VISIBLE_TAGS) {
365          extrasUl.appendChild(createTagItem(t.name, t.slug, activeSlugs.indexOf(t.slug) >= 0));
366        }
367      });
368
369      extrasWrapLi.appendChild(extrasUl);
370      ul.appendChild(extrasWrapLi);
371
372      // Unified toggle function — syncs both PC button and SP trigger
373      function toggleExpand() {
374        var isOpen = container.classList.toggle('is-expanded');
375        var label = isOpen ? '閉じる' : 'もっと見る';
376        moreBtn.textContent = label;
377        if (triggerText) triggerText.textContent = label;
378
379        // Animate extras height
380        if (isOpen) {
381          extrasUl.style.maxHeight = extrasUl.scrollHeight + 'px';
382        } else {
383          extrasUl.style.maxHeight = extrasUl.scrollHeight + 'px';
384          extrasUl.offsetHeight; // force reflow
385          extrasUl.style.maxHeight = '0';
386        }
387      }
388
389      moreBtn.addEventListener('click', toggleExpand);
390      if (trigger) trigger.addEventListener('click', toggleExpand);
391
392      // Clean up inline maxHeight after expand transition
393      extrasUl.addEventListener('transitionend', function () {
394        if (container.classList.contains('is-expanded')) {
395          extrasUl.style.maxHeight = 'none';
396        }
397      });
398    }
399
400    container.appendChild(header);
401    container.appendChild(ul);
402
403    // Insert after filter-bar__inner
404    var filterBarInner = filterBar.querySelector('.filter-bar__inner');
405    if (filterBarInner) {
406      filterBarInner.insertAdjacentElement('afterend', container);
407    } else {
408      filterBar.appendChild(container);
409    }
410  }
411
412  // ============================================================
413  // Bootstrap: render BOTH modal button + inline tags
414  // ============================================================
415  document.addEventListener('DOMContentLoaded', function () {
416    var roots = qsa(document, '[data-works-filter-root]');
417    if (!roots.length) return;
418
419    var searchResult = parseSearch();
420    var selected = new Set(searchResult.tags);
421    var activeTags = getActiveTags();
422
423    roots.forEach(function (root) {
424      // A) Modal filter — disabled, kept for rollback (see commit 75f8a7b)
425      // var api = renderOpenButton(root);
426
427      fetchAllTerms(TAG_PARAM)
428        .then(function (terms) {
429          var termArray = Array.isArray(terms) ? terms : [];
430
431          // B) Inline tags — find parent filter-bar and inject
432          var filterBar = root.closest('.filter-bar');
433          if (filterBar) {
434            renderInlineTags(filterBar, termArray, activeTags);
435          }
436        })
437        .catch(function (err) {
438          if (console && console.warn) console.warn('[works-filter] terms fetch failed:', err);
439        });
440    });
441  });
442})();

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.