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