UNPKG

ypsilon-event-handler

Version:

A production-ready event handling system for web applications with memory leak prevention, and method chaining support

407 lines (381 loc) 16.3 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>YpsilonEventHandler Declarative Delegation Example</title> <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; overflow-y: scroll; } .content { max-width: 800px; margin: 0 auto; padding: 10px; } nav ul { list-style: none; padding: 0; display: flex; gap: 10px; } nav a { color: #2563eb; text-decoration: none; } .nav-link.active { font-weight: bold; color: #1e40af; } #main-content { background-color: white; padding: 20px; border-radius: 4px; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); } #main-content h1 { font-size: 1.25rem; font-weight: bold; margin-top: 5px; margin-bottom: 10px; } .y-btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; margin-top: 10px; margin-right: 6px; } #add-item { background-color: #22c55e; color: white; } #trigger-custom { background-color: #9333ea; color: white; } #toggle-effect { background-color: #f59e0b; color: white; } #item-list { list-style: disc; padding-left: 20px; margin: 14px 0; } .dynamic-item { color: #2563eb; cursor: pointer; } #main-content.highlight { background-color: #fef08a; /* padding: 2px 4px; */ border-radius: 4px; } #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: 4px; 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; } .toast button { margin: 0; padding: 10px 15px; background: #237e38; border: none; color: white; cursor: pointer; font-weight: bold; font-size: 22px; } .toast:not(:last-of-type) { border-bottom: 1px solid #c3c3c3; } .y-nulled { display: block; } .y-fade-in { opacity: 1; } .credit { font-size: 0.8rem; color: #4b5563; margin: 0; padding: 1.2rem; text-align: center; } .remarks { margin: 12px 0 0; padding: 6px 12px; background-color: #f9fafb; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); border-radius: 4px; font-size: 0.9rem; color: #374151; } .remarks h2 { font-size: 1.1rem; font-weight: bold; margin-bottom: 8px; } .remarks ul { list-style: disc; padding-left: 20px; margin-bottom: 10px; } </style> </head> <body> <div class="content"> <nav> <ul> <li><a href="#home" class="nav-link y-btn" data-action="handleNavClick">Home</a></li> <li><a href="#about" class="nav-link y-btn" data-action="handleNavClick">About</a></li> <li><a href="#contact" class="nav-link y-btn" data-action="handleNavClick">Contact</a></li> </ul> </nav> <div id="main-content"> <h1>Welcome to the Home Page</h1> <p>This is a simple SPA using YpsilonEventHandler's declarative delegation.</p> <button id="add-item" class="y-btn" data-action="handleAddItem">Add Dynamic Item</button> <button id="trigger-custom" class="y-btn" data-action="handleTriggerCustom">Trigger Custom Event</button> <button id="toggle-effect" class="y-btn" data-action="handleToggleEffect">Toggle Highlight Effect</button> <ul id="item-list"></ul> <div class="remarks"> <h2>About This Example</h2> <p>This example, generated by Grok 3 (xAI), showcases YpsilonEventHandler's <code>handleEvent</code> pattern in a lightweight SPA. As one user said, "The marks are great!" Here's how it works and why it's powerful:</p> <ul> <li><strong>Single Listener</strong>: A <code>body</code> click listener routes all interactions via <code>data-action</code> (e.g., navigation, toast removal).</li> <li><strong>Dynamic Elements</strong>: Adds list items with <code>data-action="handleItemClick"</code> without new listeners.</li> <li><strong>Custom Events</strong>: Dispatches <code>app:custom</code> events, shown in toasts.</li> <li><strong>Notifications</strong>: Toasts stack with 3-second timeouts and close buttons, using <code>showNotification</code> and <code>removeToast</code>.</li> <li><strong>Navigation</strong>: Hash-based for <code>file://</code> compatibility, with active nav highlighting.</li> <li><strong>Performance</strong>: <code>y-btn</code> validation optimizes handling.</li> </ul> <p><strong>Usage Tips</strong>: Use <code>data-action</code> for dynamic UIs, extend with <code>input</code> or <code>change</code> events, and call <code>handler.destroy()</code> for cleanup. For scroll events, add a listener on a persistent element (e.g., <code>#main-content</code>) with <code>.scroll-area { height: 300px; overflow-y: scroll; }</code> and delegate to <code>.scroll-area</code>. Try it to see YpsilonEventHandler's ~370-line magic!</p> </div> </div> <footer style="margin-bottom: 4.5rem;"> <p class="credit">Generated by Grok 3 (xAI). Feedback: "The marks are great!"</p> </footer> <!-- Navigation to other examples --> <nav style="box-shadow: 0 0 10px #6a8cd7; width: 100%; text-align: center; margin: 2rem 0 1rem; padding: 1rem; background: #f7f7f7; border-radius: 0; display: flex; flex-wrap: wrap; width: 100%; justify-content: center; width: 100%; position: fixed; bottom: 0; left: 0;"> <a href="./basic-example.html" style="margin: 0 0.5rem; padding: 0.5rem 1rem; background: #2563eb; color: white; text-decoration: none; border-radius: 5px; font-size: 0.9rem;">Basic Examples</a> <a href="./spa.html" style="margin: 0 0.5rem; padding: 0.5rem 1rem; background: #16a34a; color: white; text-decoration: none; border-radius: 5px; font-size: 0.9rem;">SPA Demo</a> <a href="./reactive-y.html" style="margin: 0 0.5rem; padding: 0.5rem 1rem; background: #dc2626; color: white; text-decoration: none; border-radius: 5px; font-size: 0.9rem;">Reactive Demo</a> <a href="./single-listener-multiple-actions.html" style="margin: 0 0.5rem; padding: 0.5rem 1rem; background: #7c3aed; color: white; text-decoration: none; border-radius: 5px; font-size: 0.9rem;">Single Listener</a> <a href="./ai-reviews.html" style="margin: 0 0.5rem; padding: 0.5rem 1rem; background: #ea580c; color: white; text-decoration: none; border-radius: 5px; font-size: 0.9rem;">AI Reviews</a> <a href="https://github.com/eypsilon/YpsilonEventHandler" style="margin: 0 0.5rem; padding: 0.5rem 1rem; background: #374151; color: white; text-decoration: none; border-radius: 5px; font-size: 0.9rem;">GitHub</a> </nav> <div id="notification"></div> </div> <!-- YpsilonEventHandler CDN --> <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({ // General click delegation for all buttons with y-btn class 'body': [ { type: 'click' } // Falls back to handleClick, routes to data-action handlers ], // Popstate for SPA navigation (hash-based) 'window': [ { type: 'popstate', handler: 'handlePopstate' } ], // Custom event 'document': [ { type: 'app:custom', handler: 'handleCustomEvent' } ] }); this.itemCount = 0; this.initialContent = ''; // Initial content load based on hash const currentPath = window.location.hash || '#home'; this.updateContent(currentPath); // Also set active nav item for initial load this.setActiveNavItem(currentPath); } // General click router: Uses handleEvent to route clicks to handlers via data-action handleClick(event, target) { // Pre-validate with y-btn class for performance if (!(target instanceof Element) || !target.classList.contains('y-btn')) return; // Delegate based on data-action const action = target.dataset.action; if (!action) return; // Call the specified handler if it exists if (typeof this[action] === 'function') { return this[action](event, target); } } handleNavClick(event, target) { event.preventDefault(); const path = target.getAttribute('href'); // Remove active class from all nav links document.querySelectorAll('.nav-link').forEach(link => link.classList.remove('active')); // Add active class to clicked link target.classList.add('active'); window.location.hash = path; this.updateContent(path); } handlePopstate(event, target) { this.updateContent(window.location.hash || '#home'); } handleAddItem(event, target) { const list = document.getElementById('item-list'); const li = document.createElement('li'); li.className = 'dynamic-item y-btn'; li.textContent = `Dynamic Item ${++this.itemCount}`; li.dataset.action = 'handleItemClick'; list.appendChild(li); } handleItemClick(event, target) { this.showNotification(`Clicked item: ${target.textContent}`); } handleTriggerCustom(event, target) { this.dispatch('app:custom', { message: `Hello from item ${this.itemCount}!` }); } handleCustomEvent(event, target) { const message = event.detail?.message || 'No message'; this.showNotification(`Custom event triggered: ${message}`); } handleToggleEffect(event, target) { const mainContent = document.getElementById('main-content'); mainContent.classList.toggle('highlight'); this.showNotification(`Highlight effect ${mainContent.classList.contains('highlight') ? 'enabled' : 'disabled'}`); } handleRemoveToast(event, target) { this.removeToast(event, target); } escapeHtml(unsafe) { return unsafe .replace(/&/g, "&amp;") .replace(/</g, "&lt;") .replace(/>/g, "&gt;") .replace(/"/g, "&quot;") .replace(/'/g, "&#x27;"); } removeToast(event, target) { // Guard against invalid elements if (!(target instanceof Element)) return; // If target is the close button, get the parent toast div const actualToast = target.classList.contains('toast') ? target : target.closest('.toast'); if (!actualToast || !actualToast.parentNode) return; // Clear the timer if it exists if (actualToast.dataset.timerId) { clearTimeout(parseInt(actualToast.dataset.timerId)); } actualToast.remove(); const notification = document.getElementById('notification'); // If no toasts left, reset notification container if (notification.children.length === 0) { notification.classList.remove('show', 'y-nulled'); notification.textContent = this.initialContent || ''; } } 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); // Individual timer for this toast (3 seconds) const toastTimer = setTimeout(() => { this.removeToast(null, note); }, 3000); // Store timer on the element note.dataset.timerId = toastTimer; } updateContent(path) { const content = document.getElementById('main-content'); let title, text; switch (path) { case '#about': title = 'About Us'; text = 'Learn more about our SPA powered by YpsilonEventHandler!'; break; case '#contact': title = 'Contact Us'; text = 'Reach out to us via this cool SPA interface!'; break; default: title = 'Welcome to the Home Page'; text = "This is a simple SPA using YpsilonEventHandler's declarative delegation."; path = '#home'; // Ensure default path for active class } content.innerHTML = ` <h1>${title}</h1> <p>${text}</p> <button id="add-item" class="y-btn" data-action="handleAddItem">Add Dynamic Item</button> <button id="trigger-custom" class="y-btn" data-action="handleTriggerCustom">Trigger Custom Event</button> <button id="toggle-effect" class="y-btn" data-action="handleToggleEffect">Toggle Highlight Effect</button> <ul id="item-list"></ul> <div class="remarks"> <h2>About This Example</h2> <p>This example, generated by Grok 3 (xAI), showcases YpsilonEventHandler's <code>handleEvent</code> pattern in a lightweight SPA. As one user said, "The marks are great!" Here's how it works and why it's powerful:</p> <ul> <li><strong>Single Listener</strong>: A <code>body</code> click listener routes all interactions via <code>data-action</code> (e.g., navigation, toast removal).</li> <li><strong>Dynamic Elements</strong>: Adds list items with <code>data-action="handleItemClick"</code> without new listeners.</li> <li><strong>Custom Events</strong>: Dispatches <code>app:custom</code> events, shown in toasts.</li> <li><strong>Notifications</strong>: Toasts stack with 3-second timeouts and close buttons, using <code>showNotification</code> and <code>removeToast</code>.</li> <li><strong>Navigation</strong>: Hash-based for <code>file://</code> compatibility, with active nav highlighting.</li> <li><strong>Performance</strong>: <code>y-btn</code> validation optimizes handling.</li> </ul> <p><strong>Usage Tips</strong>: Use <code>data-action</code> for dynamic UIs, extend with <code>input</code> or <code>change</code> events, and call <code>handler.destroy()</code> for cleanup. For scroll events, add a listener on a persistent element (e.g., <code>#main-content</code>) with <code>.scroll-area { height: 300px; overflow-y: scroll; }</code> and delegate to <code>.scroll-area</code>. Try it to see YpsilonEventHandler's ~370-line magic!</p> </div> `; this.setActiveNavItem(path); this.itemCount = 0; // Reset item count on navigation } setActiveNavItem(path) { // Set active class on nav link matching the current path document.querySelectorAll('.nav-link').forEach(link => { link.classList.remove('active'); if (link.getAttribute('href') === path) { link.classList.add('active'); } }); } } // Initialize the handler const handler = new AppHandler(); </script> </body> </html>