select-search: recent-blok voor elke dropdown, bipp-budgetcode levert het niet meer zelf
Elke grote dropdown houdt nu zijn eigen vijf laatst gekozen waarden bij, bovenaan de lijst onder de kopregel recent. Alles zit onder een localStorage-sleutel, per veld, met een sleutel die uit name of id volgt zodat rij 3 en rij 4 van hetzelfde veld één lijst delen. Delete haalt een regel uit recent, en data-ss-recent="manual" plus window.selectSearch.remember() is er voor wie pas mag onthouden nadat de server bevestigd heeft. bipp-budgetcode gebruikt die weg en bouwt zijn eigen recent-optgroup dus niet langer zelf. Een optie zonder leesbare tekst telt voortaan als "niets gekozen", ook als ze een waarde draagt, zoals de <option value="0"> </option> van een OAF-scherm. Zo'n regel stond eerst gewoon in de lijst; wie ze koos kreeg haar daarna permanent als lege blauwe balk onder recent te zien. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4a9ea34649
commit
b831ab9f1f
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -22,25 +22,62 @@ Klik een dropdown aan en typ.
|
||||
- Zit de dropdown in groepen (`<optgroup>`), 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 `<select>` zet je
|
||||
een eigen tekst, die wint van dat label.
|
||||
- Een optie zonder leesbare tekst telt evengoed als "niets gekozen", ook al draagt ze een waarde,
|
||||
zoals de `<option value="0"> </option>` van een OAF-scherm. Een regel die je niet kunt
|
||||
lezen is geen keuze: ze blijft uit de lijst en dus ook uit *recent*. Is er geen optie met een
|
||||
lege waarde, dan mikt het kruisje op deze.
|
||||
- Staat er iets gekozen, dan neemt een kruisje de plek van het pijltje in zodra je er met de muis
|
||||
over gaat of erin staat. Dat zet de keuze terug op leeg. Het kruisje krijgt dus geen eigen
|
||||
ruimte: de tekst houdt de volle breedte en er verschuift niets. Backspace in een leeg veld doet
|
||||
hetzelfde. Zonder lege optie is er niets te wissen en blijft het kruisje weg.
|
||||
- Staat er `data-removable` op een `<option>` of op een `<optgroup>`, dan haalt `Delete` de
|
||||
regel die oplicht uit de lijst, zolang je niets getypt hebt. Ben je aan het zoeken, dan blijft
|
||||
Delete een gewone teksttoets. Het script gooit zelf niets weg: het stuurt een `select-search:remove`
|
||||
event op de `<select>`, met `detail` `{value, label, index}`. Wie de opties leverde beslist
|
||||
wat verwijderen betekent en past de `<select>` aan, waarna de lijst zichzelf hertekent met
|
||||
behoud van de zoekterm en de plek van de selectie.
|
||||
- `Delete` haalt de regel die oplicht uit haar lijst, zolang je niets getypt hebt. Ben je aan het
|
||||
zoeken, dan blijft Delete een gewone teksttoets. Op een regel uit *recent* doet het script dat
|
||||
zelf. Staat er `data-removable` op een `<option>` of op een `<optgroup>`, dan gooit het script
|
||||
zelf niets weg: het stuurt een `select-search:remove` event op de `<select>`, met `detail`
|
||||
`{value, label, index}`. Wie de opties leverde beslist wat verwijderen betekent en past de
|
||||
`<select>` aan, waarna de lijst zichzelf hertekent met behoud van de zoekterm en de plek van
|
||||
de selectie.
|
||||
- Boven 200 treffers toont het enkel het aantal. Verfijn dan je zoekterm.
|
||||
|
||||
## Recent
|
||||
|
||||
Elke dropdown houdt zijn eigen vijf laatst gekozen waarden bij, bovenaan de lijst onder de
|
||||
kopregel *recent*. Er is geen vervaltermijn: vijf plaatsen verdringen zichzelf al, en een lijst
|
||||
die na verlof leeg blijkt is enkel verwarrend. `Delete` op zo'n regel haalt ze eruit, waarna ze
|
||||
weer gewoon tussen de andere opties staat.
|
||||
|
||||
De lijst hangt aan het veld, niet aan de pagina of aan de rij. De sleutel komt uit `name` of `id`,
|
||||
met de volgnummers eruit: `ctl00$body$resultTable$ctl03$interfaceAccountList` en dezelfde dropdown
|
||||
in rij 4 delen dus één lijst. Heeft een `<select>` 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": { "<veldsleutel>": { "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 `<select>` zonder allebei. |
|
||||
| `data-ss-recent="manual"` | Het blok en `Delete` werken, maar kiezen onthoudt niets. Voor wie pas mag onthouden na een bevestiging van de server. |
|
||||
| `window.selectSearch.remember(selectOfSleutel, value, label)` | Zet een waarde in recent en tekent het blok opnieuw. |
|
||||
| `window.selectSearch.forget(selectOfSleutel, value)` | Haalt ze er weer uit. |
|
||||
|
||||
## Problemen
|
||||
|
||||
| Symptoom | Oplossing |
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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 <select> 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: { "<sleutel>": { ts: <ms>, 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 <option value="0"> </option>)
|
||||
// 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 || !label) {
|
||||
if (!o.value) {
|
||||
if (leegIndex === -1) { leegIndex = i; leegLabel = label; }
|
||||
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 {
|
||||
|
||||
@@ -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: { "<sleutel>": { ts: <ms>, 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 <option value="0"> </option>)
|
||||
// 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 || !label) {
|
||||
if (!o.value) {
|
||||
if (leegIndex === -1) { leegIndex = i; leegLabel = label; }
|
||||
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 {
|
||||
|
||||
@@ -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 <select> met veel opties een zoekveld met substring-zoeken.
|
||||
// @author glgoose
|
||||
// @license GPL-3.0-or-later
|
||||
|
||||
@@ -23,6 +23,14 @@
|
||||
<td><select id="fab" name="fabricaat" data-placeholder="fabricaat"></select></td></tr>
|
||||
<tr><td><label for="groep">Met groepen</label></td>
|
||||
<td><select id="groep" name="groep"></select></td></tr>
|
||||
<tr><td><label for="ctl00_body_resultTable_ctl03_interfaceAccountList">Grootboek rij 3</label></td>
|
||||
<td><select id="ctl00_body_resultTable_ctl03_interfaceAccountList"
|
||||
name="ctl00$body$resultTable$ctl03$interfaceAccountList"></select></td></tr>
|
||||
<tr><td><label for="ctl00_body_resultTable_ctl04_interfaceAccountList">Grootboek rij 4</label></td>
|
||||
<td><select id="ctl00_body_resultTable_ctl04_interfaceAccountList"
|
||||
name="ctl00$body$resultTable$ctl04$interfaceAccountList"></select></td></tr>
|
||||
<tr><td><label for="oaf">OAF-stijl</label></td>
|
||||
<td><select id="oaf" name="oaf"></select></td></tr>
|
||||
<tr><td><label for="klein">Klein menu</label></td>
|
||||
<td><select id="klein" name="klein">
|
||||
<option value="">--</option><option>Ja</option><option>Nee</option>
|
||||
@@ -74,14 +82,14 @@ function vulGroepen(sel){
|
||||
leeg.value = ''; leeg.textContent = 'leverancier';
|
||||
sel.appendChild(leeg);
|
||||
var blokken = [
|
||||
['recent', maak(3, 300)],
|
||||
['favorieten', maak(3, 300)],
|
||||
['alle leveranciers', maak(60, 700)]
|
||||
];
|
||||
blokken.forEach(function(blok){
|
||||
var g = document.createElement('optgroup');
|
||||
g.label = blok[0];
|
||||
// Alleen recent mag met Delete uitgedund worden. Zo is de vlag hier ook echt getest.
|
||||
if (blok[0] === 'recent') g.dataset.removable = '1';
|
||||
// Alleen favorieten mag met Delete uitgedund worden. Zo is de vlag hier ook echt getest.
|
||||
if (blok[0] === 'favorieten') g.dataset.removable = '1';
|
||||
blok[1].forEach(function(o){
|
||||
var e = document.createElement('option');
|
||||
e.value = o.v; e.textContent = o.t;
|
||||
@@ -92,8 +100,25 @@ function vulGroepen(sel){
|
||||
}
|
||||
vulGroepen(document.getElementById('groep'));
|
||||
|
||||
// De host beslist wat verwijderen betekent. Hier: de optie gaat uit recent en verhuist naar de
|
||||
// tweede groep, net als het echte script met zijn recente budgetcodes doet.
|
||||
// OAF schrijft zijn "niets gekozen" niet met een lege waarde maar met een spatie-optie die wel
|
||||
// een waarde draagt. Die regel hoort niet in de lijst te staan (je kunt ze niet lezen en dus ook
|
||||
// niet herkennen) en al helemaal niet in recent, maar het kruisje mag er wel op mikken.
|
||||
function vulOaf(sel){
|
||||
sel.innerHTML = '';
|
||||
var leeg = document.createElement('option');
|
||||
leeg.value = '0'; leeg.innerHTML = ' ';
|
||||
sel.appendChild(leeg);
|
||||
maak(40, 500).forEach(function(o){
|
||||
var e = document.createElement('option');
|
||||
e.value = o.v; e.textContent = o.t;
|
||||
sel.appendChild(e);
|
||||
});
|
||||
}
|
||||
vulOaf(document.getElementById('oaf'));
|
||||
|
||||
// De host beslist wat verwijderen betekent voor zijn eigen regels. Hier: de optie gaat uit
|
||||
// favorieten en verhuist naar de tweede groep. Het recent-blok erboven beheert select-search
|
||||
// zelf; Delete daarop komt hier dus niet langs.
|
||||
document.getElementById('groep').addEventListener('select-search:remove', function(e){
|
||||
var sel = e.currentTarget;
|
||||
var weg = sel.options[e.detail.index];
|
||||
@@ -104,6 +129,11 @@ document.getElementById('groep').addEventListener('select-search:remove', functi
|
||||
sel.value = gekozen;
|
||||
});
|
||||
|
||||
var rij3 = document.getElementById('ctl00_body_resultTable_ctl03_interfaceAccountList');
|
||||
var rij4 = document.getElementById('ctl00_body_resultTable_ctl04_interfaceAccountList');
|
||||
vul(rij3, 40, 200);
|
||||
vul(rij4, 40, 200);
|
||||
|
||||
var lev = document.getElementById('lev'), fab = document.getElementById('fab');
|
||||
function herlaad(){
|
||||
var inclusief = document.getElementById('incl').checked;
|
||||
|
||||
Reference in New Issue
Block a user