From 3e610366fe3b2482ce2240742100f5ffa06c8b8e Mon Sep 17 00:00:00 2001 From: Glenn Goossens <48956933+glgoose@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:51:05 +0100 Subject: [PATCH] select-search: drop bookmarklet and build step, .user.js is the single source Co-Authored-By: Claude Sonnet 5.5 --- AGENTS.md | 2 +- bin/dev-stub | 12 +- scripts/select-search/README.md | 21 +- scripts/select-search/bookmarklet.txt | 1 - scripts/select-search/build.sh | 64 -- scripts/select-search/src/select-search.js | 814 ------------------ .../select-search/src/userscript-header.txt | 16 - 7 files changed, 4 insertions(+), 926 deletions(-) delete mode 100644 scripts/select-search/bookmarklet.txt delete mode 100755 scripts/select-search/build.sh delete mode 100644 scripts/select-search/src/select-search.js delete mode 100644 scripts/select-search/src/userscript-header.txt diff --git a/AGENTS.md b/AGENTS.md index 6a1442f..b836dd7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,7 +31,7 @@ Eigenschappen van de stub: - **Metadata komt uit de stub**, niet uit de required file. `@match`, `@grant`, `@run-at` en `@require` moeten erin staan; `dev-stub` neemt ze over. Opnieuw genereren en installeren is alleen nodig als die wijzigen. - **Geen `@downloadURL` en `@updateURL`.** `@updateURL none` geeft een eslint-waarschuwing in de editor. Een rij die ooit van de raw-URL kwam, biedt alsnog updates aan die de stub overschrijven: verwijder die rij en maak de stub opnieuw via **Create a new script**. - **Vaste `0.0.0-dev`**, geen bump nodig: de code komt bij elke paginaload van schijf. -- **Met `src/`** wijst `@require` daarnaar. Opslaan en herladen volstaat; `build.sh` draai je pas bij publiceren. +- **`@require` wijst naar het script zelf.** Opslaan en de pagina herladen volstaat. - **Vereist** dat **Allow access to file URLs** aanstaat (`chrome://extensions`, Details). ## Publiceren diff --git a/bin/dev-stub b/bin/dev-stub index 4ad8da5..982ee6f 100755 --- a/bin/dev-stub +++ b/bin/dev-stub @@ -67,19 +67,9 @@ for f in "$dir"/*.user.js; do done if [ -z "$src" ]; then echo "dev-stub: geen *.user.js in $dir" >&2 - echo "dev-stub: bij een script met een build: draai eerst ./build.sh" >&2 exit 1 fi -# Heeft de map een build, dan wijst @require naar de bron in plaats van naar het -# gebouwde bestand: zo volstaat opslaan en de pagina herladen, zonder build.sh -# ertussen. De metadata blijft wel uit het gebouwde script komen, want de bron -# heeft geen ==UserScript==-blok. -code="$src" -if [ -f "$dir/src/$(basename "$dir").js" ]; then - code="$dir/src/$(basename "$dir").js" -fi - # Eén metadataveld uit de kop van het echte script halen. meta() { sed -n 's|^// @'"$1"' *\(.*[^ ]\) *$|\1|p' "$src" @@ -123,7 +113,7 @@ fi meta resource | while IFS= read -r value; do [ -n "$value" ] && printf '// @%-12s %s\n' resource "$value" done - printf '// @%-12s file://%s\n' require "$code" + printf '// @%-12s file://%s\n' require "$src" for key in grant connect run-at noframes; do meta "$key" | while IFS= read -r value; do [ -n "$value" ] && printf '// @%-12s %s\n' "$key" "$value" diff --git a/scripts/select-search/README.md b/scripts/select-search/README.md index 11d0c9b..a595858 100644 --- a/scripts/select-search/README.md +++ b/scripts/select-search/README.md @@ -95,33 +95,16 @@ stil: is de opslag vol of geblokkeerd, dan verdwijnt het recent-blok en werkt de | Niets gebeurt op de pagina | Check in het Tampermonkey-dashboard of de URL onder `@match` valt | | Formulier zit in een iframe van een ander domein | Niet op te lossen, browserbeperking | -## Bookmarklet - -Alternatief zonder extensie, voor wie geen extensies mag installeren. Werkt overal, maar je klikt -hem zelf aan per pagina. - -1. Kopieer de inhoud van [`bookmarklet.txt`](bookmarklet.txt). -2. Maak een bladwijzer met naam `Zoek in dropdown` en die tekst als URL. -3. Klik hem aan op de pagina met de dropdown. Nog eens klikken doet een herscan. - -Sites met een strikte Content-Security-Policy blokkeren bookmarklets. Dan is de extensie de enige -route. - ## Ontwikkelen -`src/select-search.js` bevat de volledige logica, zonder dependencies. +`select-search.user.js` bevat de volledige logica, zonder dependencies. Test met de fixture: ```sh -./build.sh # genereert select-search.user.js en bookmarklet.txt python3 -m http.server 8731 # open http://localhost:8731/test/fixture.html ``` -Hoog bij elke wijziging `@version` op in `src/userscript-header.txt`, anders krijgt niemand de -update binnen. - Tijdens het ontwikkelen laad je het script via een dev-stub in plaats van een geplakte kopie, -zie [Ontwikkelen](../../AGENTS.md#dev-stub-vs-gebruik). Draai eerst `./build.sh`, want de stub wijst -naar `select-search.user.js`. +zie [Ontwikkelen](../../AGENTS.md#dev-stub-vs-gebruik). Hoog bij elke wijziging `@version` op. ## Licentie diff --git a/scripts/select-search/bookmarklet.txt b/scripts/select-search/bookmarklet.txt deleted file mode 100644 index f61eb55..0000000 --- a/scripts/select-search/bookmarklet.txt +++ /dev/null @@ -1 +0,0 @@ -javascript:/*%0A%20*%20select-search%20-%20maakt%20elke%20%3Cselect%3E%20met%20veel%20opties%20doorzoekbaar.%0A%20*%0A%20*%20Het%20originele%20%3Cselect%3E%20blijft%20in%20de%20DOM%20en%20blijft%20de%20bron%20van%20waarheid:%0A%20*%20het%20wordt%20enkel%20visueel%20verborgen.%20Selecteren%20gebeurt%20via%20selectedIndex%20+%0A%20*%20native%20input/change%20events,%20zodat%20de%20applicatie%20niets%20merkt%20van%20de%20ingreep.%0A%20*/%0A(function%20()%20%7B%0A%20%20'use%20strict';%0A%0A%20%20if%20(window.__selectSearchLoaded)%20%7B%0A%20%20%20%20if%20(typeof%20window.__selectSearchScan%20===%20'function')%20window.__selectSearchScan();%0A%20%20%20%20return;%0A%20%20%7D%0A%20%20window.__selectSearchLoaded%20=%20true;%0A%0A%20%20var%20MIN_OPTIONS%20=%2010;%0A%20%20var%20MAX_RENDER%20=%20200;%0A%20%20var%20LIJST_BREED%20=%20420;%0A%20%20var%20MARGE%20=%208;%0A%20%20var%20uid%20=%200;%0A%20%20var%20RECENT_KEY%20=%20'selectSearchRecent';%0A%20%20var%20RECENT_MAX%20=%205;%0A%20%20var%20RECENT_FIELDS%20=%2040;%0A%0A%20%20/*%20----------------------------------------------------------------%20utils%20*/%0A%0A%20%20function%20normMap(s)%20%7B%0A%20%20%20%20var%20out%20=%20'',%20map%20=%20%5B%5D;%0A%20%20%20%20for%20(var%20i%20=%200;%20i%20%3C%20s.length;%20i++)%20%7B%0A%20%20%20%20%20%20var%20c%20=%20s.charAt(i).normalize('NFD').replace(/%5B%5Cu0300-%5Cu036f%5D/g,%20'').toLowerCase();%0A%20%20%20%20%20%20for%20(var%20j%20=%200;%20j%20%3C%20c.length;%20j++)%20%7B%20out%20+=%20c.charAt(j);%20map.push(i);%20%7D%0A%20%20%20%20%7D%0A%20%20%20%20return%20%7B%20n:%20out,%20map:%20map%20%7D;%0A%20%20%7D%0A%0A%20%20function%20escapeHtml(s)%20%7B%0A%20%20%20%20return%20s.replace(/&/g,%20'&').replace(/%3C/g,%20'<').replace(/%3E/g,%20'>');%0A%20%20%7D%0A%0A%20%20function%20tokenize(q)%20%7B%0A%20%20%20%20return%20normMap(q).n.split(/%5Cs+/).filter(function%20(t)%20%7B%20return%20t.length%20%3E%200;%20%7D);%0A%20%20%7D%0A%0A%20%20function%20debounce(fn,%20ms)%20%7B%0A%20%20%20%20var%20t%20=%20null;%0A%20%20%20%20return%20function%20()%20%7B%0A%20%20%20%20%20%20if%20(t)%20clearTimeout(t);%0A%20%20%20%20%20%20t%20=%20setTimeout(function%20()%20%7B%20t%20=%20null;%20fn();%20%7D,%20ms);%0A%20%20%20%20%7D;%0A%20%20%7D%0A%0A%20%20/*%20---------------------------------------------------------------%20recent%20*/%0A%0A%0A%20%20function%20readStore()%20%7B%0A%20%20%20%20var%20raw;%0A%20%20%20%20try%20%7B%20raw%20=%20localStorage.getItem(RECENT_KEY);%20%7D%20catch%20(e)%20%7B%20return%20null;%20%7D%0A%20%20%20%20if%20(!raw)%20return%20null;%0A%20%20%20%20var%20data;%0A%20%20%20%20try%20%7B%20data%20=%20JSON.parse(raw);%20%7D%20catch%20(e)%20%7B%20return%20null;%20%7D%0A%20%20%20%20if%20(!data%20%7C%7C%20data.v%20!==%201%20%7C%7C%20!data.fields%20%7C%7C%20typeof%20data.fields%20!==%20'object')%20return%20null;%0A%20%20%20%20return%20data;%0A%20%20%7D%0A%0A%20%20function%20writeStore(data)%20%7B%0A%20%20%20%20try%20%7B%20localStorage.setItem(RECENT_KEY,%20JSON.stringify(data));%20%7D%20catch%20(e)%20%7B%20/*%20quota,%20stil%20*/%20%7D%0A%20%20%7D%0A%0A%20%20function%20readRecent(sleutel)%20%7B%0A%20%20%20%20var%20data%20=%20readStore();%0A%20%20%20%20if%20(!data%20%7C%7C%20!Object.prototype.hasOwnProperty.call(data.fields,%20sleutel))%20return%20%5B%5D;%0A%20%20%20%20var%20veld%20=%20data.fields%5Bsleutel%5D;%0A%20%20%20%20return%20veld%20&&%20veld.items%20&&%20veld.items.length%20?%20veld.items%20:%20%5B%5D;%0A%20%20%7D%0A%0A%20%20function%20writeRecent(sleutel,%20lijst)%20%7B%0A%20%20%20%20var%20data%20=%20readStore()%20%7C%7C%20%7B%20v:%201,%20fields:%20%7B%7D%20%7D;%0A%20%20%20%20data.fields%5Bsleutel%5D%20=%20%7B%20ts:%20Date.now(),%20items:%20lijst.slice(0,%20RECENT_MAX)%20%7D;%0A%20%20%20%20var%20namen%20=%20Object.keys(data.fields);%0A%20%20%20%20if%20(namen.length%20%3E%20RECENT_FIELDS)%20%7B%0A%20%20%20%20%20%20namen.sort(function%20(a,%20b)%20%7B%20return%20(data.fields%5Ba%5D.ts%20%7C%7C%200)%20-%20(data.fields%5Bb%5D.ts%20%7C%7C%200);%20%7D);%0A%20%20%20%20%20%20for%20(var%20i%20=%200;%20i%20%3C%20namen.length%20-%20RECENT_FIELDS;%20i++)%20delete%20data.fields%5Bnamen%5Bi%5D%5D;%0A%20%20%20%20%7D%0A%20%20%20%20writeStore(data);%0A%20%20%7D%0A%0A%20%20function%20pushRecent(sleutel,%20value,%20label)%20%7B%0A%20%20%20%20if%20(!sleutel%20%7C%7C%20!value%20%7C%7C%20!label)%20return;%0A%20%20%20%20var%20lijst%20=%20readRecent(sleutel).filter(function%20(e)%20%7B%20return%20e%5B0%5D%20!==%20value;%20%7D);%0A%20%20%20%20lijst.unshift(%5Bvalue,%20label%5D);%0A%20%20%20%20writeRecent(sleutel,%20lijst);%0A%20%20%7D%0A%0A%20%20function%20dropRecent(sleutel,%20value)%20%7B%0A%20%20%20%20var%20lijst%20=%20readRecent(sleutel);%0A%20%20%20%20var%20uit%20=%20lijst.filter(function%20(e)%20%7B%20return%20e%5B0%5D%20!==%20value;%20%7D);%0A%20%20%20%20if%20(uit.length%20===%20lijst.length)%20return%20false;%0A%20%20%20%20writeRecent(sleutel,%20uit);%0A%20%20%20%20return%20true;%0A%20%20%7D%0A%0A%20%20function%20recentKey(select)%20%7B%0A%20%20%20%20if%20(select.getAttribute%20&&%20select.getAttribute('data-ss-recent')%20===%20'off')%20return%20'';%0A%20%20%20%20if%20(/%5C/login%5C.aspx$/i.test((select.ownerDocument.defaultView%20%7C%7C%20window).location.pathname))%20return%20'';%0A%20%20%20%20var%20expliciet%20=%20select.getAttribute%20&&%20select.getAttribute('data-ss-recent-key');%0A%20%20%20%20if%20(expliciet)%20return%20expliciet;%0A%20%20%20%20var%20ruw%20=%20select.name%20%7C%7C%20select.id%20%7C%7C%20'';%0A%20%20%20%20if%20(!ruw)%20return%20'';%0A%20%20%20%20return%20ruw.split(/%5B$_%5D/).filter(function%20(deel)%20%7B%0A%20%20%20%20%20%20return%20deel%20&&%20!/%5Ectl%5Cd+$/i.test(deel)%20&&%20!/%5E%5Cd+$/.test(deel);%0A%20%20%20%20%7D).join('_');%0A%20%20%7D%0A%0A%20%20/*%20------------------------------------------------------------%20highlight%20*/%0A%0A%20%20function%20matchRanges(item,%20tokens)%20%7B%0A%20%20%20%20if%20(!tokens.length)%20return%20%5B%5D;%0A%20%20%20%20var%20ranges%20=%20%5B%5D;%0A%20%20%20%20for%20(var%20t%20=%200;%20t%20%3C%20tokens.length;%20t++)%20%7B%0A%20%20%20%20%20%20var%20tok%20=%20tokens%5Bt%5D,%20from%20=%200,%20idx;%0A%20%20%20%20%20%20while%20((idx%20=%20item.norm.n.indexOf(tok,%20from))%20!==%20-1)%20%7B%0A%20%20%20%20%20%20%20%20var%20start%20=%20item.norm.map%5Bidx%5D;%0A%20%20%20%20%20%20%20%20var%20end%20=%20item.norm.map%5Bidx%20+%20tok.length%20-%201%5D%20+%201;%0A%20%20%20%20%20%20%20%20ranges.push(%5Bstart,%20end%5D);%0A%20%20%20%20%20%20%20%20from%20=%20idx%20+%20tok.length;%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%20%20if%20(!ranges.length)%20return%20ranges;%0A%20%20%20%20ranges.sort(function%20(a,%20b)%20%7B%20return%20a%5B0%5D%20-%20b%5B0%5D;%20%7D);%0A%20%20%20%20var%20merged%20=%20%5Branges%5B0%5D.slice()%5D;%0A%20%20%20%20for%20(var%20r%20=%201;%20r%20%3C%20ranges.length;%20r++)%20%7B%0A%20%20%20%20%20%20var%20last%20=%20merged%5Bmerged.length%20-%201%5D;%0A%20%20%20%20%20%20if%20(ranges%5Br%5D%5B0%5D%20%3C=%20last%5B1%5D)%20last%5B1%5D%20=%20Math.max(last%5B1%5D,%20ranges%5Br%5D%5B1%5D);%0A%20%20%20%20%20%20else%20merged.push(ranges%5Br%5D.slice());%0A%20%20%20%20%7D%0A%20%20%20%20return%20merged;%0A%20%20%7D%0A%0A%20%20function%20highlight(item,%20ranges,%20from,%20to)%20%7B%0A%20%20%20%20var%20html%20=%20'',%20pos%20=%20from;%0A%20%20%20%20for%20(var%20m%20=%200;%20m%20%3C%20ranges.length;%20m++)%20%7B%0A%20%20%20%20%20%20var%20a%20=%20Math.max(ranges%5Bm%5D%5B0%5D,%20from),%20b%20=%20Math.min(ranges%5Bm%5D%5B1%5D,%20to);%0A%20%20%20%20%20%20if%20(b%20%3C=%20a)%20continue;%0A%20%20%20%20%20%20html%20+=%20escapeHtml(item.label.slice(pos,%20a));%0A%20%20%20%20%20%20html%20+=%20'%3Cmark%3E'%20+%20escapeHtml(item.label.slice(a,%20b))%20+%20'%3C/mark%3E';%0A%20%20%20%20%20%20pos%20=%20b;%0A%20%20%20%20%7D%0A%20%20%20%20return%20html%20+%20escapeHtml(item.label.slice(pos,%20to));%0A%20%20%7D%0A%0A%20%20function%20splitPunt(label)%20%7B%0A%20%20%20%20var%20sep%20=%20label.indexOf('%20-%20');%0A%20%20%20%20if%20(sep%20%3C=%200)%20return%20-1;%0A%20%20%20%20if%20(label.lastIndexOf('%20',%20sep%20-%201)%20!==%20-1)%20return%20-1;%0A%20%20%20%20return%20sep%20+%203;%0A%20%20%7D%0A%0A%20%20/*%20-----------------------------------------------------------------%20css%20*/%0A%0A%20%20var%20CSS%20=%20%5B%0A%20%20%20%20':host%7Ball:initial;display:inline-block;vertical-align:middle;max-width:100%25%7D',%0A%20%20%20%20'*%7Bbox-sizing:border-box;font:inherit%7D',%0A%20%20%20%20'.box%7Bposition:relative;display:block;width:100%25%7D',%0A%20%20%20%20'input%7Bwidth:100%25;padding:2px%2022px%202px%204px;border:1px%20solid%20#767676;border-radius:2px;',%0A%20%20%20%20'background:#fff;color:#000;font:13px/1.4%20system-ui,-apple-system,%22Segoe%20UI%22,sans-serif;',%0A%20%20%20%20'height:100%25;min-height:22px%7D',%0A%20%20%20%20'input:focus%7Boutline:none;border-color:#0b57d0;box-shadow:0%200%200%201px%20#0b57d0%7D',%0A%20%20%20%20'input::placeholder%7Bcolor:#767676;opacity:1%7D',%0A%20%20%20%20'.caret%7Bposition:absolute;right:6px;top:50%25;transform:translateY(-50%25);pointer-events:none;',%0A%20%20%20%20'border:4px%20solid%20transparent;border-top-color:#444;margin-top:2px%7D',%0A%20%20%20%20'.wis%7Bposition:absolute;right:2px;top:50%25;transform:translateY(-50%25);width:17px;height:17px;',%0A%20%20%20%20'display:flex;align-items:center;justify-content:center;color:#767676;cursor:pointer;',%0A%20%20%20%20'visibility:hidden%7D',%0A%20%20%20%20'.box.wisbaar.gevuld:hover%20.wis,.box.wisbaar.gevuld:focus-within%20.wis%7Bvisibility:visible%7D',%0A%20%20%20%20'.box.wisbaar.gevuld:hover%20.caret,.box.wisbaar.gevuld:focus-within%20.caret%7Bvisibility:hidden%7D',%0A%20%20%20%20'.wis:hover%7Bcolor:#000%7D',%0A%20%20%20%20'ul%7Bposition:fixed;z-index:2147483647;margin:0;padding:2px%200;list-style:none;overflow-y:auto;',%0A%20%20%20%20'background:#fff;color:#000;border:1px%20solid%20#b0b0b0;border-radius:3px;',%0A%20%20%20%20'box-shadow:0%204px%2014px%20rgba(0,0,0,.22);',%0A%20%20%20%20'font:13px/1.5%20system-ui,-apple-system,%22Segoe%20UI%22,sans-serif;display:none%7D',%0A%20%20%20%20'ul.open%7Bdisplay:block%7D',%0A%20%20%20%20'li%7Bpadding:4px%208px;cursor:pointer;line-height:1.3;',%0A%20%20%20%20'padding-left:calc(8px%20+%202.5ch);text-indent:-2.5ch;overflow-wrap:break-word%7D',%0A%20%20%20%20'li%20.c%7Bwhite-space:pre%7D',%0A%20%20%20%20'li%5Bdata-i%5D+li%5Bdata-i%5D%7Bborder-top:1px%20solid%20#eee%7D',%0A%20%20%20%20'li.kop%7Bpadding:6px%208px%202px;padding-left:8px;text-indent:0;color:#777;font-size:11px;',%0A%20%20%20%20'font-weight:600;cursor:default%7D',%0A%20%20%20%20'li.active%7Bbackground:#0b57d0;color:#fff;border-top-color:#0b57d0%7D',%0A%20%20%20%20'li.active%20mark%7Bbackground:#ffe08a;color:#000%7D',%0A%20%20%20%20'li.dis%7Bcolor:#999;cursor:default%7D',%0A%20%20%20%20'li.note%7Bcolor:#666;cursor:default;font-style:italic;padding-top:5px%7D',%0A%20%20%20%20'li.empty%7Bcolor:#666;cursor:default%7D',%0A%20%20%20%20'mark%7Bbackground:#ffe08a;color:inherit;padding:0%7D'%0A%20%20%5D.join('');%0A%0A%20%20/*%20-------------------------------------------------------------%20enhance%20*/%0A%0A%20%20function%20enhance(select)%20%7B%0A%20%20%20%20var%20doc%20=%20select.ownerDocument;%0A%20%20%20%20var%20win%20=%20doc.defaultView%20%7C%7C%20window;%0A%20%20%20%20var%20id%20=%20'ss'%20+%20(++uid);%0A%20%20%20%20var%20recentSleutel%20=%20recentKey(select);%0A%20%20%20%20var%20recentManueel%20=%20select.getAttribute('data-ss-recent')%20===%20'manual';%0A%0A%20%20%20%20var%20rect%20=%20select.getBoundingClientRect();%0A%20%20%20%20var%20wrap%20=%20doc.createElement('span');%0A%20%20%20%20wrap.className%20=%20'select-search-wrap';%0A%20%20%20%20if%20(rect.width)%20wrap.style.width%20=%20rect.width%20+%20'px';%0A%20%20%20%20if%20(rect.height)%20wrap.style.height%20=%20rect.height%20+%20'px';%0A%20%20%20%20select.parentNode.insertBefore(wrap,%20select.nextSibling);%0A%0A%20%20%20%20var%20root%20=%20wrap.attachShadow(%7B%20mode:%20'open'%20%7D);%0A%20%20%20%20var%20style%20=%20doc.createElement('style');%0A%20%20%20%20style.textContent%20=%20CSS;%0A%20%20%20%20var%20box%20=%20doc.createElement('span');%0A%20%20%20%20box.className%20=%20'box';%0A%20%20%20%20var%20input%20=%20doc.createElement('input');%0A%20%20%20%20input.type%20=%20'text';%0A%20%20%20%20input.setAttribute('role',%20'combobox');%0A%20%20%20%20input.setAttribute('aria-expanded',%20'false');%0A%20%20%20%20input.setAttribute('aria-autocomplete',%20'list');%0A%20%20%20%20input.setAttribute('aria-controls',%20id%20+%20'-list');%0A%20%20%20%20input.setAttribute('autocomplete',%20'off');%0A%20%20%20%20input.setAttribute('spellcheck',%20'false');%0A%20%20%20%20input.title%20=%20'Typ%20om%20te%20zoeken.%20Alt+klik%20zet%20het%20originele%20keuzemenu%20terug.';%0A%20%20%20%20var%20caret%20=%20doc.createElement('span');%0A%20%20%20%20caret.className%20=%20'caret';%0A%20%20%20%20var%20wis%20=%20doc.createElement('span');%0A%20%20%20%20wis.className%20=%20'wis';%0A%20%20%20%20wis.setAttribute('role',%20'button');%0A%20%20%20%20wis.setAttribute('aria-label',%20'Wissen');%0A%20%20%20%20wis.innerHTML%20=%20'%3Csvg%20width=%2211%22%20height=%2211%22%20viewBox=%220%200%2012%2012%22%20fill=%22none%22%20'%0A%20%20%20%20%20%20+%20'stroke=%22currentColor%22%20stroke-width=%221.6%22%20stroke-linecap=%22round%22%20aria-hidden=%22true%22%3E'%0A%20%20%20%20%20%20+%20'%3Cpath%20d=%22M2.5%202.5l7%207M9.5%202.5l-7%207%22/%3E%3C/svg%3E';%0A%20%20%20%20var%20list%20=%20doc.createElement('ul');%0A%20%20%20%20list.id%20=%20id%20+%20'-list';%0A%20%20%20%20list.setAttribute('role',%20'listbox');%0A%20%20%20%20box.appendChild(input);%0A%20%20%20%20box.appendChild(wis);%0A%20%20%20%20box.appendChild(caret);%0A%20%20%20%20root.appendChild(style);%0A%20%20%20%20root.appendChild(box);%0A%20%20%20%20root.appendChild(list);%0A%0A%20%20%20%20var%20prevStyle%20=%20select.getAttribute('style');%0A%20%20%20%20select.style.position%20=%20'absolute';%0A%20%20%20%20select.style.opacity%20=%20'0';%0A%20%20%20%20select.style.pointerEvents%20=%20'none';%0A%20%20%20%20select.style.width%20=%20(rect.width%20%7C%7C%201)%20+%20'px';%0A%20%20%20%20select.style.height%20=%20(rect.height%20%7C%7C%201)%20+%20'px';%0A%20%20%20%20select.dataset.ssEnhanced%20=%20'1';%0A%0A%20%20%20%20var%20items%20=%20%5B%5D,%20shown%20=%20%5B%5D,%20active%20=%20-1,%20open%20=%20false;%0A%20%20%20%20var%20zoek%20=%20'';%0A%20%20%20%20var%20leegIndex%20=%20-1,%20leegLabel%20=%20'';%0A%0A%20%20%20%20function%20readOptions()%20%7B%0A%20%20%20%20%20%20items%20=%20%5B%5D;%0A%20%20%20%20%20%20leegIndex%20=%20-1;%0A%20%20%20%20%20%20leegLabel%20=%20'';%0A%20%20%20%20%20%20var%20zonderWaarde%20=%20-1,%20zonderWaardeLabel%20=%20'',%20blanco%20=%20-1;%0A%20%20%20%20%20%20var%20groepen%20=%20%5B%5D,%20gi;%0A%20%20%20%20%20%20var%20wil%20=%20%7B%7D,%20recentLijst%20=%20recentSleutel%20?%20readRecent(recentSleutel)%20:%20%5B%5D;%0A%20%20%20%20%20%20for%20(var%20r%20=%200;%20r%20%3C%20recentLijst.length;%20r++)%20wil%5B'v:'%20+%20recentLijst%5Br%5D%5B0%5D%5D%20=%20r;%0A%20%20%20%20%20%20var%20recentItems%20=%20%5B%5D,%20gewoon%20=%20%5B%5D;%0A%20%20%20%20%20%20for%20(var%20i%20=%200;%20i%20%3C%20select.options.length;%20i++)%20%7B%0A%20%20%20%20%20%20%20%20var%20o%20=%20select.options%5Bi%5D;%0A%20%20%20%20%20%20%20%20var%20label%20=%20(o.textContent%20%7C%7C%20'').replace(/%5Cs+/g,%20'%20').trim();%0A%20%20%20%20%20%20%20%20if%20(!o.value%20%7C%7C%20!label)%20%7B%0A%20%20%20%20%20%20%20%20%20%20if%20(!o.value)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(zonderWaarde%20===%20-1)%20%7B%20zonderWaarde%20=%20i;%20zonderWaardeLabel%20=%20label;%20%7D%0A%20%20%20%20%20%20%20%20%20%20%7D%20else%20if%20(blanco%20===%20-1)%20blanco%20=%20i;%0A%20%20%20%20%20%20%20%20%20%20continue;%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20var%20plek%20=%20wil%5B'v:'%20+%20o.value%5D;%0A%20%20%20%20%20%20%20%20if%20(plek%20!==%20undefined%20&&%20recentItems%5Bplek%5D%20===%20undefined)%20%7B%0A%20%20%20%20%20%20%20%20%20%20recentItems%5Bplek%5D%20=%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20index:%20i,%0A%20%20%20%20%20%20%20%20%20%20%20%20label:%20label,%0A%20%20%20%20%20%20%20%20%20%20%20%20group:%20'recent',%0A%20%20%20%20%20%20%20%20%20%20%20%20gi:%200,%0A%20%20%20%20%20%20%20%20%20%20%20%20disabled:%20o.disabled,%0A%20%20%20%20%20%20%20%20%20%20%20%20removable:%20true,%0A%20%20%20%20%20%20%20%20%20%20%20%20mru:%20true,%0A%20%20%20%20%20%20%20%20%20%20%20%20norm:%20normMap(label)%0A%20%20%20%20%20%20%20%20%20%20%7D;%0A%20%20%20%20%20%20%20%20%20%20continue;%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20var%20groep%20=%20o.parentNode%20&&%20o.parentNode.tagName%20===%20'OPTGROUP'%20?%20o.parentNode%20:%20null;%0A%20%20%20%20%20%20%20%20var%20grp%20=%20groep%20?%20(groep.label%20%7C%7C%20'')%20:%20'';%0A%20%20%20%20%20%20%20%20if%20(!grp)%20gi%20=%201;%0A%20%20%20%20%20%20%20%20else%20%7B%0A%20%20%20%20%20%20%20%20%20%20gi%20=%20groepen.indexOf(grp);%0A%20%20%20%20%20%20%20%20%20%20if%20(gi%20===%20-1)%20%7B%20groepen.push(grp);%20gi%20=%20groepen.length%20+%201;%20%7D%20else%20gi%20+=%202;%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20gewoon.push(%7B%0A%20%20%20%20%20%20%20%20%20%20index:%20i,%0A%20%20%20%20%20%20%20%20%20%20label:%20label,%0A%20%20%20%20%20%20%20%20%20%20group:%20grp,%0A%20%20%20%20%20%20%20%20%20%20gi:%20gi,%0A%20%20%20%20%20%20%20%20%20%20disabled:%20o.disabled,%0A%20%20%20%20%20%20%20%20%20%20removable:%20!!(o.dataset.removable%20%7C%7C%20(groep%20&&%20groep.dataset.removable)),%0A%20%20%20%20%20%20%20%20%20%20norm:%20normMap(label)%0A%20%20%20%20%20%20%20%20%7D);%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20if%20(zonderWaarde%20!==%20-1)%20%7B%20leegIndex%20=%20zonderWaarde;%20leegLabel%20=%20zonderWaardeLabel;%20%7D%0A%20%20%20%20%20%20else%20if%20(blanco%20!==%20-1)%20leegIndex%20=%20blanco;%0A%20%20%20%20%20%20for%20(var%20k%20=%200;%20k%20%3C%20recentItems.length;%20k++)%20%7B%0A%20%20%20%20%20%20%20%20if%20(recentItems%5Bk%5D)%20items.push(recentItems%5Bk%5D);%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20if%20(items.length)%20%7B%0A%20%20%20%20%20%20%20%20for%20(var%20g%20=%200;%20g%20%3C%20gewoon.length;%20g++)%20%7B%0A%20%20%20%20%20%20%20%20%20%20if%20(!gewoon%5Bg%5D.group)%20gewoon%5Bg%5D.group%20=%20'alle';%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20items%20=%20items.concat(gewoon);%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20currentLabel()%20%7B%0A%20%20%20%20%20%20var%20o%20=%20select.options%5Bselect.selectedIndex%5D;%0A%20%20%20%20%20%20if%20(!o%20%7C%7C%20!o.value)%20return%20'';%0A%20%20%20%20%20%20return%20(o.textContent%20%7C%7C%20'').replace(/%5Cs+/g,%20'%20').trim();%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20updateWis()%20%7B%0A%20%20%20%20%20%20box.classList.toggle('wisbaar',%20leegIndex%20!==%20-1);%0A%20%20%20%20%20%20box.classList.toggle('gevuld',%20!!currentLabel());%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20syncFromSelect()%20%7B%0A%20%20%20%20%20%20readOptions();%0A%20%20%20%20%20%20input.placeholder%20=%20select.getAttribute('data-placeholder')%20%7C%7C%20leegLabel;%0A%20%20%20%20%20%20updateWis();%0A%20%20%20%20%20%20if%20(!open)%20input.value%20=%20currentLabel();%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20clear()%20%7B%0A%20%20%20%20%20%20if%20(leegIndex%20===%20-1)%20return;%0A%20%20%20%20%20%20select.selectedIndex%20=%20leegIndex;%0A%20%20%20%20%20%20input.value%20=%20'';%0A%20%20%20%20%20%20updateWis();%0A%20%20%20%20%20%20closeList(false);%0A%20%20%20%20%20%20fire(select,%20'input');%0A%20%20%20%20%20%20fire(select,%20'change');%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20position()%20%7B%0A%20%20%20%20%20%20var%20r%20=%20input.getBoundingClientRect();%0A%20%20%20%20%20%20var%20below%20=%20win.innerHeight%20-%20r.bottom%20-%206;%0A%20%20%20%20%20%20var%20above%20=%20r.top%20-%206;%0A%20%20%20%20%20%20var%20down%20=%20below%20%3E=%20160%20%7C%7C%20below%20%3E=%20above;%0A%20%20%20%20%20%20var%20wens%20=%20Math.min(Math.max(r.width,%20LIJST_BREED),%20win.innerWidth%20-%20MARGE%20*%202);%0A%20%20%20%20%20%20list.style.left%20=%20MARGE%20+%20'px';%0A%20%20%20%20%20%20list.style.minWidth%20=%20r.width%20+%20'px';%0A%20%20%20%20%20%20list.style.maxWidth%20=%20wens%20+%20'px';%0A%20%20%20%20%20%20list.style.maxHeight%20=%20Math.max(120,%20(down%20?%20below%20:%20above))%20+%20'px';%0A%20%20%20%20%20%20var%20breed%20=%20list.offsetWidth;%0A%20%20%20%20%20%20list.style.left%20=%20Math.max(MARGE,%20Math.min(r.left,%20win.innerWidth%20-%20MARGE%20-%20breed))%20+%20'px';%0A%20%20%20%20%20%20if%20(down)%20%7B%0A%20%20%20%20%20%20%20%20list.style.top%20=%20r.bottom%20+%202%20+%20'px';%0A%20%20%20%20%20%20%20%20list.style.bottom%20=%20'auto';%0A%20%20%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20%20%20list.style.top%20=%20'auto';%0A%20%20%20%20%20%20%20%20list.style.bottom%20=%20(win.innerHeight%20-%20r.top%20+%202)%20+%20'px';%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20filter(q)%20%7B%0A%20%20%20%20%20%20var%20tokens%20=%20tokenize(q);%0A%20%20%20%20%20%20if%20(!tokens.length)%20return%20items.slice();%0A%20%20%20%20%20%20var%20emmers%20=%20%5B%5D;%0A%20%20%20%20%20%20for%20(var%20i%20=%200;%20i%20%3C%20items.length;%20i++)%20%7B%0A%20%20%20%20%20%20%20%20var%20it%20=%20items%5Bi%5D,%20n%20=%20it.norm.n,%20ok%20=%20true;%0A%20%20%20%20%20%20%20%20for%20(var%20t%20=%200;%20t%20%3C%20tokens.length;%20t++)%20%7B%0A%20%20%20%20%20%20%20%20%20%20if%20(n.indexOf(tokens%5Bt%5D)%20===%20-1)%20%7B%20ok%20=%20false;%20break;%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20if%20(!ok)%20continue;%0A%20%20%20%20%20%20%20%20if%20(!emmers%5Bit.gi%5D)%20emmers%5Bit.gi%5D%20=%20%7B%20starts:%20%5B%5D,%20rest:%20%5B%5D%20%7D;%0A%20%20%20%20%20%20%20%20(n.indexOf(tokens%5B0%5D)%20===%200%20?%20emmers%5Bit.gi%5D.starts%20:%20emmers%5Bit.gi%5D.rest).push(it);%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20var%20out%20=%20%5B%5D;%0A%20%20%20%20%20%20for%20(var%20g%20=%200;%20g%20%3C%20emmers.length;%20g++)%20%7B%0A%20%20%20%20%20%20%20%20if%20(emmers%5Bg%5D)%20out%20=%20out.concat(emmers%5Bg%5D.starts,%20emmers%5Bg%5D.rest);%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20return%20out;%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20render(q,%20behoud)%20%7B%0A%20%20%20%20%20%20var%20vorigeActieve%20=%20active;%0A%20%20%20%20%20%20zoek%20=%20q%20%7C%7C%20'';%0A%20%20%20%20%20%20var%20tokens%20=%20tokenize(q);%0A%20%20%20%20%20%20shown%20=%20filter(q);%0A%20%20%20%20%20%20var%20html%20=%20'';%0A%20%20%20%20%20%20if%20(!shown.length)%20%7B%0A%20%20%20%20%20%20%20%20html%20=%20'%3Cli%20class=%22empty%22%3EGeen%20resultaten%3C/li%3E';%0A%20%20%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20%20%20var%20n%20=%20Math.min(shown.length,%20MAX_RENDER),%20vorige%20=%20null;%0A%20%20%20%20%20%20%20%20for%20(var%20i%20=%200;%20i%20%3C%20n;%20i++)%20%7B%0A%20%20%20%20%20%20%20%20%20%20var%20it%20=%20shown%5Bi%5D;%0A%20%20%20%20%20%20%20%20%20%20if%20(it.group%20&&%20it.group%20!==%20vorige)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20html%20+=%20'%3Cli%20class=%22kop%22%3E'%20+%20escapeHtml(it.group)%20+%20'%3C/li%3E';%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20vorige%20=%20it.group;%0A%20%20%20%20%20%20%20%20%20%20var%20ranges%20=%20matchRanges(it,%20tokens);%0A%20%20%20%20%20%20%20%20%20%20var%20punt%20=%20splitPunt(it.label);%0A%20%20%20%20%20%20%20%20%20%20var%20binnen%20=%20punt%20===%20-1%0A%20%20%20%20%20%20%20%20%20%20%20%20?%20highlight(it,%20ranges,%200,%20it.label.length)%0A%20%20%20%20%20%20%20%20%20%20%20%20:%20'%3Cspan%20class=%22c%22%3E'%20+%20highlight(it,%20ranges,%200,%20punt%20-%201)%20+%20'%3C/span%3E%20'%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20+%20highlight(it,%20ranges,%20punt,%20it.label.length);%0A%20%20%20%20%20%20%20%20%20%20html%20+=%20'%3Cli%20role=%22option%22%20id=%22'%20+%20id%20+%20'-o'%20+%20i%20+%20'%22%20data-i=%22'%20+%20i%20+%20'%22'%0A%20%20%20%20%20%20%20%20%20%20%20%20+%20(it.removable%20?%20'%20data-rm=%221%22'%20:%20'')%0A%20%20%20%20%20%20%20%20%20%20%20%20+%20(it.disabled%20?%20'%20class=%22dis%22%20aria-disabled=%22true%22'%20:%20'')%20+%20'%3E'%0A%20%20%20%20%20%20%20%20%20%20%20%20+%20binnen%0A%20%20%20%20%20%20%20%20%20%20%20%20+%20'%3C/li%3E';%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20if%20(shown.length%20%3E%20n)%20%7B%0A%20%20%20%20%20%20%20%20%20%20html%20+=%20'%3Cli%20class=%22note%22%3Enog%20'%20+%20(shown.length%20-%20n)%20+%20'%20resultaten,%20verfijn%20je%20zoekterm%3C/li%3E';%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20list.innerHTML%20=%20html;%0A%20%20%20%20%20%20shown%20=%20shown.slice(0,%20MAX_RENDER);%0A%20%20%20%20%20%20var%20doel%20=%200;%0A%20%20%20%20%20%20if%20(behoud%20&&%20vorigeActieve%20%3E%200)%20doel%20=%20Math.min(vorigeActieve,%20shown.length%20-%201);%0A%20%20%20%20%20%20setActive(shown.length%20?%20doel%20:%20-1,%20false);%0A%20%20%20%20%20%20if%20(open)%20position();%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20setActive(i,%20scroll)%20%7B%0A%20%20%20%20%20%20var%20nodes%20=%20list.querySelectorAll('li%5Bdata-i%5D');%0A%20%20%20%20%20%20for%20(var%20k%20=%200;%20k%20%3C%20nodes.length;%20k++)%20nodes%5Bk%5D.classList.remove('active');%0A%20%20%20%20%20%20active%20=%20i;%0A%20%20%20%20%20%20if%20(i%20%3C%200%20%7C%7C%20i%20%3E=%20nodes.length)%20%7B%0A%20%20%20%20%20%20%20%20input.removeAttribute('aria-activedescendant');%0A%20%20%20%20%20%20%20%20return;%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20nodes%5Bi%5D.classList.add('active');%0A%20%20%20%20%20%20input.setAttribute('aria-activedescendant',%20nodes%5Bi%5D.id);%0A%20%20%20%20%20%20if%20(scroll%20!==%20false)%20%7B%0A%20%20%20%20%20%20%20%20var%20li%20=%20nodes%5Bi%5D,%20top%20=%20li.offsetTop,%20bot%20=%20top%20+%20li.offsetHeight;%0A%20%20%20%20%20%20%20%20if%20(top%20%3C%20list.scrollTop)%20list.scrollTop%20=%20top;%0A%20%20%20%20%20%20%20%20else%20if%20(bot%20%3E%20list.scrollTop%20+%20list.clientHeight)%20list.scrollTop%20=%20bot%20-%20list.clientHeight;%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20openList(q)%20%7B%0A%20%20%20%20%20%20render(q%20===%20undefined%20?%20''%20:%20q);%0A%20%20%20%20%20%20list.classList.add('open');%0A%20%20%20%20%20%20input.setAttribute('aria-expanded',%20'true');%0A%20%20%20%20%20%20open%20=%20true;%0A%20%20%20%20%20%20position();%0A%20%20%20%20%20%20win.addEventListener('scroll',%20position,%20true);%0A%20%20%20%20%20%20win.addEventListener('resize',%20position);%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20closeList(restore)%20%7B%0A%20%20%20%20%20%20list.classList.remove('open');%0A%20%20%20%20%20%20input.setAttribute('aria-expanded',%20'false');%0A%20%20%20%20%20%20input.removeAttribute('aria-activedescendant');%0A%20%20%20%20%20%20open%20=%20false;%0A%20%20%20%20%20%20active%20=%20-1;%0A%20%20%20%20%20%20win.removeEventListener('scroll',%20position,%20true);%0A%20%20%20%20%20%20win.removeEventListener('resize',%20position);%0A%20%20%20%20%20%20if%20(restore%20!==%20false)%20input.value%20=%20currentLabel();%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20choose(i)%20%7B%0A%20%20%20%20%20%20var%20it%20=%20shown%5Bi%5D;%0A%20%20%20%20%20%20if%20(!it%20%7C%7C%20it.disabled)%20return;%0A%20%20%20%20%20%20select.selectedIndex%20=%20it.index;%0A%20%20%20%20%20%20input.value%20=%20it.label;%0A%20%20%20%20%20%20updateWis();%0A%20%20%20%20%20%20closeList(false);%0A%20%20%20%20%20%20if%20(recentSleutel%20&&%20!recentManueel)%20%7B%0A%20%20%20%20%20%20%20%20pushRecent(recentSleutel,%20select.options%5Bit.index%5D.value,%20it.label);%0A%20%20%20%20%20%20%20%20readOptions();%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20fire(select,%20'input');%0A%20%20%20%20%20%20fire(select,%20'change');%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20fireRemove(it)%20%7B%0A%20%20%20%20%20%20var%20ev;%0A%20%20%20%20%20%20var%20detail%20=%20%7B%20value:%20select.options%5Bit.index%5D.value,%20label:%20it.label,%20index:%20it.index%20%7D;%0A%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20ev%20=%20new%20CustomEvent('select-search:remove',%20%7B%20bubbles:%20true,%20cancelable:%20true,%20detail:%20detail%20%7D);%0A%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20ev%20=%20doc.createEvent('CustomEvent');%0A%20%20%20%20%20%20%20%20ev.initCustomEvent('select-search:remove',%20true,%20true,%20detail);%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20select.dispatchEvent(ev);%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20fire(el,%20type)%20%7B%0A%20%20%20%20%20%20var%20ev;%0A%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20ev%20=%20new%20Event(type,%20%7B%20bubbles:%20true,%20cancelable:%20false%20%7D);%0A%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%0A%20%20%20%20%20%20%20%20ev%20=%20doc.createEvent('HTMLEvents');%0A%20%20%20%20%20%20%20%20ev.initEvent(type,%20true,%20false);%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20el.dispatchEvent(ev);%0A%20%20%20%20%20%20var%20jq%20=%20win.jQuery%20%7C%7C%20win.$;%0A%20%20%20%20%20%20if%20(jq%20&&%20jq.fn%20&&%20typeof%20jq%20===%20'function')%20%7B%0A%20%20%20%20%20%20%20%20try%20%7B%20jq(el).trigger(type);%20%7D%20catch%20(e2)%20%7B%20/*%20stil%20*/%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20detach()%20%7B%0A%20%20%20%20%20%20obs.disconnect();%0A%20%20%20%20%20%20select.removeEventListener('change',%20onSelectChange);%0A%20%20%20%20%20%20win.removeEventListener('scroll',%20position,%20true);%0A%20%20%20%20%20%20win.removeEventListener('resize',%20position);%0A%20%20%20%20%20%20if%20(prevStyle%20===%20null)%20select.removeAttribute('style');%0A%20%20%20%20%20%20else%20select.setAttribute('style',%20prevStyle);%0A%20%20%20%20%20%20delete%20select.dataset.ssEnhanced;%0A%20%20%20%20%20%20delete%20select.__selectSearchSync;%0A%20%20%20%20%20%20select.dataset.nosearch%20=%20'1';%0A%20%20%20%20%20%20if%20(wrap.parentNode)%20wrap.parentNode.removeChild(wrap);%0A%20%20%20%20%7D%0A%0A%20%20%20%20/*%20--------------------------------------------------------%20listeners%20*/%0A%0A%20%20%20%20input.addEventListener('mousedown',%20function%20(e)%20%7B%0A%20%20%20%20%20%20if%20(e.altKey)%20%7B%0A%20%20%20%20%20%20%20%20e.preventDefault();%0A%20%20%20%20%20%20%20%20detach();%0A%20%20%20%20%20%20%20%20select.focus();%0A%20%20%20%20%20%20%20%20return;%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20if%20(!open)%20%7B%20e.preventDefault();%20input.focus();%20openList('');%20input.select();%20%7D%0A%20%20%20%20%7D);%0A%20%20%20%20wis.addEventListener('mousedown',%20function%20(e)%20%7B%0A%20%20%20%20%20%20e.preventDefault();%0A%20%20%20%20%20%20e.stopPropagation();%0A%20%20%20%20%20%20clear();%0A%20%20%20%20%7D);%0A%20%20%20%20input.addEventListener('focus',%20function%20()%20%7B%20if%20(!open)%20openList('');%20%7D);%0A%20%20%20%20input.addEventListener('input',%20function%20()%20%7B%20if%20(open)%20render(input.value);%20else%20openList(input.value);%20%7D);%0A%20%20%20%20input.addEventListener('blur',%20function%20()%20%7B%0A%20%20%20%20%20%20setTimeout(function%20()%20%7B%20if%20(open)%20closeList(true);%20%7D,%20120);%0A%20%20%20%20%7D);%0A%20%20%20%20input.addEventListener('keydown',%20function%20(e)%20%7B%0A%20%20%20%20%20%20var%20n%20=%20list.querySelectorAll('li%5Bdata-i%5D').length;%0A%20%20%20%20%20%20switch%20(e.key)%20%7B%0A%20%20%20%20%20%20%20%20case%20'ArrowDown':%0A%20%20%20%20%20%20%20%20%20%20e.preventDefault();%0A%20%20%20%20%20%20%20%20%20%20if%20(!open)%20%7B%20openList('');%20break;%20%7D%0A%20%20%20%20%20%20%20%20%20%20if%20(n)%20setActive((active%20+%201)%20%25%20n);%0A%20%20%20%20%20%20%20%20%20%20break;%0A%20%20%20%20%20%20%20%20case%20'ArrowUp':%0A%20%20%20%20%20%20%20%20%20%20e.preventDefault();%0A%20%20%20%20%20%20%20%20%20%20if%20(!open)%20%7B%20openList('');%20break;%20%7D%0A%20%20%20%20%20%20%20%20%20%20if%20(n)%20setActive((active%20-%201%20+%20n)%20%25%20n);%0A%20%20%20%20%20%20%20%20%20%20break;%0A%20%20%20%20%20%20%20%20case%20'Home':%20if%20(open%20&&%20n)%20%7B%20e.preventDefault();%20setActive(0);%20%7D%20break;%0A%20%20%20%20%20%20%20%20case%20'End':%20if%20(open%20&&%20n)%20%7B%20e.preventDefault();%20setActive(n%20-%201);%20%7D%20break;%0A%20%20%20%20%20%20%20%20case%20'PageDown':%20if%20(open%20&&%20n)%20%7B%20e.preventDefault();%20setActive(Math.min(n%20-%201,%20active%20+%2010));%20%7D%20break;%0A%20%20%20%20%20%20%20%20case%20'PageUp':%20if%20(open%20&&%20n)%20%7B%20e.preventDefault();%20setActive(Math.max(0,%20active%20-%2010));%20%7D%20break;%0A%20%20%20%20%20%20%20%20case%20'Enter':%0A%20%20%20%20%20%20%20%20%20%20if%20(open)%20%7B%20e.preventDefault();%20if%20(active%20%3E=%200)%20choose(active);%20else%20closeList(true);%20%7D%0A%20%20%20%20%20%20%20%20%20%20break;%0A%20%20%20%20%20%20%20%20case%20'Tab':%0A%20%20%20%20%20%20%20%20%20%20if%20(open%20&&%20active%20%3E=%200)%20choose(active);%20else%20if%20(open)%20closeList(true);%0A%20%20%20%20%20%20%20%20%20%20break;%0A%20%20%20%20%20%20%20%20case%20'Escape':%0A%20%20%20%20%20%20%20%20%20%20if%20(open)%20%7B%20e.preventDefault();%20e.stopPropagation();%20closeList(true);%20%7D%0A%20%20%20%20%20%20%20%20%20%20break;%0A%20%20%20%20%20%20%20%20case%20'Backspace':%0A%20%20%20%20%20%20%20%20%20%20if%20(!input.value%20&&%20leegIndex%20!==%20-1%20&&%20currentLabel())%20%7B%20e.preventDefault();%20clear();%20%7D%0A%20%20%20%20%20%20%20%20%20%20break;%0A%20%20%20%20%20%20%20%20case%20'Delete':%0A%20%20%20%20%20%20%20%20%20%20if%20(!open%20%7C%7C%20zoek%20%7C%7C%20active%20%3C%200)%20break;%0A%20%20%20%20%20%20%20%20%20%20var%20doelwit%20=%20shown%5Bactive%5D;%0A%20%20%20%20%20%20%20%20%20%20if%20(!doelwit%20%7C%7C%20!doelwit.removable)%20break;%0A%20%20%20%20%20%20%20%20%20%20e.preventDefault();%0A%20%20%20%20%20%20%20%20%20%20if%20(doelwit.mru%20&&%20recentSleutel%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20&&%20dropRecent(recentSleutel,%20select.options%5Bdoelwit.index%5D.value))%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20readOptions();%0A%20%20%20%20%20%20%20%20%20%20%20%20render(zoek,%20true);%0A%20%20%20%20%20%20%20%20%20%20%20%20break;%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20fireRemove(doelwit);%0A%20%20%20%20%20%20%20%20%20%20break;%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D);%0A%20%20%20%20list.addEventListener('mousemove',%20function%20(e)%20%7B%0A%20%20%20%20%20%20var%20li%20=%20e.target;%0A%20%20%20%20%20%20while%20(li%20&&%20li.tagName%20!==%20'LI')%20li%20=%20li.parentNode;%0A%20%20%20%20%20%20if%20(!li%20%7C%7C%20!li.hasAttribute('data-i'))%20return;%0A%20%20%20%20%20%20var%20i%20=%20parseInt(li.getAttribute('data-i'),%2010);%0A%20%20%20%20%20%20if%20(i%20!==%20active)%20setActive(i,%20false);%0A%20%20%20%20%7D);%0A%20%20%20%20list.addEventListener('mousedown',%20function%20(e)%20%7B%20e.preventDefault();%20%7D);%0A%20%20%20%20list.addEventListener('click',%20function%20(e)%20%7B%0A%20%20%20%20%20%20var%20li%20=%20e.target;%0A%20%20%20%20%20%20while%20(li%20&&%20li.tagName%20!==%20'LI')%20li%20=%20li.parentNode;%0A%20%20%20%20%20%20if%20(li%20&&%20li.hasAttribute('data-i'))%20choose(parseInt(li.getAttribute('data-i'),%2010));%0A%20%20%20%20%7D);%0A%0A%20%20%20%20function%20onSelectChange()%20%7B%0A%20%20%20%20%20%20updateWis();%0A%20%20%20%20%20%20if%20(!open)%20input.value%20=%20currentLabel();%0A%20%20%20%20%7D%0A%20%20%20%20select.addEventListener('change',%20onSelectChange);%0A%0A%20%20%20%20var%20resync%20=%20debounce(function%20()%20%7B%0A%20%20%20%20%20%20syncFromSelect();%0A%20%20%20%20%20%20if%20(open)%20render(zoek,%20true);%0A%20%20%20%20%7D,%2050);%0A%20%20%20%20var%20obs%20=%20new%20MutationObserver(resync);%0A%20%20%20%20obs.observe(select,%20%7B%20childList:%20true,%20subtree:%20true%20%7D);%0A%0A%20%20%20%20wrap.__selectSearchDetach%20=%20detach;%0A%20%20%20%20wrap.__selectSearchSelect%20=%20select;%0A%20%20%20%20select.__selectSearchSync%20=%20function%20()%20%7B%0A%20%20%20%20%20%20syncFromSelect();%0A%20%20%20%20%20%20if%20(open)%20render(zoek,%20true);%0A%20%20%20%20%7D;%0A%20%20%20%20syncFromSelect();%0A%20%20%7D%0A%0A%20%20/*%20----------------------------------------------------------------%20scan%20*/%0A%0A%20%20function%20eligible(s)%20%7B%0A%20%20%20%20return%20!s.multiple%20&&%20s.size%20%3C=%201%20&&%20!s.disabled%0A%20%20%20%20%20%20&&%20!s.dataset.ssEnhanced%20&&%20!s.hasAttribute('data-nosearch')%0A%20%20%20%20%20%20&&%20s.options.length%20%3E=%20MIN_OPTIONS;%0A%20%20%7D%0A%0A%20%20function%20scanDoc(doc)%20%7B%0A%20%20%20%20var%20selects%20=%20doc.querySelectorAll('select');%0A%20%20%20%20for%20(var%20i%20=%200;%20i%20%3C%20selects.length;%20i++)%20%7B%0A%20%20%20%20%20%20if%20(eligible(selects%5Bi%5D))%20%7B%0A%20%20%20%20%20%20%20%20try%20%7B%20enhance(selects%5Bi%5D);%20%7D%20catch%20(e)%20%7B%20/*%20laat%20de%20pagina%20met%20rust%20*/%20%7D%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%20%20var%20wraps%20=%20doc.querySelectorAll('.select-search-wrap');%0A%20%20%20%20for%20(var%20w%20=%200;%20w%20%3C%20wraps.length;%20w++)%20%7B%0A%20%20%20%20%20%20var%20sel%20=%20wraps%5Bw%5D.__selectSearchSelect;%0A%20%20%20%20%20%20if%20(sel%20&&%20!doc.contains(sel)%20&&%20wraps%5Bw%5D.parentNode)%20%7B%0A%20%20%20%20%20%20%20%20wraps%5Bw%5D.parentNode.removeChild(wraps%5Bw%5D);%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%20%20var%20frames%20=%20doc.querySelectorAll('iframe,frame');%0A%20%20%20%20for%20(var%20f%20=%200;%20f%20%3C%20frames.length;%20f++)%20%7B%0A%20%20%20%20%20%20try%20%7B%0A%20%20%20%20%20%20%20%20var%20fd%20=%20frames%5Bf%5D.contentDocument;%0A%20%20%20%20%20%20%20%20if%20(fd%20&&%20fd.body)%20%7B%20attach(fd);%20%7D%0A%20%20%20%20%20%20%7D%20catch%20(e)%20%7B%20/*%20cross-origin%20*/%20%7D%0A%20%20%20%20%7D%0A%20%20%7D%0A%0A%20%20var%20attached%20=%20%5B%5D;%0A%20%20function%20attach(doc)%20%7B%0A%20%20%20%20if%20(attached.indexOf(doc)%20===%20-1)%20%7B%0A%20%20%20%20%20%20attached.push(doc);%0A%20%20%20%20%20%20new%20MutationObserver(debounce(function%20()%20%7B%20scanDoc(doc);%20%7D,%20120))%0A%20%20%20%20%20%20%20%20.observe(doc.documentElement,%20%7B%20childList:%20true,%20subtree:%20true%20%7D);%0A%20%20%20%20%7D%0A%20%20%20%20scanDoc(doc);%0A%20%20%7D%0A%0A%20%20function%20scan()%20%7B%20for%20(var%20i%20=%200;%20i%20%3C%20attached.length;%20i++)%20scanDoc(attached%5Bi%5D);%20attach(document);%20%7D%0A%20%20window.__selectSearchScan%20=%20scan;%0A%0A%20%20window.selectSearch%20=%20%7B%0A%20%20%20%20remember:%20function%20(selectOrKey,%20value,%20label)%20%7B%0A%20%20%20%20%20%20var%20sleutel%20=%20typeof%20selectOrKey%20===%20'string'%20?%20selectOrKey%20:%20recentKey(selectOrKey);%0A%20%20%20%20%20%20if%20(!sleutel%20%7C%7C%20!value)%20return;%0A%20%20%20%20%20%20pushRecent(sleutel,%20value,%20label%20==%20null%20?%20String(value)%20:%20String(label));%0A%20%20%20%20%20%20if%20(selectOrKey%20&&%20typeof%20selectOrKey.__selectSearchSync%20===%20'function')%20%7B%0A%20%20%20%20%20%20%20%20selectOrKey.__selectSearchSync();%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D,%0A%20%20%20%20forget:%20function%20(selectOrKey,%20value)%20%7B%0A%20%20%20%20%20%20var%20sleutel%20=%20typeof%20selectOrKey%20===%20'string'%20?%20selectOrKey%20:%20recentKey(selectOrKey);%0A%20%20%20%20%20%20if%20(!sleutel%20%7C%7C%20!value)%20return;%0A%20%20%20%20%20%20dropRecent(sleutel,%20value);%0A%20%20%20%20%20%20if%20(selectOrKey%20&&%20typeof%20selectOrKey.__selectSearchSync%20===%20'function')%20%7B%0A%20%20%20%20%20%20%20%20selectOrKey.__selectSearchSync();%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%7D;%0A%0A%20%20if%20(document.readyState%20===%20'loading')%20%7B%0A%20%20%20%20document.addEventListener('DOMContentLoaded',%20function%20()%20%7B%20attach(document);%20%7D);%0A%20%20%7D%20else%20%7B%0A%20%20%20%20attach(document);%0A%20%20%7D%0A%7D)();%0A \ No newline at end of file diff --git a/scripts/select-search/build.sh b/scripts/select-search/build.sh deleted file mode 100755 index fb7455b..0000000 --- a/scripts/select-search/build.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash -# Bouwt het userscript en de bookmarklet uit src/select-search.js. -# Gebruik: ./build.sh (de bookmarklet belandt ook meteen in het klembord) -set -euo pipefail - -here="$(cd "$(dirname "$0")" && pwd)" -src="$here/src/select-search.js" -hdr="$here/src/userscript-header.txt" -out="$here" - -for f in "$src" "$hdr"; do - if [ ! -f "$f" ]; then - echo "build.sh: ontbrekend bestand: $f" >&2 - exit 1 - fi -done - -if ! command -v python3 >/dev/null 2>&1; then - echo "build.sh: python3 is nodig om de bookmarklet te encoderen" >&2 - exit 1 -fi - -if grep -q 'MATCH_PLACEHOLDER' "$hdr"; then - echo "build.sh: vul eerst de echte @match-URL in $hdr in" >&2 - exit 1 -fi - -# 1. Userscript = metadatablok + ongewijzigde core. -cat "$hdr" "$src" > "$out/select-search.user.js" - -# 2. Bookmarklet = core, URL-encoded achter javascript:. -python3 - "$src" "$out/bookmarklet.txt" <<'PY' -import io, sys, urllib.parse - -src, out = sys.argv[1], sys.argv[2] -code = io.open(src, encoding='utf-8').read() -# Regelcommentaar sneuvelt zodra alles op een regel staat; strip het weg. -lines = [] -for line in code.split('\n'): - stripped = line.strip() - if stripped.startswith('//'): - continue - lines.append(line) -code = '\n'.join(lines) - -# Een bladwijzerbeheerder mag de nieuwe regels weggooien zonder dat de -# bookmarklet stukgaat: dus geen enkel regelcommentaar meer overhouden. -for n, line in enumerate(code.split('\n'), 1): - if '//' in line: - sys.exit('build.sh: regelcommentaar op regel %d breekt de bookmarklet: %s' - % (n, line.strip())) - -url = 'javascript:' + urllib.parse.quote(code, safe="!#$&'()*+,-./:;=?@_~") -io.open(out, 'w', encoding='utf-8').write(url) -print('bookmarklet: %d tekens' % len(url)) -PY - -if command -v pbcopy >/dev/null 2>&1; then - pbcopy < "$out/bookmarklet.txt" - echo "bookmarklet gekopieerd naar het klembord" -fi - -echo "geschreven: $out/select-search.user.js" -echo "geschreven: $out/bookmarklet.txt" diff --git a/scripts/select-search/src/select-search.js b/scripts/select-search/src/select-search.js deleted file mode 100644 index fea77d3..0000000 --- a/scripts/select-search/src/select-search.js +++ /dev/null @@ -1,814 +0,0 @@ -/* - * select-search - maakt elke blijft in de DOM en blijft de bron van waarheid: - * het wordt enkel visueel verborgen. Selecteren gebeurt via selectedIndex + - * native input/change events, zodat de applicatie niets merkt van de ingreep. - */ -(function () { - 'use strict'; - - if (window.__selectSearchLoaded) { - if (typeof window.__selectSearchScan === 'function') window.__selectSearchScan(); - return; - } - window.__selectSearchLoaded = true; - - // kleinere dropdowns laten we met rust; hoogstens MAX_RENDER treffers tekenen - var MIN_OPTIONS = 10; - var MAX_RENDER = 200; - // Streefbreedte van de lijst. Het veld zelf is meestal smaller dan zijn labels; onder deze - // breedte wordt de lijst een kolom van afgebroken zinnen in plaats van een leesbare lijst. - var LIJST_BREED = 420; - var MARGE = 8; - var uid = 0; - // Recent gebruikte waarden per veld. Vijf, want select-search filtert het blok mee tijdens het - // zoeken: wat buiten de top vijf valt typ je sneller dan je het herkent. Geen vervaltermijn, - // een lijst van vijf verdringt zichzelf al. - var RECENT_KEY = 'selectSearchRecent'; - var RECENT_MAX = 5; - var RECENT_FIELDS = 40; - - /* ---------------------------------------------------------------- utils */ - - function normMap(s) { - // Genormaliseerde (accentloze, lowercase) string + index-mapping terug - // naar de originele string, zodat we treffers kunnen highlighten. - var out = '', map = []; - for (var i = 0; i < s.length; i++) { - var c = s.charAt(i).normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); - for (var j = 0; j < c.length; j++) { out += c.charAt(j); map.push(i); } - } - return { n: out, map: map }; - } - - function escapeHtml(s) { - return s.replace(/&/g, '&').replace(//g, '>'); - } - - function tokenize(q) { - return normMap(q).n.split(/\s+/).filter(function (t) { return t.length > 0; }); - } - - function debounce(fn, ms) { - var t = null; - return function () { - if (t) clearTimeout(t); - t = setTimeout(function () { t = null; fn(); }, ms); - }; - } - - /* --------------------------------------------------------------- recent */ - - // Eén localStorage-sleutel voor alle velden samen: - // { v: 1, fields: { "": { ts: , items: [[value, label], ...] } } } - // ts dient enkel om het oudste veld te laten vallen zodra er meer dan RECENT_FIELDS in staan, - // zodat de sleutel niet onbeperkt groeit. Lezen en schrijven falen stil: recent is comfort, - // geen data, en een volle quota mag de dropdown niet stukmaken. - - function readStore() { - var raw; - try { raw = localStorage.getItem(RECENT_KEY); } catch (e) { return null; } - if (!raw) return null; - var data; - try { data = JSON.parse(raw); } catch (e) { return null; } - if (!data || data.v !== 1 || !data.fields || typeof data.fields !== 'object') return null; - return data; - } - - function writeStore(data) { - try { localStorage.setItem(RECENT_KEY, JSON.stringify(data)); } catch (e) { /* quota, stil */ } - } - - function readRecent(sleutel) { - var data = readStore(); - if (!data || !Object.prototype.hasOwnProperty.call(data.fields, sleutel)) return []; - var veld = data.fields[sleutel]; - return veld && veld.items && veld.items.length ? veld.items : []; - } - - function writeRecent(sleutel, lijst) { - var data = readStore() || { v: 1, fields: {} }; - data.fields[sleutel] = { ts: Date.now(), items: lijst.slice(0, RECENT_MAX) }; - var namen = Object.keys(data.fields); - if (namen.length > RECENT_FIELDS) { - namen.sort(function (a, b) { return (data.fields[a].ts || 0) - (data.fields[b].ts || 0); }); - for (var i = 0; i < namen.length - RECENT_FIELDS; i++) delete data.fields[namen[i]]; - } - writeStore(data); - } - - function pushRecent(sleutel, value, label) { - // Zonder label wordt het een onleesbare regel bovenaan de lijst, en die kan de gebruiker - // niet herkennen en dus ook niet gericht wegdoen. - if (!sleutel || !value || !label) return; - var lijst = readRecent(sleutel).filter(function (e) { return e[0] !== value; }); - lijst.unshift([value, label]); - writeRecent(sleutel, lijst); - } - - // De lijst kon alleen groeien. Dit is de weg terug: de waarde verdwijnt uit recent en staat - // daarna weer gewoon tussen alle opties. Levert false op als er niets te wissen viel. - function dropRecent(sleutel, value) { - var lijst = readRecent(sleutel); - var uit = lijst.filter(function (e) { return e[0] !== value; }); - if (uit.length === lijst.length) return false; - writeRecent(sleutel, uit); - return true; - } - - // BIPP rendert ids als ctl00_..._resultTable_ctl03_interfaceAccountList. Rij 3 en rij 4 zijn - // hetzelfde veld en horen dus één lijst te delen: de rij- en volgnummers gaan eruit. Levert dat - // niets op, dan krijgt die select geen recent-blok en blijft hij gewoon doorzoekbaar. - // Uit op de loginpagina: daar kiest iedereen een participant uit een lange lijst en schuift een - // recent-blok enkel gewoonte-keuzes bovenaan. Zoeken blijft werken. `data-ss-recent="off"` doet - // hetzelfde voor één veld. - function recentKey(select) { - if (select.getAttribute && select.getAttribute('data-ss-recent') === 'off') return ''; - if (/\/login\.aspx$/i.test((select.ownerDocument.defaultView || window).location.pathname)) return ''; - var expliciet = select.getAttribute && select.getAttribute('data-ss-recent-key'); - if (expliciet) return expliciet; - var ruw = select.name || select.id || ''; - if (!ruw) return ''; - return ruw.split(/[$_]/).filter(function (deel) { - return deel && !/^ctl\d+$/i.test(deel) && !/^\d+$/.test(deel); - }).join('_'); - } - - /* ------------------------------------------------------------ highlight */ - - // Alle treffers over het volledige label, gesorteerd en samengevoegd. Los van het tekenen, - // want een label wordt in twee stukken getekend (code en omschrijving) en een treffer mag - // over die grens heen lopen. - function matchRanges(item, tokens) { - if (!tokens.length) return []; - var ranges = []; - for (var t = 0; t < tokens.length; t++) { - var tok = tokens[t], from = 0, idx; - while ((idx = item.norm.n.indexOf(tok, from)) !== -1) { - var start = item.norm.map[idx]; - var end = item.norm.map[idx + tok.length - 1] + 1; - ranges.push([start, end]); - from = idx + tok.length; - } - } - if (!ranges.length) return ranges; - ranges.sort(function (a, b) { return a[0] - b[0]; }); - var merged = [ranges[0].slice()]; - for (var r = 1; r < ranges.length; r++) { - var last = merged[merged.length - 1]; - if (ranges[r][0] <= last[1]) last[1] = Math.max(last[1], ranges[r][1]); - else merged.push(ranges[r].slice()); - } - return merged; - } - - // Tekent label.slice(from, to). Een treffer die de grens kruist wordt afgeknipt, zodat beide - // helften hun eigen krijgen en er geen tag over de spans heen loopt. - function highlight(item, ranges, from, to) { - var html = '', pos = from; - for (var m = 0; m < ranges.length; m++) { - var a = Math.max(ranges[m][0], from), b = Math.min(ranges[m][1], to); - if (b <= a) continue; - html += escapeHtml(item.label.slice(pos, a)); - html += '' + escapeHtml(item.label.slice(a, b)) + ''; - pos = b; - } - return html + escapeHtml(item.label.slice(pos, to)); - } - - // Waar splitst "10-AD010000-CFALWERK - ADRIVA (ALGEMEEN)" in code en omschrijving? Enkel bij de - // eerste " - " en enkel als daar echt een code voor staat: één woord zonder spatie. Zo blijft - // een gewone titel met een gedachtestreepje ("Emotie en verbeelding - 2025") ongesplitst. - // De teruggegeven index staat ná de spatie: label.slice(0, punt - 1) is de code met zijn - // streepje, label.slice(punt) de omschrijving, en de spatie ertussen valt buiten beide. - function splitPunt(label) { - var sep = label.indexOf(' - '); - if (sep <= 0) return -1; - if (label.lastIndexOf(' ', sep - 1) !== -1) return -1; - return sep + 3; - } - - /* ----------------------------------------------------------------- css */ - - var CSS = [ - ':host{all:initial;display:inline-block;vertical-align:middle;max-width:100%}', - '*{box-sizing:border-box;font:inherit}', - '.box{position:relative;display:block;width:100%}', - 'input{width:100%;padding:2px 22px 2px 4px;border:1px solid #767676;border-radius:2px;', - 'background:#fff;color:#000;font:13px/1.4 system-ui,-apple-system,"Segoe UI",sans-serif;', - 'height:100%;min-height:22px}', - 'input:focus{outline:none;border-color:#0b57d0;box-shadow:0 0 0 1px #0b57d0}', - // #767676 haalt net de AA-drempel op wit; het #777 van li.kop zit er net onder. - 'input::placeholder{color:#767676;opacity:1}', - '.caret{position:absolute;right:6px;top:50%;transform:translateY(-50%);pointer-events:none;', - 'border:4px solid transparent;border-top-color:#444;margin-top:2px}', - // Het kruisje neemt de plek van het pijltje in en krijgt dus geen eigen ruimte. Zo blijft de - // volle breedte voor de tekst, staat er nooit een leeg gat, en verschuift er evenmin iets: - // enkel de zichtbaarheid van de twee wisselt. Zo doet Ant Design het ook. - '.wis{position:absolute;right:2px;top:50%;transform:translateY(-50%);width:17px;height:17px;', - 'display:flex;align-items:center;justify-content:center;color:#767676;cursor:pointer;', - 'visibility:hidden}', - '.box.wisbaar.gevuld:hover .wis,.box.wisbaar.gevuld:focus-within .wis{visibility:visible}', - '.box.wisbaar.gevuld:hover .caret,.box.wisbaar.gevuld:focus-within .caret{visibility:hidden}', - '.wis:hover{color:#000}', - 'ul{position:fixed;z-index:2147483647;margin:0;padding:2px 0;list-style:none;overflow-y:auto;', - 'background:#fff;color:#000;border:1px solid #b0b0b0;border-radius:3px;', - 'box-shadow:0 4px 14px rgba(0,0,0,.22);', - 'font:13px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif;display:none}', - 'ul.open{display:block}', - // Een label dat niet past loopt terug over een volgende regel met een kleine hangende - // inspring. Niet uitlijnen onder de omschrijving: bij een lange code schuift die inspring mee - // tot halverwege de rij en blijft er nauwelijks tekstbreedte over. 2.5ch is de breedte van - // twee cijfers plus een streepje, dus precies het "10-" waarmee elke budgetcode begint: daar - // valt de vervolgregel netjes samen met de rest van de code. Elders is het gewoon een kleine - // inspring, genoeg om te zien dat een regel bij de vorige hoort. ch en niet px, want de maat - // hoort bij de tekstgrootte en niet bij het scherm. - 'li{padding:4px 8px;cursor:pointer;line-height:1.3;', - 'padding-left:calc(8px + 2.5ch);text-indent:-2.5ch;overflow-wrap:break-word}', - // De code zelf mag nooit afbreken, ook niet op zijn eigen streepjes: white-space:pre houdt - // "10-AD113010 -" in één stuk. De spatie erna staat buiten de span, zodat het eerste woord - // van de omschrijving wel naar de volgende regel kan. - 'li .c{white-space:pre}', - // Meerregelige rijen lopen anders in elkaar over: het gat binnen een rij is even groot als - // dat tussen twee rijen. Een haarlijn tussen twee opties zegt waar de ene stopt. - 'li[data-i]+li[data-i]{border-top:1px solid #eee}', - 'li.kop{padding:6px 8px 2px;padding-left:8px;text-indent:0;color:#777;font-size:11px;', - 'font-weight:600;cursor:default}', - 'li.active{background:#0b57d0;color:#fff;border-top-color:#0b57d0}', - 'li.active mark{background:#ffe08a;color:#000}', - 'li.dis{color:#999;cursor:default}', - 'li.note{color:#666;cursor:default;font-style:italic;padding-top:5px}', - 'li.empty{color:#666;cursor:default}', - 'mark{background:#ffe08a;color:inherit;padding:0}' - ].join(''); - - /* ------------------------------------------------------------- enhance */ - - function enhance(select) { - var doc = select.ownerDocument; - var win = doc.defaultView || window; - var id = 'ss' + (++uid); - var recentSleutel = recentKey(select); - // Manuele stand: het recent-blok en Delete werken, maar er wordt niets vanzelf onthouden. - // Voor wie pas na een bevestiging van de server mag opslaan; die roept remember() zelf aan. - var recentManueel = select.getAttribute('data-ss-recent') === 'manual'; - - var rect = select.getBoundingClientRect(); - var wrap = doc.createElement('span'); - wrap.className = 'select-search-wrap'; - if (rect.width) wrap.style.width = rect.width + 'px'; - if (rect.height) wrap.style.height = rect.height + 'px'; - select.parentNode.insertBefore(wrap, select.nextSibling); - - var root = wrap.attachShadow({ mode: 'open' }); - var style = doc.createElement('style'); - style.textContent = CSS; - var box = doc.createElement('span'); - box.className = 'box'; - var input = doc.createElement('input'); - input.type = 'text'; - input.setAttribute('role', 'combobox'); - input.setAttribute('aria-expanded', 'false'); - input.setAttribute('aria-autocomplete', 'list'); - input.setAttribute('aria-controls', id + '-list'); - input.setAttribute('autocomplete', 'off'); - input.setAttribute('spellcheck', 'false'); - input.title = 'Typ om te zoeken. Alt+klik zet het originele keuzemenu terug.'; - var caret = doc.createElement('span'); - caret.className = 'caret'; - // Wissen gebeurt met een kruisje in het veld, zoals in react-select, MUI en Ant Design, en - // niet met een "geen waarde"-rij bovenaan de lijst. Ant Design toont het kruisje pas bij - // hover, dat doen wij ook, met focus erbij zodat het toetsenbord niet in de kou staat. - var wis = doc.createElement('span'); - wis.className = 'wis'; - wis.setAttribute('role', 'button'); - wis.setAttribute('aria-label', 'Wissen'); - wis.innerHTML = ''; - var list = doc.createElement('ul'); - list.id = id + '-list'; - list.setAttribute('role', 'listbox'); - box.appendChild(input); - box.appendChild(wis); - box.appendChild(caret); - root.appendChild(style); - root.appendChild(box); - root.appendChild(list); - - // Origineel blijft bestaan (form submit, app-code), maar onzichtbaar. - var prevStyle = select.getAttribute('style'); - select.style.position = 'absolute'; - select.style.opacity = '0'; - select.style.pointerEvents = 'none'; - select.style.width = (rect.width || 1) + 'px'; - select.style.height = (rect.height || 1) + 'px'; - select.dataset.ssEnhanced = '1'; - - var items = [], shown = [], active = -1, open = false; - // De zoekterm apart bijhouden. input.value is niet hetzelfde: bij een open lijst zonder - // dat er getypt is staat daar het gekozen label in (geselecteerd, klaar om over te typen), - // terwijl de lijst volledig is. Wie input.value als zoekterm leest, denkt dus dat er - // gefilterd wordt op iets wat de gebruiker nooit intypte. - var zoek = ''; - // Index van de optie met een lege waarde, de "niets gekozen"-stand. Die optie is geen keuze: - // hij komt niet in de lijst, maar levert de placeholder en is het doel van het kruisje. - var leegIndex = -1, leegLabel = ''; - - function readOptions() { - items = []; - leegIndex = -1; - leegLabel = ''; - // Twee soorten "niets gekozen". Een optie zonder waarde is de echte, een optie met een - // waarde maar zonder leesbare tekst (OAF schrijft graag ) - // is de terugval. Beide horen buiten de lijst, want een regel die je niet kunt lezen is - // geen keuze, maar het kruisje mikt liefst op de eerste: die maakt het veld echt leeg. - var zonderWaarde = -1, zonderWaardeLabel = '', blanco = -1; - // De volgorde van de optgroups bepaalt de volgorde van de blokken in de lijst, ook na - // filteren. gi 0 is het recent-blok, gi 1 zijn de opties zonder groep, echte groepen - // beginnen bij 2. - var groepen = [], gi; - // Recent verwijst naar opties die al in de select staan; wat er niet meer in zit valt - // vanzelf weg. De positie in de bewaarde lijst is de positie in het blok. - var wil = {}, recentLijst = recentSleutel ? readRecent(recentSleutel) : []; - for (var r = 0; r < recentLijst.length; r++) wil['v:' + recentLijst[r][0]] = r; - var recentItems = [], gewoon = []; - for (var i = 0; i < select.options.length; i++) { - var o = select.options[i]; - var label = (o.textContent || '').replace(/\s+/g, ' ').trim(); - if (!o.value || !label) { - if (!o.value) { - if (zonderWaarde === -1) { zonderWaarde = i; zonderWaardeLabel = label; } - } else if (blanco === -1) blanco = i; - continue; - } - var plek = wil['v:' + o.value]; - if (plek !== undefined && recentItems[plek] === undefined) { - recentItems[plek] = { - index: i, - label: label, - group: 'recent', - gi: 0, - disabled: o.disabled, - removable: true, - mru: true, - norm: normMap(label) - }; - // Een optie die in recent staat wordt in haar eigen groep overgeslagen: twee keer - // dezelfde regel in de resultaten is verwarrend, ook met een kopregel erbij. - continue; - } - var groep = o.parentNode && o.parentNode.tagName === 'OPTGROUP' ? o.parentNode : null; - var grp = groep ? (groep.label || '') : ''; - if (!grp) gi = 1; - else { - gi = groepen.indexOf(grp); - if (gi === -1) { groepen.push(grp); gi = groepen.length + 1; } else gi += 2; - } - gewoon.push({ - index: i, - label: label, - group: grp, - gi: gi, - disabled: o.disabled, - // Wie de opties levert bepaalt of een regel weg mag, met data-removable op de optie of - // op haar optgroup. Het script weet niet wat verwijderen betekent en doet het dus ook - // niet zelf: het meldt enkel dat de gebruiker erom vraagt. - removable: !!(o.dataset.removable || (groep && groep.dataset.removable)), - // Enkel het label is doorzoekbaar: de groepsnaam staat als kopregel op het scherm en - // hoort niet stilzwijgend mee te zoeken. highlight() mapt de treffers bovendien terug - // op label, dus een treffer in de groepsnaam zou buiten het label vallen. - norm: normMap(label) - }); - } - if (zonderWaarde !== -1) { leegIndex = zonderWaarde; leegLabel = zonderWaardeLabel; } - else if (blanco !== -1) leegIndex = blanco; - for (var k = 0; k < recentItems.length; k++) { - if (recentItems[k]) items.push(recentItems[k]); - } - // Zonder kopregel loopt het recent-blok naadloos over in de losse opties eronder, en dan - // zie je niet meer waar het ophoudt. Alleen nodig als er iets boven staat. - if (items.length) { - for (var g = 0; g < gewoon.length; g++) { - if (!gewoon[g].group) gewoon[g].group = 'alle'; - } - } - items = items.concat(gewoon); - } - - function currentLabel() { - var o = select.options[select.selectedIndex]; - // Staat de lege optie aan, dan is het veld leeg en neemt de placeholder het over. Anders - // zou "budgetcode" er als gewone zwarte waarde staan, alsof er wel iets gekozen is. - if (!o || !o.value) return ''; - return (o.textContent || '').replace(/\s+/g, ' ').trim(); - } - - // Het kruisje hoort alleen te bestaan waar wissen ook kan, en alleen zichtbaar te zijn als er - // iets te wissen valt. De hover- en focusvoorwaarde staat in de CSS. - function updateWis() { - box.classList.toggle('wisbaar', leegIndex !== -1); - box.classList.toggle('gevuld', !!currentLabel()); - } - - function syncFromSelect() { - readOptions(); - input.placeholder = select.getAttribute('data-placeholder') || leegLabel; - updateWis(); - if (!open) input.value = currentLabel(); - } - - function clear() { - if (leegIndex === -1) return; - select.selectedIndex = leegIndex; - input.value = ''; - updateWis(); - closeList(false); - fire(select, 'input'); - fire(select, 'change'); - } - - function position() { - var r = input.getBoundingClientRect(); - var below = win.innerHeight - r.bottom - 6; - var above = r.top - 6; - var down = below >= 160 || below >= above; - // De lijst hing vast aan de linkerrand van het veld en kon dus alleen naar rechts groeien. - // Bij een veld rechts in het formulier bleef er niets over en viel elk label terug op - // afbreken. Daarom eerst de breedte laten kiezen met de volle vensterbreedte beschikbaar, - // die meten, en de lijst dan zo ver naar links schuiven als nodig om ze te laten passen. - var wens = Math.min(Math.max(r.width, LIJST_BREED), win.innerWidth - MARGE * 2); - list.style.left = MARGE + 'px'; - list.style.minWidth = r.width + 'px'; - list.style.maxWidth = wens + 'px'; - list.style.maxHeight = Math.max(120, (down ? below : above)) + 'px'; - var breed = list.offsetWidth; - list.style.left = Math.max(MARGE, Math.min(r.left, win.innerWidth - MARGE - breed)) + 'px'; - if (down) { - list.style.top = r.bottom + 2 + 'px'; - list.style.bottom = 'auto'; - } else { - list.style.top = 'auto'; - list.style.bottom = (win.innerHeight - r.top + 2) + 'px'; - } - } - - function filter(q) { - var tokens = tokenize(q); - if (!tokens.length) return items.slice(); - // Per groep twee emmers: eerst wat met de zoekterm begint, dan de rest. De emmers worden - // in groepsvolgorde aan elkaar geplakt, zodat elke groep aaneengesloten blijft en de - // kopregel zich niet herhaalt. - var emmers = []; - for (var i = 0; i < items.length; i++) { - var it = items[i], n = it.norm.n, ok = true; - for (var t = 0; t < tokens.length; t++) { - if (n.indexOf(tokens[t]) === -1) { ok = false; break; } - } - if (!ok) continue; - if (!emmers[it.gi]) emmers[it.gi] = { starts: [], rest: [] }; - (n.indexOf(tokens[0]) === 0 ? emmers[it.gi].starts : emmers[it.gi].rest).push(it); - } - var out = []; - for (var g = 0; g < emmers.length; g++) { - if (emmers[g]) out = out.concat(emmers[g].starts, emmers[g].rest); - } - return out; - } - - // behoud: houd de blauwe balk waar hij stond. Enkel voor een herbouw van de optielijst onder - // een open lijst, want dan is de gebruiker midden in iets en zou terugspringen naar de eerste - // regel hem onderbreken. Bij openen en typen hoort de balk juist wel bovenaan te beginnen. - function render(q, behoud) { - var vorigeActieve = active; - zoek = q || ''; - var tokens = tokenize(q); - shown = filter(q); - var html = ''; - if (!shown.length) { - html = '
  • Geen resultaten
  • '; - } else { - var n = Math.min(shown.length, MAX_RENDER), vorige = null; - for (var i = 0; i < n; i++) { - var it = shown[i]; - if (it.group && it.group !== vorige) { - html += '
  • ' + escapeHtml(it.group) + '
  • '; - } - vorige = it.group; - var ranges = matchRanges(it, tokens); - var punt = splitPunt(it.label); - var binnen = punt === -1 - ? highlight(it, ranges, 0, it.label.length) - : '' + highlight(it, ranges, 0, punt - 1) + ' ' - + highlight(it, ranges, punt, it.label.length); - html += '
  • ' - + binnen - + '
  • '; - } - if (shown.length > n) { - html += '
  • nog ' + (shown.length - n) + ' resultaten, verfijn je zoekterm
  • '; - } - } - list.innerHTML = html; - shown = shown.slice(0, MAX_RENDER); - var doel = 0; - if (behoud && vorigeActieve > 0) doel = Math.min(vorigeActieve, shown.length - 1); - setActive(shown.length ? doel : -1, false); - // De lijst is zo breed als haar inhoud, dus elke filterslag verandert die breedte en - // daarmee ook hoever ze naar links geschoven moet staan. Zonder deze herberekening blijft - // ze hangen op de plek die bij de volledige lijst hoorde: na het intypen van een zoekterm - // staan er dan drie korte regels ver links van het veld waar ze bij horen. openList roept - // position() zelf aan, en daar staat open nog op false, dus dit verdubbelt niets. - if (open) position(); - } - - function setActive(i, scroll) { - var nodes = list.querySelectorAll('li[data-i]'); - for (var k = 0; k < nodes.length; k++) nodes[k].classList.remove('active'); - active = i; - if (i < 0 || i >= nodes.length) { - input.removeAttribute('aria-activedescendant'); - return; - } - nodes[i].classList.add('active'); - input.setAttribute('aria-activedescendant', nodes[i].id); - if (scroll !== false) { - var li = nodes[i], top = li.offsetTop, bot = top + li.offsetHeight; - if (top < list.scrollTop) list.scrollTop = top; - else if (bot > list.scrollTop + list.clientHeight) list.scrollTop = bot - list.clientHeight; - } - } - - function openList(q) { - render(q === undefined ? '' : q); - list.classList.add('open'); - input.setAttribute('aria-expanded', 'true'); - open = true; - position(); - win.addEventListener('scroll', position, true); - win.addEventListener('resize', position); - } - - function closeList(restore) { - list.classList.remove('open'); - input.setAttribute('aria-expanded', 'false'); - input.removeAttribute('aria-activedescendant'); - open = false; - active = -1; - win.removeEventListener('scroll', position, true); - win.removeEventListener('resize', position); - if (restore !== false) input.value = currentLabel(); - } - - function choose(i) { - var it = shown[i]; - if (!it || it.disabled) return; - select.selectedIndex = it.index; - input.value = it.label; - updateWis(); - closeList(false); - if (recentSleutel && !recentManueel) { - pushRecent(recentSleutel, select.options[it.index].value, it.label); - // Herbouwen, anders klopt het blok pas bij de volgende keer dat de opties veranderen. - readOptions(); - } - fire(select, 'input'); - fire(select, 'change'); - } - - // Het script haalt zelf niets weg. Het meldt enkel dat de gebruiker deze regel kwijt wil, en - // wie de opties leverde beslist wat dat betekent en past de met veel opties een zoekveld met substring-zoeken. -// @author glgoose -// @license GPL-3.0-or-later -// @homepageURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/src/branch/main/scripts/select-search -// @supportURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/issues -// @downloadURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/select-search/select-search.user.js -// @updateURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/select-search/select-search.user.js -// @match https://bipp.biotechpartner.be/web/* -// @run-at document-idle -// @grant none -// @noframes false -// ==/UserScript==