| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
(function () { |
| 9 |
var pending = null; |
| 10 |
document.body.addEventListener('htmx:beforeRequest', function () { |
| 11 |
var el = document.activeElement; |
| 12 |
pending = el && el.id && el.closest('#discover-sidebar') ? el.id : null; |
| 13 |
}); |
| 14 |
document.body.addEventListener('htmx:afterSettle', function () { |
| 15 |
if (!pending) return; |
| 16 |
var el = document.getElementById(pending); |
| 17 |
pending = null; |
| 18 |
|
| 19 |
|
| 20 |
if (el && el !== document.activeElement) el.focus({ preventScroll: true }); |
| 21 |
}); |
| 22 |
})(); |
| 23 |
|
| 24 |
|
| 25 |
|
| 26 |
(function() { |
| 27 |
var input = document.getElementById('tag-search'); |
| 28 |
var list = document.getElementById('tag-suggest-list'); |
| 29 |
if (!input || !list) return; |
| 30 |
var timer = null; |
| 31 |
var active = -1; |
| 32 |
|
| 33 |
function close() { |
| 34 |
list.hidden = true; |
| 35 |
list.innerHTML = ''; |
| 36 |
input.setAttribute('aria-expanded', 'false'); |
| 37 |
input.removeAttribute('aria-activedescendant'); |
| 38 |
active = -1; |
| 39 |
} |
| 40 |
|
| 41 |
|
| 42 |
|
| 43 |
|
| 44 |
function setActive(items, index) { |
| 45 |
active = index; |
| 46 |
items.forEach(function(el, i) { |
| 47 |
var on = i === index; |
| 48 |
el.classList.toggle('highlighted', on); |
| 49 |
el.setAttribute('aria-selected', on ? 'true' : 'false'); |
| 50 |
}); |
| 51 |
if (index >= 0 && items[index]) { |
| 52 |
input.setAttribute('aria-activedescendant', items[index].id); |
| 53 |
} else { |
| 54 |
input.removeAttribute('aria-activedescendant'); |
| 55 |
} |
| 56 |
} |
| 57 |
|
| 58 |
function choose(slug) { |
| 59 |
|
| 60 |
|
| 61 |
var form = document.getElementById('discover-form'); |
| 62 |
if (!form) return; |
| 63 |
var existing = form.querySelector('input[data-facet="tag"][value="' + CSS.escape(slug) + '"]'); |
| 64 |
if (!existing) { |
| 65 |
var el = document.createElement('input'); |
| 66 |
el.type = 'hidden'; |
| 67 |
el.name = 'tag'; |
| 68 |
el.value = slug; |
| 69 |
el.className = 'discover-filter'; |
| 70 |
el.dataset.facet = 'tag'; |
| 71 |
form.appendChild(el); |
| 72 |
} |
| 73 |
input.value = ''; |
| 74 |
close(); |
| 75 |
htmx.trigger(form, 'tag-added'); |
| 76 |
} |
| 77 |
|
| 78 |
function render(items) { |
| 79 |
if (!items.length) { close(); return; } |
| 80 |
list.innerHTML = ''; |
| 81 |
items.forEach(function(item, i) { |
| 82 |
var li = document.createElement('li'); |
| 83 |
li.id = 'tag-suggest-option-' + i; |
| 84 |
li.setAttribute('role', 'option'); |
| 85 |
li.setAttribute('aria-selected', i === active ? 'true' : 'false'); |
| 86 |
li.className = 'suggestion-item tag-suggest-item' + (i === active ? ' highlighted' : ''); |
| 87 |
li.dataset.slug = item.slug; |
| 88 |
var label = document.createElement('span'); |
| 89 |
label.className = 'tag-suggest-label'; |
| 90 |
label.textContent = item.label; |
| 91 |
li.appendChild(label); |
| 92 |
if (item.context) { |
| 93 |
var ctx = document.createElement('span'); |
| 94 |
ctx.className = 'suggestion-category tag-suggest-context'; |
| 95 |
ctx.textContent = item.context; |
| 96 |
li.appendChild(ctx); |
| 97 |
} |
| 98 |
li.addEventListener('mousedown', function(e) { |
| 99 |
e.preventDefault(); |
| 100 |
choose(item.slug); |
| 101 |
}); |
| 102 |
list.appendChild(li); |
| 103 |
}); |
| 104 |
list.hidden = false; |
| 105 |
input.setAttribute('aria-expanded', 'true'); |
| 106 |
} |
| 107 |
|
| 108 |
input.addEventListener('input', function() { |
| 109 |
var q = input.value.trim(); |
| 110 |
clearTimeout(timer); |
| 111 |
if (q.length < 2) { close(); return; } |
| 112 |
timer = setTimeout(function() { |
| 113 |
fetch('/discover/tag-suggest?q=' + encodeURIComponent(q)) |
| 114 |
.then(function(r) { return r.ok ? r.json() : []; }) |
| 115 |
.then(function(items) { active = -1; render(items); }) |
| 116 |
.catch(function() { close(); }); |
| 117 |
}, 150); |
| 118 |
}); |
| 119 |
|
| 120 |
input.addEventListener('keydown', function(e) { |
| 121 |
var items = list.querySelectorAll('.tag-suggest-item'); |
| 122 |
if (e.key === 'Escape') { close(); return; } |
| 123 |
if (!items.length) return; |
| 124 |
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { |
| 125 |
e.preventDefault(); |
| 126 |
var next = active + (e.key === 'ArrowDown' ? 1 : -1); |
| 127 |
if (next < 0) next = items.length - 1; |
| 128 |
if (next >= items.length) next = 0; |
| 129 |
setActive(items, next); |
| 130 |
} else if (e.key === 'Enter') { |
| 131 |
e.preventDefault(); |
| 132 |
var pick = active >= 0 ? items[active] : items[0]; |
| 133 |
if (pick) choose(pick.dataset.slug); |
| 134 |
} |
| 135 |
}); |
| 136 |
|
| 137 |
document.addEventListener('click', function(e) { |
| 138 |
if (!list.contains(e.target) && e.target !== input) close(); |
| 139 |
}); |
| 140 |
})(); |
| 141 |
|
| 142 |
|
| 143 |
|
| 144 |
(function() { |
| 145 |
function applyView(view) { |
| 146 |
var container = document.getElementById('results-container-inner'); |
| 147 |
if (container) { |
| 148 |
container.className = 'results-container results-' + view; |
| 149 |
} |
| 150 |
document.querySelectorAll('.view-btn').forEach(function(btn) { |
| 151 |
btn.classList.toggle('is-selected', btn.dataset.view === view); |
| 152 |
}); |
| 153 |
|
| 154 |
var header = document.querySelector('.table-header'); |
| 155 |
if (header) { |
| 156 |
header.classList.toggle('is-hidden', view !== 'list'); |
| 157 |
} |
| 158 |
} |
| 159 |
|
| 160 |
|
| 161 |
document.addEventListener('DOMContentLoaded', function() { |
| 162 |
var saved = safeStorageGet('discoverViewPref') || 'grid'; |
| 163 |
applyView(saved); |
| 164 |
}); |
| 165 |
|
| 166 |
|
| 167 |
document.querySelectorAll('.view-btn').forEach(function(btn) { |
| 168 |
btn.addEventListener('click', function() { |
| 169 |
var view = btn.dataset.view; |
| 170 |
applyView(view); |
| 171 |
safeStorageSet('discoverViewPref', view); |
| 172 |
}); |
| 173 |
}); |
| 174 |
|
| 175 |
|
| 176 |
document.body.addEventListener('htmx:afterSwap', function(evt) { |
| 177 |
if (evt.detail.target.id === 'results-container') { |
| 178 |
var saved = safeStorageGet('discoverViewPref') || 'grid'; |
| 179 |
applyView(saved); |
| 180 |
} |
| 181 |
}); |
| 182 |
})(); |
| 183 |
|
| 184 |
|
| 185 |
(function() { |
| 186 |
var input = document.getElementById('search-input'); |
| 187 |
var box = document.getElementById('search-suggestions'); |
| 188 |
var timer = null; |
| 189 |
var selectedIdx = -1; |
| 190 |
|
| 191 |
input.addEventListener('input', function() { |
| 192 |
clearTimeout(timer); |
| 193 |
var q = input.value.trim(); |
| 194 |
if (q.length < 2) { box.innerHTML = ''; box.style.display = 'none'; return; } |
| 195 |
timer = setTimeout(function() { |
| 196 |
fetch('/discover/suggestions?q=' + encodeURIComponent(q)) |
| 197 |
.then(function(r) { return r.json(); }) |
| 198 |
.then(function(items) { |
| 199 |
if (items.length === 0) { box.innerHTML = ''; box.style.display = 'none'; return; } |
| 200 |
selectedIdx = -1; |
| 201 |
box.innerHTML = items.map(function(s, i) { |
| 202 |
return '<a href="' + escapeHtml(s.url) + '" class="suggestion-item" data-idx="' + i + '">' |
| 203 |
+ '<span class="suggestion-label">' + escapeHtml(s.label) + '</span>' |
| 204 |
+ '<span class="suggestion-category">' + escapeHtml(s.category) + '</span></a>'; |
| 205 |
}).join(''); |
| 206 |
box.style.display = 'block'; |
| 207 |
}); |
| 208 |
}, 200); |
| 209 |
}); |
| 210 |
|
| 211 |
input.addEventListener('keydown', function(e) { |
| 212 |
var items = box.querySelectorAll('.suggestion-item'); |
| 213 |
if (!items.length) return; |
| 214 |
if (e.key === 'ArrowDown') { e.preventDefault(); selectedIdx = Math.min(selectedIdx + 1, items.length - 1); updateHighlight(items); } |
| 215 |
else if (e.key === 'ArrowUp') { e.preventDefault(); selectedIdx = Math.max(selectedIdx - 1, -1); updateHighlight(items); } |
| 216 |
else if (e.key === 'Enter' && selectedIdx >= 0) { e.preventDefault(); items[selectedIdx].click(); } |
| 217 |
else if (e.key === 'Escape') { box.style.display = 'none'; } |
| 218 |
}); |
| 219 |
|
| 220 |
function updateHighlight(items) { |
| 221 |
items.forEach(function(el, i) { el.classList.toggle('highlighted', i === selectedIdx); }); |
| 222 |
} |
| 223 |
|
| 224 |
|
| 225 |
document.addEventListener('click', function(e) { |
| 226 |
if (!box.contains(e.target) && e.target !== input) { box.style.display = 'none'; } |
| 227 |
}); |
| 228 |
})(); |
| 229 |
|