Last active
May 28, 2025 14:40
-
-
Save DerGoogler/a7bd4dfc1c8d79857aaabc73be82e930 to your computer and use it in GitHub Desktop.
WebUI X Event Handler System and a Example for it
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<link rel="stylesheet" type="text/css" href="https://mui.kernelsu.org/internal/colors.css" /> | |
<style> | |
body { | |
margin-top: var(--window-inset-top); | |
font-family: Arial, sans-serif; | |
padding: 20px; | |
background-color: var(--background); | |
color: var(--onBackground); | |
} | |
.title { | |
color: var(--onBackground); | |
margin-bottom: 1rem; | |
} | |
input { | |
padding: 10px; | |
font-size: 16px; | |
background-color: var(--tonalSurface); | |
color: var(--onSurface); | |
border: 1px solid var(--outlineVariant); | |
border-radius: 4px; | |
outline: none; | |
width: 200px; | |
margin-bottom: 20px; | |
transition: all 0.3s ease; | |
} | |
input:focus { | |
border-color: var(--primary); | |
box-shadow: 0 0 0 2px rgba(var(--primaryRGB), 0.2); | |
transform: scale(1.02); | |
} | |
.status { | |
color: var(--onBackground); | |
margin: 1rem 0; | |
transition: all 0.3s ease; | |
} | |
.status.paused { | |
color: var(--primary); | |
transform: translateX(10px); | |
} | |
/* Pulse animation for paused state */ | |
@keyframes pulse { | |
0% { | |
opacity: 1; | |
} | |
50% { | |
opacity: 0.5; | |
} | |
100% { | |
opacity: 1; | |
} | |
} | |
.pulse { | |
animation: pulse 2s infinite; | |
} | |
/* Enhanced details component styles with animations */ | |
.details-container { | |
margin: 1.5rem 0; | |
border: 1px solid var(--outlineVariant); | |
border-radius: 8px; | |
overflow: hidden; | |
background-color: var(--surfaceContainer); | |
transition: all 0.3s ease; | |
} | |
.details-container[open] { | |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); | |
} | |
.details-summary { | |
padding: 1rem; | |
background-color: var(--surfaceContainerHigh); | |
color: var(--onSurface); | |
cursor: pointer; | |
font-weight: bold; | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
transition: all 0.2s ease; | |
list-style: none; | |
} | |
.details-summary:hover { | |
background-color: var(--surfaceContainerHighest); | |
} | |
.details-summary::-webkit-details-marker { | |
display: none; | |
} | |
.details-summary::after { | |
content: "▼"; | |
font-size: 0.8em; | |
transition: transform 0.3s ease; | |
} | |
.details-container[open] .details-summary::after { | |
transform: rotate(180deg); | |
} | |
.details-content { | |
padding: 0 1rem; | |
max-height: 0; | |
overflow: hidden; | |
transition: max-height 0.3s ease, padding 0.3s ease; | |
border-top: 1px solid transparent; | |
} | |
.details-container[open] .details-content { | |
max-height: 500px; | |
padding: 1rem; | |
border-top-color: var(--outlineVariant); | |
} | |
@keyframes slideIn { | |
from { | |
opacity: 0; | |
transform: translateY(-10px); | |
} | |
to { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
} | |
.details-container[open] .details-content>* { | |
animation: slideIn 0.3s ease forwards; | |
} | |
/* Custom exit dialog styles */ | |
.dialog-overlay { | |
position: fixed; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
background-color: rgba(0, 0, 0, 0.5); | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
z-index: 1000; | |
opacity: 0; | |
pointer-events: none; | |
transition: opacity 0.3s ease; | |
} | |
.dialog-overlay.active { | |
opacity: 1; | |
pointer-events: all; | |
} | |
.dialog-container { | |
background-color: var(--surface); | |
color: var(--onSurface); | |
border-radius: 12px; | |
width: 80%; | |
max-width: 300px; | |
padding: 1.5rem; | |
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); | |
transform: translateY(20px); | |
transition: transform 0.3s ease, opacity 0.3s ease; | |
opacity: 0; | |
} | |
.dialog-overlay.active .dialog-container { | |
transform: translateY(0); | |
opacity: 1; | |
} | |
.dialog-title { | |
font-size: 1.2rem; | |
margin-bottom: 1rem; | |
color: var(--onSurface); | |
} | |
.dialog-message { | |
margin-bottom: 1.5rem; | |
color: var(--onSurfaceVariant); | |
} | |
.dialog-buttons { | |
display: flex; | |
justify-content: flex-end; | |
gap: 0.75rem; | |
} | |
.dialog-button { | |
padding: 0.5rem 1rem; | |
border-radius: 4px; | |
border: none; | |
font-weight: 500; | |
cursor: pointer; | |
transition: all 0.2s ease; | |
} | |
.dialog-button.cancel { | |
background-color: var(--secondaryContainer); | |
color: var(--onSecondaryContainer); | |
} | |
.dialog-button.cancel:hover { | |
background-color: var(--secondaryContainerHover); | |
} | |
.dialog-button.exit { | |
background-color: var(--errorContainer); | |
color: var(--onErrorContainer); | |
} | |
.dialog-button.exit:hover { | |
background-color: var(--errorContainerHover); | |
} | |
@keyframes shake { | |
0%, | |
100% { | |
transform: translateX(0); | |
} | |
20%, | |
60% { | |
transform: translateX(-5px); | |
} | |
40%, | |
80% { | |
transform: translateX(5px); | |
} | |
} | |
.shake { | |
animation: shake 0.4s ease; | |
} | |
</style> | |
</head> | |
<body> | |
<h2 class="title">Focus on Pause Demo</h2> | |
<input type="text" id="focusInput" placeholder="Will focus on pause"> | |
<p class="status" id="status">App is running</p> | |
<details class="details-container" id="appDetails"> | |
<summary class="details-summary">App Details</summary> | |
<div class="details-content"> | |
<p>This is additional information about the app state.</p> | |
<p>Current time: <span id="timeDisplay"></span></p> | |
</div> | |
</details> | |
<!-- Custom exit dialog --> | |
<div class="dialog-overlay" id="exitDialog"> | |
<div class="dialog-container"> | |
<h3 class="dialog-title">Exit Application</h3> | |
<p class="dialog-message">Are you sure you want to exit?</p> | |
<div class="dialog-buttons"> | |
<button class="dialog-button cancel" id="cancelExit">Cancel</button> | |
<button class="dialog-button exit" id="confirmExit">Exit</button> | |
</div> | |
</div> | |
</div> | |
<script type="module"> | |
import { WXEvent } from "./WXEvent.js" | |
WXEvent.initialize() | |
function getCssVar(name) { | |
return getComputedStyle(document.body).getPropertyValue(`--${name}`); | |
} | |
// Get DOM elements | |
const focusInput = document.getElementById('focusInput'); | |
const statusEl = document.getElementById('status'); | |
const appDetails = document.getElementById('appDetails'); | |
const timeDisplay = document.getElementById('timeDisplay'); | |
const exitDialog = document.getElementById('exitDialog'); | |
const cancelExitBtn = document.getElementById('cancelExit'); | |
const confirmExitBtn = document.getElementById('confirmExit'); | |
// Update time display | |
function updateTime() { | |
timeDisplay.textContent = new Date().toLocaleTimeString(); | |
} | |
setInterval(updateTime, 1000); | |
updateTime(); | |
// Handle pause/resume events | |
WXEvent.on(window, 'pause', () => { | |
statusEl.textContent = 'App is paused'; | |
statusEl.classList.add('paused', 'pulse'); | |
focusInput.focus(); | |
focusInput.placeholder = 'Focused on pause'; | |
if (appDetails.open) { | |
appDetails.classList.add('pulse'); | |
} | |
}); | |
WXEvent.on(window, 'resume', () => { | |
statusEl.textContent = 'App is running'; | |
statusEl.classList.remove('paused', 'pulse'); | |
focusInput.placeholder = 'Will focus on pause'; | |
appDetails.classList.remove('pulse'); | |
}); | |
WXEvent.on(appDetails, 'back', (event) => { | |
if (appDetails.open) { | |
event.stopImmediatePropagation(); | |
appDetails.open = false; | |
} | |
}); | |
// Show/hide exit dialog | |
function showExitDialog() { | |
exitDialog.classList.add('active'); | |
document.activeElement.blur(); // Remove focus from any focused element | |
} | |
function hideExitDialog() { | |
exitDialog.classList.remove('active'); | |
} | |
// Dialog button handlers | |
cancelExitBtn.addEventListener('click', () => { | |
exitDialog.querySelector('.dialog-container').classList.add('shake'); | |
setTimeout(() => { | |
exitDialog.querySelector('.dialog-container').classList.remove('shake'); | |
}, 400); | |
setTimeout(hideExitDialog, 200); | |
}); | |
confirmExitBtn.addEventListener('click', () => { | |
hideExitDialog(); | |
setTimeout(() => webui.exit(), 300); | |
}); | |
// If you have nested scroll elements you may need to handle it on the JavaScript side | |
WXEvent.on(window, 'refresh', () => { | |
webui.setRefreshing(true); | |
if (confirm("Do you really wanna refresh the page?")) { | |
location.reload() | |
} | |
webui.setRefreshing(false); | |
}); | |
// Handle back button with custom dialog | |
WXEvent.on(window, 'back', (event) => { | |
if (appDetails.open) { | |
appDetails.open = false; | |
return; | |
} | |
// Show our custom dialog instead of using confirm() | |
showExitDialog(); | |
event.preventDefault(); // Prevent default back behavior | |
}); | |
// Additional animation for details toggle | |
appDetails.addEventListener('toggle', function () { | |
if (this.open) { | |
this.classList.add('details-open'); | |
} else { | |
this.classList.remove('details-open'); | |
} | |
}); | |
// Close dialog when clicking outside | |
exitDialog.addEventListener('click', (e) => { | |
if (e.target === exitDialog) { | |
hideExitDialog(); | |
} | |
}); | |
</script> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class CustomWXEvent extends CustomEvent { | |
get wxOrigin() { | |
return this._wxOrigin | |
} | |
set wxOrigin(value) { | |
this._wxOrigin = value | |
} | |
constructor(type, detail) { | |
super(type, { | |
detail, | |
bubbles: true, | |
cancelable: true, | |
composed: true | |
}) | |
} | |
} | |
export class WXEvent { | |
static _initialized = false | |
static _handlers = new WeakMap() | |
static _eventTypes = { | |
WX_ON_BACK: "back", | |
WX_ON_RESUME: "resume", | |
WX_ON_REFRESH: "refresh", | |
WX_ON_PAUSE: "pause" | |
} | |
static get eventTypes() { | |
return this._eventTypes | |
} | |
static initialize() { | |
if (this._initialized) return | |
this._initialized = true | |
window.addEventListener("message", event => { | |
try { | |
if (typeof event.data !== "string") return | |
const data = JSON.parse(event.data) | |
if (!data?.type) return | |
const eventType = this._eventTypes[data.type] ?? data.type | |
this._dispatch(window, eventType, data) | |
} catch (err) { | |
console.error("[WXEvent] Message error:", err) | |
} | |
}) | |
} | |
static _dispatch(element, type, detail) { | |
const event = new CustomWXEvent(type, detail) | |
event.wxOrigin = "system" | |
element.dispatchEvent(event) | |
} | |
static on(element, type, handler) { | |
if (!this._initialized) this.initialize() | |
const wrapper = event => { | |
if (!(event instanceof CustomWXEvent)) { | |
console.warn("[WXEvent] Event is not a CustomWXEvent:", event) | |
return | |
} | |
if (event.wxOrigin === "system") handler(event) | |
} | |
if (!this._handlers.has(element)) { | |
this._handlers.set(element, new Map()) | |
} | |
const elementHandlers = this._handlers.get(element) | |
if (!elementHandlers.has(type)) { | |
elementHandlers.set(type, new Set()) | |
} | |
elementHandlers.get(type).add({ handler, wrapper }) | |
element.addEventListener(type, wrapper) | |
return () => this.off(element, type, handler) | |
} | |
static off(element, type, handler) { | |
const elementHandlers = this._handlers.get(element) | |
if (!elementHandlers?.has(type)) return | |
for (const entry of elementHandlers.get(type)) { | |
if (entry.handler === handler) { | |
element.removeEventListener(type, entry.wrapper) | |
elementHandlers.get(type).delete(entry) | |
break | |
} | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment