UNPKG

ypsilon-event-handler

Version:

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

1,367 lines (1,168 loc) 63.7 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>SPA Demo (Single Page Application) - YpsilonEventHandler</title> <meta name="description" content="YpsilonEventHandler - Single Page Application [SPA] Example Page"> <link rel="icon" type="image/x-icon" href="./favicon.ico"> <link rel="stylesheet" type="text/css" href="./assets/main.css"> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { padding-bottom: 208px; line-height: 1.6; background: #f5f5f5; font-family: system-ui, sans-serif; scroll-behavior: smooth; .scroll-to-top { padding: 6px 12px; position: fixed; bottom: -4px; right: -40px; z-index: 1001; opacity: 0; font-size: 22px; &:hover { transform: translateY(0); } } &.y-scrolled { .scroll-to-top { bottom: 4px; right: 4px; opacity: 1; } } } ul { margin: 0; padding: 0; list-style-type: none; &.assigned-listener-list { margin-top: 15px; } li { margin: 0; padding: 0; li { display: flex; align-items: center; counter-increment: section; &::before { content: counters(section, ".") ". "; padding: 6px 6px 6px 8px; display: block; color: #aaa; font-size: 14px; } &:not(:last-of-type) { border-bottom: 1px solid #f1f1f1; } } span { display: block; &.selector { padding: 8px 10px; font-weight: bold; background: #f4f4f4; } &.config { padding: 10px 2px; } } } } .table-container { margin: 20px 0; overflow: auto; table, td { border: 1px solid rgb(129, 114, 114); border-collapse: collapse; } table { width: 100%; td, tr { padding: 8px; white-space: nowrap; } } } del { text-decoration-line: overline underline line-through; text-decoration-color: #f00; text-decoration-thickness: auto; text-shadow: 0px 2px 11px #f00; opacity: .8; } ins { font-weight: bold; } hr { margin: .5rem 0 .75rem; } .d-none { display: none; } .flex-center-center { display: flex; align-items: center; justify-content: center; a, small { margin: 0 5px; } &.src-nav { margin: 0 1.5rem 1.5rem; } } .container { margin: 0 auto; padding: 0 20px; max-width: 1200px; } .header { margin-bottom: 30px; padding: 40px 0; text-align: center; color: white; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } .section { margin: 30px 0; padding: 30px; border-radius: 10px; background: white; box-shadow: 0 4px 6px rgba(0,0,0,0.1); h2 { margin-bottom: 20px; padding-bottom: 10px; color: #333; border-bottom: 2px solid rgb(122, 145, 248); } } .btn { margin: 5px 2px; padding: 12px 24px; display: inline-flex; align-items: center; white-space: nowrap; border: none; border-radius: 6px; font-size: 14px; color: white; background: #667eea; cursor: pointer; transition: all .3s ease; &:hover { background: #5a6fd8; transform: translateY(-2px); } &.btn-success { background: #28a745; &:hover { background: #218838; } } &.btn-danger { background: #dc3545; &:hover { background: #c82333; } } &.btn-warning { background: #ffc107; color: #212529; &:hover { background: #e0a800; } } } .dynamic-grid { display: grid; gap: 20px; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); } .card { padding: 20px; border: 1px solid #dee2e6; border-radius: 8px; background: #f8f9fa; transition: all .3s ease; &:hover { transform: translateY(-5px); box-shadow: 0 8px 25px rgba(0,0,0,0.15); } h3 { margin-bottom: 10px; color: #495057; } } .todo-item { margin: 10px 0; padding: 15px; display: flex; justify-content: space-between; align-items: center; border: 1px solid #ddd; border-radius: 6px; background: #fff; transition: all .3s ease; &.completed { text-decoration: line-through; opacity: 0.7; background: #d4edda; } } .form-group { margin: 15px 0; label { margin-bottom: 5px; display: block; font-weight: bold; color: #555; } &.dynamic-group { margin: 5px 0; display: flex; align-items: center; &:last-of-type { margin-bottom: 20px; } label, button { white-space: nowrap; } label { margin: 0 10px 0 0; opacity: .7; } } } .form-control { padding: 10px; width: 100%; font-size: 14px; border: 1px solid #ddd; border-radius: 4px; &:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2); } } .counter { margin-left: 10px; padding: 5px 10px; display: inline-block; border-radius: 20px; font-size: 12px; color: white; background: #667eea; } .log-container { font-family: monospace; font-size: 12px; h2 { margin: 5px 0 15px; } .log { margin: 15px 0; padding: 15px; height: 200px; background: #f5f5f5; border-radius: 6px; border: 1px solid #dee2e6; overflow-y: auto; .log-entry { margin: 2px 0; padding: 2px 0; white-space: nowrap; &:not(:last-of-type) { border-bottom: 1px solid #eee; } } } .show-logs-always { max-width: 1040px; position: fixed; left: 2px; bottom: 1px; z-index: 11111; box-shadow: 0 0 8px 2px #c1b3b3; .log { margin: 0; overscroll-behavior: contain; } } } body.show-logs-always { padding-bottom: 400px; &.y-scroll-bottom { .show-logs-always { box-shadow: 0 0 8px 0px #c1b3b3; bottom: 180px; opacity: .85; } } } .tabs { margin: 20px 0; display: flex; overflow: hidden; border-radius: 6px; background: #f8f9fa; .tab { flex: 1; padding: 15px; text-align: center; cursor: pointer; border: none; background: #e9ecef; transition: all .3s ease; &.active { color: white; background: #667eea; } } } .tab-content { padding: 20px; display: none; border-radius: 6px; background: white; &.active { display: block; } } .badge { margin-left: 8px; padding: 4px 8px; display: inline-block; border-radius: 12px; font-size: 11px; color: white; background: #6c757d; } .progress-bar { margin: 10px 0; width: 100%; height: 6px; border-radius: 3px; background: #e9ecef; overflow: hidden; } .progress-fill { height: 100%; width: 0%; background: linear-gradient(90deg, #667eea, #764ba2); transition: width .3s ease; } .notification { padding: 6px; max-width: 380px; position: fixed; top: 2px; right: 20px; z-index: 1000; border-radius: 6px; color: white; background: linear-gradient(135deg, #667eeaaa 0%, #764ba2ee 100%); transform: translateX(100%); transition: all .4s ease-in-out; color: transparent; &.show { transform: translateX(0); } } .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; span { padding: 0 15px; display: block; max-width: 95%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } button { padding: 10px 15px; background:rgb(35, 126, 56); border: none; color: white; cursor: pointer; font-weight: bold; font-size: 22px; } &:not(:last-of-type) { border-bottom: 1px solid #c3c3c3; } } .stats { margin: 20px 0; display: grid; gap: 20px; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); line-height: 1; .stat-card { padding: 20px; color: white; text-align: center; border-radius: 10px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } .stat-number { margin-bottom: 10px; font-size: 2em; font-weight: bold; } .stat-label { font-size: 0.9em; opacity: 0.9; } } .stats-placeholder { height: 0; } .stats-section { width: 100%; position: absolute; transition: all .3s ease; .container { margin: 0 auto; padding: 0 20px; .section { margin: 0; } } &.sticky { position: fixed; top: 0; left: 0; right: 0; z-index: 1000; background: rgba(255, 255, 255, 0.75); backdrop-filter: blur(10px); border-bottom: 1px solid rgba(102, 126, 234, 0.2); box-shadow: 0 0px 16px 1px rgba(0, 0, 0, 0.3); .container { padding: 0 20px; } .section { margin: 0; padding: 0 4px 1px; background: transparent; box-shadow: none; } .stats { margin: 5px 0; gap: 5px; } .stat-card { padding: 4px 6px; border-radius: 3px; } .stat-number { margin: 4px 0; font-size: 1.2em; } .stat-label { margin-bottom: 4px; font-size: 0.7em; } h2 { display: none; } } } .ypsi-buttons { flex-wrap: wrap; } .scroll-area { height: 300px; overflow-y: scroll; background: linear-gradient(to bottom, #e3f2fd, #bbdefb); border: 2px solid #2196f3; border-radius: 8px; padding: 20px; .scroll-content { background: linear-gradient(to bottom, #fff3e0, #ffcc02); padding: 20px; border-radius: 4px; } } .footer { padding: 20px; position: fixed; bottom: 0; left: 0; right: 0; z-index: 1000; color: white; text-align: center; white-space: nowrap; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); transform: translateY(100%); transition: transform .3s ease; &.visible { transform: translateY(0); } h3 { margin-bottom: 10px; } p { margin: 5px 0; opacity: 0.9; } a { color: #fff; } } .nav-container { padding: 0; box-shadow: 0 0 0 #6b7894; background: #f7f7f7; margin: 20px 0 0; } @media only screen and (min-width: 640px) { .ypsi-buttons { display: flex; justify-content: space-between; .helper-btn, .starter-btn { display: flex; align-items: center; } } } @media only screen and (max-width: 480px) { body { padding-bottom: 260px; } .stats-section { .stats { display: flex; flex-wrap: wrap; gap: 5px; } .stat-card { padding: 4px 6px; flex: 1 32%; border-radius: 3px; .stat-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin: 0 auto; max-width: 90px; } .stat-number { margin-bottom: 1px; font-size: 1.5em; } } } } .y-fade-in { animation: yFadeIn .4s ease forwards; } .y-blink { animation: yFadeIn .4s ease forwards, yBlink 1.5s ease forwards; } @keyframes yFadeIn { from { opacity: 0 } to { opacity: 1 } } @keyframes yBlink { from { color: #0fec3f } to { color: #222 } } </style> </head> <body class="y-scroll-top"> <div class="header y-fade-in"> <div class="container"> <h1>🚀 YpsilonEventHandler SPA Demo</h1> <p>Demonstrating the power of event delegation with dynamic content</p> <p><strong>⚡ ONLY 9 EVENT LISTENERS for this ENTIRE SPA! ⚡</strong></p> <p><strong>ONE handler on body handles ALL events - no matter when elements are created!</strong></p> </div> </div> <div class="stats-section y-fade-in" id="stats-section"> <div class="container"> <!-- Stats Dashboard --> <div class="section"> <h2>📊 Live Statistics</h2> <div class="stats"> <div class="stat-card"> <div class="stat-number" id="click-count">0</div> <div class="stat-label">Total Clicks</div> </div> <div class="stat-card"> <div class="stat-number" id="element-count">0</div> <div class="stat-label">Dynamic Elements</div> </div> <div class="stat-card"> <div class="stat-number" id="todo-count">0</div> <div class="stat-label">Todo Items</div> </div> <div class="stat-card"> <div class="stat-number" id="event-count">0</div> <div class="stat-label">Events Handled</div> </div> <div class="stat-card"> <div class="stat-number" id="scroll-position">0</div> <div class="stat-label">Scroll Position</div> </div> </div> </div> </div> </div> <!-- Placeholder to prevent layout shift when stats become sticky --> <div class="stats-placeholder" id="stats-placeholder"></div> <div class="container y-fade-in"> <!-- Dynamic Content Creator --> <div class="section"> <h2>🎯 Dynamic Content Creator</h2> <p>Create new elements dynamically - they work instantly without new listeners!</p> <div style="margin: 20px 0;"> <button class="btn btn-success y-btn" data-action="createCard">➕ Create Card</button> <button class="btn btn-warning y-btn" data-action="createButton">🔘 Create Button</button> <button class="btn btn-danger y-btn" data-action="clearAll">🗑️ Clear All</button> </div> <div class="dynamic-grid" id="dynamic-container"><!-- Dynamic elements --></div> </div> <!-- Todo List Manager --> <div class="section"> <h2>✅ Todo List Manager</h2> <p>Add, complete, and delete todos - all handled by the same body listener!</p> <div class="form-group"> <input type="text" class="form-control y-input" id="todo-input" placeholder="Enter a new todo..." data-action="todo-input"> <button class="btn y-btn" data-action="addTodo" style="margin-top: 10px;">➕ Add Todo</button> </div> <div id="todo-container"><!-- Todo items --></div> </div> <!-- Tab System --> <div class="section"> <h2>📑 Dynamic Tab System</h2> <p>Switch between tabs - all handled by event delegation!</p> <div class="tabs"> <button class="tab active" data-action="switchTab" data-tab="tab1">Tab 1</button> <button class="tab" data-action="switchTab" data-tab="tab2">Tab 2</button> <button class="tab" data-action="switchTab" data-tab="tab3">Tab 3</button> <button class="btn y-btn" data-action="addTab" style="margin-left: auto;">➕ Add Tab</button> </div> <div class="tab-content active" id="tab1"> <h3>Tab 1 Content</h3> <p>This is the content of tab 1. Click buttons below to test event delegation:</p> <button class="btn y-btn" data-action="tabAction" data-message="Tab 1 button clicked!">Click Me</button> <button class="btn btn-success y-btn" data-action="createElementInTab" data-target="tab1">Create Element</button> </div> <div class="tab-content" id="tab2"> <h3>Tab 2 Content</h3> <p>This is the content of tab 2. These buttons were here from the start:</p> <button class="btn y-btn" data-action="tabAction" data-message="Tab 2 button clicked!">Click Me</button> <button class="btn btn-warning y-btn" data-action="createElementInTab" data-target="tab2">Create Element</button> </div> <div class="tab-content" id="tab3"> <h3>Tab 3 Content</h3> <p>This is the content of tab 3. Event delegation works everywhere:</p> <button class="btn y-btn" data-action="tabAction" data-message="Tab 3 button clicked!">Click Me</button> <button class="btn btn-danger y-btn" data-action="createElementInTab" data-target="tab3">Create Element</button> </div> </div> <!-- Form Interactions --> <div class="section"> <h2>📝 Form Interactions</h2> <p>All form events handled by the same body listener with delegation:</p> <div class="form-group"> <label for="name-input">Name:</label> <input type="text" id="name-input" class="form-control y-input" data-action="name-input" placeholder="Enter your name"> </div> <div class="form-group"> <label for="email-input">Email:</label> <input type="email" id="email-input" class="form-control y-input" data-action="email-input" placeholder="Enter your email"> </div> <div class="form-group"> <label for="message-input">Message:</label> <textarea id="message-input" class="form-control y-input" data-action="message-input" placeholder="Enter your message" rows="3"></textarea> </div> <button class="btn y-btn" data-action="submitForm">📨 Submit Form</button> <button class="btn btn-warning y-btn" data-action="addFormField">➕ Add Field</button> </div> <!-- Scroll area --> <div class="section"> <h2>Scroll Test (Passive Listeners)</h2> <p>Scroll in the area below to test passive scroll events:</p> <div class="scroll-area" id="scroll-area"> <div class="scroll-content"> <h3>Scrollable Content</h3> <p>This scroll area demonstrates passive event listeners.</p> <p>When you scroll here, the event will be handled with passive:true automatically.</p> <p>This improves performance by telling the browser that preventDefault() won't be called.</p> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p> <p>Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p> <p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.</p> <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore.</p> <p>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia.</p> <p>Keep scrolling to see more content...</p> <p>The scroll events are being logged to the output area above.</p> <p>Notice how smooth the scrolling is with passive listeners!</p> <p>This is the power of automatic passive listener detection.</p> <p>YpsilonEventHandler handles this optimization automatically.</p> <p>No manual configuration needed!</p> <p>You're near the bottom now.</p> <p>Final paragraph of scrollable content.</p> </div> </div> </div> <!-- Assigned Event Listeners --> <div class="section" id="ypsilon-assigned"> <h2>📡 Assigned Event Listeners</h2> <p><strong>These are the event listeners assigned to this entire SPA!</strong></p> <p>No additional listeners will be created - all dynamic content uses these same <del>5</del> <ins>9</ins> listeners via event delegation.</p> <div></div> <hr /> <div class="nav-container"> <!-- Navigation to other examples --> <nav class="main-nav"> <div> <a href="./index.html" class="btn-primary">Start</a> <a href="./basic-example.html" class="btn-secondary">Basic Example</a> <a href="./reactive-y.html" class="btn-danger">Reactive Demo</a> <a href="./single-listener-multiple-actions.html" class="btn-purple">Single Listener</a> <a href="./spa.html" class="btn-disabled">SPA Demo</a> <a href="./ai-reviews.html" class="btn-warning">AI Reviews</a> <a href="https://github.com/eypsilon/YpsilonEventHandler" class="btn-dark">GitHub</a> </div> </nav> </div> </div> <!-- Staggeringly Performant --> <div class="section"> <h2>🏆 Staggeringly Performant</h2> <p>Just click or focus on any button that generates something visual, like "➕ Create Card" and keep enter pressed! While you generate, scroll using the scrollbar. It allows us to scroll fast. Try it for yourself, it took me seconds.</p> <div class="table-container"> <table class="performance-table"> <tr> <td class="stat-label-number">Clicks: <b>2,887</b></td> <td class="stat-label-number">Dynamic Elements: <b>2,627</b></td> <td class="stat-label-number">Events Handled: <b>3,664</b></td> <td class="stat-label-number">Scroll Position: <b>143k</b></td> <td class="stat-label-number">Event Listeners: <b>6</b></td> </tr> </table> </div> <p><em>All achieved with ~500 lines of vanilla JavaScript + native handleEvent interface.</em> One has to see it to believe it.</p> </div> <!-- Event Log --> <div class="section log-wrapper"> <div class="log-container"> <h2>📋 Event Log</h2> <p>Watch how ONE body listener handles ALL events <small>(max: 50)</small>:</p> <div class="log-wrapper"> <div class="log y-fade-in" id="event-log"> <div class="log-entry">Event log will appear here...</div> </div> </div> </div> <div class="ypsi-buttons"> <div class="helper-btn"> <button class="btn btn-warning y-btn" data-action="clearLog">🗑️ Clear Log</button> <button class="btn y-btn" data-action="debugHandler" style="background: #6f42c1;">🔍 Debug Handler</button> <button class="btn y-btn" data-action="testDispatch" style="background: #fd7e14;">🔥 Test Dispatch</button> <label class="btn" title="Show Event Log Always" style="font-size: 11px;"> <input type="checkbox" name="show-logs-always" value="true" data-action="showLogsAlways"> 📌 Always </label> </div> <div class="starter-btn"> <span id="handler-status" style="margin-right: 10px; font-weight: bold; color: #28a745;">ACTIVE</span> <button class="btn" style="background: #28a745;" id="recreate-btn" disabled>🔄 Recreate Handler</button> <button class="btn" style="background: #dc3545;" id="destroy-btn">💀 Destroy Handler</button> </div> </div> </div> </div> <!-- Notification --> <div class="notification" id="notification">Action completed!</div> <!-- Scroll to top --> <div class="scroll-to-top-container"> <button type="button" class="scroll-to-top btn y-btn" data-action="scrollToTop">▲</button> </div> <div class="flex-center-center src-nav"> <small>© Engin Ypsilon & Claude Van DOM</small> | <a href="https://github.com/eypsilon/YpsilonEventHandler">github</a> | <a href="https://www.npmjs.com/package/ypsilon-event-handler">npm</a> | <a href="./spa.html">reload</a> </div> <!-- Footer --> <div class="footer" id="footer"> <h3>🎉 You've reached the bottom!</h3> <p>✨ This footer appeared thanks to scroll event delegation</p> <p>🚀 ONE body listener handled ALL scroll events on this page</p> <p>💫 YpsilonEventHandler - The power of native handleEvent interface</p> </div> <script src="https://cdn.jsdelivr.net/npm/ypsilon-event-handler@1.5.0/ypsilon-event-handler.min.js"></script> <script> const config = { debounce: 300, throttle: 150, throttleResize: 500, throttleScrollArea: 400, threshold: { top: 100, bottom: 20, showFooter: 20, }, notification: { timeout: 3000 } } // SPA Event Handler - ONE handler for the entire application! class SPAEventHandler extends YpsilonEventHandler { constructor() { super({ 'body': [ { type: 'click' }, { type: 'input', debounce: config.debounce }, { type: 'change' }, { type: 'keydown' }, { type: 'testdispatch' } ], 'window': [ { type: 'scroll', throttle: config.throttle }, { type: 'resize', throttle: config.throttleResize }, { type: 'beforeunload', capture: true } ], '#scroll-area': [ { type: 'scroll', handler: 'handleAreaScroll', throttle: config.throttleScrollArea } ], }, {}, { enableStats: true }); this.counters = { clicks: 0, elements: 0, todos: 0, events: 0 }; this.tabCounter = 3; this.updateStats(); this.initializePlaceholder(); } handleVisibilitychange(event) { // Useful to perform final actions right before user leaves if (document.visibilityState == 'hidden') { console.log('Bye bye!') } } // If user has interacted with the page, this acts as a // `prevent-data-loss` tool, a pretty unrelieable `prevent-data-loss` handleBeforeunload(event) { if (this.hasUserInteracted()) { event.preventDefault(); event.returnValue = 'Are you sure you want to leave Ypsilon?'; } else { // User hasn't interacted, clean up silently this.destroy(); } } handleAreaScroll(event, target) { this._updateStats('events'); const scrollTop = target.scrollTop || 0; this.log(`📜 DIV scroll: ${Math.round(scrollTop)}px (throttled ${config.throttleScrollArea}ms)`); } handleResize(event, target) { this._updateStats('events'); if (!document.querySelector('.stats-placeholder.active')) { this.updatePlaceholderHeights(); } const width = window.innerWidth; const height = window.innerHeight; this.log(`📏 Window resize: ${width}x${height}px (throttled ${config.throttleResize}ms)`); } handleClick(event, target) { this._updateStats('clicks', 'events'); // Handle different button types based on data-action const action = target.dataset.action; if (!action) return; if (typeof this[action] === 'function') { return this[action](target, event); } this.log(`🖱️ Click: ${action} (${target.textContent.trim()})`); } handleInput(event, target) { this._updateStats('events'); const action = target.dataset.action; if (!action) return; // Handle todo input enter key if (action === 'todo-input' && event.type === 'keydown' && event.key === 'Enter') { this.addTodo(); } this.log(`⌨️ Input: ${action} = "${target.value}" (debounced ${config.debounce})`); } handleChange(event, target) { this._updateStats('events'); this.log(`🔄 Change: ${target.dataset.action || 'unknown'} = "${target.value}"`); } handleKeydown(event, target) { if (target.dataset.action === 'todo-input' && event.key === 'Enter') { this.addTodo(); } } handleScroll(event, target) { this._updateStats('events'); const scrollY = window.scrollY || window.pageYOffset; const windowHeight = window.innerHeight; const docHeight = document.documentElement.scrollHeight; // Update scroll position in stats this.updateScrollStats(scrollY, docHeight, windowHeight); // Handle sticky stats this.updateStickyStats(scrollY); // Handle footer visibility this.updateFooterVisibility(scrollY, docHeight, windowHeight); // Handle body scroll classes this.updateScrollClasses(scrollY, docHeight, windowHeight); if (!this.waitingScroll) { this.waitingScroll = true; setTimeout(() => { this.waitingScroll = false; this.log(`🌐 Window scroll: ${Math.round(scrollY)}px (throttled ${config.throttle})`); }, 250); } } showLogsAlways(target) { const logs = this.getEventLogContainer(); if (logs) { const logContainer = logs.parentNode.parentNode; const exec = target.checked ? 'add' : 'remove'; logContainer.style.minHeight = target.checked ? `${logContainer.offsetHeight + 15}px` : null ; logs.parentNode.classList[exec]('show-logs-always'); document.body.classList[exec]('show-logs-always'); } } scrollToTop() { this._updateStats('events'); window.scrollTo({ top: 0, behavior: 'smooth' }); this.log(`▲ Scrolling to top`); } createCard() { const container = document.getElementById('dynamic-container'); const cardId = `card-${Date.now()}`; const card = document.createElement('div'); card.className = 'card y-fade-in'; card.id = cardId; card.innerHTML = ` <h3>Dynamic Card #${this.counters.elements + 1}</h3> <p>This card was created dynamically but works instantly!</p> <button class="btn" data-action="dynamicBtn" data-dynamic="true">Click Me!</button> <button class="btn btn-danger y-btn" data-action="removeElement" data-target="${cardId}">🗑️ Remove</button> `; container.appendChild(card); this._updateStats('elements'); this.log(`➕ Created card: ${cardId}`); } createButton() { const container = document.getElementById('dynamic-container'); const buttonId = `btn-${Date.now()}`; const wrapper = document.createElement('div'); wrapper.className = 'card y-fade-in'; wrapper.id = buttonId; wrapper.innerHTML = ` <h3>Dynamic Button #${this.counters.elements + 1}</h3> <button class="btn btn-success" data-action="dynamicBtn" data-dynamic="true">I'm Dynamic!</button> <button class="btn btn-warning" data-action="dynamicBtn" data-dynamic="true">Me Too!</button> <button class="btn btn-danger y-btn" data-action="removeElement" data-target="${buttonId}">🗑️ Remove</button> `; container.appendChild(wrapper); this._updateStats('elements'); this.log(`➕ Created button set: ${buttonId}`); } clearAll() { const container = document.getElementById('dynamic-container'); const elementsToRemove = container.children.length; container.innerHTML = ''; this.counters.elements = Math.max(0, this.counters.elements - elementsToRemove); this.updateStats(); this.log(`🗑️ Cleared ${elementsToRemove} dynamic elements from content creator`); } addTodo() { const input = document.getElementById('todo-input'); const text = input.value.trim(); if (!text) return; const container = document.getElementById('todo-container'); const todoId = `todo-${Date.now()}`; const todo = document.createElement('div'); todo.className = 'todo-item y-fade-in'; todo.id = todoId; todo.innerHTML = ` <span>${this.escapeHtml(text)}</span> <div> <button class="btn btn-success y-btn" data-action="completeTodo" data-target="${todoId}">✅ Complete</button> <button class="btn btn-danger y-btn" data-action="deleteTodo" data-target="${todoId}">🗑️ Delete</button> </div> `; input.value = ''; container.appendChild(todo); this._updateStats('todos'); this.log(`➕ Added todo: ${text}`); } completeTodo(target) { const todoId = target.dataset.target; const todo = document.getElementById(todoId); if (todo) { todo.classList.toggle('completed'); this.log(`✅ Toggled todo: ${todoId}`); } } deleteTodo(target) { const todoId = target.dataset.target; const todo = document.getElementById(todoId); if (todo) { todo.remove(); this.counters.todos--; this.updateStats(); this.log(`🗑️ Deleted todo: ${todoId}`); } } switchTab(target) { const tabId = target.dataset.tab; // Hide all tabs document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); document.querySelectorAll('.tab').forEach(tab => { tab.classList.remove('active'); }); // Show selected tab const tabContent = document.getElementById(tabId); const tabButton = document.querySelector(`[data-tab="${tabId}"]`); if (tabContent) tabContent.classList.add('active'); if (tabButton) tabButton.classList.add('active'); this.log(`📑 Switched to tab: ${tabId}`); } addTab(target) { this.tabCounter++; const tabId = `tab${this.tabCounter}`; // Add tab button const tabsContainer = document.querySelector('.tabs'); const addButton = tabsContainer.querySelector('[data-action="addTab"]'); const newTab = document.createElement('button'); newTab.className = 'tab y-fade-in'; newTab.dataset.action = 'switchTab'; newTab.dataset.tab = tabId; newTab.textContent = `Tab ${this.tabCounter}`; tabsContainer.insertBefore(newTab, addButton); // Add tab content const tabContent = document.createElement('div'); tabContent.className = 'tab-content y-fade-in'; tabContent.id = tabId; tabContent.innerHTML = ` <h3>Tab ${this.tabCounter} Content</h3> <p>This tab was created dynamically! Event delegation still works.</p> <button class="btn y-btn" data-action="tabAction" data-message="Dynamic tab ${this.tabCounter} button clicked!">Click Me</button> <button class="btn btn-success y-btn" data-action="createElementInTab" data-target="${tabId}">Create Element</button> `; // Insert after the tabs container tabsContainer.parentNode.appendChild(tabContent); this.switchTab(newTab); this.log(`➕ Added dynamic tab: ${tabId}`); } tabAction(target, event) { this.showNotification(target.dataset.message || 'Tab action triggered!'); } dynamicBtn(target) { this.log(`🔘 Dynamic button: ${target.textContent}`, `Dynamic button clicked: ${target.textContent}`); } createElementInTab(target) { const targetTab = target.dataset.target; const tabContent = document.getElementById(targetTab); if (!tabContent) return; const elementId = `element-${Date.now()}`; const element = document.createElement('div'); const useIndex = this.counters.elements + 1; element.id = elementId; element.className = 'y-fade-in'; element.style.cssText = 'margin: 10px 0; padding: 10px; background: #f0f0f0; border-radius: 4px;'; element.innerHTML = ` <p><strong>Dynamic Element #${useIndex}</strong></p> <button class="btn" data-action="dynamicBtn" data-dynamic="true">I work too! <u class="d-none">${useIndex}</u></button> <button class="btn btn-danger y-btn" data-action="removeElement" data-target="${elementId}">Remove</button> `; tabContent.appendChild(element); this._updateStats('elements'); this.log(`➕ Created element in ${targetTab}: ${elementId}`); } addFormField() { // Find the form section by looking for the submit button const submitButton = document.querySelector('[data-action="submitForm"]'); const section = submitButton.closest('.section'); const fieldId = `field-${Date.now()}`; const inputId = `input-${Date.now()}`; const formGroup = document.createElement('div'); formGroup.id = fieldId; formGroup.className = 'form-group dynamic-group y-fade-in'; formGroup.innerHTML = ` <label for="${inputId}">Dynamic Field #${this.counters.elements + 1}:</label> <input type="text" id="${inputId}" name="${inputId}" class="form-control y-input" data-action="dynamic-field" placeholder="Dynamic field"> <button class="btn btn-danger y-btn" data-action="removeElement" data-target="${fieldId}" style="margin-top: 5px;">Remove Field</button> `; section.insertBefore(formGroup, submitButton); this._updateStats('elements'); this.log(`➕ Added form field: ${fieldId}`); } submitForm() { const inputs = document.querySelectorAll('.y-input'); const formData = {}; inputs.forEach((input, idx) => { const action = input.dataset.action; if (action && input.value.trim()) { formData[`${action}-${idx}`] = input.value.trim(); } }); this.log(`📨 Form submitted with data: ${JSON.stringify(formData)}`, 'Form submitted successfully!'); } _updateStats() { for (const update of arguments) { if (typeof this.counters[update] !== 'undefined') { this.counters[update]++; } } this.updateStats(); } updateStats() { document.getElementById('click-count').textContent = this.counters.clicks; document.getElementById('element-count').textContent = this.counters.elements; document.getElementById('todo-count').textContent = this.counters.todos; document.getElementById('event-count').textContent = this.counters.events; } updateScrollStats(scrollY, docHeight, windowHeight) { document.getElementById('scroll-position').textContent = Math.round(scrollY); } getMainStatsEl() { return document.getElementById('stats-section'); } updateStickyStats(scrollY) { const statsSection = this.getMainStatsEl(); const placeholder = document.getElementById('stats-placeholder'); // Use placeholder position to determine when to stick/unstick const placeholderRect = placeholder.getBoundingClientRect(); const shouldBeSticky = placeholderRect.top + 130 <= 0; if (shouldBeSticky && !statsSection.classList.contains('sticky')) { statsSection.classList.add('sticky'); placeholder.classList.add('active'); this._updateFadeInClass(statsSection); this.log(`📌 Stats became sticky`); } else if (!shouldBeSticky && statsSection.classList.contains('sticky')) { statsSection.classList.remove('sticky'); placeholder.classList.remove('active'); this._updateFadeInClass(statsSection); this.log(`📌 Stats unstuck with fade-in`); } } _updateFadeInClass(element) { element.classList.remove('y-fade-in'); element.offsetHeight; element.classList.add('y-fade-in'); } updateFooterVisibility(scrollY, docHeight, windowHeight) { const footer = document.getElementById('footer'); const threshold = config.threshold.showFooter; // Show when x from bottom if (scrollY + windowHeight >= docHeight - threshold) { footer.classList.add('visible'); } else { footer.classList.remove('visible'); } } updateScrollClasses(scrollY, docHeight, windowHeight) { const body = document.body; const isAtTop = scrollY <= config.threshold.top; const isAtBottom = scrollY + windowHeight >= docHeight - config.threshold.bottom; // Only change classes when needed to prevent flicke