Compare commits
13
Commits
b460ccc214
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee1dca93ea | ||
|
|
3e610366fe | ||
|
|
5320b17945 | ||
|
|
f140a32199 | ||
|
|
e04ff6c5ff | ||
|
|
3b2528d72a | ||
|
|
6d9ae45a2f | ||
|
|
fcecc68c07 | ||
|
|
fe1ed96f7f | ||
|
|
ebd84ec0d5 | ||
|
|
a548ee9174 | ||
|
|
11906d3b6b | ||
|
|
b41790dc56 |
@@ -1,74 +1,51 @@
|
||||
# ua-userscripts
|
||||
|
||||
Dev-conventies voor dit script-repo: dev-stub workflow en naamgeving.
|
||||
Repo: https://gitea.flw.uantwerpen.be/glenn/ua-userscripts. Scripts in `scripts/<map>/`, elk met een eigen README.
|
||||
|
||||
## Dev-stub vs. gebruik
|
||||
|
||||
De file in `scripts/` is de bron van waarheid voor de code. Wat in Tampermonkey staat is dus
|
||||
óf een loader die naar die file wijst, óf een kopie die van de raw-URL komt. Nooit een met de
|
||||
hand geplakte kopie, want dan zijn er twee waarheden die stil uit elkaar lopen.
|
||||
`scripts/` is de bron van waarheid. In Tampermonkey staat een van twee sporen, nooit een met de hand geplakte kopie:
|
||||
|
||||
| spoor | wat er in Tampermonkey staat | grootte | `@version` |
|
||||
| ----------- | --------------------------------------- | ----------------- | ----------- |
|
||||
| **dev** | stub met `@require file:///...` | een paar honderd byte | `0.0.0-dev` |
|
||||
| **gebruik** | volledige kopie via de raw-URL | de echte grootte | `1.0.0` |
|
||||
| spoor | in Tampermonkey | `@version` |
|
||||
| ----------- | ------------------------------- | ----------- |
|
||||
| **dev** | stub met `@require file:///...` | `0.0.0-dev` |
|
||||
| **gebruik** | volledige kopie via de raw-URL | `1.0.0` |
|
||||
|
||||
De grootte in het dashboard zegt dus in welk spoor een rij zit.
|
||||
|
||||
### Een dev-stub maken
|
||||
### Dev-stub maken en installeren
|
||||
|
||||
```sh
|
||||
bin/dev-stub bipp-budgetcode
|
||||
bin/dev-stub bipp-budgetcode # --list toont de mappen, --no-copy slaat het klembord over
|
||||
```
|
||||
|
||||
De stub gaat meteen naar het klembord. In Tampermonkey neem je het potlood van de bestaande
|
||||
rij, dan Cmd+A, Cmd+V, Cmd+S. Via de `+`-tab krijg je een tweede rij in plaats van een
|
||||
bijgewerkte, want Tampermonkey herkent een script aan `@namespace` plus `@name`.
|
||||
Claude installeert de stub zelf:
|
||||
|
||||
`bin/dev-stub --list` toont de scriptmappen, `--no-copy` slaat het klembord over.
|
||||
1. Serveer de stub (`--no-copy`) als `<map>.user.js` vanuit de scratchpad op een vrije poort.
|
||||
2. Open die URL in de Claude-in-Chrome-tab, in het Chrome-profiel **UA** (Profile 1, kies met `select_browser`; controleer via de `History` van dat profiel).
|
||||
3. De gebruiker klikt op **Install** (de `chrome-extension://`-pagina is onbereikbaar voor de tools).
|
||||
4. Stop de server.
|
||||
|
||||
Drie dingen die aan die stub vasthangen:
|
||||
Zelfde `@namespace` plus `@name` werkt een bestaande rij bij. Een andere `@name` geeft een tweede rij die dezelfde file nog eens laadt (elke sneltoets dubbel): verwijder dan eerst de oude.
|
||||
|
||||
- **De metadata komt uit de stub, niet uit de required file.** `@match`, `@grant` en `@run-at`
|
||||
moeten er dus in staan; het `==UserScript==`-blok van de file zelf wordt genegeerd. `dev-stub`
|
||||
neemt die velden over uit het echte script, zodat de twee niet uit elkaar lopen.
|
||||
- **`@downloadURL` en `@updateURL` staan er allebei niet in.** De code komt uit een lokaal
|
||||
bestand, dus er is niets te downloaden of bij te werken. `@updateURL none` is geen
|
||||
oplossing: de editor van Tampermonkey toont dan een eslint-waarschuwing
|
||||
(`use-download-and-update-url`), en zonder `@downloadURL` heeft `@updateURL` ook geen
|
||||
functie. Een rij die ooit van de raw-URL kwam, onthoudt die URL apart van het meta-blok en
|
||||
biedt dan alsnog een update aan die de stub overschrijft: verwijder zo'n rij en maak de stub
|
||||
opnieuw aan via **Create a new script**.
|
||||
- **De stub hoeft niet mee met een `@version`-bump.** Ze staat op een vaste `0.0.0-dev`. Een
|
||||
echt nummer zou daar toch niets zeggen: de code komt bij elke pagina-load van schijf, dus wat
|
||||
er draait is wat er nú in de repo staat. De kolom hoeft enkel `-dev` te tonen. Opnieuw
|
||||
genereren en plakken doe je alleen als de metadata zelf wijzigt, dus bij een andere `@match`,
|
||||
`@grant`, `@run-at` of `@require`.
|
||||
- **Heeft de map een `src/`, dan wijst `@require` daarnaar,** niet naar het gebouwde script.
|
||||
Opslaan en de pagina herladen volstaat; `build.sh` draai je pas als je publiceert, voor de
|
||||
raw-URL en de bookmarklet.
|
||||
Eigenschappen van de stub:
|
||||
|
||||
Lokale bestanden lezen mag Tampermonkey alleen als **Allow access to file URLs** aanstaat, bij
|
||||
`chrome://extensions` onder Details.
|
||||
- **Metadata komt uit de stub**, niet uit de required file. `@match`, `@grant`, `@run-at` en `@require` moeten erin staan; `dev-stub` neemt ze over. Opnieuw genereren en installeren is alleen nodig als die wijzigen.
|
||||
- **Geen `@downloadURL` en `@updateURL`.** `@updateURL none` geeft een eslint-waarschuwing in de editor. Een rij die ooit van de raw-URL kwam, biedt alsnog updates aan die de stub overschrijven: verwijder die rij en maak de stub opnieuw via **Create a new script**.
|
||||
- **Vaste `0.0.0-dev`**, geen bump nodig: de code komt bij elke paginaload van schijf.
|
||||
- **`@require` wijst naar het script zelf.** Opslaan en de pagina herladen volstaat.
|
||||
- **Vereist** dat **Allow access to file URLs** aanstaat (`chrome://extensions`, Details).
|
||||
|
||||
### Naamgeving
|
||||
## Publiceren
|
||||
|
||||
`<Toepassing>: <wat het doet>`, met de toepassing zoals je ze zelf noemt (`BIPP`, `iExpense`,
|
||||
`PeopleSoft`), daarna de dubbele punt en een kleine-letter naamwoordgroep zonder punt. Het
|
||||
dashboard sorteert op naam, dus zo staan de scripts van dezelfde toepassing bij elkaar.
|
||||
- Hoog `@version` op bij elke wijziging, anders krijgt niemand de update.
|
||||
- `@namespace` blijft `https://github.com/glgoose/ua-userscripts`. Het is alleen een identificatie, en wijzigen geeft bij iedereen een dubbele rij.
|
||||
|
||||
- Geen `[DEV]` in de naam. Dat groepeert op ontwikkelstatus in plaats van op toepassing, en de
|
||||
Version-kolom zegt met `-dev` al hetzelfde.
|
||||
- Geen technische slug in de naam. Die staat in het repo-pad en in `@homepageURL`.
|
||||
- Draait een script op meerdere sites, dan `Algemeen: ...`.
|
||||
## Naamgeving
|
||||
|
||||
`<Toepassing>: <wat het doet>`: toepassing zoals je ze zelf noemt (`BIPP`, `iExpense`, `PeopleSoft`, of `Algemeen` bij meerdere sites), dubbele punt, kleine-letter naamwoordgroep zonder punt. Het dashboard sorteert op naam, zo staan scripts van dezelfde toepassing bij elkaar.
|
||||
|
||||
- Geen `[DEV]` of technische slug in de naam.
|
||||
- Mapnaam en bestandsnaam in kebab-case, zelfde volgorde: `<toepassing>-<functie>`.
|
||||
|
||||
Hoog `@version` op bij elke wijziging, anders krijgt niemand de update binnen.
|
||||
|
||||
## Testen op BIPP
|
||||
|
||||
BIPP-pagina's zoals `OrderComposer.aspx` en `OrderRequestDetail.aspx` werken alleen via het
|
||||
menu (een `__doPostBack`, bijvoorbeeld `javascript:__doPostBack('orderRequestMenu$reviewRequestBtn','')`).
|
||||
Open je ze rechtstreeks via de URL, dan geeft de server "Object reference not set to an instance
|
||||
of an object". Dat ligt niet aan je script: navigeer eerst naar een andere BIPP-pagina en klik
|
||||
dan in het menu.
|
||||
Pagina's zoals `OrderComposer.aspx` en `OrderRequestDetail.aspx` werken alleen via het menu (een `__doPostBack`, bijvoorbeeld `javascript:__doPostBack('orderRequestMenu$reviewRequestBtn','')`). Rechtstreeks via de URL geeft de server "Object reference not set to an instance of an object": navigeer eerst naar een andere BIPP-pagina en klik dan in het menu.
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
| toepassing | wat het doet | |
|
||||
| -------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **BIPP** | zoekveld in dropdowns met veel opties | [installeren](https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/select-search/select-search.user.js) |
|
||||
| **BIPP** | budgetcode kiezen en zien in het winkelmandje | [installeren](https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/bipp-budgetcode/bipp-budgetcode.user.js) |
|
||||
| **BIPP** | "Inclusief niet-raamcontractartikelen" standaard aan | [installeren](https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/bipp-raamcontract/bipp-raamcontract.user.js) |
|
||||
| **BIPP** | budgetverantwoordelijke en aanvrager standaard op jezelf | [installeren](https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/bipp-budgetverantwoordelijke/bipp-budgetverantwoordelijke.user.js) |
|
||||
| **iExpense** | alle onkostenrapporten in één doorzoekbare tabel | [installeren](https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/iexpense-search/iexpense-search.user.js) |
|
||||
| **PeopleSoft** | werkende knop "Downloaden naar Excel" | [installeren](https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/peoplesoft-excel-download/peoplesoft-excel-download.user.js) |
|
||||
| **BIPP** | zoekveld in dropdowns met veel opties | [installeren](https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/select-search/select-search.user.js) |
|
||||
| **BIPP** | budgetcode kiezen en zien in het winkelmandje | [installeren](https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/bipp-budgetcode/bipp-budgetcode.user.js) |
|
||||
| **BIPP** | "Inclusief niet-raamcontractartikelen" standaard aan | [installeren](https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/bipp-raamcontract/bipp-raamcontract.user.js) |
|
||||
| **BIPP** | budgetverantwoordelijke en aanvrager standaard op jezelf | [installeren](https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/bipp-budgetverantwoordelijke/bipp-budgetverantwoordelijke.user.js) |
|
||||
| **BIPP** | Ctrl+letter voor Bevestigen, Zoeken, Opslaan en meer | [installeren](https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/bipp-sneltoetsen/bipp-sneltoetsen.user.js) |
|
||||
| **iExpense** | alle onkostenrapporten in één doorzoekbare tabel | [installeren](https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/iexpense-search/iexpense-search.user.js) |
|
||||
| **PeopleSoft** | werkende knop "Downloaden naar Excel" | [installeren](https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/peoplesoft-excel-download/peoplesoft-excel-download.user.js) |
|
||||
|
||||
## Installatie-instructies
|
||||
|
||||
|
||||
+1
-11
@@ -67,19 +67,9 @@ for f in "$dir"/*.user.js; do
|
||||
done
|
||||
if [ -z "$src" ]; then
|
||||
echo "dev-stub: geen *.user.js in $dir" >&2
|
||||
echo "dev-stub: bij een script met een build: draai eerst ./build.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Heeft de map een build, dan wijst @require naar de bron in plaats van naar het
|
||||
# gebouwde bestand: zo volstaat opslaan en de pagina herladen, zonder build.sh
|
||||
# ertussen. De metadata blijft wel uit het gebouwde script komen, want de bron
|
||||
# heeft geen ==UserScript==-blok.
|
||||
code="$src"
|
||||
if [ -f "$dir/src/$(basename "$dir").js" ]; then
|
||||
code="$dir/src/$(basename "$dir").js"
|
||||
fi
|
||||
|
||||
# Eén metadataveld uit de kop van het echte script halen.
|
||||
meta() {
|
||||
sed -n 's|^// @'"$1"' *\(.*[^ ]\) *$|\1|p' "$src"
|
||||
@@ -123,7 +113,7 @@ fi
|
||||
meta resource | while IFS= read -r value; do
|
||||
[ -n "$value" ] && printf '// @%-12s %s\n' resource "$value"
|
||||
done
|
||||
printf '// @%-12s file://%s\n' require "$code"
|
||||
printf '// @%-12s file://%s\n' require "$src"
|
||||
for key in grant connect run-at noframes; do
|
||||
meta "$key" | while IFS= read -r value; do
|
||||
[ -n "$value" ] && printf '// @%-12s %s\n' "$key" "$value"
|
||||
|
||||
@@ -8,7 +8,7 @@ BIPP verbergt die code vandaag volledig: na *Creëer*, kiezen, *Opslaan* en *Ter
|
||||
alleen nog `Bekijk`, in het groen, en die kleur blijft ook staan als de code weer leeg is. De
|
||||
gekozen waarde zit wel al in de pagina, maar alleen als tooltip op die link.
|
||||
|
||||
[**Installeren**](https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/bipp-budgetcode/bipp-budgetcode.user.js)
|
||||
[**Installeren**](https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/bipp-budgetcode/bipp-budgetcode.user.js)
|
||||
· gebeurt er niets, zie de [installatie-uitleg](../../README.md).
|
||||
|
||||
## Gebruik
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// ==UserScript==
|
||||
// @name BIPP: budgetcode in het winkelmandje
|
||||
// @namespace https://github.com/glgoose/ua-userscripts
|
||||
// @version 1.7.0
|
||||
// @version 1.7.1
|
||||
// @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 zet de code ook in Interne commentaar.
|
||||
// @author Glenn Goossens
|
||||
// @license GPL-3.0-or-later
|
||||
// @homepageURL https://github.com/glgoose/ua-userscripts/tree/main/scripts/bipp-budgetcode
|
||||
// @supportURL https://github.com/glgoose/ua-userscripts/issues
|
||||
// @downloadURL https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/bipp-budgetcode/bipp-budgetcode.user.js
|
||||
// @updateURL https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/bipp-budgetcode/bipp-budgetcode.user.js
|
||||
// @homepageURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/src/branch/main/scripts/bipp-budgetcode
|
||||
// @supportURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/issues
|
||||
// @downloadURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/bipp-budgetcode/bipp-budgetcode.user.js
|
||||
// @updateURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/bipp-budgetcode/bipp-budgetcode.user.js
|
||||
// @match https://bipp.biotechpartner.be/web/*
|
||||
// @grant none
|
||||
// @run-at document-idle
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Zet in BIPP het filter *Budget verantwoordelijke* op *Aanvraag evalueren* en het filter *Aanvrager* op *Aanvragen* standaard op jezelf in plaats van *Alle*, en start de zoekopdracht.
|
||||
|
||||
[**Installeren**](https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/bipp-budgetverantwoordelijke/bipp-budgetverantwoordelijke.user.js)
|
||||
[**Installeren**](https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/bipp-budgetverantwoordelijke/bipp-budgetverantwoordelijke.user.js)
|
||||
· gebeurt er niets, zie de [installatie-uitleg](../../README.md).
|
||||
|
||||
## Hoe het werkt
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// ==UserScript==
|
||||
// @name BIPP: budgetverantwoordelijke standaard op de ingelogde gebruiker
|
||||
// @namespace https://github.com/glgoose/ua-userscripts
|
||||
// @version 1.1.0
|
||||
// @version 1.1.1
|
||||
// @description Zet "Budget verantwoordelijke" bij Aanvraag evalueren en "Aanvrager" bij Aanvragen op jezelf in plaats van "Alle" en start de zoekopdracht.
|
||||
// @author glgoose
|
||||
// @license GPL-3.0-or-later
|
||||
// @homepageURL https://github.com/glgoose/ua-userscripts/tree/main/scripts/bipp-budgetverantwoordelijke
|
||||
// @supportURL https://github.com/glgoose/ua-userscripts/issues
|
||||
// @downloadURL https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/bipp-budgetverantwoordelijke/bipp-budgetverantwoordelijke.user.js
|
||||
// @updateURL https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/bipp-budgetverantwoordelijke/bipp-budgetverantwoordelijke.user.js
|
||||
// @homepageURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/src/branch/main/scripts/bipp-budgetverantwoordelijke
|
||||
// @supportURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/issues
|
||||
// @downloadURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/bipp-budgetverantwoordelijke/bipp-budgetverantwoordelijke.user.js
|
||||
// @updateURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/bipp-budgetverantwoordelijke/bipp-budgetverantwoordelijke.user.js
|
||||
// @match https://bipp.biotechpartner.be/web/*
|
||||
// @run-at document-idle
|
||||
// @grant none
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Zet het vinkje *Inclusief niet-raamcontractartikelen* op de zoekpagina van BIPP standaard aan.
|
||||
|
||||
[**Installeren**](https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/bipp-raamcontract/bipp-raamcontract.user.js)
|
||||
[**Installeren**](https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/bipp-raamcontract/bipp-raamcontract.user.js)
|
||||
· gebeurt er niets, zie de [installatie-uitleg](../../README.md).
|
||||
|
||||
## Hoe het werkt
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// ==UserScript==
|
||||
// @name BIPP: "Inclusief niet-raamcontractartikelen" standaard aan
|
||||
// @namespace https://github.com/glgoose/ua-userscripts
|
||||
// @version 1.0.0
|
||||
// @version 1.0.1
|
||||
// @description Vinkt "Inclusief niet-raamcontractartikelen" aan zodra de zoekpagina laadt.
|
||||
// @author glgoose
|
||||
// @license GPL-3.0-or-later
|
||||
// @homepageURL https://github.com/glgoose/ua-userscripts/tree/main/scripts/bipp-raamcontract
|
||||
// @supportURL https://github.com/glgoose/ua-userscripts/issues
|
||||
// @downloadURL https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/bipp-raamcontract/bipp-raamcontract.user.js
|
||||
// @updateURL https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/bipp-raamcontract/bipp-raamcontract.user.js
|
||||
// @homepageURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/src/branch/main/scripts/bipp-raamcontract
|
||||
// @supportURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/issues
|
||||
// @downloadURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/bipp-raamcontract/bipp-raamcontract.user.js
|
||||
// @updateURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/bipp-raamcontract/bipp-raamcontract.user.js
|
||||
// @match https://bipp.biotechpartner.be/web/*
|
||||
// @run-at document-idle
|
||||
// @grant none
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# bipp-sneltoetsen
|
||||
|
||||
Sneltoetsen in BIPP, per rol van de knop. Ze werken in de Nederlandse en de Engelse interface, want de toets hangt aan de rol en niet aan een letter uit het label. De tooltip toont de sneltoets, en zolang je Ctrl, Cmd of Alt ingedrukt houdt verschijnt de sneltoets onder elke knop die er een heeft.
|
||||
|
||||
| toets | rol | NL | EN |
|
||||
| ------------------------------ | ---------- | --------------------------- | ---------------------- |
|
||||
| Ctrl+Enter (Mac ook Cmd+Enter) | hoofdactie | Bevestigen of Goedkeuren, anders Indienen | Authorization or Approve, anders Submit |
|
||||
| Ctrl+S | opslaan | Opslaan | Save |
|
||||
| Ctrl+[ | terug | Terug | Back |
|
||||
| Alt+S | zoeken | Zoeken | Search |
|
||||
| Alt+R | reset | Reset | Reset |
|
||||
| Alt+N | nieuw | Nieuw | New |
|
||||
| Alt+C | annuleren | Annuleren | Cancel |
|
||||
| Alt+H | wachtstand | In wachtstand | On hold |
|
||||
|
||||
Ctrl voor de standaardtoetsen, Alt voor de zijacties. Op een Mac is Alt de Option-toets.
|
||||
|
||||
[**Installeren**](https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/bipp-sneltoetsen/bipp-sneltoetsen.user.js)
|
||||
· gebeurt er niets, zie de [installatie-uitleg](../../README.md).
|
||||
|
||||
## Hoe het werkt
|
||||
|
||||
- Het script zoekt op elke pagina onder `/web/` per sneltoets de eerste zichtbare, actieve knop met een van de labels van die rol. Is er geen, dan doet de toets niets en gaat hij gewoon naar de browser.
|
||||
- Staan Bevestigen (of Goedkeuren) en Indienen allebei op de pagina, dan krijgt alleen Bevestigen Ctrl+Enter.
|
||||
- Ctrl+S overschrijft de opslaantoets van de browser, maar alleen op pagina's met een Opslaan-knop. Cmd (macOS) blijft bij de browser, behalve voor Enter.
|
||||
- De zijacties staan op Alt: Chrome geeft Ctrl+N (nieuw venster) niet aan een pagina, Ctrl+F en Ctrl+R zijn zoeken en herladen, en Ctrl+Alt is AltGr op Windows. Op een Mac typt Option+letter normaal een teken, dat het script onderdrukt op pagina's met zo'n knop.
|
||||
- De hints onder de knoppen staan op één regel. Raken ze elkaar, dan schuift de volgende hint naar rechts.
|
||||
- Annuleren en In wachtstand klikken de knop zoals een muisklik, dus een bevestigingsdialoog van BIPP verschijnt gewoon.
|
||||
- Zet BIPP zelf al een tooltip op de knop (Bevestigen, Indienen), dan komt de sneltoets op een eigen regel eronder.
|
||||
- Pagina's die hun knoppen later opbouwen krijgen tot 15 seconden de tijd.
|
||||
|
||||
## Licentie
|
||||
|
||||
[GPL-3.0-or-later](../../LICENSE).
|
||||
@@ -0,0 +1,129 @@
|
||||
// ==UserScript==
|
||||
// @name BIPP: sneltoetsen voor knoppen
|
||||
// @namespace https://github.com/glgoose/ua-userscripts
|
||||
// @version 1.3.0
|
||||
// @description Sneltoetsen per rol van de knop, in het Nederlands en het Engels: Ctrl+Enter (Bevestigen/Authorization/Approve, anders Indienen/Submit), Ctrl+S (Opslaan/Save), Ctrl+[ (Terug/Back), Alt+S (Zoeken/Search), Alt+R (Reset), Alt+N (Nieuw/New), Alt+C (Annuleren/Cancel), Alt+H (In wachtstand/On hold). Bij ingedrukte Ctrl, Cmd of Alt verschijnt de sneltoets onder de knop.
|
||||
// @author glgoose
|
||||
// @license GPL-3.0-or-later
|
||||
// @homepageURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/src/branch/main/scripts/bipp-sneltoetsen
|
||||
// @supportURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/issues
|
||||
// @downloadURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/bipp-sneltoetsen/bipp-sneltoetsen.user.js
|
||||
// @updateURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/bipp-sneltoetsen/bipp-sneltoetsen.user.js
|
||||
// @match https://bipp.biotechpartner.be/web/*
|
||||
// @run-at document-idle
|
||||
// @grant none
|
||||
// ==/UserScript==
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// Een sneltoets hangt aan de rol van de knop, niet aan een letter uit het label, dus NL en EN delen dezelfde toets.
|
||||
// labels: kleine letters, in volgorde van voorrang (de eerste die op de pagina staat wint).
|
||||
// match: welke toetscombinatie het is. hint: wat de tooltip en de hint onder de knop tonen.
|
||||
var KEYS = [
|
||||
{ labels: ['bevestigen', 'authorization', 'approve', 'goedkeuren', 'confirm', 'indienen', 'submit'],
|
||||
// Op een Mac werkt Cmd+Enter ook, maar de hint blijft overal Ctrl+Enter.
|
||||
hint: 'Ctrl+Enter',
|
||||
match: function (e) { return (e.code === 'Enter' || e.code === 'NumpadEnter') && (e.ctrlKey || e.metaKey) && !(e.ctrlKey && e.metaKey) && !e.altKey && !e.shiftKey; } },
|
||||
{ labels: ['zoeken', 'search'], hint: 'Alt+S', match: alt('KeyS') },
|
||||
{ labels: ['opslaan', 'save'], hint: 'Ctrl+S', match: ctrl('KeyS') },
|
||||
{ labels: ['terug', 'back'], hint: 'Ctrl+[', match: ctrl('BracketLeft') },
|
||||
{ labels: ['reset'], hint: 'Alt+R', match: alt('KeyR') },
|
||||
{ labels: ['nieuw', 'new'], hint: 'Alt+N', match: alt('KeyN') },
|
||||
{ labels: ['annuleren', 'cancel'], hint: 'Alt+C', match: alt('KeyC') },
|
||||
{ labels: ['in wachtstand', 'on hold'], hint: 'Alt+H', match: alt('KeyH') }
|
||||
];
|
||||
var MARK = 'data-bipp-sneltoets';
|
||||
|
||||
// Alt voor de zijacties: Ctrl+N, Ctrl+F en Ctrl+R zijn browsertoetsen, en Ctrl+Alt is AltGr op Windows.
|
||||
function alt(code) {
|
||||
return function (e) { return e.code === code && e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey; };
|
||||
}
|
||||
|
||||
// Alleen Ctrl: Cmd blijft bij de browser (Cmd+F, Cmd+S, Cmd+R zijn zoeken, opslaan en herladen).
|
||||
function ctrl(code) {
|
||||
return function (e) { return e.code === code && e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey; };
|
||||
}
|
||||
|
||||
function norm(s) { return (s || '').replace(/\s+/g, ' ').trim().toLowerCase(); }
|
||||
|
||||
function visible(el) { return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length); }
|
||||
|
||||
// Per label (op volgorde van voorrang) de eerste zichtbare, actieve knop. Een postback-pagina kan er verborgen kopieën van hebben.
|
||||
function findButton(k) {
|
||||
var btns = document.querySelectorAll('input[type="submit"], input[type="button"], button');
|
||||
for (var l = 0; l < k.labels.length; l++) {
|
||||
for (var i = 0; i < btns.length; i++) {
|
||||
var b = btns[i];
|
||||
if (norm(b.value || b.textContent) === k.labels[l] && !b.disabled && visible(b)) return b;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function mark(btn, k) {
|
||||
if (btn.getAttribute(MARK)) return;
|
||||
btn.setAttribute(MARK, k.hint);
|
||||
// BIPP zet soms zelf een title (Bevestigen): dan komt de sneltoets op een eigen regel eronder.
|
||||
btn.title = btn.title ? btn.title + '\n' + k.hint : k.hint;
|
||||
}
|
||||
|
||||
var tips = [];
|
||||
function showTips() {
|
||||
hideTips();
|
||||
KEYS.forEach(function (k) {
|
||||
var btn = findButton(k);
|
||||
if (!btn) return;
|
||||
var r = btn.getBoundingClientRect();
|
||||
var tip = document.createElement('span');
|
||||
tip.textContent = k.hint;
|
||||
tip.style.cssText = 'position:absolute;z-index:99999;pointer-events:none;white-space:nowrap;' +
|
||||
'font:11px ui-monospace,Menlo,Consolas,monospace;background:#1f2937;color:#fff;border-radius:4px;padding:1px 6px;' +
|
||||
'left:' + (r.left + window.scrollX + r.width / 2) + 'px;top:' + (r.bottom + window.scrollY + 4) + 'px;transform:translateX(-50%)';
|
||||
document.body.appendChild(tip);
|
||||
tips.push(tip);
|
||||
});
|
||||
spread();
|
||||
}
|
||||
|
||||
// Smalle knoppen naast elkaar geven hints die elkaar raken: schuif de volgende hint naar rechts, met een kleine tussenruimte.
|
||||
function spread() {
|
||||
var prevRight = -Infinity;
|
||||
tips.slice().sort(function (a, b) { return a.getBoundingClientRect().left - b.getBoundingClientRect().left; })
|
||||
.forEach(function (tip) {
|
||||
var r = tip.getBoundingClientRect();
|
||||
var shift = Math.max(0, prevRight + 4 - r.left);
|
||||
if (shift) tip.style.left = (parseFloat(tip.style.left) + shift) + 'px';
|
||||
prevRight = r.right + shift;
|
||||
});
|
||||
}
|
||||
function hideTips() {
|
||||
tips.forEach(function (t) { if (t.parentNode) t.parentNode.removeChild(t); });
|
||||
tips = [];
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Control' || e.key === 'Meta' || e.key === 'Alt') { if (!e.repeat) showTips(); return; }
|
||||
for (var i = 0; i < KEYS.length; i++) {
|
||||
if (!KEYS[i].match(e)) continue;
|
||||
var btn = findButton(KEYS[i]);
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
hideTips();
|
||||
btn.click();
|
||||
return;
|
||||
}
|
||||
}, true);
|
||||
document.addEventListener('keyup', function (e) { if (e.key === 'Control' || e.key === 'Meta' || e.key === 'Alt') hideTips(); }, true);
|
||||
window.addEventListener('blur', hideTips);
|
||||
|
||||
// De pagina bouwt de knoppen soms pas na een postback op: probeer het een paar keer.
|
||||
var tries = 0;
|
||||
(function init() {
|
||||
var left = 0;
|
||||
KEYS.forEach(function (k) {
|
||||
var btn = findButton(k);
|
||||
if (btn) mark(btn, k); else left++;
|
||||
});
|
||||
if (left && ++tries < 30) setTimeout(init, 500);
|
||||
})();
|
||||
})();
|
||||
@@ -4,7 +4,7 @@ Vervangt op Expenses Home de tabel *Update Expense Reports* door dezelfde tabel
|
||||
tegelijk. Zoeken en sorteren gebeurt lokaal, dus zonder de trage stap langs de server die OAF
|
||||
bij elke klik doet.
|
||||
|
||||
[**Installeren**](https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/iexpense-search/iexpense-search.user.js)
|
||||
[**Installeren**](https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/iexpense-search/iexpense-search.user.js)
|
||||
· gebeurt er niets, zie de [installatie-uitleg](../../README.md).
|
||||
|
||||
## Gebruik
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// ==UserScript==
|
||||
// @name iExpense: doorzoekbare rapportentabel
|
||||
// @namespace https://github.com/glgoose/ua-userscripts
|
||||
// @version 1.8.0
|
||||
// @version 1.8.1
|
||||
// @description Vervangt "Update Expense Reports" op Expenses Home door dezelfde tabel met alle rijen tegelijk, doorzoekbaar en sorteerbaar zonder server round trips, in een scrollvenster van vaste hoogte zodat de pagina niet verspringt, en zet Notifications bovenaan met Track Submitted onderaan
|
||||
// @author Glenn Goossens
|
||||
// @license GPL-3.0-or-later
|
||||
// @homepageURL https://github.com/glgoose/ua-userscripts/tree/main/scripts/iexpense-search
|
||||
// @supportURL https://github.com/glgoose/ua-userscripts/issues
|
||||
// @downloadURL https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/iexpense-search/iexpense-search.user.js
|
||||
// @updateURL https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/iexpense-search/iexpense-search.user.js
|
||||
// @homepageURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/src/branch/main/scripts/iexpense-search
|
||||
// @supportURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/issues
|
||||
// @downloadURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/iexpense-search/iexpense-search.user.js
|
||||
// @updateURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/iexpense-search/iexpense-search.user.js
|
||||
// @match https://ofprd.uantwerpen.be/OA_HTML/*
|
||||
// @grant none
|
||||
// @run-at document-idle
|
||||
|
||||
@@ -6,7 +6,7 @@ script vangt de klik op en bouwt zelf een `.xlsx` uit wat er in de tabel staat.
|
||||
De grid-knop van PeopleSoft zelf levert een bestand op dat Excel niet wil of niet goed leest.
|
||||
Deze aanpak omzeilt de server volledig: wat je op je scherm ziet is wat in het bestand komt.
|
||||
|
||||
[**Installeren**](https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/peoplesoft-excel-download/peoplesoft-excel-download.user.js)
|
||||
[**Installeren**](https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/peoplesoft-excel-download/peoplesoft-excel-download.user.js)
|
||||
· gebeurt er niets, zie de [installatie-uitleg](../../README.md).
|
||||
|
||||
## Gebruik
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// ==UserScript==
|
||||
// @name PeopleSoft: werkende "Downloaden naar Excel"
|
||||
// @namespace https://github.com/glgoose/ua-userscripts
|
||||
// @version 1.1.0
|
||||
// @version 1.1.1
|
||||
// @description Vervangt de kapotte PeopleSoft grid-knop "Downloaden naar Excel" door een echte XLSX-export die uit de DOM wordt opgebouwd.
|
||||
// @author Glenn Goossens
|
||||
// @license GPL-3.0-or-later
|
||||
// @homepageURL https://github.com/glgoose/ua-userscripts/tree/main/scripts/peoplesoft-excel-download
|
||||
// @supportURL https://github.com/glgoose/ua-userscripts/issues
|
||||
// @downloadURL https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/peoplesoft-excel-download/peoplesoft-excel-download.user.js
|
||||
// @updateURL https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/peoplesoft-excel-download/peoplesoft-excel-download.user.js
|
||||
// @homepageURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/src/branch/main/scripts/peoplesoft-excel-download
|
||||
// @supportURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/issues
|
||||
// @downloadURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/peoplesoft-excel-download/peoplesoft-excel-download.user.js
|
||||
// @updateURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/peoplesoft-excel-download/peoplesoft-excel-download.user.js
|
||||
// @match https://app.psoft.uantwerpen.be/*
|
||||
// @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js
|
||||
// @grant GM_info
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// ==UserScript==
|
||||
// @name Algemeen: UA-proxy waarschuwing op uitgeverspagina's
|
||||
// @namespace https://github.com/glgoose/ua-userscripts
|
||||
// @version 1.0.0
|
||||
// @version 1.0.1
|
||||
// @description Rode balk op uitgeverspagina's als de UA-proxy uit staat, met een knop om te verbinden.
|
||||
// @author glgoose
|
||||
// @license GPL-3.0-or-later
|
||||
// @homepageURL https://github.com/glgoose/ua-userscripts/tree/main/scripts/publisher-uaproxy-banner
|
||||
// @supportURL https://github.com/glgoose/ua-userscripts/issues
|
||||
// @homepageURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/src/branch/main/scripts/publisher-uaproxy-banner
|
||||
// @supportURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/issues
|
||||
// @match *://*.tandfonline.com/*
|
||||
// @match *://*.taylorfrancis.com/*
|
||||
// @match *://*.sagepub.com/*
|
||||
@@ -87,9 +87,9 @@
|
||||
var state = s && s.state;
|
||||
if (state === 'up' || (s && s.on_ua && state === 'down')) { hide(); return; }
|
||||
if (!bar) build();
|
||||
var base = 'UA proxy down: this page loaded without UA access, so a Zotero save gets no PDF.';
|
||||
var base = 'UA proxy down';
|
||||
if (!s) {
|
||||
text.textContent = 'uaproxyd is not running: this page loaded without UA access.';
|
||||
text.textContent = 'uaproxyd is not running';
|
||||
btn.disabled = true;
|
||||
} else if (state === 'connecting') {
|
||||
text.textContent = s.mfa ? 'Enter ' + s.mfa + ' in the Authenticator app.' : 'Connecting to the UA proxy.';
|
||||
@@ -99,6 +99,7 @@
|
||||
btn.disabled = false;
|
||||
}
|
||||
btn.textContent = state === 'connecting' ? 'Connecting' : 'Connect';
|
||||
bar.style.background = state === 'connecting' ? '#b45309' : '#b3261e';
|
||||
}
|
||||
|
||||
function hide() {
|
||||
|
||||
@@ -6,7 +6,7 @@ letters, dus "Poule" vinden in "Restaurant Poule & Poulette B.V." lukt niet. Met
|
||||
Het originele `<select>` blijft de bron van waarheid en wordt enkel verborgen. Het formulier
|
||||
verstuurt dus exact hetzelfde als voorheen.
|
||||
|
||||
[**Installeren**](https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/select-search/select-search.user.js)
|
||||
[**Installeren**](https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/select-search/select-search.user.js)
|
||||
· gebeurt er niets, zie de [installatie-uitleg](../../README.md).
|
||||
|
||||
## Gebruik
|
||||
@@ -81,6 +81,7 @@ stil: is de opslag vol of geblokkeerd, dan verdwijnt het recent-blok en werkt de
|
||||
| | |
|
||||
|---|---|
|
||||
| `data-ss-recent-key="..."` | Eigen veldsleutel, in plaats van die uit `name` of `id`. Nodig bij een `<select>` zonder allebei. |
|
||||
| `data-ss-recent="off"` | Geen recent-blok en niets onthouden voor dit veld, zoeken blijft werken. Op een pagina die op `Login.aspx` eindigt staat dit standaard aan. |
|
||||
| `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. |
|
||||
@@ -94,33 +95,16 @@ stil: is de opslag vol of geblokkeerd, dan verdwijnt het recent-blok en werkt de
|
||||
| Niets gebeurt op de pagina | Check in het Tampermonkey-dashboard of de URL onder `@match` valt |
|
||||
| Formulier zit in een iframe van een ander domein | Niet op te lossen, browserbeperking |
|
||||
|
||||
## Bookmarklet
|
||||
|
||||
Alternatief zonder extensie, voor wie geen extensies mag installeren. Werkt overal, maar je klikt
|
||||
hem zelf aan per pagina.
|
||||
|
||||
1. Kopieer de inhoud van [`bookmarklet.txt`](bookmarklet.txt).
|
||||
2. Maak een bladwijzer met naam `Zoek in dropdown` en die tekst als URL.
|
||||
3. Klik hem aan op de pagina met de dropdown. Nog eens klikken doet een herscan.
|
||||
|
||||
Sites met een strikte Content-Security-Policy blokkeren bookmarklets. Dan is de extensie de enige
|
||||
route.
|
||||
|
||||
## Ontwikkelen
|
||||
|
||||
`src/select-search.js` bevat de volledige logica, zonder dependencies.
|
||||
`select-search.user.js` bevat de volledige logica, zonder dependencies. Test met de fixture:
|
||||
|
||||
```sh
|
||||
./build.sh # genereert select-search.user.js en bookmarklet.txt
|
||||
python3 -m http.server 8731 # open http://localhost:8731/test/fixture.html
|
||||
```
|
||||
|
||||
Hoog bij elke wijziging `@version` op in `src/userscript-header.txt`, anders krijgt niemand de
|
||||
update binnen.
|
||||
|
||||
Tijdens het ontwikkelen laad je het script via een dev-stub in plaats van een geplakte kopie,
|
||||
zie [Ontwikkelen](../../AGENTS.md#dev-stub-vs-gebruik). Draai eerst `./build.sh`, want de stub wijst
|
||||
naar `select-search.user.js`.
|
||||
zie [Ontwikkelen](../../AGENTS.md#dev-stub-vs-gebruik). Hoog bij elke wijziging `@version` op.
|
||||
|
||||
## Licentie
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,64 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Bouwt het userscript en de bookmarklet uit src/select-search.js.
|
||||
# Gebruik: ./build.sh (de bookmarklet belandt ook meteen in het klembord)
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "$0")" && pwd)"
|
||||
src="$here/src/select-search.js"
|
||||
hdr="$here/src/userscript-header.txt"
|
||||
out="$here"
|
||||
|
||||
for f in "$src" "$hdr"; do
|
||||
if [ ! -f "$f" ]; then
|
||||
echo "build.sh: ontbrekend bestand: $f" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "build.sh: python3 is nodig om de bookmarklet te encoderen" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -q 'MATCH_PLACEHOLDER' "$hdr"; then
|
||||
echo "build.sh: vul eerst de echte @match-URL in $hdr in" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. Userscript = metadatablok + ongewijzigde core.
|
||||
cat "$hdr" "$src" > "$out/select-search.user.js"
|
||||
|
||||
# 2. Bookmarklet = core, URL-encoded achter javascript:.
|
||||
python3 - "$src" "$out/bookmarklet.txt" <<'PY'
|
||||
import io, sys, urllib.parse
|
||||
|
||||
src, out = sys.argv[1], sys.argv[2]
|
||||
code = io.open(src, encoding='utf-8').read()
|
||||
# Regelcommentaar sneuvelt zodra alles op een regel staat; strip het weg.
|
||||
lines = []
|
||||
for line in code.split('\n'):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('//'):
|
||||
continue
|
||||
lines.append(line)
|
||||
code = '\n'.join(lines)
|
||||
|
||||
# Een bladwijzerbeheerder mag de nieuwe regels weggooien zonder dat de
|
||||
# bookmarklet stukgaat: dus geen enkel regelcommentaar meer overhouden.
|
||||
for n, line in enumerate(code.split('\n'), 1):
|
||||
if '//' in line:
|
||||
sys.exit('build.sh: regelcommentaar op regel %d breekt de bookmarklet: %s'
|
||||
% (n, line.strip()))
|
||||
|
||||
url = 'javascript:' + urllib.parse.quote(code, safe="!#$&'()*+,-./:;=?@_~")
|
||||
io.open(out, 'w', encoding='utf-8').write(url)
|
||||
print('bookmarklet: %d tekens' % len(url))
|
||||
PY
|
||||
|
||||
if command -v pbcopy >/dev/null 2>&1; then
|
||||
pbcopy < "$out/bookmarklet.txt"
|
||||
echo "bookmarklet gekopieerd naar het klembord"
|
||||
fi
|
||||
|
||||
echo "geschreven: $out/select-search.user.js"
|
||||
echo "geschreven: $out/bookmarklet.txt"
|
||||
@@ -1,14 +1,14 @@
|
||||
// ==UserScript==
|
||||
// @name BIPP: doorzoekbare dropdowns
|
||||
// @namespace https://github.com/glgoose/ua-userscripts
|
||||
// @version 1.8.3
|
||||
// @version 1.8.5
|
||||
// @description Geeft elke <select> met veel opties een zoekveld met substring-zoeken.
|
||||
// @author glgoose
|
||||
// @license GPL-3.0-or-later
|
||||
// @homepageURL https://github.com/glgoose/ua-userscripts/tree/main/scripts/select-search
|
||||
// @supportURL https://github.com/glgoose/ua-userscripts/issues
|
||||
// @downloadURL https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/select-search/select-search.user.js
|
||||
// @updateURL https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/select-search/select-search.user.js
|
||||
// @homepageURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/src/branch/main/scripts/select-search
|
||||
// @supportURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/issues
|
||||
// @downloadURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/select-search/select-search.user.js
|
||||
// @updateURL https://gitea.flw.uantwerpen.be/glenn/ua-userscripts/raw/branch/main/scripts/select-search/select-search.user.js
|
||||
// @match https://bipp.biotechpartner.be/web/*
|
||||
// @run-at document-idle
|
||||
// @grant none
|
||||
@@ -136,7 +136,12 @@
|
||||
// BIPP rendert ids als ctl00_..._resultTable_ctl03_interfaceAccountList. Rij 3 en rij 4 zijn
|
||||
// hetzelfde veld en horen dus één lijst te delen: de rij- en volgnummers gaan eruit. Levert dat
|
||||
// niets op, dan krijgt die select geen recent-blok en blijft hij gewoon doorzoekbaar.
|
||||
// Uit op de loginpagina: daar kiest iedereen een participant uit een lange lijst en schuift een
|
||||
// recent-blok enkel gewoonte-keuzes bovenaan. Zoeken blijft werken. `data-ss-recent="off"` doet
|
||||
// hetzelfde voor één veld.
|
||||
function recentKey(select) {
|
||||
if (select.getAttribute && select.getAttribute('data-ss-recent') === 'off') return '';
|
||||
if (/\/login\.aspx$/i.test((select.ownerDocument.defaultView || window).location.pathname)) return '';
|
||||
var expliciet = select.getAttribute && select.getAttribute('data-ss-recent-key');
|
||||
if (expliciet) return expliciet;
|
||||
var ruw = select.name || select.id || '';
|
||||
|
||||
@@ -1,809 +0,0 @@
|
||||
/*
|
||||
* select-search - maakt elke <select> met veel opties doorzoekbaar.
|
||||
*
|
||||
* Het originele <select> blijft in de DOM en blijft de bron van waarheid:
|
||||
* het wordt enkel visueel verborgen. Selecteren gebeurt via selectedIndex +
|
||||
* native input/change events, zodat de applicatie niets merkt van de ingreep.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
if (window.__selectSearchLoaded) {
|
||||
if (typeof window.__selectSearchScan === 'function') window.__selectSearchScan();
|
||||
return;
|
||||
}
|
||||
window.__selectSearchLoaded = true;
|
||||
|
||||
// kleinere dropdowns laten we met rust; hoogstens MAX_RENDER treffers tekenen
|
||||
var MIN_OPTIONS = 10;
|
||||
var MAX_RENDER = 200;
|
||||
// Streefbreedte van de lijst. Het veld zelf is meestal smaller dan zijn labels; onder deze
|
||||
// breedte wordt de lijst een kolom van afgebroken zinnen in plaats van een leesbare lijst.
|
||||
var LIJST_BREED = 420;
|
||||
var MARGE = 8;
|
||||
var uid = 0;
|
||||
// Recent gebruikte waarden per veld. Vijf, want select-search filtert het blok mee tijdens het
|
||||
// zoeken: wat buiten de top vijf valt typ je sneller dan je het herkent. Geen vervaltermijn,
|
||||
// een lijst van vijf verdringt zichzelf al.
|
||||
var RECENT_KEY = 'selectSearchRecent';
|
||||
var RECENT_MAX = 5;
|
||||
var RECENT_FIELDS = 40;
|
||||
|
||||
/* ---------------------------------------------------------------- utils */
|
||||
|
||||
function normMap(s) {
|
||||
// Genormaliseerde (accentloze, lowercase) string + index-mapping terug
|
||||
// naar de originele string, zodat we treffers kunnen highlighten.
|
||||
var out = '', map = [];
|
||||
for (var i = 0; i < s.length; i++) {
|
||||
var c = s.charAt(i).normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
|
||||
for (var j = 0; j < c.length; j++) { out += c.charAt(j); map.push(i); }
|
||||
}
|
||||
return { n: out, map: map };
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function tokenize(q) {
|
||||
return normMap(q).n.split(/\s+/).filter(function (t) { return t.length > 0; });
|
||||
}
|
||||
|
||||
function debounce(fn, ms) {
|
||||
var t = null;
|
||||
return function () {
|
||||
if (t) clearTimeout(t);
|
||||
t = setTimeout(function () { t = null; fn(); }, ms);
|
||||
};
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- recent */
|
||||
|
||||
// Eén localStorage-sleutel voor alle velden samen:
|
||||
// { v: 1, fields: { "<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 */
|
||||
|
||||
// Alle treffers over het volledige label, gesorteerd en samengevoegd. Los van het tekenen,
|
||||
// want een label wordt in twee stukken getekend (code en omschrijving) en een treffer mag
|
||||
// over die grens heen lopen.
|
||||
function matchRanges(item, tokens) {
|
||||
if (!tokens.length) return [];
|
||||
var ranges = [];
|
||||
for (var t = 0; t < tokens.length; t++) {
|
||||
var tok = tokens[t], from = 0, idx;
|
||||
while ((idx = item.norm.n.indexOf(tok, from)) !== -1) {
|
||||
var start = item.norm.map[idx];
|
||||
var end = item.norm.map[idx + tok.length - 1] + 1;
|
||||
ranges.push([start, end]);
|
||||
from = idx + tok.length;
|
||||
}
|
||||
}
|
||||
if (!ranges.length) return ranges;
|
||||
ranges.sort(function (a, b) { return a[0] - b[0]; });
|
||||
var merged = [ranges[0].slice()];
|
||||
for (var r = 1; r < ranges.length; r++) {
|
||||
var last = merged[merged.length - 1];
|
||||
if (ranges[r][0] <= last[1]) last[1] = Math.max(last[1], ranges[r][1]);
|
||||
else merged.push(ranges[r].slice());
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
// Tekent label.slice(from, to). Een treffer die de grens kruist wordt afgeknipt, zodat beide
|
||||
// helften hun eigen <mark> krijgen en er geen tag over de spans heen loopt.
|
||||
function highlight(item, ranges, from, to) {
|
||||
var html = '', pos = from;
|
||||
for (var m = 0; m < ranges.length; m++) {
|
||||
var a = Math.max(ranges[m][0], from), b = Math.min(ranges[m][1], to);
|
||||
if (b <= a) continue;
|
||||
html += escapeHtml(item.label.slice(pos, a));
|
||||
html += '<mark>' + escapeHtml(item.label.slice(a, b)) + '</mark>';
|
||||
pos = b;
|
||||
}
|
||||
return html + escapeHtml(item.label.slice(pos, to));
|
||||
}
|
||||
|
||||
// Waar splitst "10-AD010000-CFALWERK - ADRIVA (ALGEMEEN)" in code en omschrijving? Enkel bij de
|
||||
// eerste " - " en enkel als daar echt een code voor staat: één woord zonder spatie. Zo blijft
|
||||
// een gewone titel met een gedachtestreepje ("Emotie en verbeelding - 2025") ongesplitst.
|
||||
// De teruggegeven index staat ná de spatie: label.slice(0, punt - 1) is de code met zijn
|
||||
// streepje, label.slice(punt) de omschrijving, en de spatie ertussen valt buiten beide.
|
||||
function splitPunt(label) {
|
||||
var sep = label.indexOf(' - ');
|
||||
if (sep <= 0) return -1;
|
||||
if (label.lastIndexOf(' ', sep - 1) !== -1) return -1;
|
||||
return sep + 3;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------- css */
|
||||
|
||||
var CSS = [
|
||||
':host{all:initial;display:inline-block;vertical-align:middle;max-width:100%}',
|
||||
'*{box-sizing:border-box;font:inherit}',
|
||||
'.box{position:relative;display:block;width:100%}',
|
||||
'input{width:100%;padding:2px 22px 2px 4px;border:1px solid #767676;border-radius:2px;',
|
||||
'background:#fff;color:#000;font:13px/1.4 system-ui,-apple-system,"Segoe UI",sans-serif;',
|
||||
'height:100%;min-height:22px}',
|
||||
'input:focus{outline:none;border-color:#0b57d0;box-shadow:0 0 0 1px #0b57d0}',
|
||||
// #767676 haalt net de AA-drempel op wit; het #777 van li.kop zit er net onder.
|
||||
'input::placeholder{color:#767676;opacity:1}',
|
||||
'.caret{position:absolute;right:6px;top:50%;transform:translateY(-50%);pointer-events:none;',
|
||||
'border:4px solid transparent;border-top-color:#444;margin-top:2px}',
|
||||
// Het kruisje neemt de plek van het pijltje in en krijgt dus geen eigen ruimte. Zo blijft de
|
||||
// volle breedte voor de tekst, staat er nooit een leeg gat, en verschuift er evenmin iets:
|
||||
// enkel de zichtbaarheid van de twee wisselt. Zo doet Ant Design het ook.
|
||||
'.wis{position:absolute;right:2px;top:50%;transform:translateY(-50%);width:17px;height:17px;',
|
||||
'display:flex;align-items:center;justify-content:center;color:#767676;cursor:pointer;',
|
||||
'visibility:hidden}',
|
||||
'.box.wisbaar.gevuld:hover .wis,.box.wisbaar.gevuld:focus-within .wis{visibility:visible}',
|
||||
'.box.wisbaar.gevuld:hover .caret,.box.wisbaar.gevuld:focus-within .caret{visibility:hidden}',
|
||||
'.wis:hover{color:#000}',
|
||||
'ul{position:fixed;z-index:2147483647;margin:0;padding:2px 0;list-style:none;overflow-y:auto;',
|
||||
'background:#fff;color:#000;border:1px solid #b0b0b0;border-radius:3px;',
|
||||
'box-shadow:0 4px 14px rgba(0,0,0,.22);',
|
||||
'font:13px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif;display:none}',
|
||||
'ul.open{display:block}',
|
||||
// Een label dat niet past loopt terug over een volgende regel met een kleine hangende
|
||||
// inspring. Niet uitlijnen onder de omschrijving: bij een lange code schuift die inspring mee
|
||||
// tot halverwege de rij en blijft er nauwelijks tekstbreedte over. 2.5ch is de breedte van
|
||||
// twee cijfers plus een streepje, dus precies het "10-" waarmee elke budgetcode begint: daar
|
||||
// valt de vervolgregel netjes samen met de rest van de code. Elders is het gewoon een kleine
|
||||
// inspring, genoeg om te zien dat een regel bij de vorige hoort. ch en niet px, want de maat
|
||||
// hoort bij de tekstgrootte en niet bij het scherm.
|
||||
'li{padding:4px 8px;cursor:pointer;line-height:1.3;',
|
||||
'padding-left:calc(8px + 2.5ch);text-indent:-2.5ch;overflow-wrap:break-word}',
|
||||
// De code zelf mag nooit afbreken, ook niet op zijn eigen streepjes: white-space:pre houdt
|
||||
// "10-AD113010 -" in één stuk. De spatie erna staat buiten de span, zodat het eerste woord
|
||||
// van de omschrijving wel naar de volgende regel kan.
|
||||
'li .c{white-space:pre}',
|
||||
// Meerregelige rijen lopen anders in elkaar over: het gat binnen een rij is even groot als
|
||||
// dat tussen twee rijen. Een haarlijn tussen twee opties zegt waar de ene stopt.
|
||||
'li[data-i]+li[data-i]{border-top:1px solid #eee}',
|
||||
'li.kop{padding:6px 8px 2px;padding-left:8px;text-indent:0;color:#777;font-size:11px;',
|
||||
'font-weight:600;cursor:default}',
|
||||
'li.active{background:#0b57d0;color:#fff;border-top-color:#0b57d0}',
|
||||
'li.active mark{background:#ffe08a;color:#000}',
|
||||
'li.dis{color:#999;cursor:default}',
|
||||
'li.note{color:#666;cursor:default;font-style:italic;padding-top:5px}',
|
||||
'li.empty{color:#666;cursor:default}',
|
||||
'mark{background:#ffe08a;color:inherit;padding:0}'
|
||||
].join('');
|
||||
|
||||
/* ------------------------------------------------------------- enhance */
|
||||
|
||||
function enhance(select) {
|
||||
var doc = select.ownerDocument;
|
||||
var win = doc.defaultView || window;
|
||||
var id = 'ss' + (++uid);
|
||||
var recentSleutel = recentKey(select);
|
||||
// Manuele stand: het recent-blok en Delete werken, maar er wordt niets vanzelf onthouden.
|
||||
// Voor wie pas na een bevestiging van de server mag opslaan; die roept remember() zelf aan.
|
||||
var recentManueel = select.getAttribute('data-ss-recent') === 'manual';
|
||||
|
||||
var rect = select.getBoundingClientRect();
|
||||
var wrap = doc.createElement('span');
|
||||
wrap.className = 'select-search-wrap';
|
||||
if (rect.width) wrap.style.width = rect.width + 'px';
|
||||
if (rect.height) wrap.style.height = rect.height + 'px';
|
||||
select.parentNode.insertBefore(wrap, select.nextSibling);
|
||||
|
||||
var root = wrap.attachShadow({ mode: 'open' });
|
||||
var style = doc.createElement('style');
|
||||
style.textContent = CSS;
|
||||
var box = doc.createElement('span');
|
||||
box.className = 'box';
|
||||
var input = doc.createElement('input');
|
||||
input.type = 'text';
|
||||
input.setAttribute('role', 'combobox');
|
||||
input.setAttribute('aria-expanded', 'false');
|
||||
input.setAttribute('aria-autocomplete', 'list');
|
||||
input.setAttribute('aria-controls', id + '-list');
|
||||
input.setAttribute('autocomplete', 'off');
|
||||
input.setAttribute('spellcheck', 'false');
|
||||
input.title = 'Typ om te zoeken. Alt+klik zet het originele keuzemenu terug.';
|
||||
var caret = doc.createElement('span');
|
||||
caret.className = 'caret';
|
||||
// Wissen gebeurt met een kruisje in het veld, zoals in react-select, MUI en Ant Design, en
|
||||
// niet met een "geen waarde"-rij bovenaan de lijst. Ant Design toont het kruisje pas bij
|
||||
// hover, dat doen wij ook, met focus erbij zodat het toetsenbord niet in de kou staat.
|
||||
var wis = doc.createElement('span');
|
||||
wis.className = 'wis';
|
||||
wis.setAttribute('role', 'button');
|
||||
wis.setAttribute('aria-label', 'Wissen');
|
||||
wis.innerHTML = '<svg width="11" height="11" viewBox="0 0 12 12" fill="none" '
|
||||
+ 'stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true">'
|
||||
+ '<path d="M2.5 2.5l7 7M9.5 2.5l-7 7"/></svg>';
|
||||
var list = doc.createElement('ul');
|
||||
list.id = id + '-list';
|
||||
list.setAttribute('role', 'listbox');
|
||||
box.appendChild(input);
|
||||
box.appendChild(wis);
|
||||
box.appendChild(caret);
|
||||
root.appendChild(style);
|
||||
root.appendChild(box);
|
||||
root.appendChild(list);
|
||||
|
||||
// Origineel blijft bestaan (form submit, app-code), maar onzichtbaar.
|
||||
var prevStyle = select.getAttribute('style');
|
||||
select.style.position = 'absolute';
|
||||
select.style.opacity = '0';
|
||||
select.style.pointerEvents = 'none';
|
||||
select.style.width = (rect.width || 1) + 'px';
|
||||
select.style.height = (rect.height || 1) + 'px';
|
||||
select.dataset.ssEnhanced = '1';
|
||||
|
||||
var items = [], shown = [], active = -1, open = false;
|
||||
// De zoekterm apart bijhouden. input.value is niet hetzelfde: bij een open lijst zonder
|
||||
// dat er getypt is staat daar het gekozen label in (geselecteerd, klaar om over te typen),
|
||||
// terwijl de lijst volledig is. Wie input.value als zoekterm leest, denkt dus dat er
|
||||
// gefilterd wordt op iets wat de gebruiker nooit intypte.
|
||||
var zoek = '';
|
||||
// Index van de optie met een lege waarde, de "niets gekozen"-stand. Die optie is geen keuze:
|
||||
// hij komt niet in de lijst, maar levert de placeholder en is het doel van het kruisje.
|
||||
var leegIndex = -1, leegLabel = '';
|
||||
|
||||
function readOptions() {
|
||||
items = [];
|
||||
leegIndex = -1;
|
||||
leegLabel = '';
|
||||
// Twee soorten "niets gekozen". Een optie zonder waarde is de echte, een optie met een
|
||||
// waarde maar zonder leesbare tekst (OAF schrijft graag <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. gi 0 is het recent-blok, gi 1 zijn de opties zonder groep, echte groepen
|
||||
// beginnen bij 2.
|
||||
var groepen = [], gi;
|
||||
// Recent verwijst naar opties die al in de select staan; wat er niet meer in zit valt
|
||||
// vanzelf weg. De positie in de bewaarde lijst is de positie in het blok.
|
||||
var wil = {}, recentLijst = recentSleutel ? readRecent(recentSleutel) : [];
|
||||
for (var r = 0; r < recentLijst.length; r++) wil['v:' + recentLijst[r][0]] = r;
|
||||
var recentItems = [], gewoon = [];
|
||||
for (var i = 0; i < select.options.length; i++) {
|
||||
var o = select.options[i];
|
||||
var label = (o.textContent || '').replace(/\s+/g, ' ').trim();
|
||||
if (!o.value || !label) {
|
||||
if (!o.value) {
|
||||
if (zonderWaarde === -1) { zonderWaarde = i; zonderWaardeLabel = label; }
|
||||
} else if (blanco === -1) blanco = i;
|
||||
continue;
|
||||
}
|
||||
var plek = wil['v:' + o.value];
|
||||
if (plek !== undefined && recentItems[plek] === undefined) {
|
||||
recentItems[plek] = {
|
||||
index: i,
|
||||
label: label,
|
||||
group: 'recent',
|
||||
gi: 0,
|
||||
disabled: o.disabled,
|
||||
removable: true,
|
||||
mru: true,
|
||||
norm: normMap(label)
|
||||
};
|
||||
// Een optie die in recent staat wordt in haar eigen groep overgeslagen: twee keer
|
||||
// dezelfde regel in de resultaten is verwarrend, ook met een kopregel erbij.
|
||||
continue;
|
||||
}
|
||||
var groep = o.parentNode && o.parentNode.tagName === 'OPTGROUP' ? o.parentNode : null;
|
||||
var grp = groep ? (groep.label || '') : '';
|
||||
if (!grp) gi = 1;
|
||||
else {
|
||||
gi = groepen.indexOf(grp);
|
||||
if (gi === -1) { groepen.push(grp); gi = groepen.length + 1; } else gi += 2;
|
||||
}
|
||||
gewoon.push({
|
||||
index: i,
|
||||
label: label,
|
||||
group: grp,
|
||||
gi: gi,
|
||||
disabled: o.disabled,
|
||||
// Wie de opties levert bepaalt of een regel weg mag, met data-removable op de optie of
|
||||
// op haar optgroup. Het script weet niet wat verwijderen betekent en doet het dus ook
|
||||
// niet zelf: het meldt enkel dat de gebruiker erom vraagt.
|
||||
removable: !!(o.dataset.removable || (groep && groep.dataset.removable)),
|
||||
// Enkel het label is doorzoekbaar: de groepsnaam staat als kopregel op het scherm en
|
||||
// hoort niet stilzwijgend mee te zoeken. highlight() mapt de treffers bovendien terug
|
||||
// op label, dus een treffer in de groepsnaam zou buiten het label vallen.
|
||||
norm: normMap(label)
|
||||
});
|
||||
}
|
||||
if (zonderWaarde !== -1) { leegIndex = zonderWaarde; leegLabel = zonderWaardeLabel; }
|
||||
else if (blanco !== -1) leegIndex = blanco;
|
||||
for (var k = 0; k < recentItems.length; k++) {
|
||||
if (recentItems[k]) items.push(recentItems[k]);
|
||||
}
|
||||
// Zonder kopregel loopt het recent-blok naadloos over in de losse opties eronder, en dan
|
||||
// zie je niet meer waar het ophoudt. Alleen nodig als er iets boven staat.
|
||||
if (items.length) {
|
||||
for (var g = 0; g < gewoon.length; g++) {
|
||||
if (!gewoon[g].group) gewoon[g].group = 'alle';
|
||||
}
|
||||
}
|
||||
items = items.concat(gewoon);
|
||||
}
|
||||
|
||||
function currentLabel() {
|
||||
var o = select.options[select.selectedIndex];
|
||||
// Staat de lege optie aan, dan is het veld leeg en neemt de placeholder het over. Anders
|
||||
// zou "budgetcode" er als gewone zwarte waarde staan, alsof er wel iets gekozen is.
|
||||
if (!o || !o.value) return '';
|
||||
return (o.textContent || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
// Het kruisje hoort alleen te bestaan waar wissen ook kan, en alleen zichtbaar te zijn als er
|
||||
// iets te wissen valt. De hover- en focusvoorwaarde staat in de CSS.
|
||||
function updateWis() {
|
||||
box.classList.toggle('wisbaar', leegIndex !== -1);
|
||||
box.classList.toggle('gevuld', !!currentLabel());
|
||||
}
|
||||
|
||||
function syncFromSelect() {
|
||||
readOptions();
|
||||
input.placeholder = select.getAttribute('data-placeholder') || leegLabel;
|
||||
updateWis();
|
||||
if (!open) input.value = currentLabel();
|
||||
}
|
||||
|
||||
function clear() {
|
||||
if (leegIndex === -1) return;
|
||||
select.selectedIndex = leegIndex;
|
||||
input.value = '';
|
||||
updateWis();
|
||||
closeList(false);
|
||||
fire(select, 'input');
|
||||
fire(select, 'change');
|
||||
}
|
||||
|
||||
function position() {
|
||||
var r = input.getBoundingClientRect();
|
||||
var below = win.innerHeight - r.bottom - 6;
|
||||
var above = r.top - 6;
|
||||
var down = below >= 160 || below >= above;
|
||||
// De lijst hing vast aan de linkerrand van het veld en kon dus alleen naar rechts groeien.
|
||||
// Bij een veld rechts in het formulier bleef er niets over en viel elk label terug op
|
||||
// afbreken. Daarom eerst de breedte laten kiezen met de volle vensterbreedte beschikbaar,
|
||||
// die meten, en de lijst dan zo ver naar links schuiven als nodig om ze te laten passen.
|
||||
var wens = Math.min(Math.max(r.width, LIJST_BREED), win.innerWidth - MARGE * 2);
|
||||
list.style.left = MARGE + 'px';
|
||||
list.style.minWidth = r.width + 'px';
|
||||
list.style.maxWidth = wens + 'px';
|
||||
list.style.maxHeight = Math.max(120, (down ? below : above)) + 'px';
|
||||
var breed = list.offsetWidth;
|
||||
list.style.left = Math.max(MARGE, Math.min(r.left, win.innerWidth - MARGE - breed)) + 'px';
|
||||
if (down) {
|
||||
list.style.top = r.bottom + 2 + 'px';
|
||||
list.style.bottom = 'auto';
|
||||
} else {
|
||||
list.style.top = 'auto';
|
||||
list.style.bottom = (win.innerHeight - r.top + 2) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function filter(q) {
|
||||
var tokens = tokenize(q);
|
||||
if (!tokens.length) return items.slice();
|
||||
// Per groep twee emmers: eerst wat met de zoekterm begint, dan de rest. De emmers worden
|
||||
// in groepsvolgorde aan elkaar geplakt, zodat elke groep aaneengesloten blijft en de
|
||||
// kopregel zich niet herhaalt.
|
||||
var emmers = [];
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var it = items[i], n = it.norm.n, ok = true;
|
||||
for (var t = 0; t < tokens.length; t++) {
|
||||
if (n.indexOf(tokens[t]) === -1) { ok = false; break; }
|
||||
}
|
||||
if (!ok) continue;
|
||||
if (!emmers[it.gi]) emmers[it.gi] = { starts: [], rest: [] };
|
||||
(n.indexOf(tokens[0]) === 0 ? emmers[it.gi].starts : emmers[it.gi].rest).push(it);
|
||||
}
|
||||
var out = [];
|
||||
for (var g = 0; g < emmers.length; g++) {
|
||||
if (emmers[g]) out = out.concat(emmers[g].starts, emmers[g].rest);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// behoud: houd de blauwe balk waar hij stond. Enkel voor een herbouw van de optielijst onder
|
||||
// een open lijst, want dan is de gebruiker midden in iets en zou terugspringen naar de eerste
|
||||
// regel hem onderbreken. Bij openen en typen hoort de balk juist wel bovenaan te beginnen.
|
||||
function render(q, behoud) {
|
||||
var vorigeActieve = active;
|
||||
zoek = q || '';
|
||||
var tokens = tokenize(q);
|
||||
shown = filter(q);
|
||||
var html = '';
|
||||
if (!shown.length) {
|
||||
html = '<li class="empty">Geen resultaten</li>';
|
||||
} else {
|
||||
var n = Math.min(shown.length, MAX_RENDER), vorige = null;
|
||||
for (var i = 0; i < n; i++) {
|
||||
var it = shown[i];
|
||||
if (it.group && it.group !== vorige) {
|
||||
html += '<li class="kop">' + escapeHtml(it.group) + '</li>';
|
||||
}
|
||||
vorige = it.group;
|
||||
var ranges = matchRanges(it, tokens);
|
||||
var punt = splitPunt(it.label);
|
||||
var binnen = punt === -1
|
||||
? highlight(it, ranges, 0, it.label.length)
|
||||
: '<span class="c">' + highlight(it, ranges, 0, punt - 1) + '</span> '
|
||||
+ highlight(it, ranges, punt, it.label.length);
|
||||
html += '<li role="option" id="' + id + '-o' + i + '" data-i="' + i + '"'
|
||||
+ (it.removable ? ' data-rm="1"' : '')
|
||||
+ (it.disabled ? ' class="dis" aria-disabled="true"' : '') + '>'
|
||||
+ binnen
|
||||
+ '</li>';
|
||||
}
|
||||
if (shown.length > n) {
|
||||
html += '<li class="note">nog ' + (shown.length - n) + ' resultaten, verfijn je zoekterm</li>';
|
||||
}
|
||||
}
|
||||
list.innerHTML = html;
|
||||
shown = shown.slice(0, MAX_RENDER);
|
||||
var doel = 0;
|
||||
if (behoud && vorigeActieve > 0) doel = Math.min(vorigeActieve, shown.length - 1);
|
||||
setActive(shown.length ? doel : -1, false);
|
||||
// De lijst is zo breed als haar inhoud, dus elke filterslag verandert die breedte en
|
||||
// daarmee ook hoever ze naar links geschoven moet staan. Zonder deze herberekening blijft
|
||||
// ze hangen op de plek die bij de volledige lijst hoorde: na het intypen van een zoekterm
|
||||
// staan er dan drie korte regels ver links van het veld waar ze bij horen. openList roept
|
||||
// position() zelf aan, en daar staat open nog op false, dus dit verdubbelt niets.
|
||||
if (open) position();
|
||||
}
|
||||
|
||||
function setActive(i, scroll) {
|
||||
var nodes = list.querySelectorAll('li[data-i]');
|
||||
for (var k = 0; k < nodes.length; k++) nodes[k].classList.remove('active');
|
||||
active = i;
|
||||
if (i < 0 || i >= nodes.length) {
|
||||
input.removeAttribute('aria-activedescendant');
|
||||
return;
|
||||
}
|
||||
nodes[i].classList.add('active');
|
||||
input.setAttribute('aria-activedescendant', nodes[i].id);
|
||||
if (scroll !== false) {
|
||||
var li = nodes[i], top = li.offsetTop, bot = top + li.offsetHeight;
|
||||
if (top < list.scrollTop) list.scrollTop = top;
|
||||
else if (bot > list.scrollTop + list.clientHeight) list.scrollTop = bot - list.clientHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function openList(q) {
|
||||
render(q === undefined ? '' : q);
|
||||
list.classList.add('open');
|
||||
input.setAttribute('aria-expanded', 'true');
|
||||
open = true;
|
||||
position();
|
||||
win.addEventListener('scroll', position, true);
|
||||
win.addEventListener('resize', position);
|
||||
}
|
||||
|
||||
function closeList(restore) {
|
||||
list.classList.remove('open');
|
||||
input.setAttribute('aria-expanded', 'false');
|
||||
input.removeAttribute('aria-activedescendant');
|
||||
open = false;
|
||||
active = -1;
|
||||
win.removeEventListener('scroll', position, true);
|
||||
win.removeEventListener('resize', position);
|
||||
if (restore !== false) input.value = currentLabel();
|
||||
}
|
||||
|
||||
function choose(i) {
|
||||
var it = shown[i];
|
||||
if (!it || it.disabled) return;
|
||||
select.selectedIndex = it.index;
|
||||
input.value = it.label;
|
||||
updateWis();
|
||||
closeList(false);
|
||||
if (recentSleutel && !recentManueel) {
|
||||
pushRecent(recentSleutel, select.options[it.index].value, it.label);
|
||||
// Herbouwen, anders klopt het blok pas bij de volgende keer dat de opties veranderen.
|
||||
readOptions();
|
||||
}
|
||||
fire(select, 'input');
|
||||
fire(select, 'change');
|
||||
}
|
||||
|
||||
// Het script haalt zelf niets weg. Het meldt enkel dat de gebruiker deze regel kwijt wil, en
|
||||
// wie de opties leverde beslist wat dat betekent en past de <select> aan. Luistert er niemand,
|
||||
// dan gebeurt er niets, en dat is geen fout.
|
||||
function fireRemove(it) {
|
||||
var ev;
|
||||
var detail = { value: select.options[it.index].value, label: it.label, index: it.index };
|
||||
try {
|
||||
ev = new CustomEvent('select-search:remove', { bubbles: true, cancelable: true, detail: detail });
|
||||
} catch (e) {
|
||||
ev = doc.createEvent('CustomEvent');
|
||||
ev.initCustomEvent('select-search:remove', true, true, detail);
|
||||
}
|
||||
select.dispatchEvent(ev);
|
||||
}
|
||||
|
||||
function fire(el, type) {
|
||||
var ev;
|
||||
try {
|
||||
ev = new Event(type, { bubbles: true, cancelable: false });
|
||||
} catch (e) {
|
||||
ev = doc.createEvent('HTMLEvents');
|
||||
ev.initEvent(type, true, false);
|
||||
}
|
||||
el.dispatchEvent(ev);
|
||||
// Legacy apps binden vaak via jQuery; native events bereiken die handlers
|
||||
// wel, maar een expliciete trigger dekt ook .change()-only bindings.
|
||||
var jq = win.jQuery || win.$;
|
||||
if (jq && jq.fn && typeof jq === 'function') {
|
||||
try { jq(el).trigger(type); } catch (e2) { /* stil */ }
|
||||
}
|
||||
}
|
||||
|
||||
function detach() {
|
||||
obs.disconnect();
|
||||
select.removeEventListener('change', onSelectChange);
|
||||
win.removeEventListener('scroll', position, true);
|
||||
win.removeEventListener('resize', position);
|
||||
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);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------- listeners */
|
||||
|
||||
input.addEventListener('mousedown', function (e) {
|
||||
// ontsnappingsluik: native select terug
|
||||
if (e.altKey) {
|
||||
e.preventDefault();
|
||||
detach();
|
||||
select.focus();
|
||||
return;
|
||||
}
|
||||
if (!open) { e.preventDefault(); input.focus(); openList(''); input.select(); }
|
||||
});
|
||||
// mousedown in plaats van click, met preventDefault: zo houdt het invoerveld zijn focus en
|
||||
// blijft het kruisje zichtbaar terwijl je klikt. stopPropagation houdt de lijst dicht.
|
||||
wis.addEventListener('mousedown', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
clear();
|
||||
});
|
||||
input.addEventListener('focus', function () { if (!open) openList(''); });
|
||||
input.addEventListener('input', function () { if (open) render(input.value); else openList(input.value); });
|
||||
input.addEventListener('blur', function () {
|
||||
setTimeout(function () { if (open) closeList(true); }, 120);
|
||||
});
|
||||
input.addEventListener('keydown', function (e) {
|
||||
var n = list.querySelectorAll('li[data-i]').length;
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
if (!open) { openList(''); break; }
|
||||
if (n) setActive((active + 1) % n);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
if (!open) { openList(''); break; }
|
||||
if (n) setActive((active - 1 + n) % n);
|
||||
break;
|
||||
case 'Home': if (open && n) { e.preventDefault(); setActive(0); } break;
|
||||
case 'End': if (open && n) { e.preventDefault(); setActive(n - 1); } break;
|
||||
case 'PageDown': if (open && n) { e.preventDefault(); setActive(Math.min(n - 1, active + 10)); } break;
|
||||
case 'PageUp': if (open && n) { e.preventDefault(); setActive(Math.max(0, active - 10)); } break;
|
||||
case 'Enter':
|
||||
if (open) { e.preventDefault(); if (active >= 0) choose(active); else closeList(true); }
|
||||
break;
|
||||
case 'Tab':
|
||||
if (open && active >= 0) choose(active); else if (open) closeList(true);
|
||||
break;
|
||||
case 'Escape':
|
||||
if (open) { e.preventDefault(); e.stopPropagation(); closeList(true); }
|
||||
break;
|
||||
// Backspace op een leeg invoerveld wist de keuze, zoals in react-select. Staat er nog
|
||||
// tekst, dan doet Backspace gewoon wat je verwacht en blijft de keuze staan.
|
||||
case 'Backspace':
|
||||
if (!input.value && leegIndex !== -1 && currentLabel()) { e.preventDefault(); clear(); }
|
||||
break;
|
||||
// Delete haalt de actieve regel uit haar lijst, zoals de adresbalk van een browser dat met
|
||||
// een suggestie doet. Alleen zolang er niets getypt is: filtert de gebruiker, dan blijft
|
||||
// Delete de gewone teksttoets en gebeurt er niets met de lijst.
|
||||
case 'Delete':
|
||||
if (!open || zoek || active < 0) break;
|
||||
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;
|
||||
}
|
||||
});
|
||||
// De muis verzet dezelfde balk als de pijltjes: één begrip van "waar sta ik", zodat Enter en
|
||||
// Delete altijd op de regel slaan die oplicht. mousemove en niet mouseover, want een muis die
|
||||
// toevallig boven de lijst ligt mag de pijltjes niet blijven overrulen.
|
||||
list.addEventListener('mousemove', function (e) {
|
||||
var li = e.target;
|
||||
while (li && li.tagName !== 'LI') li = li.parentNode;
|
||||
if (!li || !li.hasAttribute('data-i')) return;
|
||||
var i = parseInt(li.getAttribute('data-i'), 10);
|
||||
// Geen scroll: de regel ligt per definitie al onder de cursor en dus in beeld.
|
||||
if (i !== active) setActive(i, false);
|
||||
});
|
||||
list.addEventListener('mousedown', function (e) { e.preventDefault(); });
|
||||
list.addEventListener('click', function (e) {
|
||||
var li = e.target;
|
||||
while (li && li.tagName !== 'LI') li = li.parentNode;
|
||||
if (li && li.hasAttribute('data-i')) choose(parseInt(li.getAttribute('data-i'), 10));
|
||||
});
|
||||
|
||||
function onSelectChange() {
|
||||
updateWis();
|
||||
if (!open) input.value = currentLabel();
|
||||
}
|
||||
select.addEventListener('change', onSelectChange);
|
||||
|
||||
// De app vervangt de optielijst (bv. "Inclusief niet-raamcontractartikelen").
|
||||
var resync = debounce(function () {
|
||||
syncFromSelect();
|
||||
if (open) render(zoek, true);
|
||||
}, 50);
|
||||
var obs = new MutationObserver(resync);
|
||||
obs.observe(select, { childList: true, subtree: true });
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- scan */
|
||||
|
||||
function eligible(s) {
|
||||
return !s.multiple && s.size <= 1 && !s.disabled
|
||||
&& !s.dataset.ssEnhanced && !s.hasAttribute('data-nosearch')
|
||||
&& s.options.length >= MIN_OPTIONS;
|
||||
}
|
||||
|
||||
function scanDoc(doc) {
|
||||
var selects = doc.querySelectorAll('select');
|
||||
for (var i = 0; i < selects.length; i++) {
|
||||
if (eligible(selects[i])) {
|
||||
try { enhance(selects[i]); } catch (e) { /* laat de pagina met rust */ }
|
||||
}
|
||||
}
|
||||
// Wrappers waarvan de app het originele select heeft weggegooid opruimen.
|
||||
var wraps = doc.querySelectorAll('.select-search-wrap');
|
||||
for (var w = 0; w < wraps.length; w++) {
|
||||
var sel = wraps[w].__selectSearchSelect;
|
||||
if (sel && !doc.contains(sel) && wraps[w].parentNode) {
|
||||
wraps[w].parentNode.removeChild(wraps[w]);
|
||||
}
|
||||
}
|
||||
var frames = doc.querySelectorAll('iframe,frame');
|
||||
for (var f = 0; f < frames.length; f++) {
|
||||
try {
|
||||
var fd = frames[f].contentDocument;
|
||||
if (fd && fd.body) { attach(fd); }
|
||||
} catch (e) { /* cross-origin */ }
|
||||
}
|
||||
}
|
||||
|
||||
var attached = [];
|
||||
function attach(doc) {
|
||||
if (attached.indexOf(doc) === -1) {
|
||||
attached.push(doc);
|
||||
new MutationObserver(debounce(function () { scanDoc(doc); }, 120))
|
||||
.observe(doc.documentElement, { childList: true, subtree: true });
|
||||
}
|
||||
scanDoc(doc);
|
||||
}
|
||||
|
||||
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 {
|
||||
attach(document);
|
||||
}
|
||||
})();
|
||||
@@ -1,16 +0,0 @@
|
||||
// ==UserScript==
|
||||
// @name BIPP: doorzoekbare dropdowns
|
||||
// @namespace https://github.com/glgoose/ua-userscripts
|
||||
// @version 1.8.3
|
||||
// @description Geeft elke <select> met veel opties een zoekveld met substring-zoeken.
|
||||
// @author glgoose
|
||||
// @license GPL-3.0-or-later
|
||||
// @homepageURL https://github.com/glgoose/ua-userscripts/tree/main/scripts/select-search
|
||||
// @supportURL https://github.com/glgoose/ua-userscripts/issues
|
||||
// @downloadURL https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/select-search/select-search.user.js
|
||||
// @updateURL https://raw.githubusercontent.com/glgoose/ua-userscripts/main/scripts/select-search/select-search.user.js
|
||||
// @match https://bipp.biotechpartner.be/web/*
|
||||
// @run-at document-idle
|
||||
// @grant none
|
||||
// @noframes false
|
||||
// ==/UserScript==
|
||||
Reference in New Issue
Block a user