waybetter/crates/waybetter-host/tab-bridge.js
CRT 8324bc94af Find in page: the shell's find bar per pane (Ctrl+F, the app menu, the palette) over the engine's own find
Host: `tab_find {id, action, query}` (shell-only; start, next, previous, stop). On Windows it drives WebView2's own find (`ICoreWebView2Find` from `ICoreWebView2_28`, find options from `ICoreWebView2Environment15`; its built-in find bar suppressed, all matches highlighted) and reports the engine's match count and active match to the shell as `tab-find` events. On macOS, Linux and an older WebView2 Runtime native code evaluates the engine's `window.find` in the tab instead (next and previous work, no count); WKWebView's and WebKitGTK's native find are not wired here because this tree cannot be compiled or run for them on this machine. The query is one typed line (1..=256 characters, no control characters), goes to the engine and is kept nowhere. Tabs gain no command: a tab forwards the existing key name `find` (Ctrl+F) through tab_shortcut.

Frame: FindBar.tsx takes the address field's place in its pane's toolbar while open, so the page does not move and in split view the bar is the searched pane's. "3/17" where the engine counts, "No matches" for none; Enter / F3 next, Shift+Enter / Shift+F3 previous, Esc closes, clears the highlights and gives the page the keyboard. `features.find` (on by default) gates the menu row, the palette entry and the keys. Only what the page has rendered can be found, as in a browser.

Also: this makes `pub mod find` in lib.rs (already in HEAD) resolve again.
2026-09-30 22:34:48 +07:00

144 lines
7 KiB
JavaScript

