Skip to content

Instantly share code, notes, and snippets.

@DerGoogler
Last active May 28, 2025 14:40
Show Gist options
  • Save DerGoogler/a7bd4dfc1c8d79857aaabc73be82e930 to your computer and use it in GitHub Desktop.
Save DerGoogler/a7bd4dfc1c8d79857aaabc73be82e930 to your computer and use it in GitHub Desktop.
WebUI X Event Handler System and a Example for it
<!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>
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