From 5d4df257b0ea558371536b0b07f8c3a0ee200a81 Mon Sep 17 00:00:00 2001 From: Glenn Goossens <48956933+glgoose@users.noreply.github.com> Date: Mon, 14 Sep 2026 10:11:22 +0000 Subject: [PATCH] select-search: kleine hangende inspring in plaats van afkappen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Ze kiest nu eerst haar breedte met het volle venster beschikbaar, en schuift daarna zo ver naar links als nodig om te passen. Een label dat dan nog niet op één regel past loopt terug met een hangende inspring van 2.5ch, de breedte van twee cijfers plus een streepje, dus precies het "10-" waarmee elke budgetcode begint. Uitlijnen onder de omschrijving lag meer voor de hand, maar die inspring is zo breed als de code en bij een lange code bleef er nauwelijks tekstbreedte over. De code zelf breekt nooit af, ook niet op zijn eigen streepjes: daarvoor staat ze in een eigen span met white-space:pre, met de spatie erbuiten zodat het eerste woord van de omschrijving wel kan terugvallen. Tussen twee opties staat een haarlijn, want een meerregelige rij loopt anders over in de volgende. highlight() tekent nu een stuk van het label in plaats van het geheel, zodat een zoektreffer over de grens tussen code en omschrijving heen blijft kloppen. Het title-attribuut kan weg: met terugloop staat het volledige label altijd op het scherm. Co-Authored-By: Claude Opus 5 --- scripts/select-search/README.md | 7 ++ scripts/select-search/bookmarklet.txt | 2 +- scripts/select-search/select-search.user.js | 88 +++++++++++++++---- scripts/select-search/src/select-search.js | 86 ++++++++++++++---- .../select-search/src/userscript-header.txt | 2 +- 5 files changed, 152 insertions(+), 33 deletions(-) diff --git a/scripts/select-search/README.md b/scripts/select-search/README.md index f1f99a1..228a207 100644 --- a/scripts/select-search/README.md +++ b/scripts/select-search/README.md @@ -46,6 +46,13 @@ Klik een dropdown aan en typ. `{value, label, index}`. Wie de opties leverde beslist wat verwijderen betekent en past de ` met veel opties een zoekveld met substring-zoeken. // @author glgoose // @license GPL-3.0-or-later @@ -33,6 +33,10 @@ // 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, @@ -144,8 +148,11 @@ /* ------------------------------------------------------------ highlight */ - function highlight(item, tokens) { - if (!tokens.length) return escapeHtml(item.label); + // 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; @@ -156,7 +163,7 @@ from = idx + tok.length; } } - if (!ranges.length) return escapeHtml(item.label); + 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++) { @@ -164,13 +171,33 @@ if (ranges[r][0] <= last[1]) last[1] = Math.max(last[1], ranges[r][1]); else merged.push(ranges[r].slice()); } - var html = '', pos = 0; - for (var m = 0; m < merged.length; m++) { - html += escapeHtml(item.label.slice(pos, merged[m][0])); - html += '' + escapeHtml(item.label.slice(merged[m][0], merged[m][1])) + ''; - pos = merged[m][1]; + 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)); + 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 */ @@ -201,9 +228,25 @@ '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}', - 'li{padding:3px 8px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}', - 'li.kop{padding:6px 8px 2px;color:#777;font-size:11px;font-weight:600;cursor:default}', - 'li.active{background:#0b57d0;color:#fff}', + // 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}', @@ -402,10 +445,17 @@ var below = win.innerHeight - r.bottom - 6; var above = r.top - 6; var down = below >= 160 || below >= above; - list.style.left = r.left + 'px'; + // 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 = Math.max(r.width, Math.min(560, win.innerWidth - r.left - 8)) + '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'; @@ -457,10 +507,16 @@ 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 += '
  • ' - + highlight(it, tokens) + + binnen + '
  • '; } if (shown.length > n) { diff --git a/scripts/select-search/src/select-search.js b/scripts/select-search/src/select-search.js index 18ac3a3..162ccfe 100644 --- a/scripts/select-search/src/select-search.js +++ b/scripts/select-search/src/select-search.js @@ -17,6 +17,10 @@ // 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, @@ -128,8 +132,11 @@ /* ------------------------------------------------------------ highlight */ - function highlight(item, tokens) { - if (!tokens.length) return escapeHtml(item.label); + // 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; @@ -140,7 +147,7 @@ from = idx + tok.length; } } - if (!ranges.length) return escapeHtml(item.label); + 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++) { @@ -148,13 +155,33 @@ if (ranges[r][0] <= last[1]) last[1] = Math.max(last[1], ranges[r][1]); else merged.push(ranges[r].slice()); } - var html = '', pos = 0; - for (var m = 0; m < merged.length; m++) { - html += escapeHtml(item.label.slice(pos, merged[m][0])); - html += '' + escapeHtml(item.label.slice(merged[m][0], merged[m][1])) + ''; - pos = merged[m][1]; + 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)); + 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 */ @@ -185,9 +212,25 @@ '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}', - 'li{padding:3px 8px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}', - 'li.kop{padding:6px 8px 2px;color:#777;font-size:11px;font-weight:600;cursor:default}', - 'li.active{background:#0b57d0;color:#fff}', + // 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}', @@ -386,10 +429,17 @@ var below = win.innerHeight - r.bottom - 6; var above = r.top - 6; var down = below >= 160 || below >= above; - list.style.left = r.left + 'px'; + // 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 = Math.max(r.width, Math.min(560, win.innerWidth - r.left - 8)) + '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'; @@ -441,10 +491,16 @@ 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 += '
  • ' - + highlight(it, tokens) + + binnen + '
  • '; } if (shown.length > n) { diff --git a/scripts/select-search/src/userscript-header.txt b/scripts/select-search/src/userscript-header.txt index f1eecc4..3ae55ba 100644 --- a/scripts/select-search/src/userscript-header.txt +++ b/scripts/select-search/src/userscript-header.txt @@ -1,7 +1,7 @@ // ==UserScript== // @name BIPP: doorzoekbare dropdowns // @namespace https://github.com/glgoose/ua-userscripts -// @version 1.6.0 +// @version 1.8.2 // @description Geeft elke