// wayBetter tab bridge (waybetter-host `bridge.rs` serves this file). Injected into every browser
// tab (top frame only). Tells the shell about SPA URL changes, the favicon and focus and, when the
// app enables them, forwards browser shortcuts pressed while the page has focus and link drags.
// It can only call `tab_report` / `tab_shortcut` / `tab_link_drag`, which affect this tab's own
// address bar, forward a shortcut, or tell the shell a link drag started/entered/ended here.
(() => {
if (window.top !== window || window.__wbTabBridge) return;
window.__wbTabBridge = true;
const options = { shortcuts: true, linkDrags: true }; // waybetter-host: bridge::script() options
const invoke = (cmd, args) => {
const ipc = window.__TAURI_INTERNALS__;
if (ipc && typeof ipc.invoke === 'function') ipc.invoke(cmd, args).catch(() => {});
};
let lastUrl = '';
let lastTitle = '';
let lastIcon = '';
const favicon = () => {
const link = document.querySelector('link[rel~="icon"][href], link[rel="shortcut icon"][href]');
try { return link ? new URL(link.getAttribute('href'), location.href).href : `${location.origin}/favicon.ico`; } catch { return ''; }
};
const report = (extra) => {
const url = location.href;
const title = document.title || '';
const icon = favicon();
if (!extra && url === lastUrl && title === lastTitle && icon === lastIcon) return;
lastUrl = url; lastTitle = title; lastIcon = icon;
invoke('tab_report', { report: { url, title, favicon: icon, ...extra } });
};
let queued = false;
const schedule = () => { if (!queued) { queued = true; setTimeout(() => { queued = false; report(); }, 50); } };
for (const method of ['pushState', 'replaceState']) {
const original = history[method];
history[method] = function (...args) {
const result = original.apply(this, args);
schedule();
return result;
};
}
addEventListener('popstate', schedule);
addEventListener('hashchange', schedule);
addEventListener('focus', () => report({ focused: true }));
addEventListener('pointerdown', () => report({ focused: true }), true);
const watchHead = () => {
if (!document.head) return;
new MutationObserver(schedule).observe(document.head, { childList: true, subtree: true, attributes: true, attributeFilter: ['href'] });
schedule();
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', watchHead, { once: true });
else watchHead();
// Power User Mode: a deck column (the X runtime marks <html data-wb-column>) forwards the deck
// keys; bare keys only while nothing editable has focus.
const typing = () => {
const el = document.activeElement;
return !!el && (el.isContentEditable || /^(input|textarea|select)$/i.test(el.tagName));
};
const deckKey = (event) => {
const mod = event.ctrlKey || event.metaKey;
if (mod && event.shiftKey && !event.altKey && event.code === 'KeyD') return 'deck-toggle';
if (!document.documentElement || !document.documentElement.hasAttribute('data-wb-column')) return null;
if (event.ctrlKey && event.altKey && !event.metaKey && !event.shiftKey && event.code === 'KeyN' && !typing()) return 'deck-compose';
if (event.altKey && !mod && !event.shiftKey) {
const digit = /^Digit([1-9])$/.exec(event.code);
if (digit) return `column-${digit[1]}`;
if (event.key === 'PageUp') return 'column-page-prev';
if (event.key === 'PageDown') return 'column-page-next';
}
if (!mod && !event.altKey && !typing()) {
if (event.key === '[') return 'column-prev';
if (event.key === ']') return 'column-next';
}
return null;
};
const shortcut = (event) => {
const deck = deckKey(event);
if (deck) return deck;
const mod = event.ctrlKey || event.metaKey;
const key = event.key.toLowerCase();
if (mod && !event.altKey) {
if (key === 't' && event.shiftKey) return 'reopen-tab';
if (key === 't') return 'new-tab';
if (key === 'w' && event.shiftKey) return 'open-panel';
if (key === 'w') return 'close-tab';
if (key === 'l') return 'focus-address';
if (key === 'k' && !event.shiftKey) return 'command-palette';
// Find in page is the shell's bar (`tab_find`): this tab only says the keys were pressed.
if (key === 'f' && !event.shiftKey) return 'find';
if (key === 'tab') return event.shiftKey ? 'prev-tab' : 'next-tab';
if (key === 'r') return event.shiftKey ? 'hard-reload' : 'reload';
// Physical key: layouts like Turkish-Q produce other characters on this key.
if (event.code === 'Backslash' || event.code === 'IntlBackslash') return event.shiftKey ? 'split-down' : 'split-right';
// Page zoom is the shell's (`tab_zoom`): this tab only says which key was pressed.
if (key === '=' || key === '+' || event.code === 'NumpadAdd') return 'zoom-in';
if (key === '-' || event.code === 'NumpadSubtract') return 'zoom-out';
if (key === '0' || event.code === 'Numpad0') return 'zoom-reset';
}
if (key === 'f5') return 'reload';
// The app menu, like a browser's: F10, and Alt+F where Alt is not a typing key (not on macOS).
if (key === 'f10' && !mod && !event.altKey && !event.shiftKey) return 'app-menu';
if (event.altKey && !mod && !event.shiftKey && event.code === 'KeyF' && !/Mac/i.test(navigator.platform)) return 'app-menu';
if (event.altKey && !mod && key === 'arrowleft') return 'back';
if (event.altKey && !mod && key === 'arrowright') return 'forward';
return null;
};
// Link drags: the shell hides the tab webviews and shows its split/tab drop zones, so a link
// dragged out of this page (or into it from another app) can be dropped as a new pane.
if (options.linkDrags) {
let localDrag = false;
let lastEnter = 0;
const linkTypes = (types) => {
const list = Array.from(types || []);
return !list.includes('Files') && (list.includes('text/uri-list') || list.includes('text/x-moz-url'));
};
addEventListener('dragstart', (event) => {
localDrag = true;
// Links, and images inside links (sites like X wrap photos in `/status/.../photo/N` anchors).
const target = event.target;
const anchor = target && target.closest ? target.closest('a[href]') : null;
const url = anchor ? String(anchor.href) : '';
if (/^https?:\/\//i.test(url) && url.length <= 4096) invoke('tab_link_drag', { phase: 'start', url });
}, true);
addEventListener('dragend', () => {
localDrag = false;
invoke('tab_link_drag', { phase: 'end' });
}, true);
addEventListener('dragenter', (event) => {
if (localDrag || !event.dataTransfer || !linkTypes(event.dataTransfer.types)) return;
const now = Date.now();
if (now - lastEnter < 500) return;
lastEnter = now;
invoke('tab_link_drag', { phase: 'enter' });
}, true);
}
if (options.shortcuts) addEventListener('keydown', (event) => {
const action = shortcut(event);
if (!action) return;
event.preventDefault();
event.stopImmediatePropagation();
invoke('tab_shortcut', { shortcut: action });
}, true);
})();