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 // Capture a screenshot and cache it for overview mode
803 captureScreenshot() {
804 // Debounce: clear any pending capture
805 if (this.screenshotTimeout) {
806 clearTimeout(this.screenshotTimeout);
807 }
808
809 // Delay capture to allow page to render
810 this.screenshotTimeout = setTimeout(() => {
811 this.getContentIframe()
812 .takeScreenshot()
813 .then((blob) => {
814 if (blob) {
815 // Revoke old URL to free memory
816 if (this.screenshotUrl) {
817 URL.revokeObjectURL(this.screenshotUrl);
818 }
819 this.screenshotUrl = URL.createObjectURL(blob);
820 }
821 })
822 .catch(() => {
823 // Ignore screenshot errors (e.g., for off-screen webviews)
824 });
825 }, 500);
826 }
827
828 onfocus() {
829 this.dispatchEvent(
830 new CustomEvent("webview-focus", {
831 bubbles: true,
832 detail: { webviewId: this.webviewId },
833 }),
834 );
835 }
836
837 doReload() {
838 this.ensureIframe();
839 this.iframe.reload();
840 }
841
842 goBack() {
843 this.ensureIframe();
844 this.themeColor = WebView.defaultThemeColor;
845 this.iframe.goBack();
846 }
847
848 goForward() {
849 this.ensureIframe();
850 this.themeColor = WebView.defaultThemeColor;
851 this.iframe.goForward();
852 }
853
854 close() {
855 this.dispatchEvent(
856 new CustomEvent("webview-close", {
857 bubbles: true,
858 detail: { webviewId: this.webviewId },
859 }),
860 );
861 }
862
863 split(direction) {
864 this.dispatchEvent(
865 new CustomEvent("webview-split", {
866 bubbles: true,
867 detail: { webviewId: this.webviewId, direction },
868 }),
869 );
870 }
871
872 splitHorizontal() {
873 this.split("horizontal");
874 this.menuOpen = false;
875 }
876
877 splitVertical() {
878 this.split("vertical");
879 this.menuOpen = false;
880 }
881
882 toggleMenu(e) {
883 e.stopPropagation();
884 if (!this.menuOpen) {
885 // Get the position of the menu button relative to the web-view
886 const buttonRect = e.target.getBoundingClientRect();
887 const hostRect = this.getBoundingClientRect();
888 this.menuPosition = {
889 x: buttonRect.right - hostRect.left,
890 y: buttonRect.bottom - hostRect.top,
891 };
892 }
893 this.menuOpen = !this.menuOpen;
894 }
895
896 closeMenu() {
897 this.menuOpen = false;
898 }
899
900 openUrlBar(e) {
901 e.stopPropagation();
902 if (this.active) {
903 this.urlBarOpen = true;
904 }
905 }
906
907 closeUrlBar() {
908 this.urlBarOpen = false;
909 }
910
911 handleNavigate(url) {
912 this.ensureIframe();
913 this.iframe.load(url);
914 this.urlBarOpen = false;
915 }
916
917 handleSelectWebView(windowId, webviewId) {
918 // Use BroadcastChannel to tell the target window to select the webview
919 const channel = new BroadcastChannel("servo-search");
920 channel.postMessage({
921 type: "selectWebView",
922 id: Date.now(),
923 targetWindowId: windowId,
924 webviewId: webviewId,
925 });
926 channel.close();
927 this.urlBarOpen = false;
928 }
929
930 render() {
931 // Only render adopt-* attributes if they have values
932 const adoptAttrs = {};
933 if (this.attrs["adopt-webview-id"]) {
934 adoptAttrs["adopt-webview-id"] = this.attrs["adopt-webview-id"];
935 }
936 if (this.attrs["adopt-browsing-context-id"]) {
937 adoptAttrs["adopt-browsing-context-id"] =
938 this.attrs["adopt-browsing-context-id"];
939 }
940 if (this.attrs["adopt-pipeline-id"]) {
941 adoptAttrs["adopt-pipeline-id"] = this.attrs["adopt-pipeline-id"];
942 }
943 this.attrs = {};
944
945 return html`
946 <link rel="stylesheet" href="web_view.css" />
947 <div
948 class="wrapper ${this.active ? "active" : ""}"
949 @click=${this.onfocus}
950 >
951 <div
952 class="bar ${this.active ? "" : "hidden"}"
953 style="background-color: ${this
954 .themeColor}; color: contrast-color(${this.themeColor});"
955 >
956 <div class="load-progress load-${this.loadStatus}"></div>
957 <img src="${this.favicon}" class="icon" />
958 <lucide-icon
959 name="arrow-left"
960 class="icon enabled-${this.canGoBack}"
961 color="contrast-color(${this.themeColor})"
962 @click="${this.goBack}"
963 ></lucide-icon>
964 <lucide-icon
965 name="arrow-right"
966 class="icon enabled-${this.canGoForward}"
967 color="contrast-color(${this.themeColor})"
968 @click="${this.goForward}"
969 ></lucide-icon>
970 <lucide-icon
971 name="rotate-ccw"
972 class="icon"
973 color="contrast-color(${this.themeColor})"
974 @click="${this.doReload}"
975 ></lucide-icon>
976 <span class="title" @click=${this.openUrlBar}>${this.title}</span>
977 <div class="menu-container">
978 <lucide-icon
979 @click=${this.toggleMenu}
980 name="ellipsis-vertical"
981 class="icon"
982 color="contrast-color(${this.themeColor})"
983 ></lucide-icon>
984 </div>
985 <lucide-icon
986 @click=${this.close}
987 name="x"
988 class="icon"
989 color="contrast-color(${this.themeColor})"
990 ></lucide-icon>
991 </div>
992 <webview-menu
993 ?open=${this.menuOpen}
994 .x=${this.menuPosition?.x || 0}
995 .y=${this.menuPosition?.y || 0}
996 @menu-action=${this.handleMenuAction}
997 @menu-closed=${this.handleMenuClosed}
998 ></webview-menu>
999 <context-menu
1000 ?open=${this.contextMenu !== null}
1001 .items=${this.contextMenu?.items || []}
1002 .x=${this.contextMenu?.x || 0}
1003 .y=${this.contextMenu?.y || 0}
1004 .controlId=${this.contextMenu?.controlId || ""}
1005 @menu-action=${this.handleContextMenuAction}
1006 @menu-cancel=${this.handleContextMenuCancel}
1007 @menu-closed=${this.handleContextMenuClosed}
1008 ></context-menu>
1009 ${this.renderUrlBarOverlay()}
1010 <div class="iframe-container">
1011 <iframe
1012 embed
1013 adopt-webview-id=${adoptAttrs["adopt-webview-id"]}
1014 adopt-browsing-context-id=${adoptAttrs["adopt-browsing-context-id"]}
1015 adopt-pipeline-id=${adoptAttrs["adopt-pipeline-id"]}
1016 src="${this.src}"
1017 @embedtitlechange=${this.ontitlechange}
1018 @embedfaviconchange=${this.onfaviconchange}
1019 @embedthemecolorchange=${this.onthemecolorchange}
1020 @embedurlchange=${this.onurlchange}
1021 @embedinputreceived=${this.onfocus}
1022 @embedcontrolshow=${this.oncontrolshow}
1023 @embedcontrolhide=${this.oncontrolhide}
1024 @embeddialogshow=${this.ondialogshow}
1025 @embednotificationshow=${this.onnotificationshow}
1026 @embedloadstatuschange=${this.onloadstatuschange}
1027 ></iframe>
1028 ${this.renderDialog()} ${this.renderPermissionPrompt()}
1029 <select-control
1030 ?open=${this.selectControl !== null}
1031 .options=${this.selectControl?.options || []}
1032 .selectedIndex=${this.selectControl?.selectedIndex ?? -1}
1033 .x=${this.selectControl?.x || 0}
1034 .y=${this.selectControl?.y || 0}
1035 .controlId=${this.selectControl?.controlId || ""}
1036 @select-option=${this.handleSelectOption}
1037 @select-cancel=${this.handleSelectCancel}
1038 @menu-closed=${this.handleSelectClosed}
1039 ></select-control>
1040
1041 ${this.renderColorPicker()}
1042 </div>
1043 </div>
1044 `;
1045 }
1046}
1047
1048customElements.define("web-view", WebView);