UNPKG

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
<!DOCTYPE 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>