detached 9de5dca

find/omnibox: prevent page scroll while overlay visible; ensure list scrolls on arrow nav; fade overlay when navigating matches; unfade on input focus; stopPropagation on arrows

commit
9de5dcabe5bef728a2f12f58f941dd1618f08c17
author
bamen · · 2025-09-09 18:20 UTC
committer
bamen · · 2025-09-09 17:50 UTC
parent
87979c4
signature
unsigned
1 file · +51 -7
M pkg/webkit/ucm_js_cgo.go +51 -7
@@ -14,6 +14,7 @@ func getOmniboxScript() string {
1414 const MAX_MATCHES = 2000;
1515 const H = {
1616 el: null,
17+ box: null,
1718 input: null,
1819 list: null,
1920 visible: false,
@@ -23,6 +24,8 @@ func getOmniboxScript() string {
2324 selectedIndex: -1,
2425 debounceTimer: 0,
2526 highlightNodes: [],
27+ faded: false,
28+ prevOverflow: '',
2629 post(msg){ try { window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify(msg)); } catch(_){} },
2730 render(){
2831 if (!H.el) H.mount();
@@ -31,10 +34,47 @@ func getOmniboxScript() string {
3134 H.input.placeholder = H.mode === 'find' ? 'Find in page…' : 'Type URL or search…';
3235 H.input.focus();
3336 },
34- setMode(m){ H.mode = m === 'find' ? 'find' : 'omnibox'; H.selectedIndex = -1; H.paintList(); },
37+ setMode(m){ H.mode = m === 'find' ? 'find' : 'omnibox'; H.selectedIndex = -1; H.setFaded(false); H.paintList(); },
3538 open(mode, initial){ H.setMode(mode||'omnibox'); H.toggle(true); if (typeof initial==='string') { H.input.value = initial; H.onInput(); } },
36- close(){ if (H.mode==='find') H.clearHighlights(); H.toggle(false); },
37- toggle(v){ H.visible = (typeof v==='boolean')? v : !H.visible; H.render(); },
39+ close(){ if (H.mode==='find') H.clearHighlights(); H.setFaded(false); H.toggle(false); },
40+ toggle(v){
41+ const newState = (typeof v==='boolean')? v : !H.visible;
42+ if (newState && !H.visible) {
43+ // Disable page scrolling while overlay is visible
44+ H.prevOverflow = document.documentElement.style.overflow;
45+ document.documentElement.style.overflow = 'hidden';
46+ } else if (!newState && H.visible) {
47+ // Restore page scrolling on close
48+ document.documentElement.style.overflow = H.prevOverflow || '';
49+ H.prevOverflow = '';
50+ }
51+ H.visible = newState;
52+ H.render();
53+ },
54+ setFaded(v){
55+ H.faded = !!v;
56+ if (!H.box || !H.input || !H.list) return;
57+ if (H.faded) {
58+ H.box.style.background = 'rgba(27,27,27,0.25)';
59+ H.input.style.background = 'rgba(18,18,18,0.35)';
60+ H.input.style.color = '#eee';
61+ } else {
62+ H.box.style.background = '#1b1b1b';
63+ H.input.style.background = '#121212';
64+ H.input.style.color = '#eee';
65+ }
66+ },
67+ scrollListToSelection(){
68+ if (!H.list) return;
69+ let idx = H.selectedIndex|0;
70+ if (idx < 0) return;
71+ // Account for header in find mode
72+ const childIdx = (H.mode==='find' ? 1 : 0) + idx;
73+ const el = H.list.children[childIdx];
74+ if (el && el.scrollIntoView) {
75+ try { el.scrollIntoView({block:'nearest'}); } catch(_) { el.scrollIntoView(); }
76+ }
77+ },
3878 mount(){
3979 const root = document.createElement('div');
4080 root.id = 'dumber-omnibox-root';
@@ -51,9 +91,12 @@ func getOmniboxScript() string {
5191 style.textContent = '.dumber-find-highlight{background:#ffeb3b;color:#000;padding:0 1px;border-radius:2px;box-shadow:0 0 0 1px #c8b900 inset}';
5292 document.documentElement.appendChild(style);
5393 box.appendChild(input); box.appendChild(list); root.appendChild(box); document.documentElement.appendChild(root);
94+ H.box = box;
5495 // Focus and select input on hover for quick typing
5596 input.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); input.select(); } catch(_) { input.focus(); } } });
5697 root.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); } catch(_) { input.focus(); } } });
98+ // Clear fading when user goes back to input typing
99+ input.addEventListener('focus', ()=> H.setFaded(false));
57100 input.addEventListener('keydown', (e)=>{
58101 if (e.key === 'Escape'){ H.close(); }
59102 else if (H.mode==='omnibox' && e.key === 'Enter'){
@@ -74,13 +117,14 @@ func getOmniboxScript() string {
74117 H.close();
75118 }
76119 } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp'){
77- e.preventDefault();
120+ e.preventDefault(); e.stopPropagation();
78121 const n = (H.mode==='find'? H.matches.length : H.suggestions.length);
79122 if (n){
80123 H.selectedIndex = (H.selectedIndex||0) + (e.key==='ArrowDown'?1:-1);
81124 if (H.selectedIndex<0) H.selectedIndex = n-1; if (H.selectedIndex>=n) H.selectedIndex = 0;
82125 H.paintList();
83- H.revealSelection();
126+ H.scrollListToSelection();
127+ if (H.mode==='find') { H.revealSelection(); H.setFaded(true); }
84128 }
85129 }
86130 });
@@ -105,7 +149,7 @@ func getOmniboxScript() string {
105149 const url = document.createElement('div'); url.textContent = s.url || ''; url.style.cssText = 'flex:1;color:#9ad;word-break:break-all;';
106150 const title = document.createElement('div'); title.textContent = s.title || ''; title.style.cssText = 'flex:1;color:#ccc;opacity:.9;';
107151 item.appendChild(title); item.appendChild(url);
108- item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); });
152+ item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); H.scrollListToSelection(); });
109153 item.addEventListener('click',()=>{ H.post({type:'navigate', url:s.url}); H.toggle(false); });
110154 list.appendChild(item);
111155 });
@@ -120,7 +164,7 @@ func getOmniboxScript() string {
120164 item.style.cssText = 'padding:8px 10px;cursor:pointer;border-bottom:1px solid #2a2a2a;'+(i===H.selectedIndex?'background:#0a0a0a;':'');
121165 const ctx = document.createElement('div'); ctx.textContent = m.context || ''; ctx.style.cssText = 'color:#ddd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
122166 item.appendChild(ctx);
123- item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); });
167+ item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); H.scrollListToSelection(); H.setFaded(true); });
124168 item.addEventListener('click',()=>{ H.selectedIndex=i; H.revealSelection(); H.paintList(); H.close(); });
125169 list.appendChild(item);
126170 });
@@ -14,6 +14,7 @@ func getOmniboxScript() string {
14 const MAX_MATCHES = 2000;14 const MAX_MATCHES = 2000;
15 const H = {15 const H = {
16 el: null,16 el: null,
17+ box: null,
17 input: null,18 input: null,
18 list: null,19 list: null,
19 visible: false,20 visible: false,
@@ -23,6 +24,8 @@ func getOmniboxScript() string {
23 selectedIndex: -1,24 selectedIndex: -1,
24 debounceTimer: 0,25 debounceTimer: 0,
25 highlightNodes: [],26 highlightNodes: [],
27+ faded: false,
28+ prevOverflow: '',
26 post(msg){ try { window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify(msg)); } catch(_){} },29 post(msg){ try { window.webkit?.messageHandlers?.dumber?.postMessage(JSON.stringify(msg)); } catch(_){} },
27 render(){30 render(){
28 if (!H.el) H.mount();31 if (!H.el) H.mount();
@@ -31,10 +34,47 @@ func getOmniboxScript() string {
31 H.input.placeholder = H.mode === 'find' ? 'Find in page…' : 'Type URL or search…';34 H.input.placeholder = H.mode === 'find' ? 'Find in page…' : 'Type URL or search…';
32 H.input.focus();35 H.input.focus();
33 },36 },
34- setMode(m){ H.mode = m === 'find' ? 'find' : 'omnibox'; H.selectedIndex = -1; H.paintList(); },37+ setMode(m){ H.mode = m === 'find' ? 'find' : 'omnibox'; H.selectedIndex = -1; H.setFaded(false); H.paintList(); },
35 open(mode, initial){ H.setMode(mode||'omnibox'); H.toggle(true); if (typeof initial==='string') { H.input.value = initial; H.onInput(); } },38 open(mode, initial){ H.setMode(mode||'omnibox'); H.toggle(true); if (typeof initial==='string') { H.input.value = initial; H.onInput(); } },
36- close(){ if (H.mode==='find') H.clearHighlights(); H.toggle(false); },39+ close(){ if (H.mode==='find') H.clearHighlights(); H.setFaded(false); H.toggle(false); },
37- toggle(v){ H.visible = (typeof v==='boolean')? v : !H.visible; H.render(); },40+ toggle(v){
41+ const newState = (typeof v==='boolean')? v : !H.visible;
42+ if (newState && !H.visible) {
43+ // Disable page scrolling while overlay is visible
44+ H.prevOverflow = document.documentElement.style.overflow;
45+ document.documentElement.style.overflow = 'hidden';
46+ } else if (!newState && H.visible) {
47+ // Restore page scrolling on close
48+ document.documentElement.style.overflow = H.prevOverflow || '';
49+ H.prevOverflow = '';
50+ }
51+ H.visible = newState;
52+ H.render();
53+ },
54+ setFaded(v){
55+ H.faded = !!v;
56+ if (!H.box || !H.input || !H.list) return;
57+ if (H.faded) {
58+ H.box.style.background = 'rgba(27,27,27,0.25)';
59+ H.input.style.background = 'rgba(18,18,18,0.35)';
60+ H.input.style.color = '#eee';
61+ } else {
62+ H.box.style.background = '#1b1b1b';
63+ H.input.style.background = '#121212';
64+ H.input.style.color = '#eee';
65+ }
66+ },
67+ scrollListToSelection(){
68+ if (!H.list) return;
69+ let idx = H.selectedIndex|0;
70+ if (idx < 0) return;
71+ // Account for header in find mode
72+ const childIdx = (H.mode==='find' ? 1 : 0) + idx;
73+ const el = H.list.children[childIdx];
74+ if (el && el.scrollIntoView) {
75+ try { el.scrollIntoView({block:'nearest'}); } catch(_) { el.scrollIntoView(); }
76+ }
77+ },
38 mount(){78 mount(){
39 const root = document.createElement('div');79 const root = document.createElement('div');
40 root.id = 'dumber-omnibox-root';80 root.id = 'dumber-omnibox-root';
@@ -51,9 +91,12 @@ func getOmniboxScript() string {
51 style.textContent = '.dumber-find-highlight{background:#ffeb3b;color:#000;padding:0 1px;border-radius:2px;box-shadow:0 0 0 1px #c8b900 inset}';91 style.textContent = '.dumber-find-highlight{background:#ffeb3b;color:#000;padding:0 1px;border-radius:2px;box-shadow:0 0 0 1px #c8b900 inset}';
52 document.documentElement.appendChild(style);92 document.documentElement.appendChild(style);
53 box.appendChild(input); box.appendChild(list); root.appendChild(box); document.documentElement.appendChild(root);93 box.appendChild(input); box.appendChild(list); root.appendChild(box); document.documentElement.appendChild(root);
94+ H.box = box;
54 // Focus and select input on hover for quick typing95 // Focus and select input on hover for quick typing
55 input.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); input.select(); } catch(_) { input.focus(); } } });96 input.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); input.select(); } catch(_) { input.focus(); } } });
56 root.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); } catch(_) { input.focus(); } } });97 root.addEventListener('mouseenter', ()=>{ if (H.visible) { try { input.focus({preventScroll:true}); } catch(_) { input.focus(); } } });
98+ // Clear fading when user goes back to input typing
99+ input.addEventListener('focus', ()=> H.setFaded(false));
57 input.addEventListener('keydown', (e)=>{100 input.addEventListener('keydown', (e)=>{
58 if (e.key === 'Escape'){ H.close(); }101 if (e.key === 'Escape'){ H.close(); }
59 else if (H.mode==='omnibox' && e.key === 'Enter'){102 else if (H.mode==='omnibox' && e.key === 'Enter'){
@@ -74,13 +117,14 @@ func getOmniboxScript() string {
74 H.close();117 H.close();
75 }118 }
76 } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp'){119 } else if (e.key === 'ArrowDown' || e.key === 'ArrowUp'){
77- e.preventDefault();120+ e.preventDefault(); e.stopPropagation();
78 const n = (H.mode==='find'? H.matches.length : H.suggestions.length);121 const n = (H.mode==='find'? H.matches.length : H.suggestions.length);
79 if (n){122 if (n){
80 H.selectedIndex = (H.selectedIndex||0) + (e.key==='ArrowDown'?1:-1);123 H.selectedIndex = (H.selectedIndex||0) + (e.key==='ArrowDown'?1:-1);
81 if (H.selectedIndex<0) H.selectedIndex = n-1; if (H.selectedIndex>=n) H.selectedIndex = 0;124 if (H.selectedIndex<0) H.selectedIndex = n-1; if (H.selectedIndex>=n) H.selectedIndex = 0;
82 H.paintList();125 H.paintList();
83- H.revealSelection();126+ H.scrollListToSelection();
127+ if (H.mode==='find') { H.revealSelection(); H.setFaded(true); }
84 }128 }
85 }129 }
86 });130 });
@@ -105,7 +149,7 @@ func getOmniboxScript() string {
105 const url = document.createElement('div'); url.textContent = s.url || ''; url.style.cssText = 'flex:1;color:#9ad;word-break:break-all;';149 const url = document.createElement('div'); url.textContent = s.url || ''; url.style.cssText = 'flex:1;color:#9ad;word-break:break-all;';
106 const title = document.createElement('div'); title.textContent = s.title || ''; title.style.cssText = 'flex:1;color:#ccc;opacity:.9;';150 const title = document.createElement('div'); title.textContent = s.title || ''; title.style.cssText = 'flex:1;color:#ccc;opacity:.9;';
107 item.appendChild(title); item.appendChild(url);151 item.appendChild(title); item.appendChild(url);
108- item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); });152+ item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); H.scrollListToSelection(); });
109 item.addEventListener('click',()=>{ H.post({type:'navigate', url:s.url}); H.toggle(false); });153 item.addEventListener('click',()=>{ H.post({type:'navigate', url:s.url}); H.toggle(false); });
110 list.appendChild(item);154 list.appendChild(item);
111 });155 });
@@ -120,7 +164,7 @@ func getOmniboxScript() string {
120 item.style.cssText = 'padding:8px 10px;cursor:pointer;border-bottom:1px solid #2a2a2a;'+(i===H.selectedIndex?'background:#0a0a0a;':'');164 item.style.cssText = 'padding:8px 10px;cursor:pointer;border-bottom:1px solid #2a2a2a;'+(i===H.selectedIndex?'background:#0a0a0a;':'');
121 const ctx = document.createElement('div'); ctx.textContent = m.context || ''; ctx.style.cssText = 'color:#ddd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';165 const ctx = document.createElement('div'); ctx.textContent = m.context || ''; ctx.style.cssText = 'color:#ddd;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
122 item.appendChild(ctx);166 item.appendChild(ctx);
123- item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); });167+ item.addEventListener('mouseenter', ()=>{ H.selectedIndex = i; H.paintList(); H.scrollListToSelection(); H.setFaded(true); });
124 item.addEventListener('click',()=>{ H.selectedIndex=i; H.revealSelection(); H.paintList(); H.close(); });168 item.addEventListener('click',()=>{ H.selectedIndex=i; H.revealSelection(); H.paintList(); H.close(); });
125 list.appendChild(item);169 list.appendChild(item);
126 });170 });

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close