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.