ypsilon-event-handler
Version:
A production-ready event handling system for web applications with memory leak prevention, and method chaining support
594 lines • 20.5 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Comprehensive Template | Generated by Grok 3 (xAI) - YpsilonEventHandler</title>
<meta name="description" content="A complete, working template demonstrating all YpsilonEventHandler patterns. Perfect starting point for any project. Generated by Grok 3 (xAI).">
<link rel="icon" type="image/x-icon" href="./favicon.ico">
<link rel="stylesheet" type="text/css" href="./assets/main.css">
<style>
body {
font-family: Arial, sans-serif;
background-color: #f3f4f6;
margin: 0;
padding: 20px;
}
header, section, footer {
margin-bottom: 20px;
padding: 15px;
border-radius: 8px;
background-color: white;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
header, section {
padding: 25px 20px;
}
header p {
margin: 0;
font-size: 0.9rem;
color: #4b5563;
}
header p strong {
color: #1e40af;
}
h1 {
font-size: 1.5rem;
font-weight: bold;
margin: 0 0 15px;
}
h2 {
font-size: 1.2rem;
font-weight: bold;
margin: 0 0 15px;
color: #1e40af;
}
p {
margin: 0 0 10px;
font-size: 0.9rem;
color: #374151;
}
.y-btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
white-space: nowrap;
margin-right: 8px;
margin-top: 5px;
font-size: 0.9rem;
transition: transform 0.2s, box-shadow 0.2s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.y-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
#test-custom, #create-dynamic {
background-color: #9333ea;
color: white;
}
#scroll-top {
background-color: #22c55e;
color: white;
}
#remove-element, #remove-all {
background-color: #ef4444;
color: white;
}
#increment-count, #reset-count {
background-color: #2563eb;
color: white;
}
.dynamic-button {
background-color: #6b7280;
color: white;
}
.removable {
padding: 10px;
border-radius: 6px;
background-color: #fef2f2;
margin-top: 10px;
font-size: 0.9rem;
border: 1px solid #fecaca;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
#custom-events, #input-display, #change-display, #keyboard-events, #window-events {
padding: 10px;
border-radius: 6px;
background-color: #f9fafb;
font-size: 0.9rem;
color: #374151;
border: 1px solid #e5e7eb;
}
textarea {
width: 100%;
padding: 8px;
border: 1px solid #d1d5db;
border-radius: 6px;
margin-top: 10px;
font-size: 0.9rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: box-shadow 0.2s;
}
textarea:focus {
outline: none;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-color: #2563eb;
}
select {
padding: 8px;
border: 1px solid #d1d5db;
border-radius: 6px;
margin-top: 10px;
font-size: 0.9rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: box-shadow 0.2s;
}
select:focus {
outline: none;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-color: #2563eb;
}
#notification {
position: fixed;
bottom: 20px;
right: 20px;
width: 300px;
}
.toast {
margin-bottom: 0;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
background: #28a745;
color: white;
border-radius: 6px;
border: 1px solid #16a34a;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
cursor: default;
transition: all .4s ease-in-out;
}
.toast span {
padding: 0 15px;
display: block;
max-width: 95%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.9rem;
}
.toast button {
margin: 0;
padding: 10px 15px;
background: #237e38;
border: none;
color: white;
cursor: pointer;
font-weight: bold;
font-size: 18px;
border-radius: 0 6px 6px 0;
transition: background 0.2s;
}
.toast button:hover {
background: #1e6832;
}
.toast:not(:last-of-type) {
border-bottom: 1px solid #c3c3c3;
}
.y-nulled {
display: block;
}
.y-fade-in {
opacity: 1;
}
#scroll-area {
max-height: 200px;
overflow-y: auto;
margin-top: 10px;
padding: 10px;
border: 1px solid #d1d5db;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.scroll-to-top-container {
padding: 0;
position: fixed;
bottom: 80px;
right: 20px;
width: 44px;
height: 44px;
opacity: 1;
outline: 0;
}
.scroll-to-top {
font-size: 18px;
margin: 0;
padding: 0.5rem 0;
line-height: 1;
opacity: 0;
position: absolute;
bottom: 0;
right: 0;
z-index: 1001;
background: #22c55e;
transform: translate(68px, 0);
transition: opacity 0.6s ease-in-out, transform 0.3s ease-in-out;
border: none;
border-radius: 50%;
cursor: pointer;
color: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
width: 44px;
height: 44px;
}
.y-scrolled .scroll-to-top {
transform: translate(-1px, 0);
opacity: 1;
}
.scroll-to-top:hover {
transform: translate(-1px, -2px);
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15);
}
nav ul {
list-style: none;
padding: 0;
display: flex;
gap: 10px;
}
nav a {
color: #2563eb;
text-decoration: none;
font-size: 0.9rem;
padding: 6px 12px;
border-radius: 6px;
transition: background 0.2s, box-shadow 0.2s;
}
nav a:hover {
background: #e5e7eb;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.nav-link.active {
font-weight: bold;
color: #1e40af;
background: #dbeafe;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
[data-state] {
font-weight: bold;
color: #1e40af;
}
#input-display {
white-space: pre;
}
blockquote {
background-color: #fff;
display: inline-block;
margin: .3rem 0 0;
padding: 1em 1em 0;
position: relative;
}
blockquote p {
margin: 0;
font-weight: 500;
&:after,
&:before { content: '"'; }
}
blockquote cite {
margin-top: .3rem;
display: block;
font-style: italic;
font-size: .9em;
color: #444;
&:before { content: "— "; }
}
</style>
</head>
<body>
<header id="main-content">
<h1>YpsilonEventHandler - Comprehensive Template</h1>
<p>A complete, working template demonstrating all YpsilonEventHandler patterns. Perfect starting point for any project. Generated by Grok 3 (xAI).</p>
<blockquote><p>Just remove what you don't like and start from there.</p><cite>Ypsilon Team</cite></blockquote>
</header>
<section id="click-events">
<h2>🖱️ Click Events with Data-Action Routing</h2>
<p><strong>🤯 Mind-Blowing Fact:</strong></p>
<p>
ZERO of these buttons have event listeners attached to them!
We have ONE click listener on the <body> element that controls
ALL buttons on this page. New buttons added dynamically?
Still work instantly, because for the listener nothing has changed, it's still the same element it's listening to - no re-assignment needed!
</p>
<p>
<button id="test-custom" class="y-btn" data-action="handleTestCustom">Test Custom Event</button>
<button id="scroll-top" class="y-btn" data-action="handleScrollTop">Scroll to Top</button>
<button id="remove-element" class="y-btn" data-action="handleRemoveElement">Remove Element</button>
<button id="remove-all" class="y-btn" data-action="handleRemoveAll">Remove All .removable</button>
<button id="create-dynamic" class="y-btn" data-action="handleCreateDynamic">Create Dynamic Button</button>
</p>
<p><strong>✨ Dynamic Button Playground:</strong></p>
<p>Click "Create Dynamic Button" to add new buttons. They work instantly without any event listener setup!</p>
<div id="dynamic-buttons"></div>
<div id="removable-element" class="removable">Removable Element: Click "Remove Element" to remove this box.</div>
<div class="removable">Removable item #1</div>
<div class="removable">Removable item #2</div>
</section>
<section id="custom-events-section">
<h2>🚀 Custom Events</h2>
<p>YpsilonEventHandler includes a powerful <code>dispatch()</code> method for custom events.</p>
<div id="custom-events">Custom events will appear here...</div>
</section>
<section id="input-events">
<h2>📝 Input Events with Debouncing</h2>
<textarea id="input-field" data-action="handleInput" placeholder="Type something..."></textarea>
<div id="input-display">Input events will appear here...</div>
</section>
<section id="change-events">
<h2>🔄 Change Events</h2>
<p>
<select id="select-field" data-action="handleChange">
<option value="">Select an option</option>
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
<option value="option3">Option 3</option>
</select>
</p>
<div id="change-display">Change events will appear here...</div>
</section>
<section id="keyboard-events">
<h2>⌨️ Keyboard Events</h2>
<p>Press any key while focused on this page to see keydown events in the console.</p>
<div id="keyboard-events">Press a key to see keydown events...</div>
</section>
<section id="window-events">
<h2>📏 Window Events</h2>
<p>Resize the window or scroll to see throttled events in the console.</p>
<div id="window-events">Window events will appear here...</div>
</section>
<section id="state-management">
<h2>⚡ State Management (Ypsiwork-inspired)</h2>
<p>Reactive state management with automatic UI updates using <code>data-state</code> attributes.</p>
<p>
<button id="increment-count" class="y-btn" data-action="handleIncrementCount">Increment Click Count</button>
<button id="reset-count" class="y-btn" data-action="handleResetCount">Reset Count</button>
</p>
<p>Click count: <span data-state="clickCount">0</span></p>
<p>Last key pressed: <span data-state="lastKey">none</span></p>
<p>Current input: <span data-state="currentInput">empty</span></p>
<p>Window size: <span data-state="windowSize">0x0</span></p>
<p>State updates automatically sync across all <code>data-state</code> elements!</p>
</section>
<section id="scroll-area">
<p>Scroll content for testing scroll events</p>
<div style="height: 1000px;"></div>
</section>
<footer>
<nav>
<ul>
<li><a href="./basic-example.html" class="nav-link y-btn" data-action="handleNavClick">Basic Examples</a></li>
<li><a href="./spa.html" class="nav-link y-btn" data-action="handleNavClick">Full SPA Demo</a></li>
<li><a href="./reactive-y.html" class="nav-link y-btn" data-action="handleNavClick">Reactive Demo</a></li>
<li><a href="./single-listener-multiple-actions.html" class="nav-link y-btn" data-action="handleNavClick">Single Listener</a></li>
<li><a href="./ypsilon-feat-grok-example.html" class="nav-link y-btn" data-action="handleNavClick">Grok's SPA</a></li>
<li><a href="./ai-reviews.html" class="nav-link y-btn" data-action="handleExternalLink">AI Reviews</a></li>
<li><a href="https://github.com/eypsilon/YpsilonEventHandler" class="nav-link y-btn" data-action="handleExternalLink">GitHub</a></li>
</ul>
</nav>
<div id="scroll-top-container" class="scroll-to-top-container">
<button id="scroll-top-btn" class="y-btn scroll-to-top" data-action="handleScrollTop">▲</button>
</div>
</footer>
<div id="notification"></div>
<script src="https://cdn.jsdelivr.net/npm/ypsilon-event-handler@1.5.0/ypsilon-event-handler.min.js"></script>
<script>
class AppHandler extends YpsilonEventHandler {
constructor() {
super({
'body': [
'click',
'change',
{ type: 'input', debounce: 300 },
],
'document': [
'keydown',
{ type: 'app:custom', handler: 'handleCustomEvent' },
],
'window': [
'popstate',
{ type: 'resize', throttle: 200 },
{ type: 'scroll', throttle: 200 },
],
'#scroll-area': [
{ type: 'scroll', handler: 'handleScrollArea', throttle: 200 },
],
});
this.state = {
clickCount: 0,
lastKey: 'none',
currentInput: 'empty',
windowSize: '0x0',
};
this.itemCount = 0;
this.initialContent = '';
this.setActiveNav();
if (window.scrollY > 0) {
document.body.classList.add('y-scrolled');
}
}
handleClick(event, target) {
if (!(target instanceof Element) || !target.classList.contains('y-btn')) return;
const action = target.dataset.action;
if (!action) return;
if (typeof this[action] === 'function') {
return this[action](event, target);
}
}
handleNavClick(event, target) {
event.preventDefault();
const path = target.getAttribute('href');
document.querySelectorAll('.nav-link').forEach(link => link.classList.remove('active'));
target.classList.add('active');
window.location = path;
}
handlePopstate(event, target) {
this.setActiveNav();
}
setActiveNav() {
const currentPath = window.location.pathname.split('/').pop() || 'comprehensive-example.html';
document.querySelectorAll('.nav-link').forEach(link => {
link.classList.remove('active');
const linkPath = link.getAttribute('href').split('/').pop();
if (linkPath === currentPath) {
link.classList.add('active');
}
});
}
handleExternalLink(event, target) {
event.preventDefault();
this.showNotification(`External link clicked: ${target.getAttribute('href')}`);
}
handleTestCustom(event, target) {
this.dispatch('app:custom', { message: `Custom event triggered!` });
}
handleCustomEvent(event, target) {
const message = event.detail?.message || 'No message';
document.getElementById('custom-events').textContent = message;
this.showNotification(`Custom event: ${message}`);
}
handleScrollTop(event, target) {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
handleRemoveElement(event, target) {
const element = document.getElementById('removable-element');
if (element) {
element.remove();
this.showNotification('Element removed!');
}
}
handleRemoveAll(event, target) {
document.querySelectorAll('.removable').forEach(el => el.remove());
this.showNotification('All removable elements cleared!');
}
handleCreateDynamic(event, target) {
const container = document.getElementById('dynamic-buttons');
const button = document.createElement('button');
button.className = 'y-btn dynamic-button';
button.textContent = `Dynamic Button ${++this.itemCount}`;
button.dataset.action = 'handleDynamicClick';
container.appendChild(button);
this.showNotification(`Created dynamic button ${this.itemCount}`);
}
handleDynamicClick(event, target) {
this.showNotification(`Dynamic button clicked: ${target.textContent}`);
}
handleInput(event, target) {
if (target.id !== 'input-field') return;
const value = target.value || 'empty';
this.state.currentInput = value;
this.updateStateElements('currentInput');
document.getElementById('input-display').textContent = `Input: ${value}`;
}
handleChange(event, target) {
if (target.id !== 'select-field') return;
const value = target.value || 'none';
document.getElementById('change-display').textContent = `Selected: ${value}`;
this.showNotification(`Option selected: ${value}`);
}
handleKeydown(event, target) {
const key = event.key || 'unknown';
this.state.lastKey = key;
this.updateStateElements('lastKey');
document.getElementById('keyboard-events').textContent = `Key pressed: ${key}`;
console.log(`Keydown: ${key}`);
}
handleResize(event, target) {
const size = `${window.innerWidth}x${window.innerHeight}`;
this.state.windowSize = size;
this.updateStateElements('windowSize');
document.getElementById('window-events').textContent = `Window resized: ${size}`;
console.log(`Resize: ${size}`);
}
handleScroll(event, target) {
console.log('Window scroll triggered');
document.getElementById('window-events').textContent = `Window scrolled: ${window.scrollY}px`;
if (window.scrollY > 0) {
document.body.classList.add('y-scrolled');
} else {
document.body.classList.remove('y-scrolled');
}
}
handleScrollArea(event, target) {
console.log('Scroll-area scrolled');
document.getElementById('window-events').textContent = `Scroll-area scrolled: ${target.scrollTop}px`;
}
handleIncrementCount(event, target) {
this.state.clickCount++;
this.updateStateElements('clickCount');
this.showNotification(`Click count: ${this.state.clickCount}`);
}
handleResetCount(event, target) {
this.state.clickCount = 0;
this.updateStateElements('clickCount');
this.showNotification('Click count reset!');
}
updateStateElements(changedKey) {
if (changedKey) {
document.querySelectorAll(`[data-state="${changedKey}"]`).forEach(el => {
el.textContent = this.state[changedKey];
});
} else {
Object.keys(this.state).forEach(key => {
document.querySelectorAll(`[data-state="${key}"]`).forEach(el => {
el.textContent = this.state[key];
});
});
}
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
showNotification(message) {
const notification = document.getElementById('notification');
if (!notification.classList.contains('y-nulled')) {
notification.classList.add('y-nulled');
if (!this.initialContent) {
this.initialContent = notification.innerHTML;
}
notification.textContent = '';
}
const note = document.createElement('div');
note.innerHTML = `
<span>${this.escapeHtml(message)}</span>
<button class="y-btn" data-action="handleRemoveToast">×</button>
`;
notification.classList.add('show');
note.className = 'toast y-fade-in';
notification.appendChild(note);
const toastTimer = setTimeout(() => {
this.handleRemoveToast(null, note);
}, 3000);
note.dataset.timerId = toastTimer;
}
handleRemoveToast(event, target) {
if (!(target instanceof Element)) return;
const actualToast = target.classList.contains('toast') ? target : target.closest('.toast');
if (!actualToast || !actualToast.parentNode) return;
if (actualToast.dataset.timerId) {
clearTimeout(parseInt(actualToast.dataset.timerId));
}
actualToast.remove();
const notification = document.getElementById('notification');
if (notification.children.length === 0) {
notification.classList.remove('show', 'y-nulled');
notification.textContent = this.initialContent || '';
}
}
}
const handler = new AppHandler();
</script>
</body>
</html>