refactor(omnibox): move keyboard blocking to main-world
- commit
- e2080dd036b29e1cb821971a6d2ce3782932336a
- author
- bnema · · 2025-11-18 22:22 UTC
- parent
- 72a6840
- signature
- unsigned
3 files · +128 -0
M CHANGELOG.md +4 -0
| @@ -6,10 +6,14 @@ All notable changes to this project will be documented in this file. | |||
| 6 | 6 | ||
| 7 | 7 | ### Added | |
| 8 | 8 | - **Omnibox initial history display**: Configurable behavior when opening empty omnibox via `omnibox.initial_behavior` ("recent", "most_visited", "none"). Shows initial history on open and when input is cleared. Default: recent visits. | |
| 9 | + | - **Omnibox keyboard blocking**: Prevent page scripts from stealing focus while omnibox is active. | |
| 9 | 10 | ||
| 10 | 11 | ### Fixed | |
| 11 | 12 | - **Omnibox flash on load**: Load the injected GUI bundle at document-start and initialize the omnibox as soon as DOM is ready, ensuring it is ready without briefly flashing during page startup or new pane creation. | |
| 12 | 13 | ||
| 14 | + | ### Changed | |
| 15 | + | - **Refactor keyboard blocking**: Move logic to main-world.ts, reduce handler.go verbosity. | |
| 16 | + | ||
| 13 | 17 | ## [0.14.1] - 2025-11-18 | |
| 14 | 18 | ||
| 15 | 19 | ### Added | |
| @@ -6,10 +6,14 @@ All notable changes to this project will be documented in this file. | |||||
| 6 | 6 | ||||
| 7 | ### Added | 7 | ### Added | ||
| 8 | - **Omnibox initial history display**: Configurable behavior when opening empty omnibox via `omnibox.initial_behavior` ("recent", "most_visited", "none"). Shows initial history on open and when input is cleared. Default: recent visits. | 8 | - **Omnibox initial history display**: Configurable behavior when opening empty omnibox via `omnibox.initial_behavior` ("recent", "most_visited", "none"). Shows initial history on open and when input is cleared. Default: recent visits. | ||
| 9 | + | - **Omnibox keyboard blocking**: Prevent page scripts from stealing focus while omnibox is active. | |||
| 9 | 10 | ||||
| 10 | ### Fixed | 11 | ### Fixed | ||
| 11 | - **Omnibox flash on load**: Load the injected GUI bundle at document-start and initialize the omnibox as soon as DOM is ready, ensuring it is ready without briefly flashing during page startup or new pane creation. | 12 | - **Omnibox flash on load**: Load the injected GUI bundle at document-start and initialize the omnibox as soon as DOM is ready, ensuring it is ready without briefly flashing during page startup or new pane creation. | ||
| 12 | 13 | ||||
| 14 | + | ### Changed | |||
| 15 | + | - **Refactor keyboard blocking**: Move logic to main-world.ts, reduce handler.go verbosity. | |||
| 16 | + | ||||
| 13 | ## [0.14.1] - 2025-11-18 | 17 | ## [0.14.1] - 2025-11-18 | ||
| 14 | 18 | ||||
| 15 | ### Added | 19 | ### Added | ||
M gui/src/injected/main-world.ts +104 -0
| @@ -36,6 +36,12 @@ declare global { | |||
| 36 | 36 | __dumber_webview_id?: string | number; | |
| 37 | 37 | __dumber_is_active?: boolean; | |
| 38 | 38 | __dumber_teardown?: () => void; | |
| 39 | + | __dumber_enableKeyboardBlocking?: () => void; | |
| 40 | + | __dumber_disableKeyboardBlocking?: () => void; | |
| 41 | + | __dumber_keyboard_blocking_active?: boolean; | |
| 42 | + | __dumber_keyboard_blocker?: (e: Event) => void; | |
| 43 | + | __dumber_focus_guard?: (e: Event) => void; | |
| 44 | + | __dumber_click_blocker?: (e: Event) => void; | |
| 39 | 45 | webkit?: { | |
| 40 | 46 | messageHandlers?: { | |
| 41 | 47 | dumber?: { | |
| @@ -994,6 +1000,104 @@ interface DumberAPI { | |||
| 994 | 1000 | ||
| 995 | 1001 | // Initialize console capture | |
| 996 | 1002 | setupConsoleCapture(); | |
| 1003 | + | ||
| 1004 | + | // ============================================================================ | |
| 1005 | + | // KEYBOARD AND FOCUS BLOCKING FOR OMNIBOX | |
| 1006 | + | // ============================================================================ | |
| 1007 | + | ||
| 1008 | + | const SHADOW_HOST_ID = 'dumber-ui-root'; | |
| 1009 | + | ||
| 1010 | + | const isFromOmnibox = (target: EventTarget | null): boolean => { | |
| 1011 | + | if (!target || !(target instanceof Element)) { | |
| 1012 | + | return false; | |
| 1013 | + | } | |
| 1014 | + | return target.id === SHADOW_HOST_ID || target.closest(`#${SHADOW_HOST_ID}`) !== null; | |
| 1015 | + | }; | |
| 1016 | + | ||
| 1017 | + | window.__dumber_enableKeyboardBlocking = function(): void { | |
| 1018 | + | if (window.__dumber_keyboard_blocking_active) { | |
| 1019 | + | return; | |
| 1020 | + | } | |
| 1021 | + | ||
| 1022 | + | window.__dumber_keyboard_blocking_active = true; | |
| 1023 | + | ||
| 1024 | + | window.__dumber_keyboard_blocker = function(e: Event): void { | |
| 1025 | + | if (isFromOmnibox(e.target)) { | |
| 1026 | + | return; | |
| 1027 | + | } | |
| 1028 | + | e.stopPropagation(); | |
| 1029 | + | e.stopImmediatePropagation(); | |
| 1030 | + | }; | |
| 1031 | + | ||
| 1032 | + | window.__dumber_focus_guard = function(e: Event): void { | |
| 1033 | + | if (e.type === 'focus') { | |
| 1034 | + | if (!isFromOmnibox(e.target)) { | |
| 1035 | + | e.stopPropagation(); | |
| 1036 | + | e.stopImmediatePropagation(); | |
| 1037 | + | e.preventDefault(); | |
| 1038 | + | } | |
| 1039 | + | } else if (e.type === 'blur') { | |
| 1040 | + | if (isFromOmnibox(e.target)) { | |
| 1041 | + | e.stopPropagation(); | |
| 1042 | + | e.stopImmediatePropagation(); | |
| 1043 | + | e.preventDefault(); | |
| 1044 | + | setTimeout(() => { | |
| 1045 | + | const shadowHost = document.getElementById(SHADOW_HOST_ID); | |
| 1046 | + | if (shadowHost && shadowHost.shadowRoot && window.__dumber_keyboard_blocking_active) { | |
| 1047 | + | const input = shadowHost.shadowRoot.querySelector('.omnibox-input-field') as HTMLInputElement; | |
| 1048 | + | if (input) { | |
| 1049 | + | input.focus(); | |
| 1050 | + | } | |
| 1051 | + | } | |
| 1052 | + | }, 0); | |
| 1053 | + | } | |
| 1054 | + | } | |
| 1055 | + | }; | |
| 1056 | + | ||
| 1057 | + | window.__dumber_click_blocker = function(e: Event): void { | |
| 1058 | + | if (isFromOmnibox(e.target)) { | |
| 1059 | + | return; | |
| 1060 | + | } | |
| 1061 | + | e.stopPropagation(); | |
| 1062 | + | e.stopImmediatePropagation(); | |
| 1063 | + | e.preventDefault(); | |
| 1064 | + | }; | |
| 1065 | + | ||
| 1066 | + | document.addEventListener('keydown', window.__dumber_keyboard_blocker, true); | |
| 1067 | + | document.addEventListener('keyup', window.__dumber_keyboard_blocker, true); | |
| 1068 | + | document.addEventListener('keypress', window.__dumber_keyboard_blocker, true); | |
| 1069 | + | document.addEventListener('focus', window.__dumber_focus_guard, true); | |
| 1070 | + | document.addEventListener('blur', window.__dumber_focus_guard, true); | |
| 1071 | + | document.addEventListener('click', window.__dumber_click_blocker, true); | |
| 1072 | + | document.addEventListener('mousedown', window.__dumber_click_blocker, true); | |
| 1073 | + | }; | |
| 1074 | + | ||
| 1075 | + | window.__dumber_disableKeyboardBlocking = function(): void { | |
| 1076 | + | if (!window.__dumber_keyboard_blocking_active) { | |
| 1077 | + | return; | |
| 1078 | + | } | |
| 1079 | + | ||
| 1080 | + | window.__dumber_keyboard_blocking_active = false; | |
| 1081 | + | ||
| 1082 | + | if (window.__dumber_keyboard_blocker) { | |
| 1083 | + | document.removeEventListener('keydown', window.__dumber_keyboard_blocker, true); | |
| 1084 | + | document.removeEventListener('keyup', window.__dumber_keyboard_blocker, true); | |
| 1085 | + | document.removeEventListener('keypress', window.__dumber_keyboard_blocker, true); | |
| 1086 | + | delete window.__dumber_keyboard_blocker; | |
| 1087 | + | } | |
| 1088 | + | ||
| 1089 | + | if (window.__dumber_focus_guard) { | |
| 1090 | + | document.removeEventListener('focus', window.__dumber_focus_guard, true); | |
| 1091 | + | document.removeEventListener('blur', window.__dumber_focus_guard, true); | |
| 1092 | + | delete window.__dumber_focus_guard; | |
| 1093 | + | } | |
| 1094 | + | ||
| 1095 | + | if (window.__dumber_click_blocker) { | |
| 1096 | + | document.removeEventListener('click', window.__dumber_click_blocker, true); | |
| 1097 | + | document.removeEventListener('mousedown', window.__dumber_click_blocker, true); | |
| 1098 | + | delete window.__dumber_click_blocker; | |
| 1099 | + | } | |
| 1100 | + | }; | |
| 997 | 1101 | } catch (err) { | |
| 998 | 1102 | console.warn("[dumber] unified bridge init failed", err); | |
| 999 | 1103 | } | |
| @@ -36,6 +36,12 @@ declare global { | |||||
| 36 | __dumber_webview_id?: string | number; | 36 | __dumber_webview_id?: string | number; | ||
| 37 | __dumber_is_active?: boolean; | 37 | __dumber_is_active?: boolean; | ||
| 38 | __dumber_teardown?: () => void; | 38 | __dumber_teardown?: () => void; | ||
| 39 | + | __dumber_enableKeyboardBlocking?: () => void; | |||
| 40 | + | __dumber_disableKeyboardBlocking?: () => void; | |||
| 41 | + | __dumber_keyboard_blocking_active?: boolean; | |||
| 42 | + | __dumber_keyboard_blocker?: (e: Event) => void; | |||
| 43 | + | __dumber_focus_guard?: (e: Event) => void; | |||
| 44 | + | __dumber_click_blocker?: (e: Event) => void; | |||
| 39 | webkit?: { | 45 | webkit?: { | ||
| 40 | messageHandlers?: { | 46 | messageHandlers?: { | ||
| 41 | dumber?: { | 47 | dumber?: { | ||
| @@ -994,6 +1000,104 @@ interface DumberAPI { | |||||
| 994 | 1000 | ||||
| 995 | // Initialize console capture | 1001 | // Initialize console capture | ||
| 996 | setupConsoleCapture(); | 1002 | setupConsoleCapture(); | ||
| 1003 | + | ||||
| 1004 | + | // ============================================================================ | |||
| 1005 | + | // KEYBOARD AND FOCUS BLOCKING FOR OMNIBOX | |||
| 1006 | + | // ============================================================================ | |||
| 1007 | + | ||||
| 1008 | + | const SHADOW_HOST_ID = 'dumber-ui-root'; | |||
| 1009 | + | ||||
| 1010 | + | const isFromOmnibox = (target: EventTarget | null): boolean => { | |||
| 1011 | + | if (!target || !(target instanceof Element)) { | |||
| 1012 | + | return false; | |||
| 1013 | + | } | |||
| 1014 | + | return target.id === SHADOW_HOST_ID || target.closest(`#${SHADOW_HOST_ID}`) !== null; | |||
| 1015 | + | }; | |||
| 1016 | + | ||||
| 1017 | + | window.__dumber_enableKeyboardBlocking = function(): void { | |||
| 1018 | + | if (window.__dumber_keyboard_blocking_active) { | |||
| 1019 | + | return; | |||
| 1020 | + | } | |||
| 1021 | + | ||||
| 1022 | + | window.__dumber_keyboard_blocking_active = true; | |||
| 1023 | + | ||||
| 1024 | + | window.__dumber_keyboard_blocker = function(e: Event): void { | |||
| 1025 | + | if (isFromOmnibox(e.target)) { | |||
| 1026 | + | return; | |||
| 1027 | + | } | |||
| 1028 | + | e.stopPropagation(); | |||
| 1029 | + | e.stopImmediatePropagation(); | |||
| 1030 | + | }; | |||
| 1031 | + | ||||
| 1032 | + | window.__dumber_focus_guard = function(e: Event): void { | |||
| 1033 | + | if (e.type === 'focus') { | |||
| 1034 | + | if (!isFromOmnibox(e.target)) { | |||
| 1035 | + | e.stopPropagation(); | |||
| 1036 | + | e.stopImmediatePropagation(); | |||
| 1037 | + | e.preventDefault(); | |||
| 1038 | + | } | |||
| 1039 | + | } else if (e.type === 'blur') { | |||
| 1040 | + | if (isFromOmnibox(e.target)) { | |||
| 1041 | + | e.stopPropagation(); | |||
| 1042 | + | e.stopImmediatePropagation(); | |||
| 1043 | + | e.preventDefault(); | |||
| 1044 | + | setTimeout(() => { | |||
| 1045 | + | const shadowHost = document.getElementById(SHADOW_HOST_ID); | |||
| 1046 | + | if (shadowHost && shadowHost.shadowRoot && window.__dumber_keyboard_blocking_active) { | |||
| 1047 | + | const input = shadowHost.shadowRoot.querySelector('.omnibox-input-field') as HTMLInputElement; | |||
| 1048 | + | if (input) { | |||
| 1049 | + | input.focus(); | |||
| 1050 | + | } | |||
| 1051 | + | } | |||
| 1052 | + | }, 0); | |||
| 1053 | + | } | |||
| 1054 | + | } | |||
| 1055 | + | }; | |||
| 1056 | + | ||||
| 1057 | + | window.__dumber_click_blocker = function(e: Event): void { | |||
| 1058 | + | if (isFromOmnibox(e.target)) { | |||
| 1059 | + | return; | |||
| 1060 | + | } | |||
| 1061 | + | e.stopPropagation(); | |||
| 1062 | + | e.stopImmediatePropagation(); | |||
| 1063 | + | e.preventDefault(); | |||
| 1064 | + | }; | |||
| 1065 | + | ||||
| 1066 | + | document.addEventListener('keydown', window.__dumber_keyboard_blocker, true); | |||
| 1067 | + | document.addEventListener('keyup', window.__dumber_keyboard_blocker, true); | |||
| 1068 | + | document.addEventListener('keypress', window.__dumber_keyboard_blocker, true); | |||
| 1069 | + | document.addEventListener('focus', window.__dumber_focus_guard, true); | |||
| 1070 | + | document.addEventListener('blur', window.__dumber_focus_guard, true); | |||
| 1071 | + | document.addEventListener('click', window.__dumber_click_blocker, true); | |||
| 1072 | + | document.addEventListener('mousedown', window.__dumber_click_blocker, true); | |||
| 1073 | + | }; | |||
| 1074 | + | ||||
| 1075 | + | window.__dumber_disableKeyboardBlocking = function(): void { | |||
| 1076 | + | if (!window.__dumber_keyboard_blocking_active) { | |||
| 1077 | + | return; | |||
| 1078 | + | } | |||
| 1079 | + | ||||
| 1080 | + | window.__dumber_keyboard_blocking_active = false; | |||
| 1081 | + | ||||
| 1082 | + | if (window.__dumber_keyboard_blocker) { | |||
| 1083 | + | document.removeEventListener('keydown', window.__dumber_keyboard_blocker, true); | |||
| 1084 | + | document.removeEventListener('keyup', window.__dumber_keyboard_blocker, true); | |||
| 1085 | + | document.removeEventListener('keypress', window.__dumber_keyboard_blocker, true); | |||
| 1086 | + | delete window.__dumber_keyboard_blocker; | |||
| 1087 | + | } | |||
| 1088 | + | ||||
| 1089 | + | if (window.__dumber_focus_guard) { | |||
| 1090 | + | document.removeEventListener('focus', window.__dumber_focus_guard, true); | |||
| 1091 | + | document.removeEventListener('blur', window.__dumber_focus_guard, true); | |||
| 1092 | + | delete window.__dumber_focus_guard; | |||
| 1093 | + | } | |||
| 1094 | + | ||||
| 1095 | + | if (window.__dumber_click_blocker) { | |||
| 1096 | + | document.removeEventListener('click', window.__dumber_click_blocker, true); | |||
| 1097 | + | document.removeEventListener('mousedown', window.__dumber_click_blocker, true); | |||
| 1098 | + | delete window.__dumber_click_blocker; | |||
| 1099 | + | } | |||
| 1100 | + | }; | |||
| 997 | } catch (err) { | 1101 | } catch (err) { | ||
| 998 | console.warn("[dumber] unified bridge init failed", err); | 1102 | console.warn("[dumber] unified bridge init failed", err); | ||
| 999 | } | 1103 | } | ||
M internal/app/messaging/handler.go +20 -0
| @@ -126,6 +126,8 @@ func (h *Handler) Handle(payload string) { | |||
| 126 | 126 | h.handleToggleFavorite(msg) | |
| 127 | 127 | case "is_favorite": | |
| 128 | 128 | h.handleIsFavorite(msg) | |
| 129 | + | case "keyboard_blocking": | |
| 130 | + | h.handleKeyboardBlocking(msg) | |
| 129 | 131 | } | |
| 130 | 132 | } | |
| 131 | 133 | ||
| @@ -852,3 +854,21 @@ func (h *Handler) handleIsFavorite(msg Message) { | |||
| 852 | 854 | ||
| 853 | 855 | _ = h.webView.InjectScript("window.__dumber_is_favorite && window.__dumber_is_favorite(" + string(b) + ")") | |
| 854 | 856 | } | |
| 857 | + | ||
| 858 | + | // handleKeyboardBlocking enables or disables keyboard event blocking for omnibox | |
| 859 | + | func (h *Handler) handleKeyboardBlocking(msg Message) { | |
| 860 | + | if h.webView == nil { | |
| 861 | + | return | |
| 862 | + | } | |
| 863 | + | ||
| 864 | + | var script string | |
| 865 | + | if msg.Action == "enable" { | |
| 866 | + | script = "window.__dumber_enableKeyboardBlocking?.()" | |
| 867 | + | } else { | |
| 868 | + | script = "window.__dumber_disableKeyboardBlocking?.()" | |
| 869 | + | } | |
| 870 | + | ||
| 871 | + | if err := h.webView.InjectScript(script); err != nil { | |
| 872 | + | log.Printf("[ERROR] Failed to inject keyboard blocking script: %v", err) | |
| 873 | + | } | |
| 874 | + | } | |
| @@ -126,6 +126,8 @@ func (h *Handler) Handle(payload string) { | |||||
| 126 | h.handleToggleFavorite(msg) | 126 | h.handleToggleFavorite(msg) | ||
| 127 | case "is_favorite": | 127 | case "is_favorite": | ||
| 128 | h.handleIsFavorite(msg) | 128 | h.handleIsFavorite(msg) | ||
| 129 | + | case "keyboard_blocking": | |||
| 130 | + | h.handleKeyboardBlocking(msg) | |||
| 129 | } | 131 | } | ||
| 130 | } | 132 | } | ||
| 131 | 133 | ||||
| @@ -852,3 +854,21 @@ func (h *Handler) handleIsFavorite(msg Message) { | |||||
| 852 | 854 | ||||
| 853 | _ = h.webView.InjectScript("window.__dumber_is_favorite && window.__dumber_is_favorite(" + string(b) + ")") | 855 | _ = h.webView.InjectScript("window.__dumber_is_favorite && window.__dumber_is_favorite(" + string(b) + ")") | ||
| 854 | } | 856 | } | ||
| 857 | + | ||||
| 858 | + | // handleKeyboardBlocking enables or disables keyboard event blocking for omnibox | |||
| 859 | + | func (h *Handler) handleKeyboardBlocking(msg Message) { | |||
| 860 | + | if h.webView == nil { | |||
| 861 | + | return | |||
| 862 | + | } | |||
| 863 | + | ||||
| 864 | + | var script string | |||
| 865 | + | if msg.Action == "enable" { | |||
| 866 | + | script = "window.__dumber_enableKeyboardBlocking?.()" | |||
| 867 | + | } else { | |||
| 868 | + | script = "window.__dumber_disableKeyboardBlocking?.()" | |||
| 869 | + | } | |||
| 870 | + | ||||
| 871 | + | if err := h.webView.InjectScript(script); err != nil { | |||
| 872 | + | log.Printf("[ERROR] Failed to inject keyboard blocking script: %v", err) | |||
| 873 | + | } | |||
| 874 | + | } | |||