// ==UserScript== // @name BIPP: doorzoekbare dropdowns // @namespace https://github.com/glgoose/ua-userscripts // @version 1.8.5 // @description Geeft elke met veel opties doorzoekbaar. * * Het originele aan. Luistert er niemand, // dan gebeurt er niets, en dat is geen fout. function fireRemove(it) { var ev; var detail = { value: select.options[it.index].value, label: it.label, index: it.index }; try { ev = new CustomEvent('select-search:remove', { bubbles: true, cancelable: true, detail: detail }); } catch (e) { ev = doc.createEvent('CustomEvent'); ev.initCustomEvent('select-search:remove', true, true, detail); } select.dispatchEvent(ev); } function fire(el, type) { var ev; try { ev = new Event(type, { bubbles: true, cancelable: false }); } catch (e) { ev = doc.createEvent('HTMLEvents'); ev.initEvent(type, true, false); } el.dispatchEvent(ev); // Legacy apps binden vaak via jQuery; native events bereiken die handlers // wel, maar een expliciete trigger dekt ook .change()-only bindings. var jq = win.jQuery || win.$; if (jq && jq.fn && typeof jq === 'function') { try { jq(el).trigger(type); } catch (e2) { /* stil */ } } } function detach() { obs.disconnect(); select.removeEventListener('change', onSelectChange); win.removeEventListener('scroll', position, true); win.removeEventListener('resize', position); if (prevStyle === null) select.removeAttribute('style'); else select.setAttribute('style', prevStyle); delete select.dataset.ssEnhanced; delete select.__selectSearchSync; select.dataset.nosearch = '1'; if (wrap.parentNode) wrap.parentNode.removeChild(wrap); } /* -------------------------------------------------------- listeners */ input.addEventListener('mousedown', function (e) { // ontsnappingsluik: native select terug if (e.altKey) { e.preventDefault(); detach(); select.focus(); return; } if (!open) { e.preventDefault(); input.focus(); openList(''); input.select(); } }); // mousedown in plaats van click, met preventDefault: zo houdt het invoerveld zijn focus en // blijft het kruisje zichtbaar terwijl je klikt. stopPropagation houdt de lijst dicht. wis.addEventListener('mousedown', function (e) { e.preventDefault(); e.stopPropagation(); clear(); }); input.addEventListener('focus', function () { if (!open) openList(''); }); input.addEventListener('input', function () { if (open) render(input.value); else openList(input.value); }); input.addEventListener('blur', function () { setTimeout(function () { if (open) closeList(true); }, 120); }); input.addEventListener('keydown', function (e) { var n = list.querySelectorAll('li[data-i]').length; switch (e.key) { case 'ArrowDown': e.preventDefault(); if (!open) { openList(''); break; } if (n) setActive((active + 1) % n); break; case 'ArrowUp': e.preventDefault(); if (!open) { openList(''); break; } if (n) setActive((active - 1 + n) % n); break; case 'Home': if (open && n) { e.preventDefault(); setActive(0); } break; case 'End': if (open && n) { e.preventDefault(); setActive(n - 1); } break; case 'PageDown': if (open && n) { e.preventDefault(); setActive(Math.min(n - 1, active + 10)); } break; case 'PageUp': if (open && n) { e.preventDefault(); setActive(Math.max(0, active - 10)); } break; case 'Enter': if (open) { e.preventDefault(); if (active >= 0) choose(active); else closeList(true); } break; case 'Tab': if (open && active >= 0) choose(active); else if (open) closeList(true); break; case 'Escape': if (open) { e.preventDefault(); e.stopPropagation(); closeList(true); } break; // Backspace op een leeg invoerveld wist de keuze, zoals in react-select. Staat er nog // tekst, dan doet Backspace gewoon wat je verwacht en blijft de keuze staan. case 'Backspace': if (!input.value && leegIndex !== -1 && currentLabel()) { e.preventDefault(); clear(); } break; // Delete haalt de actieve regel uit haar lijst, zoals de adresbalk van een browser dat met // een suggestie doet. Alleen zolang er niets getypt is: filtert de gebruiker, dan blijft // Delete de gewone teksttoets en gebeurt er niets met de lijst. case 'Delete': if (!open || zoek || active < 0) break; var doelwit = shown[active]; if (!doelwit || !doelwit.removable) break; e.preventDefault(); if (doelwit.mru && recentSleutel && dropRecent(recentSleutel, select.options[doelwit.index].value)) { readOptions(); render(zoek, true); break; } fireRemove(doelwit); break; } }); // De muis verzet dezelfde balk als de pijltjes: één begrip van "waar sta ik", zodat Enter en // Delete altijd op de regel slaan die oplicht. mousemove en niet mouseover, want een muis die // toevallig boven de lijst ligt mag de pijltjes niet blijven overrulen. list.addEventListener('mousemove', function (e) { var li = e.target; while (li && li.tagName !== 'LI') li = li.parentNode; if (!li || !li.hasAttribute('data-i')) return; var i = parseInt(li.getAttribute('data-i'), 10); // Geen scroll: de regel ligt per definitie al onder de cursor en dus in beeld. if (i !== active) setActive(i, false); }); list.addEventListener('mousedown', function (e) { e.preventDefault(); }); list.addEventListener('click', function (e) { var li = e.target; while (li && li.tagName !== 'LI') li = li.parentNode; if (li && li.hasAttribute('data-i')) choose(parseInt(li.getAttribute('data-i'), 10)); }); function onSelectChange() { updateWis(); if (!open) input.value = currentLabel(); } select.addEventListener('change', onSelectChange); // De app vervangt de optielijst (bv. "Inclusief niet-raamcontractartikelen"). var resync = debounce(function () { syncFromSelect(); if (open) render(zoek, true); }, 50); var obs = new MutationObserver(resync); obs.observe(select, { childList: true, subtree: true }); wrap.__selectSearchDetach = detach; wrap.__selectSearchSelect = select; // Haak voor remember(): de opties veranderen niet, alleen het recent-blok, dus de // MutationObserver merkt er niets van. select.__selectSearchSync = function () { syncFromSelect(); if (open) render(zoek, true); }; syncFromSelect(); } /* ---------------------------------------------------------------- scan */ function eligible(s) { return !s.multiple && s.size <= 1 && !s.disabled && !s.dataset.ssEnhanced && !s.hasAttribute('data-nosearch') && s.options.length >= MIN_OPTIONS; } function scanDoc(doc) { var selects = doc.querySelectorAll('select'); for (var i = 0; i < selects.length; i++) { if (eligible(selects[i])) { try { enhance(selects[i]); } catch (e) { /* laat de pagina met rust */ } } } // Wrappers waarvan de app het originele select heeft weggegooid opruimen. var wraps = doc.querySelectorAll('.select-search-wrap'); for (var w = 0; w < wraps.length; w++) { var sel = wraps[w].__selectSearchSelect; if (sel && !doc.contains(sel) && wraps[w].parentNode) { wraps[w].parentNode.removeChild(wraps[w]); } } var frames = doc.querySelectorAll('iframe,frame'); for (var f = 0; f < frames.length; f++) { try { var fd = frames[f].contentDocument; if (fd && fd.body) { attach(fd); } } catch (e) { /* cross-origin */ } } } var attached = []; function attach(doc) { if (attached.indexOf(doc) === -1) { attached.push(doc); new MutationObserver(debounce(function () { scanDoc(doc); }, 120)) .observe(doc.documentElement, { childList: true, subtree: true }); } scanDoc(doc); } function scan() { for (var i = 0; i < attached.length; i++) scanDoc(attached[i]); attach(document); } window.__selectSearchScan = scan; // Voor wie pas mag onthouden nadat de server bevestigd heeft, zie data-ss-recent="manual". // Eerste argument mag de select zelf zijn of rechtstreeks de veldsleutel. window.selectSearch = { remember: function (selectOrKey, value, label) { var sleutel = typeof selectOrKey === 'string' ? selectOrKey : recentKey(selectOrKey); if (!sleutel || !value) return; pushRecent(sleutel, value, label == null ? String(value) : String(label)); if (selectOrKey && typeof selectOrKey.__selectSearchSync === 'function') { selectOrKey.__selectSearchSync(); } }, forget: function (selectOrKey, value) { var sleutel = typeof selectOrKey === 'string' ? selectOrKey : recentKey(selectOrKey); if (!sleutel || !value) return; dropRecent(sleutel, value); if (selectOrKey && typeof selectOrKey.__selectSearchSync === 'function') { selectOrKey.__selectSearchSync(); } } }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function () { attach(document); }); } else { attach(document); } })();