diff --git a/scripts/bipp-budgetcode/README.md b/scripts/bipp-budgetcode/README.md index 4cec2ab..733358f 100644 --- a/scripts/bipp-budgetcode/README.md +++ b/scripts/bipp-budgetcode/README.md @@ -17,8 +17,10 @@ gekozen waarde zit wel al in de pagina, maar alleen als tooltip op die link. staat. - Zoeken in die lijst van ruim vijfduizend codes gaat via [select-search](../select-search/README.md), dat op dezelfde pagina meeloopt. -- Bovenaan de lijst staan onder *recent* de codes die je het laatst gebruikt hebt. Die - staan verderop niet nog een keer: elke code komt maar één keer in de lijst voor. +- Bovenaan de lijst staan onder *recent* de vijf codes die je het laatst gebruikt hebt. Dat blok + komt van [select-search](../select-search/README.md#recent) en werkt intussen op elke grote + dropdown op BIPP, niet enkel op de budgetcode. Hier telt een code pas als de server ze + aanvaard heeft: mislukt het opslaan, dan blijft recent ongemoeid. - Is de grootboekrekening ook gekozen, dan komt de code in **Interne commentaar** te staan. Stond daar al iets, dan komt ze op een nieuwe regel eronder. Kies je daarna een andere code, dan wordt die regel vervangen in plaats van dat er een bijkomt. diff --git a/scripts/bipp-budgetcode/bipp-budgetcode.user.js b/scripts/bipp-budgetcode/bipp-budgetcode.user.js index 7f0c55a..fb608d3 100644 --- a/scripts/bipp-budgetcode/bipp-budgetcode.user.js +++ b/scripts/bipp-budgetcode/bipp-budgetcode.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name BIPP: budgetcode in het winkelmandje // @namespace https://github.com/glgoose/ua-userscripts -// @version 1.4.0 +// @version 1.5.0 // @description Zet de budgetcode als dropdown in de winkelmandjerij zelf, onder de grootboekrekening, zodat je niet meer via de aparte pagina Selectie analytische velden moet en de gekozen code ook gewoon ziet staan. Slaat op via een achtergrond-postback en vult de code aan in Interne commentaar. // @author Glenn Goossens // @license GPL-3.0-or-later @@ -30,9 +30,7 @@ var SELECTOR_URL = 'AnalyticalFieldSelector.aspx'; var CACHE_KEY = 'bippBudgetcodeOptions'; - var MRU_KEY = 'bippBudgetcodeRecent'; var TTL_MS = 7 * 24 * 60 * 60 * 1000; - var MRU_MAX = 8; var EMPTY_LABEL = 'budgetcode'; // ---------- kleine hulpjes ---------- @@ -117,24 +115,6 @@ return loading; } - // ---------- recent gebruikte codes ---------- - - function readMru() { - try { return JSON.parse(localStorage.getItem(MRU_KEY)) || []; } catch (e) { return []; } - } - function pushMru(value, label) { - if (!value) return; - var list = readMru().filter(function (e) { return e[0] !== value; }); - list.unshift([value, label]); - try { localStorage.setItem(MRU_KEY, JSON.stringify(list.slice(0, MRU_MAX))); } catch (e) { /* stil */ } - } - // De lijst kon alleen groeien. Dit is de weg terug: de code verdwijnt uit recent en staat - // daarna weer gewoon tussen alle budgetcodes. - function removeMru(value) { - var list = readMru().filter(function (e) { return e[0] !== value; }); - try { localStorage.setItem(MRU_KEY, JSON.stringify(list)); } catch (e) { /* stil */ } - } - // ---------- formulier posten ---------- function serialize(form, extra) { @@ -401,27 +381,12 @@ // dubbel in. De select zelf blijft dezelfde node, dus dit verschuift niets. while (select.children.length > 1) select.removeChild(select.lastChild); - var mru = readMru(); - var inRecent = {}; - if (mru.length) { - var recent = document.createElement('optgroup'); - recent.label = 'recent'; - // Hiermee weet select-search dat deze regels weg mogen met Delete. Alle budgetcodes krijgt - // die vlag niet, daar valt niets te verwijderen. - recent.dataset.removable = '1'; - mru.forEach(function (e) { - inRecent[e[0]] = true; - recent.appendChild(new Option(e[1], e[0])); - }); - select.appendChild(recent); - } - + // Het recent-blok komt van select-search, dat het bovenaan de lijst zet en de dubbels er + // hier uit haalt. Wij leveren enkel de volledige lijst. var all = document.createElement('optgroup'); all.label = 'alle budgetcodes'; data.options.forEach(function (e) { - // Wat al onder recent staat komt hier niet nog eens: twee keer dezelfde regel in de - // zoekresultaten is verwarrend, ook al staat er een groepslabel bij. - if (e[0] && !inRecent[e[0]]) all.appendChild(new Option(e[1], e[0])); + if (e[0]) all.appendChild(new Option(e[1], e[0])); }); select.appendChild(all); @@ -437,7 +402,11 @@ var select = document.createElement('select'); select.className = 'TextBox'; - // Geen name-attribuut: dan serialiseert FormData ons veld niet mee. + // Geen name-attribuut: dan serialiseert FormData ons veld niet mee. select-search leidt zijn + // veldsleutel normaal uit name of id af, dus die krijgt hij hier expliciet mee. Manueel, want + // een code hoort pas in recent te staan als de server ze aanvaard heeft, zie onPick. + select.dataset.ssRecent = 'manual'; + select.dataset.ssRecentKey = 'budgetcode'; select.appendChild(new Option(EMPTY_LABEL, '')); var code = currentCode(row); @@ -498,7 +467,9 @@ row.pending = null; // De guard kan de zichtbare waarde ondertussen teruggezet hebben. setValue(row, code); - if (code) pushMru(code, label); + if (code && window.selectSearch && window.selectSearch.remember) { + window.selectSearch.remember(select, code, label); + } // Geen vinkje. De zandloper verdwijnt en de dropdowntekst wordt weer zwart, en dat is // de bevestiging al. Een tick die een seconde later toch weer weggaat voegt niets toe // en vraagt aandacht voor iets wat al gelukt is. @@ -543,14 +514,6 @@ return; } fillOptions(u.select, data, row.savedCode); - // select-search meldt dat een regel uit recent weg mag, wij weten pas wat dat betekent. - // Opnieuw opbouwen zet de code vanzelf terug onder alle budgetcodes. Er wordt niets - // opgeslagen: fillOptions vuurt geen change, dus onPick en de POST-keten blijven erbuiten. - u.select.addEventListener('select-search:remove', function (e) { - if (!e.detail || !e.detail.value) return; - removeMru(e.detail.value); - fillOptions(u.select, data, u.select.value); - }); if (typeof window.__selectSearchScan === 'function') window.__selectSearchScan(); }); })['catch'](function (err) { diff --git a/scripts/select-search/README.md b/scripts/select-search/README.md index 2573b00..3f8de39 100644 --- a/scripts/select-search/README.md +++ b/scripts/select-search/README.md @@ -22,25 +22,62 @@ Klik een dropdown aan en typ. - Zit de dropdown in groepen (``), dan komt elke groepsnaam als grijze kopregel boven zijn blok te staan. Die regel is geen resultaat: pijltjes slaan hem over en Enter kiest hem nooit. Opties zonder groep blijven los bovenaan. +- Bovenaan staat onder *recent* wat je in dat veld het laatst gekozen hebt, tot vijf waarden. + Ze staan niet nog eens in hun eigen groep. Zie [Recent](#recent). - Ook bij zoeken blijven de groepen aaneengesloten en in de volgorde van de dropdown zelf, zodat - een groep als *recent* bovenaan blijft en geen enkele kopregel zich herhaalt. Binnen een groep - komt wat met de zoekterm begint eerst. + *recent* bovenaan blijft en geen enkele kopregel zich herhaalt. Binnen een groep komt wat met + de zoekterm begint eerst. - Er wordt enkel op het label gezocht, niet op de groepsnaam. - Een optie met een lege waarde geldt als "niets gekozen". Ze staat niet in de lijst, maar haar label wordt de grijze placeholder van het veld. Met `data-placeholder` op de ``, met `detail` `{value, label, index}`. Wie de opties leverde beslist - wat verwijderen betekent en past de ``, met `detail` + `{value, label, index}`. Wie de opties leverde beslist wat verwijderen betekent en past de + `` geen `name` en geen `id`, dan krijgt hij geen +recent-blok en blijft hij verder gewoon doorzoekbaar. + +Alles staat samen onder één localStorage-sleutel, `selectSearchRecent`: + +```json +{ "v": 1, "fields": { "": { "ts": 0, "items": [["waarde", "label"]] } } } +``` + +`ts` dient enkel om boven de 40 velden het oudste veld te laten vallen. Lezen en schrijven falen +stil: is de opslag vol of geblokkeerd, dan verdwijnt het recent-blok en werkt de rest gewoon door. + +### Voor wie de opties levert + +| | | +|---|---| +| `data-ss-recent-key="..."` | Eigen veldsleutel, in plaats van die uit `name` of `id`. Nodig bij een ` met veel opties een zoekveld met substring-zoeken. // @author glgoose // @license GPL-3.0-or-later @@ -34,6 +34,12 @@ var MIN_OPTIONS = 10; var MAX_RENDER = 200; 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 */ @@ -64,6 +70,78 @@ }; } + /* --------------------------------------------------------------- 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. + function recentKey(select) { + 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 */ function highlight(item, tokens) { @@ -139,6 +217,10 @@ 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'); @@ -206,24 +288,53 @@ 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. Opties zonder groep houden gi 0 en blijven dus vooraan. + // 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) { - if (leegIndex === -1) { leegIndex = i; leegLabel = label; } + 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 = 0; + if (!grp) gi = 1; else { gi = groepen.indexOf(grp); - if (gi === -1) { groepen.push(grp); gi = groepen.length; } else gi += 1; + if (gi === -1) { groepen.push(grp); gi = groepen.length + 1; } else gi += 2; } - items.push({ + gewoon.push({ index: i, label: label, group: grp, @@ -239,6 +350,19 @@ 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() { @@ -395,6 +519,11 @@ 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'); } @@ -439,6 +568,7 @@ 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); } @@ -506,6 +636,12 @@ 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; } @@ -544,6 +680,12 @@ 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(); } @@ -592,6 +734,27 @@ 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 { diff --git a/scripts/select-search/src/select-search.js b/scripts/select-search/src/select-search.js index 8fd7b02..18ac3a3 100644 --- a/scripts/select-search/src/select-search.js +++ b/scripts/select-search/src/select-search.js @@ -18,6 +18,12 @@ var MIN_OPTIONS = 10; var MAX_RENDER = 200; 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 */ @@ -48,6 +54,78 @@ }; } + /* --------------------------------------------------------------- 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. + function recentKey(select) { + 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 */ function highlight(item, tokens) { @@ -123,6 +201,10 @@ 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'); @@ -190,24 +272,53 @@ 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. Opties zonder groep houden gi 0 en blijven dus vooraan. + // 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) { - if (leegIndex === -1) { leegIndex = i; leegLabel = label; } + 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 = 0; + if (!grp) gi = 1; else { gi = groepen.indexOf(grp); - if (gi === -1) { groepen.push(grp); gi = groepen.length; } else gi += 1; + if (gi === -1) { groepen.push(grp); gi = groepen.length + 1; } else gi += 2; } - items.push({ + gewoon.push({ index: i, label: label, group: grp, @@ -223,6 +334,19 @@ 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() { @@ -379,6 +503,11 @@ 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'); } @@ -423,6 +552,7 @@ 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); } @@ -490,6 +620,12 @@ 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; } @@ -528,6 +664,12 @@ 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(); } @@ -576,6 +718,27 @@ 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 { diff --git a/scripts/select-search/src/userscript-header.txt b/scripts/select-search/src/userscript-header.txt index ef59751..f1eecc4 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.5.0 +// @version 1.6.0 // @description Geeft elke + + + + + +