Rewild Your Web
web browser dweb
at main 829 lines 24 kB view raw
1// SPDX-License-Identifier: AGPL-3.0-or-later 2 3import { WebView } from "./web_view.js"; 4import { LayoutManager } from "./layout_manager.js"; 5import { MobileLayoutManager } from "./mobile_layout_manager.js"; 6import "./system_menu.js"; 7import "./mobile_action_bar.js"; 8import "./mobile_notification_sheet.js"; 9import "./mobile_overview.js"; 10import "./mobile_radial_menu.js"; 11 12let NEW_FRAME_DEFAULT_URL = navigator.servo.getStringPreference( 13 "browserhtml.start_url", 14); 15 16// Unique ID for this browser window (used for cross-window communication) 17const windowId = `browser-${Date.now()}-${Math.random().toString(36).slice(2)}`; 18 19navigator.embedder.addEventListener("servoerror", (event) => { 20 console.error(`[Servo] ${event.type} ${event.detail}`); 21}); 22 23// ============================================================================ 24// Global State and Event Handlers 25// ============================================================================ 26 27let layoutManager = null; 28let systemMenu = null; 29let notificationPanel = null; 30let mobileActionBar = null; 31let mobileNotificationSheet = null; 32let mobileOverview = null; 33let mobileRadialMenu = null; 34let isMobileMode = false; 35 36// Detect if we should use mobile mode 37function detectMobileMode() { 38 // Check if mobile simulation is forced via preference 39 const mobileSimulation = navigator.servo.getBoolPreference( 40 "browserhtml.mobile_simulation", 41 ); 42 if (mobileSimulation) { 43 return true; 44 } 45 46 // maxTouchPoints is not yet supported on Servo 47 // const isTouchDevice = navigator.maxTouchPoints > 0; 48 const isNarrowScreen = window.matchMedia("(max-width: 768px)").matches; 49 return isNarrowScreen; 50} 51 52// Create the appropriate layout manager based on device type 53function createLayoutManager(rootElement, webViewBuilder) { 54 isMobileMode = detectMobileMode(); 55 56 if (isMobileMode) { 57 document.body.classList.add("mobile-mode"); 58 return new MobileLayoutManager(rootElement, webViewBuilder); 59 } else { 60 document.body.classList.remove("mobile-mode"); 61 return new LayoutManager(rootElement, webViewBuilder); 62 } 63} 64 65navigator.embedder.addEventListener("preferencechanged", (event) => { 66 console.log( 67 `[System PrefChanged] ${event.detail.name} : ${event.detail.value}`, 68 ); 69 if (event.detail.name == "browserhtml.start_url") { 70 NEW_FRAME_DEFAULT_URL = event.detail.value; 71 } 72}); 73 74// Notification state 75const MAX_NOTIFICATIONS = 50; 76let notifications = []; 77 78function updateNotificationBadge() { 79 const badge = document.getElementById("notifications-badge"); 80 if (!badge) { 81 return; 82 } 83 84 const count = notifications.length; 85 if (count > 0) { 86 badge.textContent = 87 count > MAX_NOTIFICATIONS ? `${MAX_NOTIFICATIONS}+` : count; 88 badge.classList.remove("hidden"); 89 } else { 90 badge.classList.add("hidden"); 91 } 92} 93 94function addNotification(notification) { 95 // Add timestamp if not present 96 if (!notification.timestamp) { 97 notification.timestamp = Date.now(); 98 } 99 100 // Generate a unique ID if not present 101 if (!notification.id) { 102 notification.id = `notif-${Date.now()}-${Math.random() 103 .toString(36) 104 .slice(2)}`; 105 } 106 107 // Check for same-tag replacement 108 if (notification.tag) { 109 const existingIndex = notifications.findIndex( 110 (n) => n.tag === notification.tag, 111 ); 112 if (existingIndex !== -1) { 113 // Replace existing notification with same tag 114 notifications[existingIndex] = notification; 115 } else { 116 // Add to front 117 notifications.unshift(notification); 118 } 119 } else { 120 // No tag, just add to front 121 notifications.unshift(notification); 122 } 123 124 // Enforce max limit (FIFO) 125 if (notifications.length > MAX_NOTIFICATIONS) { 126 notifications = notifications.slice(0, MAX_NOTIFICATIONS); 127 } 128 129 // Update panel and badge 130 if (notificationPanel) { 131 notificationPanel.notifications = [...notifications]; 132 } 133 updateNotificationBadge(); 134} 135 136function dismissNotification(notification) { 137 const index = notifications.findIndex((n) => n.id === notification.id); 138 if (index !== -1) { 139 notifications.splice(index, 1); 140 if (notificationPanel) { 141 notificationPanel.notifications = [...notifications]; 142 } 143 updateNotificationBadge(); 144 } 145} 146 147function clearAllNotifications() { 148 notifications = []; 149 if (notificationPanel) { 150 notificationPanel.notifications = []; 151 } 152 updateNotificationBadge(); 153} 154 155window.servo = { 156 /** 157 * Called by Servo when an embedded webview has opened a new embedded webview 158 * via window.open() or target="_blank" link, and the constellation has already 159 * created the webview. The iframe should adopt the pre-created webview. 160 */ 161 adoptNewWebView: function (url, webviewId, browsingContextId, pipelineId) { 162 console.log( 163 "servo.adoptNewWebView:", 164 url, 165 webviewId, 166 browsingContextId, 167 pipelineId, 168 ); 169 170 const attrs = { 171 "adopt-webview-id": webviewId, 172 "adopt-browsing-context-id": browsingContextId, 173 "adopt-pipeline-id": pipelineId, 174 }; 175 try { 176 const webView = new WebView(url, "", attrs); 177 layoutManager.addWebView(webView); 178 } catch (e) { 179 console.error(e); 180 } 181 }, 182 183 openKeyboard: function (inputType, currentValue, position) { 184 console.log(`servo.openKeyboard: ${inputType} ${currentValue} ${position}`); 185 openVirtualKeyboard({ 186 inputType, 187 currentValue, 188 placeholder: "", 189 position, 190 }); 191 }, 192 193 closeEmbedderControl: function (controlId) { 194 console.log(`servo.closeEmbedderControl ${controlId}`); 195 // TODO: proper control tracking (add controlId to openKeyboard). 196 closeVirtualKeyboard(); 197 }, 198}; 199 200function createNewView() { 201 const webView = new WebView(NEW_FRAME_DEFAULT_URL, "", {}); 202 layoutManager.addWebView(webView); 203} 204 205function switchToHomescreen() { 206 if (layoutManager.homescreenWebviewId) { 207 layoutManager.setActiveWebView(layoutManager.homescreenWebviewId); 208 } 209} 210 211function openSettingsView() { 212 const settingsView = new WebView( 213 "http://settings.localhost:8888/index.html", 214 "Settings", 215 {}, 216 ); 217 layoutManager.addWebView(settingsView); 218} 219 220// Update the mobile tab overview with current tabs 221function updateTabOverviewData() { 222 if (!mobileOverview || !layoutManager) { 223 return; 224 } 225 226 // Use getOverviewTabs() if available (excludes homescreen in mobile mode) 227 let tabs; 228 if (layoutManager.getOverviewTabs) { 229 tabs = layoutManager.getOverviewTabs(); 230 } else { 231 tabs = []; 232 for (const [id, entry] of layoutManager.webviews) { 233 tabs.push({ 234 id: id, 235 title: entry.webview.title || "Untitled", 236 favicon: entry.webview.favicon || "", 237 screenshotUrl: entry.webview.screenshotUrl || null, 238 }); 239 } 240 } 241 242 mobileOverview.tabs = tabs; 243 mobileOverview.activeTabId = layoutManager.activeWebviewId; 244 245 // Also update notification sheet tab count 246 if (mobileNotificationSheet) { 247 mobileNotificationSheet.tabCount = tabs.length; 248 } 249} 250 251// Handle radial menu actions 252function handleRadialMenuAction(detail) { 253 const { action, originX, originY } = detail; 254 255 switch (action) { 256 case "new-view": 257 createNewView(); 258 break; 259 case "home": 260 switchToHomescreen(); 261 break; 262 case "back": 263 layoutManager.goBack(); 264 break; 265 case "forward": 266 layoutManager.goForward(); 267 break; 268 case "reload": 269 layoutManager.reload(); 270 break; 271 case "close-view": 272 if (layoutManager.activeWebviewId) { 273 layoutManager.removeWebView(layoutManager.activeWebviewId); 274 } 275 break; 276 case "overview": 277 layoutManager.showOverview(); 278 break; 279 case "settings": 280 openSettingsView(); 281 break; 282 case "context-menu": 283 // Show the full context menu from the radial menu 284 const entry = layoutManager.getActiveEntry(); 285 if (entry?.webview?.showPendingContextMenu) { 286 entry.webview.showPendingContextMenu(); 287 } 288 break; 289 default: 290 console.log(`[RadialMenu] Unsupported action: ${action}`); 291 } 292} 293 294// Global keyboard shortcuts 295 296// New tab 297Mousetrap.bindGlobal("mod+t", (e) => { 298 createNewView(); 299 return false; 300}); 301 302// New OS Window 303Mousetrap.bindGlobal("mod+n", (e) => { 304 console.log("Requesting new OS window"); 305 navigator.embedder.openNewOSWindow("http://system.localhost:8888/index.html"); 306 return false; 307}); 308 309Mousetrap.bindGlobal("mod+f", (e) => { 310 console.log("Requesting new floating search window"); 311 navigator.embedder.openNewOSWindow( 312 "http://system.localhost:8888/search.html", 313 "notitle,ontop", 314 ); 315 return false; 316}); 317 318// Close current tab 319Mousetrap.bindGlobal("mod+w", (e) => { 320 if (layoutManager.activeWebviewId) { 321 layoutManager.removeWebView(layoutManager.activeWebviewId); 322 } 323 return false; 324}); 325 326// Close current OS Window 327Mousetrap.bindGlobal("mod+shift+w", (e) => { 328 console.log("Closing current OS window"); 329 navigator.embedder.closeCurrentOSWindow(); 330 return false; 331}); 332 333// Exit application 334Mousetrap.bindGlobal("mod+q", (e) => { 335 console.log("Bye bye"); 336 navigator.embedder.exit(); 337 return false; 338}); 339 340// Whole system UI reload 341Mousetrap.bindGlobal("mod+shift+r", (e) => { 342 window.location.reload(); 343 return false; 344}); 345 346// Navigate to next panel (Cmd+] or Ctrl+Tab) 347Mousetrap.bindGlobal(["mod+]", "ctrl+tab"], (e) => { 348 layoutManager.nextPanel(); 349 return false; 350}); 351 352// Navigate to previous panel (Cmd+[ or Ctrl+Shift+Tab) 353Mousetrap.bindGlobal(["mod+[", "ctrl+shift+tab"], (e) => { 354 layoutManager.prevPanel(); 355 return false; 356}); 357 358// Toggle overview mode (Cmd+E or Ctrl+E) 359Mousetrap.bindGlobal("mod+e", (e) => { 360 layoutManager.toggleOverview(); 361 return false; 362}); 363 364// Exit overview mode or close system menu with Escape 365Mousetrap.bindGlobal("escape", (e) => { 366 if (mobileRadialMenu && mobileRadialMenu.open) { 367 mobileRadialMenu.hide(); 368 return false; 369 } 370 if (mobileOverview && mobileOverview.open) { 371 mobileOverview.open = false; 372 return false; 373 } 374 if (mobileNotificationSheet && mobileNotificationSheet.open) { 375 mobileNotificationSheet.open = false; 376 return false; 377 } 378 if (mobileActionBar && mobileActionBar.open) { 379 mobileActionBar.hide(); 380 return false; 381 } 382 if (notificationPanel && notificationPanel.open) { 383 notificationPanel.open = false; 384 return false; 385 } 386 if (systemMenu && systemMenu.open) { 387 systemMenu.open = false; 388 return false; 389 } 390 if (layoutManager.overviewMode) { 391 layoutManager.hideOverview(); 392 return false; 393 } 394}); 395 396// Open URL bar on active webview (Cmd+L or Ctrl+L) 397Mousetrap.bindGlobal("mod+l", (e) => { 398 if (layoutManager.activeWebviewId) { 399 const entry = layoutManager.webviews.get(layoutManager.activeWebviewId); 400 if (entry) { 401 entry.webview.openUrlBar(e); 402 } 403 } 404 return false; 405}); 406 407async function openVirtualKeyboard(detail) { 408 // Check if virtual keyboard is enabled. 409 if ( 410 !navigator.servo.getBoolPreference( 411 "browserhtml.ime_virtual_keyboard_enabled", 412 ) 413 ) { 414 return; 415 } 416 417 // Load the keyboard if needed. 418 let keyboardFrame = document.getElementById("keyboard-frame"); 419 if (!keyboardFrame) { 420 keyboardFrame = document.createElement("iframe"); 421 keyboardFrame.setAttribute("embed", true); 422 keyboardFrame.setAttribute("hidefocus", true); 423 keyboardFrame.setAttribute("id", "keyboard-frame"); 424 keyboardFrame.setAttribute("src", "//keyboard.localhost:8888/index.html"); 425 document.getElementById("footer").append(keyboardFrame); 426 // Wait for the frame to be loaded. 427 let loaded = new Promise((resolve) => { 428 keyboardFrame.addEventListener("embedloadstatuschange", (event) => { 429 if (event.detail == "complete") { 430 resolve(); 431 } 432 }); 433 }); 434 await loaded; 435 } 436 437 console.log("[VirtualKeyboard] Show event received:", detail); 438 const footer = document.getElementById("footer"); 439 footer.classList.add("visible"); 440 document.body.classList.add("keyboard-open"); 441 442 // Send input context to keyboard iframe 443 444 if (keyboardFrame.contentWindow) { 445 keyboardFrame.contentWindow.postMessage( 446 { 447 type: "show", 448 inputType: detail.inputType, 449 currentValue: detail.currentValue, 450 placeholder: detail.placeholder, 451 }, 452 "*", 453 ); 454 } 455} 456 457function closeVirtualKeyboard() { 458 if ( 459 !navigator.servo.getBoolPreference( 460 "browserhtml.ime_virtual_keyboard_enabled", 461 ) 462 ) { 463 return; 464 } 465 466 const footer = document.getElementById("footer"); 467 footer.classList.remove("visible"); 468 document.body.classList.remove("keyboard-open"); 469 470 // Notify keyboard iframe 471 const keyboardFrame = document.getElementById("keyboard-frame"); 472 if (keyboardFrame.contentWindow) { 473 keyboardFrame.contentWindow.postMessage({ type: "hide" }, "*"); 474 } 475} 476 477// Initialize on DOM ready 478document.addEventListener("DOMContentLoaded", () => { 479 layoutManager = createLayoutManager(document.getElementById("root"), () => { 480 return new WebView(NEW_FRAME_DEFAULT_URL, "", {}); 481 }); 482 483 // Setup mobile action bar if in mobile mode 484 if (isMobileMode) { 485 // Create homescreen webview first 486 const homescreenUrl = navigator.servo.getStringPreference( 487 "browserhtml.homescreen_url", 488 ); 489 const homescreen = new WebView(homescreenUrl, "Home", {}); 490 layoutManager.addWebView(homescreen); 491 layoutManager.setHomescreen(homescreen.webviewId); 492 493 // Create mobile action bar 494 mobileActionBar = document.createElement("mobile-action-bar"); 495 document.body.appendChild(mobileActionBar); 496 mobileActionBar.setLayoutManager(layoutManager); 497 layoutManager.setActionBar(mobileActionBar); 498 499 // Handle new tab action from action bar 500 mobileActionBar.addEventListener("action-new-tab", () => { 501 createNewView(); 502 }); 503 504 // Handle home action from action bar 505 mobileActionBar.addEventListener("action-home", () => { 506 switchToHomescreen(); 507 }); 508 509 // Create mobile notification sheet 510 mobileNotificationSheet = document.createElement( 511 "mobile-notification-sheet", 512 ); 513 document.body.appendChild(mobileNotificationSheet); 514 mobileNotificationSheet.tabCount = layoutManager.getTabCount(); 515 516 mobileNotificationSheet.addEventListener("notification-click", (e) => { 517 const notification = e.detail.notification; 518 if (notification.webviewId && layoutManager.webviews) { 519 for (const [id, entry] of layoutManager.webviews) { 520 if (id.toString() === notification.webviewId) { 521 layoutManager.setActiveWebView(id); 522 break; 523 } 524 } 525 } 526 dismissNotification(notification); 527 mobileNotificationSheet.open = false; 528 }); 529 530 mobileNotificationSheet.addEventListener("notification-dismiss", (e) => { 531 dismissNotification(e.detail.notification); 532 mobileNotificationSheet.notifications = [...notifications]; 533 }); 534 535 mobileNotificationSheet.addEventListener("notification-clear-all", () => { 536 clearAllNotifications(); 537 mobileNotificationSheet.notifications = []; 538 }); 539 540 mobileNotificationSheet.addEventListener("sheet-closed", () => { 541 mobileNotificationSheet.open = false; 542 }); 543 544 // Create mobile tab overview 545 mobileOverview = document.createElement("mobile-overview"); 546 document.body.appendChild(mobileOverview); 547 548 mobileOverview.addEventListener("tab-select", (e) => { 549 layoutManager.setActiveWebView(e.detail.tabId); 550 }); 551 552 mobileOverview.addEventListener("tab-close", (e) => { 553 layoutManager.removeWebView(e.detail.tabId); 554 updateTabOverviewData(); 555 }); 556 557 mobileOverview.addEventListener("tab-new", () => { 558 createNewView(); 559 mobileOverview.open = false; 560 }); 561 562 mobileOverview.addEventListener("tab-home", () => { 563 switchToHomescreen(); 564 mobileOverview.open = false; 565 }); 566 567 mobileOverview.addEventListener("overview-close", () => { 568 mobileOverview.open = false; 569 }); 570 571 // Create mobile radial menu 572 mobileRadialMenu = document.createElement("mobile-radial-menu"); 573 document.body.appendChild(mobileRadialMenu); 574 575 // Radial menu is now triggered via contextmenu event from webviews, 576 // not via the gesture handler's long-press detection. 577 578 mobileRadialMenu.addEventListener("radial-action", (e) => { 579 handleRadialMenuAction(e.detail); 580 }); 581 582 // Handle radial menu dismiss (close without action) 583 mobileRadialMenu.addEventListener("radial-dismiss", () => { 584 // Dismiss pending context menu on active webview 585 const entry = layoutManager.getActiveEntry(); 586 if (entry?.webview?.dismissPendingContextMenu) { 587 entry.webview.dismissPendingContextMenu(); 588 } 589 }); 590 591 // Override layout manager's showOverview to use mobile tab overview 592 layoutManager.showOverview = function () { 593 updateTabOverviewData(); 594 mobileOverview.open = true; 595 }; 596 597 layoutManager.hideOverview = function () { 598 mobileOverview.open = false; 599 }; 600 } 601 602 // Desktop-only header handlers 603 const headerSpan = document.querySelector("header span"); 604 if (headerSpan) { 605 headerSpan.onmousedown = (e) => { 606 e.preventDefault(); 607 navigator.embedder.startWindowDrag(); 608 }; 609 } 610 611 const headerResize = document.querySelector("header .resize"); 612 if (headerResize) { 613 headerResize.onmousedown = (e) => { 614 e.preventDefault(); 615 navigator.embedder.startWindowResize(); 616 }; 617 } 618 619 const headerClose = document.querySelector("header .close"); 620 if (headerClose) { 621 headerClose.onmousedown = (e) => { 622 e.preventDefault(); 623 navigator.embedder.closeCurrentOSWindow(); 624 }; 625 } 626 627 const plusIcon = document.getElementById("plus-icon"); 628 if (plusIcon) { 629 plusIcon.onclick = () => { 630 createNewView(); 631 }; 632 } 633 634 // Setup system menu (desktop only) 635 if (!isMobileMode) { 636 systemMenu = document.createElement("system-menu"); 637 document.body.appendChild(systemMenu); 638 639 const menuIcon = document.getElementById("menu-icon"); 640 if (menuIcon) { 641 menuIcon.onclick = () => { 642 systemMenu.open = !systemMenu.open; 643 }; 644 } 645 646 systemMenu.addEventListener("menu-action", (e) => { 647 switch (e.detail.action) { 648 case "new-tab": 649 createNewView(); 650 break; 651 case "new-window": 652 navigator.embedder.openNewOSWindow( 653 "http://system.localhost:8888/index.html", 654 ); 655 break; 656 case "new-search": 657 navigator.embedder.openNewOSWindow( 658 "http://system.localhost:8888/search.html", 659 "notitle,ontop", 660 ); 661 break; 662 case "overview": 663 layoutManager.toggleOverview(); 664 break; 665 case "settings": 666 openSettingsView(); 667 break; 668 case "reload-ui": 669 window.location.reload(); 670 break; 671 case "quit": 672 navigator.embedder.exit(); 673 break; 674 } 675 }); 676 } 677 678 // Setup notification panel 679 notificationPanel = document.getElementById("notification-panel"); 680 681 const notificationsIconContainer = document.getElementById( 682 "notifications-icon-container", 683 ); 684 if (notificationsIconContainer) { 685 notificationsIconContainer.onclick = () => { 686 notificationPanel.open = !notificationPanel.open; 687 }; 688 } 689 690 // Listen for mobile notification show event (from edge gesture) 691 document.addEventListener("mobile-show-notifications", () => { 692 if (isMobileMode && mobileNotificationSheet) { 693 mobileNotificationSheet.notifications = [...notifications]; 694 mobileNotificationSheet.tabCount = layoutManager.getTabCount(); 695 mobileNotificationSheet.open = true; 696 } else if (notificationPanel) { 697 notificationPanel.open = true; 698 } 699 }); 700 701 notificationPanel.addEventListener("notification-click", (e) => { 702 const notification = e.detail.notification; 703 704 // Focus the source webview if possible 705 if (notification.webviewId && layoutManager.webviews) { 706 // Try to find the webview by its ID 707 for (const [id, entry] of layoutManager.webviews) { 708 if (id.toString() === notification.webviewId) { 709 layoutManager.setActiveWebView(id); 710 layoutManager.scrollToPanel(entry.panelIndex); 711 break; 712 } 713 } 714 } 715 716 // Dismiss the notification (no actions supported for now) 717 dismissNotification(notification); 718 719 // Close the panel 720 notificationPanel.open = false; 721 }); 722 723 notificationPanel.addEventListener("notification-dismiss", (e) => { 724 dismissNotification(e.detail.notification); 725 }); 726 727 notificationPanel.addEventListener("notification-clear-all", () => { 728 clearAllNotifications(); 729 }); 730 731 notificationPanel.addEventListener("panel-closed", () => { 732 notificationPanel.open = false; 733 }); 734 735 // Listen for notifications from webviews 736 document 737 .getElementById("root") 738 .addEventListener("webview-notification", (e) => { 739 console.log("[Notification] Received from webview:", e.detail); 740 addNotification({ 741 webviewId: e.detail.webviewId?.toString(), 742 title: e.detail.title, 743 body: e.detail.body, 744 tag: e.detail.tag, 745 iconUrl: e.detail.iconUrl, 746 }); 747 }); 748 749 // Listen for virtual keyboard show/hide events from webviews 750 document 751 .getElementById("root") 752 .addEventListener("webview-inputmethod-show", (event) => { 753 openVirtualKeyboard(event.detail); 754 }); 755 756 document 757 .getElementById("root") 758 .addEventListener("webview-inputmethod-hide", () => { 759 console.log("[Keyboard] Hide event received"); 760 closeVirtualKeyboard(); 761 }); 762 763 // Listen for radial menu show events from webviews (mobile mode) 764 document 765 .getElementById("root") 766 .addEventListener("webview-show-radial-menu", (e) => { 767 if (isMobileMode && mobileRadialMenu) { 768 console.log("[RadialMenu] Show event received from webview:", e.detail); 769 mobileRadialMenu.canGoBack = e.detail.canGoBack; 770 mobileRadialMenu.canGoForward = e.detail.canGoForward; 771 mobileRadialMenu.isHomescreen = 772 layoutManager.activeWebviewId === layoutManager.homescreenWebviewId; 773 mobileRadialMenu.show(e.detail.x, e.detail.y, e.detail.contextMenu); 774 } 775 }); 776 777 const params = new URLSearchParams(window.location.search); 778 const openValue = params.get("open"); 779 if (openValue) { 780 const webView = new WebView(openValue, "", {}); 781 layoutManager.addWebView(webView); 782 } else if (!isMobileMode) { 783 // In mobile mode, homescreen is already created as the initial view 784 createNewView(); 785 } 786 787 // BroadcastChannel for receiving URLs from search window 788 const searchChannel = new BroadcastChannel("servo-search"); 789 searchChannel.onmessage = (e) => { 790 if (e.data.type === "discover") { 791 // Respond to discovery with our window ID 792 searchChannel.postMessage({ type: "available", windowId: windowId }); 793 } else if ( 794 e.data.type === "openUrl" && 795 e.data.targetWindowId === windowId 796 ) { 797 // This message is targeted to us - handle it 798 searchChannel.postMessage({ type: "ack", id: e.data.id }); 799 const webView = new WebView(e.data.url, "", {}); 800 layoutManager.addWebView(webView); 801 } else if (e.data.type === "listWebViews") { 802 // Respond with list of our web-views 803 const webviews = []; 804 for (const [webviewId, entry] of layoutManager.webviews) { 805 webviews.push({ 806 webviewId: webviewId, 807 title: entry.webview.title || "", 808 url: entry.webview.url || "", 809 }); 810 } 811 searchChannel.postMessage({ 812 type: "webviewList", 813 windowId: windowId, 814 webviews: webviews, 815 }); 816 } else if ( 817 e.data.type === "selectWebView" && 818 e.data.targetWindowId === windowId 819 ) { 820 // Select and focus the specified web-view 821 searchChannel.postMessage({ type: "ack", id: e.data.id }); 822 layoutManager.setActiveWebView(e.data.webviewId); 823 const entry = layoutManager.webviews.get(e.data.webviewId); 824 if (entry) { 825 layoutManager.scrollToPanel(entry.panelIndex); 826 } 827 } 828 }; 829});