Rewild Your Web
web
browser
dweb
1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3import {
4 LitElement,
5 html,
6 css,
7} from "//shared.localhost:8888/third_party/lit/lit-all.min.js";
8
9export class MobileRadialMenu extends LitElement {
10 static properties = {
11 open: { type: Boolean, reflect: true },
12 x: { type: Number },
13 y: { type: Number },
14 canGoBack: { type: Boolean },
15 canGoForward: { type: Boolean },
16 contextMenu: { type: Object },
17 isHomescreen: { type: Boolean },
18 };
19
20 // 8 zones clockwise from top
21 static zones = [
22 { id: 0, action: "home", icon: "house", angle: -90 },
23 { id: 1, action: "overview", icon: "layout-grid", angle: -45 },
24 { id: 2, action: "forward", icon: "arrow-right", angle: 0 },
25 { id: 3, action: "context-menu", icon: "ellipsis-vertical", angle: 45 },
26 { id: 4, action: "close-view", icon: "x", angle: 90 },
27 { id: 5, action: "settings", icon: "settings", angle: 135 },
28 { id: 6, action: "back", icon: "arrow-left", angle: 180 },
29 { id: 7, action: "reload", icon: "rotate-ccw", angle: -135 },
30 ];
31
32 static styles = css`
33 @import url(//system.localhost:8888/mobile_radial_menu.css);
34 `;
35
36 constructor() {
37 super();
38 this.open = false;
39 this.x = 0;
40 this.y = 0;
41 this.canGoBack = false;
42 this.canGoForward = false;
43 this.contextMenu = null;
44 this.isHomescreen = false;
45 }
46
47 show(x, y, contextMenu = null) {
48 // Adjust position to keep menu on screen
49 const menuRadius = 100;
50 const padding = 20;
51
52 this.x = Math.max(
53 menuRadius + padding,
54 Math.min(window.innerWidth - menuRadius - padding, x)
55 );
56 this.y = Math.max(
57 menuRadius + padding,
58 Math.min(window.innerHeight - menuRadius - padding, y)
59 );
60 this.contextMenu = contextMenu;
61 this.open = true;
62 }
63
64 hide() {
65 this.open = false;
66 // Dispatch dismiss event so pending context menu can be closed
67 this.dispatchEvent(
68 new CustomEvent("radial-dismiss", {
69 bubbles: true,
70 composed: true,
71 })
72 );
73 this.contextMenu = null;
74 }
75
76 handleOverlayTap(e) {
77 // Tapping on overlay (outside menu) closes the menu
78 e.preventDefault();
79 this.hide();
80 }
81
82 handleZoneTap(zone, e) {
83 console.log(`[RadialMenu] handleZoneTap ${zone.action}`);
84 e.preventDefault();
85 e.stopPropagation();
86
87 // Check if action is disabled
88 if (this.isZoneDisabled(zone)) {
89 return;
90 }
91
92 // For context-menu action, close without dispatching dismiss
93 // (the context menu will be shown instead)
94 if (zone.action === "context-menu") {
95 this.dispatchEvent(
96 new CustomEvent("radial-action", {
97 bubbles: true,
98 composed: true,
99 detail: {
100 action: zone.action,
101 contextMenu: this.contextMenu,
102 },
103 })
104 );
105 this.open = false;
106 this.contextMenu = null;
107 return;
108 }
109
110 // Dispatch action
111 this.dispatchEvent(
112 new CustomEvent("radial-action", {
113 bubbles: true,
114 composed: true,
115 detail: {
116 action: zone.action,
117 originX: this.x,
118 originY: this.y,
119 },
120 })
121 );
122
123 this.hide();
124 }
125
126 isZoneDisabled(zone) {
127 if (zone.action === "back") {
128 return !this.canGoBack;
129 }
130 if (zone.action === "forward") {
131 return !this.canGoForward;
132 }
133 if (zone.action === "context-menu") {
134 return !this.contextMenu || this.contextMenu.items.length === 0;
135 }
136 // Disable close-view, reload and home actions when on homescreen
137 if (
138 zone.action === "close-view" ||
139 zone.action === "home" ||
140 zone.action === "reload"
141 ) {
142 return this.isHomescreen;
143 }
144 return false;
145 }
146
147 render() {
148 const menuStyle = `left: ${this.x}px; top: ${this.y}px;`;
149
150 return html`
151 <div class="overlay" @click=${this.handleOverlayTap}></div>
152 <div class="menu" style=${menuStyle}>
153 <div class="center">
154 <div class="center-dot"></div>
155 </div>
156 ${MobileRadialMenu.zones.map(
157 (zone) => html`
158 <div
159 class="zone ${this.isZoneDisabled(zone) ? "disabled" : ""}"
160 data-zone=${zone.id}
161 @click=${(e) => this.handleZoneTap(zone, e)}
162 >
163 <lucide-icon name=${zone.icon}></lucide-icon>
164 </div>
165 `
166 )}
167 </div>
168 `;
169 }
170}
171
172customElements.define("mobile-radial-menu", MobileRadialMenu);