v0.1

UX
#95 - Konfetti auf Klick
Lass lustiges Konfetti auf Klick fliegen!
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #209 v0.1 💙 AUTOCOMPLETE FROM CMS -->
<script>
(function() {
'use strict';
var CONFIG = {
attr: 'ms-code-search',
groupAttr: 'ms-code-search-group',
minCharsAttr: 'data-ms-search-min',
maxResultsAttr: 'data-ms-search-max',
debounceAttr: 'data-ms-search-debounce',
activeAttr: 'data-ms-search-active',
defaultMin: 1,
defaultMax: 10,
defaultDebounce: 200
};
// -- Helpers --
function debounce(fn, ms) {
var timer;
return function() {
var ctx = this, args = arguments;
clearTimeout(timer);
timer = setTimeout(function() { fn.apply(ctx, args); }, ms);
};
}
function normalize(str) {
return (str || '').toLowerCase().trim();
}
function findPaired(role, group) {
var all = document.querySelectorAll('[' + CONFIG.attr + '="' + role + '"]');
if (!group) return all[0] || null;
for (var i = 0; i < all.length; i++) {
if ((all[i].getAttribute(CONFIG.groupAttr) || '') === group) return all[i];
}
return all[0] || null;
}
// -- Init each search instance --
function initSearch(input) {
var group = input.getAttribute(CONFIG.groupAttr) || '';
var list = findPaired('list', group);
var emptyEl = findPaired('empty', group);
var clearBtn = findPaired('clear', group);
if (!list) {
console.warn('MemberScript # number209: No element with ' + CONFIG.attr + '="list" found');
return;
}
var minChars = parseInt(input.getAttribute(CONFIG.minCharsAttr), 10) || CONFIG.defaultMin;
var maxResults = parseInt(input.getAttribute(CONFIG.maxResultsAttr), 10) || CONFIG.defaultMax;
var debounceMs = parseInt(input.getAttribute(CONFIG.debounceAttr), 10) || CONFIG.defaultDebounce;
// -- Collect CMS items and their searchable text --
var items = [];
var children = list.children;
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child === emptyEl) continue;
var fields = child.querySelectorAll('[' + CONFIG.attr + '="field"]');
var texts = [];
for (var j = 0; j < fields.length; j++) {
texts.push(normalize(fields[j].textContent));
}
if (texts.length === 0) texts.push(normalize(child.textContent));
items.push({ el: child, texts: texts });
}
// -- Initial visibility: hide everything the script controls --
list.style.display = 'none';
if (emptyEl) emptyEl.style.display = 'none';
if (clearBtn) clearBtn.style.display = 'none';
for (var h = 0; h < items.length; h++) items[h].el.style.display = 'none';
var activeIndex = -1;
// -- Show or hide clear button based on input value --
function updateClearBtn() {
if (!clearBtn) return;
clearBtn.style.display = input.value.length > 0 ? 'flex' : 'none';
}
function getVisible() {
var v = [];
for (var k = 0; k < items.length; k++) {
if (items[k].el.style.display !== 'none') v.push(items[k]);
}
return v;
}
function clearActive() {
for (var k = 0; k < items.length; k++) {
items[k].el.removeAttribute(CONFIG.activeAttr);
}
activeIndex = -1;
}
function setActive(idx) {
var visible = getVisible();
clearActive();
if (idx < 0 || idx >= visible.length) return;
activeIndex = idx;
visible[idx].el.setAttribute(CONFIG.activeAttr, ' keywordtrue');
try { visible[idx].el.scrollIntoView({ block: 'nearest' }); } catch (e) {}
}
function closeDropdown() {
list.style.display = 'none';
if (emptyEl) emptyEl.style.display = 'none';
clearActive();
input.setAttribute('aria-expanded', ' keywordfalse');
}
function openDropdown() {
list.style.display = 'block';
input.setAttribute('aria-expanded', ' keywordtrue');
}
// -- Filter logic --
function filterItems(query) {
var q = normalize(query);
updateClearBtn();
if (q.length < minChars) { closeDropdown(); return; }
var words = q.split(/\s+/);
var shown = 0;
for (var k = 0; k < items.length; k++) {
var match = false;
for (var t = 0; t < items[k].texts.length; t++) {
var allMatch = true;
for (var w = 0; w < words.length; w++) {
if (items[k].texts[t].indexOf(words[w]) === -1) {
allMatch = false;
break;
}
}
if (allMatch) { match = true; break; }
}
if (match && shown < maxResults) {
items[k].el.style.display = 'block';
shown++;
} else {
items[k].el.style.display = 'none';
}
}
if (shown > 0) {
openDropdown();
if (emptyEl) emptyEl.style.display = 'none';
} else {
list.style.display = 'none';
if (emptyEl) emptyEl.style.display = 'block';
input.setAttribute('aria-expanded', ' keywordfalse');
}
clearActive();
}
// -- Event handlers --
var onInput = debounce(function() { filterItems(input.value); }, debounceMs);
input.addEventListener('input', onInput);
input.addEventListener('focus', function() {
if (normalize(input.value).length >= minChars) filterItems(input.value);
});
input.addEventListener('keydown', function(e) {
var visible = getVisible();
if (visible.length === 0 && e.key !== 'Escape') return;
if (e.key === 'ArrowDown') {
e.preventDefault();
setActive(activeIndex + 1 >= visible.length ? 0 : activeIndex + 1);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setActive(activeIndex - 1 < 0 ? visible.length - 1 : activeIndex - 1);
} else if (e.key === 'Enter' && activeIndex >= 0) {
e.preventDefault();
var item = visible[activeIndex];
var link = item.el.querySelector('a');
if (link && link.href) {
window.location.href = link.href;
} else {
var field = item.el.querySelector('[' + CONFIG.attr + '="field"]');
input.value = field ? field.textContent.trim() : item.el.textContent.trim();
closeDropdown();
updateClearBtn();
}
} else if (e.key === 'Escape') {
closeDropdown();
input.blur();
}
});
// Close when clicking outside
document.addEventListener('click', function(e) {
if (!input.contains(e.target) && !list.contains(e.target) &&
!(clearBtn && clearBtn.contains(e.target))) {
closeDropdown();
}
});
// Handle clicks on results
list.addEventListener('click', function(e) {
for (var k = 0; k < items.length; k++) {
if (items[k].el.contains(e.target)) {
var anchor = e.target.tagName === 'A' ? e.target : e.target.closest ? e.target.closest('a') : null;
if (anchor && anchor.href) {
closeDropdown();
return;
}
var field = items[k].el.querySelector('[' + CONFIG.attr + '="field"]');
input.value = field ? field.textContent.trim() : items[k].el.textContent.trim();
closeDropdown();
updateClearBtn();
return;
}
}
});
// Clear button
if (clearBtn) {
clearBtn.addEventListener('click', function(e) {
e.preventDefault();
input.value = '';
closeDropdown();
updateClearBtn();
input.focus();
});
}
// ARIA setup
input.setAttribute('role', 'combobox');
input.setAttribute('aria-autocomplete', 'list');
input.setAttribute('aria-expanded', ' keywordfalse');
list.setAttribute('role', 'listbox');
console.log('MemberScript # number209: Autocomplete ready with ' + items.length + ' items');
}
// -- Init --
function init() {
var inputs = document.querySelectorAll('[' + CONFIG.attr + '="input"]');
if (inputs.length === 0) return;
for (var i = 0; i < inputs.length; i++) initSearch(inputs[i]);
}
window.ms209 = { refresh: init };
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>More scripts in UX