Rewild Your Web
web browser dweb
at main 1068 lines 29 kB view raw
1// SPDX-License-Identifier: AGPL-3.0-or-later 2 3import { 4 LitElement, 5 html, 6} from "//shared.localhost:8888/third_party/lit/lit-all.min.js"; 7import "./webview_menu.js"; 8import "./url_bar_overlay.js"; 9import "./context_menu.js"; 10import "./select_control.js"; 11import "./color_picker.js"; 12 13export class WebView extends LitElement { 14 constructor(src, title, attrs = {}) { 15 super(); 16 17 this.src = src; 18 this.title = title; 19 this.favicon = ""; 20 this.canGoBack = false; 21 this.canGoForward = false; 22 this.themeColor = WebView.defaultThemeColor; 23 this.active = false; 24 25 this.iframe = undefined; 26 this.attrs = attrs; 27 this.webviewId = null; // Set by LayoutManager 28 this.menuOpen = false; 29 this.urlBarOpen = false; 30 this.currentUrl = src || ""; 31 32 // Cached screenshot for overview mode 33 this.screenshotUrl = null; 34 35 // Context menu state 36 this.contextMenu = null; 37 38 // Select control state 39 this.selectControl = null; 40 41 // Color picker state 42 this.colorPicker = null; 43 44 // Load status for progress indicator 45 this.loadStatus = "idle"; 46 } 47 48 handleMenuAction(e) { 49 const action = e.detail.action; 50 switch (action) { 51 case "split-horizontal": 52 this.splitHorizontal(); 53 break; 54 case "split-vertical": 55 this.splitVertical(); 56 break; 57 case "reduce-size": 58 this.resizePanel(-10); 59 break; 60 case "increase-size": 61 this.resizePanel(10); 62 break; 63 case "zoom-in": 64 this.zoomIn(); 65 break; 66 case "zoom-out": 67 this.zoomOut(); 68 break; 69 case "zoom-reset": 70 this.zoomReset(); 71 break; 72 } 73 } 74 75 handleMenuClosed() { 76 this.menuOpen = false; 77 } 78 79 resizePanel(delta) { 80 this.dispatchEvent( 81 new CustomEvent("webview-resize-panel", { 82 bubbles: true, 83 detail: { webviewId: this.webviewId, delta }, 84 }), 85 ); 86 } 87 88 zoomIn() { 89 this.ensureIframe(); 90 if (this.iframe) { 91 const currentZoom = this.iframe.getPageZoom(); 92 console.log("Current zoom:", currentZoom); 93 this.iframe.setPageZoom(currentZoom + 0.1); 94 } 95 } 96 97 zoomOut() { 98 this.ensureIframe(); 99 if (this.iframe) { 100 const currentZoom = this.iframe.getPageZoom(); 101 console.log("Current zoom:", currentZoom); 102 this.iframe.setPageZoom(currentZoom - 0.1); 103 } 104 } 105 106 zoomReset() { 107 this.ensureIframe(); 108 if (this.iframe) { 109 this.iframe.setPageZoom(1.0); 110 } 111 } 112 113 connectedCallback() { 114 super.connectedCallback(); 115 } 116 117 disconnectedCallback() { 118 super.disconnectedCallback(); 119 } 120 121 static defaultThemeColor = "gray"; 122 123 static properties = { 124 src: {}, 125 title: { state: true }, 126 favicon: { state: true }, 127 canGoBack: { state: true }, 128 canGoForward: { state: true }, 129 themeColor: { state: true }, 130 active: { state: true }, 131 menuOpen: { state: true }, 132 menuPosition: { state: true }, 133 urlBarOpen: { state: true }, 134 currentUrl: { state: true }, 135 contextMenu: { state: true }, 136 selectControl: { state: true }, 137 colorPicker: { state: true }, 138 loadStatus: { state: true }, 139 }; 140 141 ensureIframe() { 142 if (!this.iframe) { 143 this.iframe = this.shadowRoot.querySelector("iframe"); 144 // Update the screenshot when resizing the web-view 145 const resizeObserver = new ResizeObserver((entries) => { 146 this.captureScreenshot(); 147 }); 148 resizeObserver.observe(this); 149 } 150 } 151 152 // Get the content iframe element (for screenshot capture, etc.) 153 getContentIframe() { 154 this.ensureIframe(); 155 return this.iframe; 156 } 157 158 ontitlechange(event) { 159 if (event.detail) { 160 console.log(`ontitlechange: ${event.detail}`); 161 this.title = event.detail; 162 } 163 } 164 165 onfaviconchange(event) { 166 const blob = event.detail; 167 if (blob) { 168 // Revoke old URL to free memory 169 if (this.favicon && this.favicon.startsWith("blob:")) { 170 URL.revokeObjectURL(this.favicon); 171 } 172 this.favicon = URL.createObjectURL(blob); 173 174 // Notify parent (LayoutManager) about favicon change 175 this.dispatchEvent( 176 new CustomEvent("webview-favicon-change", { 177 bubbles: true, 178 detail: { webviewId: this.webviewId, favicon: this.favicon }, 179 }), 180 ); 181 } 182 } 183 184 onthemecolorchange(event) { 185 this.themeColor = event.detail; 186 } 187 188 onloadstatuschange(event) { 189 console.log("[WebView] Load status changed:", event.detail); 190 this.loadStatus = event.detail; 191 192 // Auto-reset to idle after complete animation finishes 193 if (event.detail === "complete") { 194 setTimeout(() => { 195 this.loadStatus = "idle"; 196 }, 500); 197 } 198 } 199 200 oncontrolshow(event) { 201 const detail = event.detail; 202 console.log("[EmbedderControl] SHOW event received:", detail); 203 204 if (detail.controlType === "select" && detail.selectParameters?.options) { 205 const params = detail.selectParameters; 206 207 // Show the select control 208 this.selectControl = { 209 controlId: detail.controlId, 210 options: params.options, 211 selectedIndex: params.selectedIndex, 212 x: detail.position?.x || 0, 213 y: detail.position?.y || 0, 214 }; 215 } else if (detail.controlType === "color" && detail.colorParameters) { 216 const params = detail.colorParameters; 217 218 // Show the color picker 219 this.colorPicker = { 220 controlId: detail.controlId, 221 currentColor: params.currentColor, 222 x: detail.position?.x || 0, 223 y: detail.position?.y || 0, 224 }; 225 } else if ( 226 detail.controlType === "contextmenu" && 227 detail.contextMenuParameters?.items 228 ) { 229 const params = detail.contextMenuParameters; 230 231 // Map action IDs to icons (matching title bar icons) 232 const actionIcons = { 233 GoBack: "arrow-left", 234 GoForward: "arrow-right", 235 Reload: "rotate-ccw", 236 }; 237 238 // Enrich items with icons 239 const items = params.items.map((item) => ({ 240 ...item, 241 icon: actionIcons[item.id] || item.icon, 242 })); 243 244 // In mobile mode, show the radial menu instead of the regular context menu 245 if (document.body.classList.contains("mobile-mode")) { 246 // Extract navigation state from context menu items 247 const navState = { 248 canGoBack: params.items.some( 249 (item) => item.id === "GoBack" && !item.disabled, 250 ), 251 canGoForward: params.items.some( 252 (item) => item.id === "GoForward" && !item.disabled, 253 ), 254 }; 255 256 // Filter items to remove actions that are part of radial menu 257 const filteredItems = items.filter( 258 (item) => 259 item.id !== "GoBack" && 260 item.id !== "GoForward" && 261 item.id !== "Reload", 262 ); 263 264 // Store pending context menu for later 265 this.pendingContextMenu = { 266 controlId: detail.controlId, 267 items: filteredItems, 268 x: detail.position?.x || 0, 269 y: detail.position?.y || 0, 270 }; 271 272 // Dispatch event to show radial menu at the touch position 273 this.dispatchEvent( 274 new CustomEvent("webview-show-radial-menu", { 275 bubbles: true, 276 composed: true, 277 detail: { 278 x: detail.position?.x || 0, 279 y: detail.position?.y || 0, 280 canGoBack: navState.canGoBack, 281 canGoForward: navState.canGoForward, 282 contextMenu: this.pendingContextMenu, 283 }, 284 }), 285 ); 286 287 // Don't respond yet - radial menu will handle it 288 return; 289 } 290 291 // Show the context menu 292 this.contextMenu = { 293 controlId: detail.controlId, 294 items, 295 x: detail.position?.x || 0, 296 y: detail.position?.y || 0, 297 }; 298 } else if ( 299 detail.controlType === "permission" && 300 detail.permissionParameters 301 ) { 302 const params = detail.permissionParameters; 303 304 // Show the permission prompt 305 this.currentPermission = { 306 controlId: detail.controlId, 307 feature: params.feature, 308 featureName: params.featureName, 309 }; 310 this.requestUpdate(); 311 } else if ( 312 detail.controlType === "inputmethod" && 313 detail.inputMethodParameters 314 ) { 315 const params = detail.inputMethodParameters; 316 317 // Bubble up to parent system window for virtual keyboard 318 this.dispatchEvent( 319 new CustomEvent("webview-inputmethod-show", { 320 bubbles: true, 321 composed: true, 322 detail: { 323 controlId: detail.controlId, 324 inputType: params.inputType || "text", 325 currentValue: params.currentValue || "", 326 placeholder: params.placeholder || "", 327 position: detail.position, 328 }, 329 }), 330 ); 331 } 332 } 333 334 oncontrolhide(event) { 335 console.log("[EmbedderControl] HIDE event received:", event.detail); 336 337 // Close context menu if it's the one being hidden 338 if ( 339 this.contextMenu && 340 this.contextMenu.controlId === event.detail.controlId 341 ) { 342 this.contextMenu = null; 343 } 344 345 // Close select control if it's the one being hidden 346 if ( 347 this.selectControl && 348 this.selectControl.controlId === event.detail.controlId 349 ) { 350 this.selectControl = null; 351 } 352 353 // Close color picker if it's the one being hidden 354 if ( 355 this.colorPicker && 356 this.colorPicker.controlId === event.detail.controlId 357 ) { 358 this.colorPicker = null; 359 } 360 361 // Close permission prompt if it's the one being hidden 362 if ( 363 this.currentPermission && 364 this.currentPermission.controlId === event.detail.controlId 365 ) { 366 this.currentPermission = null; 367 this.requestUpdate(); 368 } 369 370 // Bubble up inputmethod hide event to parent system window 371 // We always send the hide event as it's handled at the system level 372 this.dispatchEvent( 373 new CustomEvent("webview-inputmethod-hide", { 374 bubbles: true, 375 composed: true, 376 detail: { controlId: event.detail.controlId }, 377 }), 378 ); 379 } 380 381 ondialogshow(event) { 382 const detail = event.detail; 383 console.log("[EmbedderDialog] SHOW event received:", detail); 384 const dialogType = detail.dialogType; 385 const controlId = detail.controlId; 386 const message = detail.message; 387 const defaultValue = detail.defaultValue; 388 389 // Store the current dialog info for rendering 390 this.currentDialog = { 391 type: dialogType, 392 controlId, 393 message, 394 defaultValue: defaultValue || "", 395 }; 396 this.requestUpdate(); 397 } 398 399 onnotificationshow(event) { 400 const detail = event.detail; 401 console.log("[Notification] SHOW event received:", detail); 402 403 // Dispatch the notification to the parent (index.js) for global handling 404 // Include the webviewId so the notification center can focus the source webview 405 this.dispatchEvent( 406 new CustomEvent("webview-notification", { 407 bubbles: true, 408 composed: true, 409 detail: { 410 webviewId: this.webviewId, 411 title: detail.title, 412 body: detail.body, 413 tag: detail.tag, 414 iconUrl: detail.iconUrl, 415 }, 416 }), 417 ); 418 } 419 420 handleDialogConfirm(inputValue = null) { 421 this.ensureIframe(); 422 const dialog = this.currentDialog; 423 if (!dialog) { 424 return; 425 } 426 427 console.log("[EmbedderDialog] User confirmed dialog:", dialog.type); 428 429 switch (dialog.type) { 430 case "alert": 431 this.iframe.respondToAlert(dialog.controlId); 432 break; 433 case "confirm": 434 this.iframe.respondToConfirm(dialog.controlId, true); 435 break; 436 case "prompt": 437 this.iframe.respondToPrompt(dialog.controlId, inputValue); 438 break; 439 } 440 441 this.currentDialog = null; 442 this.requestUpdate(); 443 } 444 445 handleDialogCancel() { 446 this.ensureIframe(); 447 const dialog = this.currentDialog; 448 if (!dialog) { 449 return; 450 } 451 452 console.log("[EmbedderDialog] User cancelled dialog:", dialog.type); 453 454 switch (dialog.type) { 455 case "alert": 456 // Alert only has OK, but handle cancel just in case 457 this.iframe.respondToAlert(dialog.controlId); 458 break; 459 case "confirm": 460 this.iframe.respondToConfirm(dialog.controlId, false); 461 break; 462 case "prompt": 463 this.iframe.respondToPrompt(dialog.controlId, null); 464 break; 465 } 466 467 this.currentDialog = null; 468 this.requestUpdate(); 469 } 470 471 handlePermissionAllow() { 472 this.ensureIframe(); 473 const permission = this.currentPermission; 474 if (!permission) { 475 return; 476 } 477 478 console.log( 479 "[EmbedderPermission] User allowed permission:", 480 permission.feature, 481 ); 482 this.iframe.respondToPermissionPrompt(permission.controlId, true); 483 this.currentPermission = null; 484 this.requestUpdate(); 485 } 486 487 handlePermissionDeny() { 488 this.ensureIframe(); 489 const permission = this.currentPermission; 490 if (!permission) { 491 return; 492 } 493 494 console.log( 495 "[EmbedderPermission] User denied permission:", 496 permission.feature, 497 ); 498 this.iframe.respondToPermissionPrompt(permission.controlId, false); 499 this.currentPermission = null; 500 this.requestUpdate(); 501 } 502 503 handleContextMenuAction(e) { 504 const { action, controlId } = e.detail; 505 console.log( 506 "[ContextMenu] Action selected:", 507 action, 508 "Control ID:", 509 controlId, 510 ); 511 512 this.ensureIframe(); 513 514 // Send the action back to the embedded webview for handling 515 // The embedded webview will process the action (GoBack, Copy, Paste, etc.) 516 this.iframe.respondToContextMenu(controlId, action); 517 this.contextMenu = null; 518 } 519 520 handleContextMenuCancel(e) { 521 const { controlId } = e.detail; 522 console.log("[ContextMenu] Menu cancelled, Control ID:", controlId); 523 524 this.ensureIframe(); 525 // Send null action to indicate cancellation 526 this.iframe.respondToContextMenu(controlId, null); 527 this.contextMenu = null; 528 } 529 530 handleContextMenuClosed() { 531 this.contextMenu = null; 532 } 533 534 // Show the pending context menu (called from radial menu's context-menu action) 535 showPendingContextMenu() { 536 if (this.pendingContextMenu) { 537 this.contextMenu = this.pendingContextMenu; 538 this.pendingContextMenu = null; 539 } 540 } 541 542 // Dismiss the pending context menu without showing it 543 dismissPendingContextMenu() { 544 if (this.pendingContextMenu) { 545 this.ensureIframe(); 546 this.iframe.respondToContextMenu(this.pendingContextMenu.controlId, null); 547 this.pendingContextMenu = null; 548 } 549 } 550 551 handleSelectOption(e) { 552 const { controlId, index } = e.detail; 553 console.log( 554 "[SelectControl] Option selected, index:", 555 index, 556 "Control ID:", 557 controlId, 558 ); 559 560 this.ensureIframe(); 561 this.iframe.respondToSelectControl(controlId, index); 562 this.selectControl = null; 563 } 564 565 handleSelectCancel(e) { 566 const { controlId } = e.detail; 567 console.log("[SelectControl] Selection cancelled, Control ID:", controlId); 568 569 this.ensureIframe(); 570 // Send -1 to indicate cancellation (no selection) 571 this.iframe.respondToSelectControl(controlId, -1); 572 this.selectControl = null; 573 } 574 575 handleSelectClosed() { 576 this.selectControl = null; 577 } 578 579 handleColorConfirm(e) { 580 const { controlId, color } = e.detail; 581 console.log( 582 "[ColorPicker] Color confirmed:", 583 color, 584 "Control ID:", 585 controlId, 586 ); 587 588 this.ensureIframe(); 589 this.iframe.respondToColorPicker(controlId, color); 590 this.colorPicker = null; 591 } 592 593 handleColorCancel(e) { 594 const { controlId } = e.detail; 595 console.log("[ColorPicker] Color cancelled, Control ID:", controlId); 596 597 this.ensureIframe(); 598 // Send null to indicate cancellation 599 this.iframe.respondToColorPicker(controlId, null); 600 this.colorPicker = null; 601 } 602 603 handleColorClosed() { 604 this.colorPicker = null; 605 } 606 607 renderDialog() { 608 if (!this.currentDialog) { 609 return ""; 610 } 611 612 const dialog = this.currentDialog; 613 614 if (dialog.type === "alert") { 615 return html` 616 <div class="dialog-overlay" @click=${(e) => e.stopPropagation()}> 617 <div class="dialog-box"> 618 <div class="dialog-message">${dialog.message}</div> 619 <div class="dialog-buttons"> 620 <button 621 class="dialog-button primary" 622 @click=${() => this.handleDialogConfirm()} 623 > 624 OK 625 </button> 626 </div> 627 </div> 628 </div> 629 `; 630 } else if (dialog.type === "confirm") { 631 return html` 632 <div class="dialog-overlay" @click=${(e) => e.stopPropagation()}> 633 <div class="dialog-box"> 634 <div class="dialog-message">${dialog.message}</div> 635 <div class="dialog-buttons"> 636 <button 637 class="dialog-button" 638 @click=${() => this.handleDialogCancel()} 639 > 640 Cancel 641 </button> 642 <button 643 class="dialog-button primary" 644 @click=${() => this.handleDialogConfirm()} 645 > 646 OK 647 </button> 648 </div> 649 </div> 650 </div> 651 `; 652 } else if (dialog.type === "prompt") { 653 return html` 654 <div class="dialog-overlay" @click=${(e) => e.stopPropagation()}> 655 <div class="dialog-box"> 656 <div class="dialog-message">${dialog.message}</div> 657 <input 658 type="text" 659 class="dialog-input" 660 .value=${dialog.defaultValue} 661 @keydown=${(e) => { 662 if (e.key === "Enter") { 663 this.handleDialogConfirm(e.target.value); 664 } else if (e.key === "Escape") { 665 this.handleDialogCancel(); 666 } 667 }} 668 id="dialog-prompt-input" 669 /> 670 <div class="dialog-buttons"> 671 <button 672 class="dialog-button" 673 @click=${() => this.handleDialogCancel()} 674 > 675 Cancel 676 </button> 677 <button 678 class="dialog-button primary" 679 @click=${() => { 680 const input = this.shadowRoot.querySelector( 681 "#dialog-prompt-input", 682 ); 683 this.handleDialogConfirm(input?.value || ""); 684 }} 685 > 686 OK 687 </button> 688 </div> 689 </div> 690 </div> 691 `; 692 } 693 694 return ""; 695 } 696 697 renderColorPicker() { 698 if (!this.colorPicker) { 699 return ""; 700 } 701 702 return html`<color-picker 703 ?open=${this.colorPicker !== null} 704 .currentColor=${this.colorPicker?.currentColor || "#000000"} 705 .x=${this.colorPicker?.x || 0} 706 .y=${this.colorPicker?.y || 0} 707 .controlId=${this.colorPicker?.controlId || ""} 708 @color-confirm=${this.handleColorConfirm} 709 @color-cancel=${this.handleColorCancel} 710 > 711 </color-picker>`; 712 } 713 714 renderPermissionPrompt() { 715 if (!this.currentPermission) { 716 return ""; 717 } 718 719 const permission = this.currentPermission; 720 721 // Permission-specific icons 722 const permissionIcons = { 723 geolocation: "map-pin", 724 camera: "camera", 725 microphone: "mic", 726 notifications: "bell", 727 bluetooth: "bluetooth", 728 }; 729 730 const icon = permissionIcons[permission.feature] || "shield"; 731 732 return html` 733 <div class="dialog-overlay" @click=${(e) => e.stopPropagation()}> 734 <div class="dialog-box permission-prompt"> 735 <div class="permission-icon"> 736 <lucide-icon name="${icon}"></lucide-icon> 737 </div> 738 <div class="permission-title">Permission Request</div> 739 <div class="dialog-message"> 740 This site wants to use your 741 <strong>${permission.featureName}</strong>. 742 </div> 743 <div class="dialog-buttons"> 744 <button 745 class="dialog-button" 746 @click=${() => this.handlePermissionDeny()} 747 > 748 Block 749 </button> 750 <button 751 class="dialog-button primary" 752 @click=${() => this.handlePermissionAllow()} 753 > 754 Allow 755 </button> 756 </div> 757 </div> 758 </div> 759 `; 760 } 761 762 renderUrlBarOverlay() { 763 if (!this.urlBarOpen) { 764 return ""; 765 } 766 767 return html`<url-bar-overlay 768 ?open=${this.urlBarOpen} 769 .url=${this.currentUrl} 770 .onNavigate=${(url) => this.handleNavigate(url)} 771 .onSelectWebView=${(windowId, webviewId) => 772 this.handleSelectWebView(windowId, webviewId)} 773 @close=${this.closeUrlBar} 774 ></url-bar-overlay>`; 775 } 776 777 onurlchange(event) { 778 this.ensureIframe(); 779 this.canGoBack = this.iframe.canGoBack(); 780 this.canGoForward = this.iframe.canGoForward(); 781 this.currentUrl = event.detail; 782 783 // Dispatch navigation state change event for action bar updates 784 this.dispatchEvent( 785 new CustomEvent("navigation-state-changed", { 786 bubbles: true, 787 composed: true, 788 detail: { 789 webviewId: this.webviewId, 790 canGoBack: this.canGoBack, 791 canGoForward: this.canGoForward, 792 url: this.currentUrl, 793 }, 794 }), 795 ); 796 797 // Capture screenshot for overview mode after a short delay 798 // to allow the page to render 799 this.captureScreenshot(); 800 } 801 802 updated(_changedProperties) { 803 if (this.active) { 804 if (this.rafHandle) { 805 cancelAnimationFrame(this.rafHandle); 806 } 807 this.rafHandle = requestAnimationFrame(() => 808 this.captureScreenshot(true), 809 ); 810 } 811 } 812 813 // Capture a screenshot and cache it for overview mode 814 captureScreenshot(immediate = false) { 815 // Debounce: clear any pending capture 816 if (this.screenshotTimeout) { 817 clearTimeout(this.screenshotTimeout); 818 } 819 820 let doCapture = () => { 821 this.getContentIframe() 822 .takeScreenshot() 823 .then((blob) => { 824 if (blob) { 825 // Revoke old URL to free memory 826 if (this.screenshotUrl) { 827 URL.revokeObjectURL(this.screenshotUrl); 828 } 829 this.screenshotUrl = URL.createObjectURL(blob); 830 } 831 }) 832 .catch(() => { 833 // Ignore screenshot errors (e.g., for off-screen webviews) 834 }); 835 }; 836 837 if (immediate) { 838 doCapture(); 839 return; 840 } 841 842 // Delay capture to allow page to render 843 this.screenshotTimeout = setTimeout(() => { 844 doCapture(); 845 }, 400); 846 } 847 848 onfocus() { 849 this.dispatchEvent( 850 new CustomEvent("webview-focus", { 851 bubbles: true, 852 detail: { webviewId: this.webviewId }, 853 }), 854 ); 855 } 856 857 doReload() { 858 this.ensureIframe(); 859 this.iframe.reload(); 860 } 861 862 goBack() { 863 this.ensureIframe(); 864 this.themeColor = WebView.defaultThemeColor; 865 this.iframe.goBack(); 866 } 867 868 goForward() { 869 this.ensureIframe(); 870 this.themeColor = WebView.defaultThemeColor; 871 this.iframe.goForward(); 872 } 873 874 close() { 875 this.dispatchEvent( 876 new CustomEvent("webview-close", { 877 bubbles: true, 878 detail: { webviewId: this.webviewId }, 879 }), 880 ); 881 } 882 883 split(direction) { 884 this.dispatchEvent( 885 new CustomEvent("webview-split", { 886 bubbles: true, 887 detail: { webviewId: this.webviewId, direction }, 888 }), 889 ); 890 } 891 892 splitHorizontal() { 893 this.split("horizontal"); 894 this.menuOpen = false; 895 } 896 897 splitVertical() { 898 this.split("vertical"); 899 this.menuOpen = false; 900 } 901 902 toggleMenu(e) { 903 e.stopPropagation(); 904 if (!this.menuOpen) { 905 // Get the position of the menu button relative to the web-view 906 const buttonRect = e.target.getBoundingClientRect(); 907 const hostRect = this.getBoundingClientRect(); 908 this.menuPosition = { 909 x: buttonRect.right - hostRect.left, 910 y: buttonRect.bottom - hostRect.top, 911 }; 912 } 913 this.menuOpen = !this.menuOpen; 914 } 915 916 closeMenu() { 917 this.menuOpen = false; 918 } 919 920 openUrlBar(e) { 921 e.stopPropagation(); 922 if (this.active) { 923 this.urlBarOpen = true; 924 } 925 } 926 927 closeUrlBar() { 928 this.urlBarOpen = false; 929 } 930 931 handleNavigate(url) { 932 this.ensureIframe(); 933 this.iframe.load(url); 934 this.urlBarOpen = false; 935 } 936 937 handleSelectWebView(windowId, webviewId) { 938 // Use BroadcastChannel to tell the target window to select the webview 939 const channel = new BroadcastChannel("servo-search"); 940 channel.postMessage({ 941 type: "selectWebView", 942 id: Date.now(), 943 targetWindowId: windowId, 944 webviewId: webviewId, 945 }); 946 channel.close(); 947 this.urlBarOpen = false; 948 } 949 950 render() { 951 // Only render adopt-* attributes if they have values 952 const adoptAttrs = {}; 953 if (this.attrs["adopt-webview-id"]) { 954 adoptAttrs["adopt-webview-id"] = this.attrs["adopt-webview-id"]; 955 } 956 if (this.attrs["adopt-browsing-context-id"]) { 957 adoptAttrs["adopt-browsing-context-id"] = 958 this.attrs["adopt-browsing-context-id"]; 959 } 960 if (this.attrs["adopt-pipeline-id"]) { 961 adoptAttrs["adopt-pipeline-id"] = this.attrs["adopt-pipeline-id"]; 962 } 963 this.attrs = {}; 964 965 return html` 966 <link rel="stylesheet" href="web_view.css" /> 967 <div 968 class="wrapper ${this.active ? "active" : ""}" 969 @click=${this.onfocus} 970 > 971 <div 972 class="bar ${this.active ? "" : "hidden"}" 973 style="background-color: ${this 974 .themeColor}; color: contrast-color(${this.themeColor});" 975 > 976 <div class="load-progress load-${this.loadStatus}"></div> 977 <img src="${this.favicon}" class="icon" /> 978 <lucide-icon 979 name="arrow-left" 980 class="icon enabled-${this.canGoBack}" 981 color="contrast-color(${this.themeColor})" 982 @click="${this.goBack}" 983 ></lucide-icon> 984 <lucide-icon 985 name="arrow-right" 986 class="icon enabled-${this.canGoForward}" 987 color="contrast-color(${this.themeColor})" 988 @click="${this.goForward}" 989 ></lucide-icon> 990 <lucide-icon 991 name="rotate-ccw" 992 class="icon" 993 color="contrast-color(${this.themeColor})" 994 @click="${this.doReload}" 995 ></lucide-icon> 996 <span class="title" @click=${this.openUrlBar}>${this.title}</span> 997 <div class="menu-container"> 998 <lucide-icon 999 @click=${this.toggleMenu} 1000 name="ellipsis-vertical" 1001 class="icon" 1002 color="contrast-color(${this.themeColor})" 1003 ></lucide-icon> 1004 </div> 1005 <lucide-icon 1006 @click=${this.close} 1007 name="x" 1008 class="icon" 1009 color="contrast-color(${this.themeColor})" 1010 ></lucide-icon> 1011 </div> 1012 <webview-menu 1013 ?open=${this.menuOpen} 1014 .x=${this.menuPosition?.x || 0} 1015 .y=${this.menuPosition?.y || 0} 1016 @menu-action=${this.handleMenuAction} 1017 @menu-closed=${this.handleMenuClosed} 1018 ></webview-menu> 1019 <context-menu 1020 ?open=${this.contextMenu !== null} 1021 .items=${this.contextMenu?.items || []} 1022 .x=${this.contextMenu?.x || 0} 1023 .y=${this.contextMenu?.y || 0} 1024 .controlId=${this.contextMenu?.controlId || ""} 1025 @menu-action=${this.handleContextMenuAction} 1026 @menu-cancel=${this.handleContextMenuCancel} 1027 @menu-closed=${this.handleContextMenuClosed} 1028 ></context-menu> 1029 ${this.renderUrlBarOverlay()} 1030 <div class="iframe-container"> 1031 <iframe 1032 embed 1033 adopt-webview-id=${adoptAttrs["adopt-webview-id"]} 1034 adopt-browsing-context-id=${adoptAttrs["adopt-browsing-context-id"]} 1035 adopt-pipeline-id=${adoptAttrs["adopt-pipeline-id"]} 1036 src="${this.src}" 1037 @embedtitlechange=${this.ontitlechange} 1038 @embedfaviconchange=${this.onfaviconchange} 1039 @embedthemecolorchange=${this.onthemecolorchange} 1040 @embedurlchange=${this.onurlchange} 1041 @embedinputreceived=${this.onfocus} 1042 @embedcontrolshow=${this.oncontrolshow} 1043 @embedcontrolhide=${this.oncontrolhide} 1044 @embeddialogshow=${this.ondialogshow} 1045 @embednotificationshow=${this.onnotificationshow} 1046 @embedloadstatuschange=${this.onloadstatuschange} 1047 ></iframe> 1048 ${this.renderDialog()} ${this.renderPermissionPrompt()} 1049 <select-control 1050 ?open=${this.selectControl !== null} 1051 .options=${this.selectControl?.options || []} 1052 .selectedIndex=${this.selectControl?.selectedIndex ?? -1} 1053 .x=${this.selectControl?.x || 0} 1054 .y=${this.selectControl?.y || 0} 1055 .controlId=${this.selectControl?.controlId || ""} 1056 @select-option=${this.handleSelectOption} 1057 @select-cancel=${this.handleSelectCancel} 1058 @menu-closed=${this.handleSelectClosed} 1059 ></select-control> 1060 1061 ${this.renderColorPicker()} 1062 </div> 1063 </div> 1064 `; 1065 } 1066} 1067 1068customElements.define("web-view", WebView);