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
@@ -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) {
|
||||
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 {
|
||||
|
||||
@@ -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) {
|
||||
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 {
|
||||
|
||||
@@ -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