Rewild Your Web
web
browser
dweb
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);