bipp-sneltoetsen: toetsen per rol (NL en EN), Alt voor zijacties, Ctrl+Enter ook op Approve en Goedkeuren

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Glenn Goossens
2026-10-27 15:47:31 +01:00
co-authored by Claude Sonnet 5.5
parent 3e610366fe
commit ee1dca93ea
2 changed files with 70 additions and 75 deletions
@@ -1,8 +1,8 @@
// ==UserScript==
// @name BIPP: sneltoetsen voor knoppen
// @namespace https://github.com/glgoose/ua-userscripts
// @version 1.1.2
// @description Ctrl+letter drukt op Bevestigen (B), Zoeken (Z), Reset (R), Nieuw (N), Opslaan (S), Indienen (I) en Terug (T). De letter is onderstreept en bij ingedrukte Ctrl verschijnt de sneltoets onder de knop.
// @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
@@ -16,76 +16,55 @@
(function () {
'use strict';
// Knoplabel (kleine letters), de letter die onderstreept wordt, en de toets.
// 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 = [
{ label: 'bevestigen', letter: 'b', code: 'KeyB' },
{ label: 'zoeken', letter: 'z', code: 'KeyZ' },
{ label: 'reset', letter: 'r', code: 'KeyR' },
{ label: 'nieuw', letter: 'n', code: 'KeyN' },
{ label: 'opslaan', letter: 's', code: 'KeyS' },
{ label: 'indienen', letter: 'i', code: 'KeyI' },
{ label: 'terug', letter: 't', code: 'KeyT' }
{ 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';
function hint(k) { return 'Ctrl+' + k.letter.toUpperCase(); }
// 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); }
// Eerste zichtbare, actieve knop met dit label. Een postback-pagina kan er verborgen kopieën van hebben.
// 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 i = 0; i < btns.length; i++) {
var b = btns[i];
if (norm(b.value || b.textContent) === k.label && !b.disabled && visible(b)) return b;
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;
}
// <button>: de letter in een <u>. <input> kan geen HTML bevatten, dus daar komt een lijntje onder de letter.
function underline(btn, k) {
function mark(btn, k) {
if (btn.getAttribute(MARK)) return;
btn.setAttribute(MARK, k.letter);
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' + hint(k) : hint(k);
if (btn.tagName === 'BUTTON' && btn.children.length === 0) {
var t = btn.textContent;
var i = t.toLowerCase().indexOf(k.letter);
btn.textContent = '';
btn.appendChild(document.createTextNode(t.slice(0, i)));
var u = document.createElement('u');
u.textContent = t.charAt(i);
btn.appendChild(u);
btn.appendChild(document.createTextNode(t.slice(i + 1)));
return;
}
drawLine(btn, k);
window.addEventListener('resize', function () { drawLine(btn, k); });
}
function drawLine(btn, k) {
var old = btn.nextSibling && btn.nextSibling.getAttribute && btn.nextSibling.getAttribute(MARK + '-line');
if (old) btn.parentNode.removeChild(btn.nextSibling);
var cs = getComputedStyle(btn);
var ctx = document.createElement('canvas').getContext('2d');
ctx.font = cs.fontStyle + ' ' + cs.fontWeight + ' ' + cs.fontSize + ' ' + cs.fontFamily;
var text = btn.value || btn.textContent;
var i = text.toLowerCase().indexOf(k.letter);
var before = ctx.measureText(text.slice(0, i)).width;
var letter = ctx.measureText(text.charAt(i)).width;
var total = ctx.measureText(text).width;
var inner = btn.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
var start = parseFloat(cs.paddingLeft) + btn.clientLeft + Math.max(0, (inner - total) / 2);
var line = document.createElement('span');
line.setAttribute(MARK + '-line', '1');
var fs = parseFloat(cs.fontSize);
// position:absolute houdt de lijn buiten de flow, dus er verschuift niets.
line.style.cssText = 'position:absolute;pointer-events:none;height:1px;background:currentColor;color:' + cs.color +
';left:' + (btn.offsetLeft + start + before) + 'px;width:' + letter + 'px;top:' +
(btn.offsetTop + btn.offsetHeight / 2 + fs * 0.55) + 'px';
btn.parentNode.insertBefore(line, btn.nextSibling);
btn.title = btn.title ? btn.title + '\n' + k.hint : k.hint;
}
var tips = [];
@@ -96,13 +75,26 @@
if (!btn) return;
var r = btn.getBoundingClientRect();
var tip = document.createElement('span');
tip.textContent = hint(k);
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); });
@@ -110,10 +102,9 @@
}
document.addEventListener('keydown', function (e) {
if (e.key === 'Control') { if (!e.repeat) showTips(); return; }
if (!e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return;
if (e.key === 'Control' || e.key === 'Meta' || e.key === 'Alt') { if (!e.repeat) showTips(); return; }
for (var i = 0; i < KEYS.length; i++) {
if (e.code !== KEYS[i].code) continue;
if (!KEYS[i].match(e)) continue;
var btn = findButton(KEYS[i]);
if (!btn) return;
e.preventDefault();
@@ -122,7 +113,7 @@
return;
}
}, true);
document.addEventListener('keyup', function (e) { if (e.key === 'Control') hideTips(); }, 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.
@@ -131,7 +122,7 @@
var left = 0;
KEYS.forEach(function (k) {
var btn = findButton(k);
if (btn) underline(btn, k); else left++;
if (btn) mark(btn, k); else left++;
});
if (left && ++tries < 30) setTimeout(init, 500);
})();