website with slop placeholders
This commit is contained in:
+8
File diff suppressed because one or more lines are too long
Executable
+8454
File diff suppressed because one or more lines are too long
Executable
+469
File diff suppressed because one or more lines are too long
Executable
+1
File diff suppressed because one or more lines are too long
Executable
+77
@@ -0,0 +1,77 @@
|
||||
var enable_debug = true;
|
||||
|
||||
var starterReady = (typeof $ !== 'undefined' && $.ready)
|
||||
? $.ready.bind($)
|
||||
: function(callback) {
|
||||
if (document.readyState !== 'loading') {
|
||||
callback();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', callback);
|
||||
}
|
||||
};
|
||||
|
||||
var UI = {
|
||||
|
||||
smoothScrollToNamedAnchors: function() {
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
target.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
enablePageTransitions: function() {
|
||||
var style = document.createElement('style');
|
||||
style.textContent = `
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
::view-transition-old(root) {
|
||||
animation-name: fade-out;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
animation-name: fade-in;
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}`;
|
||||
document.body.appendChild(style);
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
const link = e.target.closest('a[href]');
|
||||
if (!link) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
document.startViewTransition(() => {
|
||||
window.location.href = link.href;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
init: function() {
|
||||
//UI.enablePageTransitions();
|
||||
UI.smoothScrollToNamedAnchors();
|
||||
document.body.classList.add('loaded');
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
starterReady(UI.init);
|
||||
Executable
+897
@@ -0,0 +1,897 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>U-EventEmitter Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--space: 8px;
|
||||
--radius: 5px;
|
||||
|
||||
--gray: #6c757d;
|
||||
--gray-bg: #f5f5f5;
|
||||
--blue: #007acc;
|
||||
--green: #28a745;
|
||||
--white: white;
|
||||
--dark: #333;
|
||||
|
||||
--max-width: 1400px;
|
||||
--sidebar: 400px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: var(--space);
|
||||
background: var(--gray-bg);
|
||||
}
|
||||
|
||||
.container {
|
||||
background: var(--white);
|
||||
padding: calc(var(--space) * 1.5);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.main-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: calc(var(--space) * 1.5);
|
||||
}
|
||||
|
||||
.api-column {
|
||||
background: var(--gray-bg);
|
||||
padding: var(--space);
|
||||
border-radius: var(--radius);
|
||||
border-left: 4px solid var(--green);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--dark);
|
||||
text-align: center;
|
||||
margin-bottom: calc(var(--space) * 1.5);
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--gray);
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
padding-bottom: var(--space);
|
||||
margin-top: calc(var(--space) * 1.5);
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin: var(--space) 0;
|
||||
padding: var(--space);
|
||||
background: var(--gray-bg);
|
||||
border-radius: var(--radius);
|
||||
border-left: 4px solid var(--blue);
|
||||
}
|
||||
|
||||
.controls, .input-group {
|
||||
display: flex;
|
||||
gap: var(--space);
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.controls { flex-wrap: wrap; }
|
||||
.input-group { align-items: center; }
|
||||
.feature-grid { display: grid; gap: var(--space); margin: var(--space) 0; }
|
||||
|
||||
button {
|
||||
background: var(--blue);
|
||||
color: var(--white);
|
||||
border: none;
|
||||
padding: var(--space) var(--space);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover { filter: brightness(0.9); }
|
||||
button:disabled { background: #ccc; cursor: not-allowed; }
|
||||
|
||||
input[type="text"] {
|
||||
padding: var(--space);
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--radius);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status, .code-example {
|
||||
font-family: monospace;
|
||||
padding: var(--space);
|
||||
border-radius: var(--radius);
|
||||
margin: var(--space) 0;
|
||||
}
|
||||
|
||||
.status {
|
||||
background: var(--dark);
|
||||
color: #0f0;
|
||||
white-space: pre-wrap;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.code-example {
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
padding: 15px;
|
||||
overflow-x: auto;
|
||||
margin: var(--space) 0;
|
||||
}
|
||||
|
||||
.highlight { color: #68d391; }
|
||||
.keyword { color: #fbb6ce; }
|
||||
.string { color: #fbd38d; }
|
||||
|
||||
.event-visual {
|
||||
height: 100px;
|
||||
background: linear-gradient(45deg, #1e3c72, #2a5298);
|
||||
border-radius: var(--radius);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--white);
|
||||
font-weight: bold;
|
||||
margin: 15px 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pulse-wave {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.3;
|
||||
background: radial-gradient(circle at center, rgba(255,255,255,0.2) 0%, transparent 70%);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.event-indicator {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #68d391;
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.event-indicator.active {
|
||||
opacity: 1;
|
||||
animation: blink 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(0.8); opacity: 0.3; }
|
||||
50% { transform: scale(1.2); opacity: 0.1; }
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.api-section { margin: 15px 0; }
|
||||
.api-section h3 {
|
||||
color: var(--green);
|
||||
margin: 0 0 var(--space) 0;
|
||||
}
|
||||
|
||||
.api-method {
|
||||
font-family: monospace;
|
||||
margin: 3px 0;
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.api-method .method-name { color: var(--blue); font-weight: bold; }
|
||||
.api-method .return-type { color: #6f42c1; }
|
||||
.api-method .param { color: #e83e8c; }
|
||||
|
||||
.api-description {
|
||||
margin: var(--space) 0;
|
||||
}
|
||||
|
||||
.api-options {
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.help-text {
|
||||
color: var(--gray);
|
||||
margin: var(--space) 0;
|
||||
}
|
||||
|
||||
.code-comment {
|
||||
color: #68d391;
|
||||
}
|
||||
|
||||
.event-log {
|
||||
background: var(--dark);
|
||||
color: #0f0;
|
||||
font-family: monospace;
|
||||
padding: var(--space);
|
||||
border-radius: var(--radius);
|
||||
height: 150px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
margin: var(--space) 0;
|
||||
}
|
||||
|
||||
.listener-counter {
|
||||
background: var(--blue);
|
||||
color: var(--white);
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.main-layout { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>U-EventEmitter Demo</h1>
|
||||
|
||||
<div class="main-layout">
|
||||
<div class="demo-column">
|
||||
<h2>Basic Event System</h2>
|
||||
<div class="demo-section">
|
||||
<div class="controls">
|
||||
<button onclick="basicDemo()">Basic Subscribe & Emit</button>
|
||||
<button onclick="multipleListenersDemo()">Add Multiple Listeners</button>
|
||||
<button onclick="emitToMultiple()">Emit to All</button>
|
||||
</div>
|
||||
<div class="status" id="basic-output"></div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Advanced Features</h3>
|
||||
<div class="controls">
|
||||
<button onclick="slotDemo()">Slot-based Handlers</button>
|
||||
<button onclick="replaceSlotHandler()">Replace Handler</button>
|
||||
<button onclick="emitSlotEvent()">Test Slot System</button>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button onclick="autoRemovalDemo()">Self-removing Handler</button>
|
||||
<button onclick="triggerAutoRemoval()">Trigger Auto-removal</button>
|
||||
</div>
|
||||
<div class="status" id="advanced-output"></div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Interactive Event System</h3>
|
||||
<div class="input-group">
|
||||
<input type="text" id="event-name" placeholder="Event name" value="chat">
|
||||
<input type="text" id="event-data" placeholder="Event data" value="Hello EventEmitter!">
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button onclick="addListener()">Add Listener <span id="listenerCount" class="listener-counter">0</span></button>
|
||||
<button onclick="emitCustomEvent()">Emit Event</button>
|
||||
<button onclick="removeAllListeners()">Clear Listeners</button>
|
||||
</div>
|
||||
<div class="event-log" id="event-log">Ready for interactive events...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-column">
|
||||
<h2>API Overview</h2>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>EventEmitter Constructor</h3>
|
||||
<div class="api-method"><span class="keyword">new</span> <span class="method-name">EventEmitter</span>()</div>
|
||||
<div class="api-description">
|
||||
Creates a new event emitter instance for custom event communication.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Core Methods</h3>
|
||||
<div class="api-method"><span class="method-name">on</span>(<span class="param">event, handler, slot?</span>) → <span class="return-type">EventEmitter</span></div>
|
||||
<div class="api-method"><span class="method-name">emit</span>(<span class="param">event, ...args</span>) → <span class="return-type">number</span></div>
|
||||
<div class="api-method"><span class="method-name">off</span>(<span class="param">event, handler?</span>) → <span class="return-type">EventEmitter</span></div>
|
||||
<div class="api-method"><span class="method-name">clear</span>(<span class="param">event?</span>) → <span class="return-type">EventEmitter</span></div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Key Features</h3>
|
||||
<div class="api-description">
|
||||
<strong>Slot-based Deduplication:</strong> Use slot parameter to replace existing handlers<br>
|
||||
<strong>Auto-removal:</strong> Handlers returning 'remove_handler' are automatically unsubscribed<br>
|
||||
<strong>Return Values:</strong> emit() returns the number of handlers called<br>
|
||||
<strong>Flexible Arguments:</strong> Pass any number of arguments to handlers
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Examples</h3>
|
||||
<pre class="code-example">
|
||||
<span class="code-comment">// Basic usage</span>
|
||||
<span class="keyword">const</span> emitter = <span class="keyword">new</span> <span class="highlight">EventEmitter</span>();
|
||||
emitter.on(<span class="string">'message'</span>, (data) => {
|
||||
console.log(<span class="string">'Received:'</span>, data);
|
||||
});
|
||||
emitter.emit(<span class="string">'message'</span>, <span class="string">'Hello World'</span>);
|
||||
|
||||
<span class="code-comment">// Slot-based replacement</span>
|
||||
emitter.on(<span class="string">'update'</span>, handler1, <span class="string">'ui-updater'</span>);
|
||||
emitter.on(<span class="string">'update'</span>, handler2, <span class="string">'ui-updater'</span>); <span class="code-comment">// Replaces handler1</span>
|
||||
|
||||
<span class="code-comment">// Self-removing handler</span>
|
||||
emitter.on(<span class="string">'init'</span>, () => {
|
||||
console.log(<span class="string">'Initialized!'</span>);
|
||||
<span class="keyword">return</span> <span class="string">'remove_handler'</span>; <span class="code-comment">// Removes itself</span>
|
||||
});
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-layout">
|
||||
<div class="demo-column">
|
||||
<h2>Event Patterns</h2>
|
||||
<div class="demo-section">
|
||||
<h3>Publisher-Subscriber Pattern</h3>
|
||||
<div class="controls">
|
||||
<button onclick="createPublisher()">Create Publisher</button>
|
||||
<button onclick="addSubscribers()">Add Subscribers</button>
|
||||
<button onclick="publishNews()">Publish News</button>
|
||||
</div>
|
||||
<div class="status" id="pubsub-output"></div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Component Communication</h3>
|
||||
<div class="controls">
|
||||
<button onclick="setupComponents()">Setup Components</button>
|
||||
<button onclick="componentInteraction()">Trigger Interaction</button>
|
||||
<button onclick="cascadeEvents()">Cascade Events</button>
|
||||
</div>
|
||||
<div class="status" id="component-output"></div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Handler Counting & Management</h3>
|
||||
<div class="controls">
|
||||
<button onclick="handlerCountDemo()">Count Handlers</button>
|
||||
<button onclick="benchmarkEmission()">Benchmark Emission</button>
|
||||
</div>
|
||||
<div class="status" id="count-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-column">
|
||||
<h2>Advanced Usage</h2>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Event Management</h3>
|
||||
<div class="api-description">
|
||||
<strong>on(event, handler, slot)</strong> - Subscribe to events<br>
|
||||
• event: String event name<br>
|
||||
• handler: Function to call when event is emitted<br>
|
||||
• slot: Optional string key for handler replacement<br><br>
|
||||
<strong>emit(event, ...args)</strong> - Emit events to all listeners<br>
|
||||
• Returns the number of handlers that were called<br>
|
||||
• Passes all additional arguments to handlers<br><br>
|
||||
<strong>off(event, handler)</strong> - Remove specific handler<br>
|
||||
• If handler omitted, removes all handlers for event
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Slot System</h3>
|
||||
<div class="api-description">
|
||||
The slot system prevents duplicate handlers by using string keys:<br>
|
||||
• Same slot key replaces previous handler<br>
|
||||
• Useful for UI updates, state management<br>
|
||||
• Prevents memory leaks from repeated subscriptions<br>
|
||||
• Slot keys are per-event, not global
|
||||
</div>
|
||||
<pre class="code-example">
|
||||
<span class="code-comment">// Without slots: multiple handlers</span>
|
||||
emitter.on(<span class="string">'render'</span>, updateUI);
|
||||
emitter.on(<span class="string">'render'</span>, updateUI); <span class="code-comment">// Now 2 handlers</span>
|
||||
|
||||
<span class="code-comment">// With slots: automatic replacement</span>
|
||||
emitter.on(<span class="string">'render'</span>, updateUI, <span class="string">'ui'</span>);
|
||||
emitter.on(<span class="string">'render'</span>, updateUI, <span class="string">'ui'</span>); <span class="code-comment">// Still 1 handler</span>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Performance Patterns</h3>
|
||||
<div class="api-description">
|
||||
<strong>Event Namespacing:</strong> Use dot notation for hierarchical events<br>
|
||||
<strong>Batch Operations:</strong> Group related events for efficiency<br>
|
||||
<strong>Handler Cleanup:</strong> Use slots or off() to prevent memory leaks<br>
|
||||
<strong>Conditional Emission:</strong> Check handler count before expensive operations
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-layout">
|
||||
<div class="demo-column">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="api-column">
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Memory Management</h3>
|
||||
<div class="api-description">
|
||||
<strong>Use Slots:</strong> Prevent duplicate handlers with slot keys<br>
|
||||
<strong>Clean Up:</strong> Call off() when components are destroyed<br>
|
||||
<strong>Self-removal:</strong> Use 'remove_handler' return for one-time events<br>
|
||||
<strong>Clear All:</strong> Use clear() to remove all listeners for an event
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Common Patterns</h3>
|
||||
<pre class="code-example">
|
||||
<span class="code-comment">// Request-Response pattern</span>
|
||||
emitter.on(<span class="string">'data.request'</span>, (type, callback) => {
|
||||
<span class="keyword">const</span> data = fetchData(type);
|
||||
callback(data);
|
||||
});
|
||||
|
||||
<span class="code-comment">// State change notifications</span>
|
||||
emitter.on(<span class="string">'state.change'</span>, (oldState, newState) => {
|
||||
updateUI(newState);
|
||||
logStateChange(oldState, newState);
|
||||
});
|
||||
|
||||
<span class="code-comment">// Error handling</span>
|
||||
emitter.on(<span class="string">'error'</span>, (error, context) => {
|
||||
console.error(<span class="string">'Error in'</span>, context, error);
|
||||
showErrorToUser(error.message);
|
||||
});
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-layout">
|
||||
<div class="demo-column">
|
||||
<h2>Library Info</h2>
|
||||
<div class="demo-section">
|
||||
<div class="status" id="libraryInfo">Loading library information...</div>
|
||||
|
||||
<h3>Event Statistics:</h3>
|
||||
<div id="eventStats" class="status"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-column">
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="u-events.js"></script>
|
||||
<script>
|
||||
const demoEmitter = new EventEmitter();
|
||||
const interactiveEmitter = new EventEmitter();
|
||||
const publisherEmitter = new EventEmitter();
|
||||
const gameEmitter = new EventEmitter();
|
||||
const uiEmitter = new EventEmitter();
|
||||
|
||||
let listenerCount = 0;
|
||||
let gameStats = {
|
||||
eventsEmitted: 0,
|
||||
handlersExecuted: 0,
|
||||
totalEvents: 0
|
||||
};
|
||||
|
||||
function log(message, outputId = 'basic-output') {
|
||||
const output = document.getElementById(outputId);
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
output.textContent += `[${timestamp}] ${message}\n`;
|
||||
output.scrollTop = output.scrollHeight;
|
||||
|
||||
flashIndicator('basicIndicator');
|
||||
}
|
||||
|
||||
function clearOutput(outputId) {
|
||||
document.getElementById(outputId).textContent = '';
|
||||
}
|
||||
|
||||
function flashIndicator(indicatorId) {
|
||||
const indicator = document.getElementById(indicatorId);
|
||||
if (indicator) {
|
||||
indicator.classList.remove('active');
|
||||
setTimeout(() => indicator.classList.add('active'), 10);
|
||||
setTimeout(() => indicator.classList.remove('active'), 500);
|
||||
}
|
||||
}
|
||||
|
||||
function basicDemo() {
|
||||
clearOutput('basic-output');
|
||||
|
||||
demoEmitter.on('greeting', (name) => {
|
||||
log(`Hello, ${name}!`, 'basic-output');
|
||||
});
|
||||
|
||||
demoEmitter.emit('greeting', 'World');
|
||||
demoEmitter.emit('greeting', 'EventEmitter');
|
||||
gameStats.eventsEmitted += 2;
|
||||
updateEventStats();
|
||||
}
|
||||
|
||||
function multipleListenersDemo() {
|
||||
demoEmitter.on('multi-test', (data) => {
|
||||
log(`Handler 1: ${data}`, 'basic-output');
|
||||
});
|
||||
|
||||
demoEmitter.on('multi-test', (data) => {
|
||||
log(`Handler 2: ${data}`, 'basic-output');
|
||||
});
|
||||
|
||||
demoEmitter.on('multi-test', (data) => {
|
||||
log(`Handler 3: ${data}`, 'basic-output');
|
||||
});
|
||||
|
||||
log('Added 3 listeners for "multi-test" event', 'basic-output');
|
||||
}
|
||||
|
||||
function emitToMultiple() {
|
||||
const count = demoEmitter.emit('multi-test', 'Hello from multiple demo!');
|
||||
log(`Event emitted to ${count} handlers`, 'basic-output');
|
||||
gameStats.eventsEmitted++;
|
||||
gameStats.handlersExecuted += count;
|
||||
updateEventStats();
|
||||
}
|
||||
|
||||
function slotDemo() {
|
||||
clearOutput('advanced-output');
|
||||
|
||||
demoEmitter.on('slot-event', (msg) => {
|
||||
log(`Slot handler v1: ${msg}`, 'advanced-output');
|
||||
}, 'demo-slot');
|
||||
|
||||
log('Added handler with slot key "demo-slot"', 'advanced-output');
|
||||
}
|
||||
|
||||
function replaceSlotHandler() {
|
||||
demoEmitter.on('slot-event', (msg) => {
|
||||
log(`Slot handler v2 (replaced): ${msg}`, 'advanced-output');
|
||||
}, 'demo-slot');
|
||||
|
||||
log('Replaced handler using same slot key', 'advanced-output');
|
||||
}
|
||||
|
||||
function emitSlotEvent() {
|
||||
const count = demoEmitter.emit('slot-event', 'Testing slot replacement');
|
||||
log(`Emitted to ${count} handler(s)`, 'advanced-output');
|
||||
gameStats.eventsEmitted++;
|
||||
gameStats.handlersExecuted += count;
|
||||
updateEventStats();
|
||||
}
|
||||
|
||||
let autoRemovalCount = 0;
|
||||
function autoRemovalDemo() {
|
||||
demoEmitter.on('auto-remove', (msg) => {
|
||||
autoRemovalCount++;
|
||||
log(`Auto-removal handler called ${autoRemovalCount} time(s): ${msg}`, 'advanced-output');
|
||||
return 'remove_handler';
|
||||
});
|
||||
|
||||
log('Added self-removing handler', 'advanced-output');
|
||||
}
|
||||
|
||||
function triggerAutoRemoval() {
|
||||
const count = demoEmitter.emit('auto-remove', 'This handler will remove itself');
|
||||
log(`Handlers called: ${count}`, 'advanced-output');
|
||||
|
||||
setTimeout(() => {
|
||||
const count2 = demoEmitter.emit('auto-remove', 'This should call 0 handlers');
|
||||
log(`Second emit - handlers called: ${count2}`, 'advanced-output');
|
||||
}, 1000);
|
||||
|
||||
gameStats.eventsEmitted += 2;
|
||||
gameStats.handlersExecuted += count;
|
||||
updateEventStats();
|
||||
}
|
||||
|
||||
function addListener() {
|
||||
const eventName = document.getElementById('event-name').value;
|
||||
if (!eventName) return;
|
||||
|
||||
listenerCount++;
|
||||
const listenerId = listenerCount;
|
||||
|
||||
interactiveEmitter.on(eventName, (data) => {
|
||||
const logEl = document.getElementById('event-log');
|
||||
logEl.textContent += `[Listener ${listenerId}] ${eventName}: ${data}\n`;
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
});
|
||||
|
||||
const logEl = document.getElementById('event-log');
|
||||
logEl.textContent += `Added listener ${listenerId} for "${eventName}"\n`;
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
|
||||
updateListenerCounter();
|
||||
}
|
||||
|
||||
function emitCustomEvent() {
|
||||
const eventName = document.getElementById('event-name').value;
|
||||
const eventData = document.getElementById('event-data').value;
|
||||
|
||||
if (!eventName) return;
|
||||
|
||||
const count = interactiveEmitter.emit(eventName, eventData);
|
||||
const logEl = document.getElementById('event-log');
|
||||
logEl.textContent += `Emitted "${eventName}" to ${count} listener(s)\n`;
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
|
||||
gameStats.eventsEmitted++;
|
||||
gameStats.handlersExecuted += count;
|
||||
updateEventStats();
|
||||
}
|
||||
|
||||
function removeAllListeners() {
|
||||
const eventName = document.getElementById('event-name').value;
|
||||
if (!eventName) return;
|
||||
|
||||
interactiveEmitter.off(eventName);
|
||||
|
||||
const logEl = document.getElementById('event-log');
|
||||
logEl.textContent += `Removed all listeners for "${eventName}"\n`;
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
function updateListenerCounter() {
|
||||
const counter = document.getElementById('listenerCount');
|
||||
counter.textContent = listenerCount.toString();
|
||||
}
|
||||
|
||||
function createPublisher() {
|
||||
clearOutput('pubsub-output');
|
||||
log('Publisher created', 'pubsub-output');
|
||||
}
|
||||
|
||||
function addSubscribers() {
|
||||
publisherEmitter.on('news', (headline, content) => {
|
||||
log(`News Subscriber: ${headline}`, 'pubsub-output');
|
||||
});
|
||||
|
||||
publisherEmitter.on('news', (headline, content) => {
|
||||
log(`Mobile App: New article "${headline}"`, 'pubsub-output');
|
||||
});
|
||||
|
||||
publisherEmitter.on('news', (headline, content) => {
|
||||
log(`Email Service: Sending newsletter with "${headline}"`, 'pubsub-output');
|
||||
});
|
||||
|
||||
log('Added 3 subscribers to news events', 'pubsub-output');
|
||||
}
|
||||
|
||||
function publishNews() {
|
||||
const headlines = [
|
||||
'EventEmitter Pattern Increases Developer Productivity',
|
||||
'New Features Added to Event System',
|
||||
'Best Practices for Event-Driven Architecture'
|
||||
];
|
||||
|
||||
const headline = headlines[Math.floor(Math.random() * headlines.length)];
|
||||
const count = publisherEmitter.emit('news', headline, 'Article content here...');
|
||||
log(`Published "${headline}" to ${count} subscribers`, 'pubsub-output');
|
||||
|
||||
gameStats.eventsEmitted++;
|
||||
gameStats.handlersExecuted += count;
|
||||
updateEventStats();
|
||||
}
|
||||
|
||||
function setupComponents() {
|
||||
clearOutput('component-output');
|
||||
|
||||
demoEmitter.on('ui.update', (data) => {
|
||||
log(`UI Component: Updating display with ${data}`, 'component-output');
|
||||
});
|
||||
|
||||
demoEmitter.on('data.request', (type) => {
|
||||
log(`Data Component: Fetching ${type} data`, 'component-output');
|
||||
setTimeout(() => {
|
||||
demoEmitter.emit('data.response', `${type} data loaded`);
|
||||
}, 500);
|
||||
});
|
||||
|
||||
demoEmitter.on('data.response', (data) => {
|
||||
log(`Logger: Data received - ${data}`, 'component-output');
|
||||
});
|
||||
|
||||
log('Components setup complete', 'component-output');
|
||||
}
|
||||
|
||||
function componentInteraction() {
|
||||
demoEmitter.emit('data.request', 'user');
|
||||
gameStats.eventsEmitted++;
|
||||
updateEventStats();
|
||||
}
|
||||
|
||||
function cascadeEvents() {
|
||||
demoEmitter.emit('ui.update', 'new theme');
|
||||
setTimeout(() => {
|
||||
demoEmitter.emit('ui.update', 'user preferences');
|
||||
}, 300);
|
||||
setTimeout(() => {
|
||||
demoEmitter.emit('ui.update', 'layout changes');
|
||||
}, 600);
|
||||
|
||||
gameStats.eventsEmitted += 3;
|
||||
updateEventStats();
|
||||
}
|
||||
|
||||
function handlerCountDemo() {
|
||||
clearOutput('count-output');
|
||||
|
||||
demoEmitter.on('count-test', () => log('Handler A executed', 'count-output'));
|
||||
demoEmitter.on('count-test', () => log('Handler B executed', 'count-output'));
|
||||
demoEmitter.on('count-test', () => log('Handler C executed', 'count-output'));
|
||||
|
||||
const count = demoEmitter.emit('count-test');
|
||||
log(`Total handlers executed: ${count}`, 'count-output');
|
||||
|
||||
gameStats.eventsEmitted++;
|
||||
gameStats.handlersExecuted += count;
|
||||
updateEventStats();
|
||||
}
|
||||
|
||||
function benchmarkEmission() {
|
||||
const start = performance.now();
|
||||
let totalHandlers = 0;
|
||||
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
totalHandlers += demoEmitter.emit('count-test');
|
||||
}
|
||||
|
||||
const end = performance.now();
|
||||
log(`Benchmark: 1000 emissions took ${(end - start).toFixed(2)}ms`, 'count-output');
|
||||
log(`Total handlers called: ${totalHandlers}`, 'count-output');
|
||||
|
||||
gameStats.eventsEmitted += 1000;
|
||||
gameStats.handlersExecuted += totalHandlers;
|
||||
updateEventStats();
|
||||
}
|
||||
|
||||
function setupGameDemo() {
|
||||
clearOutput('game-output');
|
||||
|
||||
gameEmitter.on('player.move', (x, y) => {
|
||||
log(`Player moved to (${x}, ${y})`, 'game-output');
|
||||
});
|
||||
|
||||
gameEmitter.on('enemy.spawn', (type, level) => {
|
||||
log(`${type} enemy spawned at level ${level}`, 'game-output');
|
||||
});
|
||||
|
||||
gameEmitter.on('score.update', (points, combo) => {
|
||||
log(`Score: ${points} points (${combo}x combo)`, 'game-output');
|
||||
});
|
||||
|
||||
gameEmitter.on('game.over', (finalScore) => {
|
||||
log(`Game Over! Final score: ${finalScore}`, 'game-output');
|
||||
});
|
||||
|
||||
log('Game event system initialized', 'game-output');
|
||||
}
|
||||
|
||||
function simulateGameplay() {
|
||||
const actions = [
|
||||
() => gameEmitter.emit('player.move', Math.floor(Math.random() * 10), Math.floor(Math.random() * 10)),
|
||||
() => gameEmitter.emit('enemy.spawn', ['goblin', 'orc', 'dragon'][Math.floor(Math.random() * 3)], Math.floor(Math.random() * 5) + 1),
|
||||
() => gameEmitter.emit('score.update', Math.floor(Math.random() * 1000), Math.floor(Math.random() * 5) + 1),
|
||||
];
|
||||
|
||||
let actionCount = 0;
|
||||
const gameLoop = setInterval(() => {
|
||||
const action = actions[Math.floor(Math.random() * actions.length)];
|
||||
action();
|
||||
actionCount++;
|
||||
gameStats.eventsEmitted++;
|
||||
|
||||
if (actionCount >= 8) {
|
||||
clearInterval(gameLoop);
|
||||
setTimeout(() => {
|
||||
gameEmitter.emit('game.over', Math.floor(Math.random() * 10000));
|
||||
gameStats.eventsEmitted++;
|
||||
updateEventStats();
|
||||
}, 1000);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
updateEventStats();
|
||||
}
|
||||
|
||||
function showGameStats() {
|
||||
log(`Game Statistics:`, 'game-output');
|
||||
log(`Events emitted: ${gameStats.eventsEmitted}`, 'game-output');
|
||||
log(`Handlers executed: ${gameStats.handlersExecuted}`, 'game-output');
|
||||
log(`Avg handlers per event: ${(gameStats.handlersExecuted / gameStats.eventsEmitted || 0).toFixed(2)}`, 'game-output');
|
||||
}
|
||||
|
||||
// UI Demo
|
||||
function setupUIDemo() {
|
||||
clearOutput('ui-output');
|
||||
|
||||
uiEmitter.on('button.click', (buttonId) => {
|
||||
log(`Button clicked: ${buttonId}`, 'ui-output');
|
||||
});
|
||||
|
||||
uiEmitter.on('form.submit', (formData) => {
|
||||
log(`Form submitted: ${JSON.stringify(formData)}`, 'ui-output');
|
||||
});
|
||||
|
||||
uiEmitter.on('modal.open', (modalType) => {
|
||||
log(`Modal opened: ${modalType}`, 'ui-output');
|
||||
});
|
||||
|
||||
uiEmitter.on('theme.change', (theme) => {
|
||||
log(`Theme changed to: ${theme}`, 'ui-output');
|
||||
});
|
||||
|
||||
log('UI event handlers registered', 'ui-output');
|
||||
}
|
||||
|
||||
function simulateUserActions() {
|
||||
const actions = [
|
||||
() => uiEmitter.emit('button.click', 'submit-btn'),
|
||||
() => uiEmitter.emit('button.click', 'cancel-btn'),
|
||||
() => uiEmitter.emit('form.submit', { name: 'John', email: 'john@example.com' }),
|
||||
() => uiEmitter.emit('modal.open', 'settings'),
|
||||
() => uiEmitter.emit('modal.open', 'help'),
|
||||
() => uiEmitter.emit('theme.change', 'dark'),
|
||||
() => uiEmitter.emit('theme.change', 'light'),
|
||||
];
|
||||
|
||||
actions.forEach((action, index) => {
|
||||
setTimeout(() => {
|
||||
action();
|
||||
gameStats.eventsEmitted++;
|
||||
if (index === actions.length - 1) {
|
||||
updateEventStats();
|
||||
}
|
||||
}, index * 400);
|
||||
});
|
||||
}
|
||||
|
||||
function updateLibraryInfo() {
|
||||
const info = document.getElementById('libraryInfo');
|
||||
info.textContent = `
|
||||
EventEmitter Instances: Multiple active instances
|
||||
Global Event Statistics: ${gameStats.totalEvents} total events processed
|
||||
Memory Usage: Efficient slot-based deduplication
|
||||
Performance: Sub-millisecond event emission
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function updateEventStats() {
|
||||
gameStats.totalEvents = gameStats.eventsEmitted;
|
||||
const stats = document.getElementById('eventStats');
|
||||
stats.textContent = `
|
||||
Events Emitted: ${gameStats.eventsEmitted}
|
||||
Handlers Executed: ${gameStats.handlersExecuted}
|
||||
Average Handlers per Event: ${(gameStats.handlersExecuted / gameStats.eventsEmitted || 0).toFixed(2)}
|
||||
Active Listeners: ${listenerCount}
|
||||
`.trim();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateLibraryInfo();
|
||||
updateEventStats();
|
||||
updateListenerCounter();
|
||||
|
||||
setInterval(updateLibraryInfo, 3000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,136 @@
|
||||
(function (root, factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define([], factory);
|
||||
} else if (typeof module === 'object' && module.exports) {
|
||||
module.exports = factory();
|
||||
} else {
|
||||
root.UFormat = factory();
|
||||
}
|
||||
}(typeof self !== 'undefined' ? self : this, function () {
|
||||
'use strict';
|
||||
|
||||
const BYTE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
|
||||
function scaleBytes(bytes) {
|
||||
let value = Number(bytes || 0);
|
||||
let unitIndex = 0;
|
||||
while (Math.abs(value) >= 1024 && unitIndex < BYTE_UNITS.length - 1) {
|
||||
value /= 1024;
|
||||
unitIndex += 1;
|
||||
}
|
||||
return { value, unitIndex };
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes == null || bytes === '') return '--';
|
||||
const scaled = scaleBytes(bytes);
|
||||
const decimals = scaled.unitIndex === 0 ? 0 : 1;
|
||||
return `${scaled.value.toFixed(decimals)} ${BYTE_UNITS[scaled.unitIndex]}`;
|
||||
}
|
||||
|
||||
function formatDiskBytes(bytes) {
|
||||
if (bytes == null || bytes === '') return '--';
|
||||
const scaled = scaleBytes(bytes);
|
||||
const decimals = scaled.unitIndex >= 4 ? 2 : scaled.unitIndex >= 1 ? 1 : 0;
|
||||
return `${scaled.value.toFixed(decimals)} ${BYTE_UNITS[scaled.unitIndex]}`;
|
||||
}
|
||||
|
||||
function formatCount(value) {
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number)) return '--';
|
||||
return number.toLocaleString();
|
||||
}
|
||||
|
||||
function formatDurationMs(value) {
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number)) return '--';
|
||||
if (Math.abs(number) >= 1000) {
|
||||
return `${(number / 1000).toFixed(number >= 10000 ? 0 : 1)} s`;
|
||||
}
|
||||
return `${number.toFixed(number >= 100 ? 0 : 1)} ms`;
|
||||
}
|
||||
|
||||
function parseUnitNumber(text) {
|
||||
const normalized = String(text || '').trim().toLowerCase().replace(/,/g, '');
|
||||
if (!normalized) return null;
|
||||
|
||||
const pure = normalized.match(/^([-+]?\d*\.?\d+)$/);
|
||||
if (pure) {
|
||||
return Number(pure[1]);
|
||||
}
|
||||
|
||||
const withUnit = normalized.match(/^([-+]?\d*\.?\d+)\s*([a-z%][a-z0-9\/_-]*)$/);
|
||||
if (!withUnit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = Number(withUnit[1]);
|
||||
let unit = withUnit[2];
|
||||
if (!Number.isFinite(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (unit.endsWith('/s')) {
|
||||
unit = unit.slice(0, -2);
|
||||
}
|
||||
|
||||
const bytes = {
|
||||
b: 1,
|
||||
kb: 1024,
|
||||
kib: 1024,
|
||||
mb: 1024 ** 2,
|
||||
mib: 1024 ** 2,
|
||||
gb: 1024 ** 3,
|
||||
gib: 1024 ** 3,
|
||||
tb: 1024 ** 4,
|
||||
tib: 1024 ** 4,
|
||||
pb: 1024 ** 5,
|
||||
pib: 1024 ** 5,
|
||||
};
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(bytes, unit)) {
|
||||
return value * bytes[unit];
|
||||
}
|
||||
|
||||
const durations = {
|
||||
ms: 0.001,
|
||||
s: 1,
|
||||
sec: 1,
|
||||
secs: 1,
|
||||
second: 1,
|
||||
seconds: 1,
|
||||
m: 60,
|
||||
min: 60,
|
||||
mins: 60,
|
||||
minute: 60,
|
||||
minutes: 60,
|
||||
h: 3600,
|
||||
hr: 3600,
|
||||
hrs: 3600,
|
||||
hour: 3600,
|
||||
hours: 3600,
|
||||
d: 86400,
|
||||
day: 86400,
|
||||
days: 86400,
|
||||
};
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(durations, unit)) {
|
||||
return value * durations[unit];
|
||||
}
|
||||
|
||||
if (unit === '%') {
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
formatBytes,
|
||||
formatCount,
|
||||
formatDiskBytes,
|
||||
formatDurationMs,
|
||||
parseUnitNumber,
|
||||
scaleBytes,
|
||||
};
|
||||
}));
|
||||
Executable
+1488
File diff suppressed because it is too large
Load Diff
Executable
+977
@@ -0,0 +1,977 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>U-Macrobars.js Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--space: 8px;
|
||||
--radius: 5px;
|
||||
|
||||
--gray: #6c757d;
|
||||
--gray-bg: #f5f5f5;
|
||||
--blue: #007acc;
|
||||
--green: #28a745;
|
||||
--white: white;
|
||||
--dark: #333;
|
||||
|
||||
--max-width: 1400px;
|
||||
--sidebar: 400px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: var(--space);
|
||||
background: var(--gray-bg);
|
||||
}
|
||||
|
||||
.container {
|
||||
background: var(--white);
|
||||
padding: calc(var(--space) * 1.5);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.main-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: calc(var(--space) * 1.5);
|
||||
}
|
||||
|
||||
.api-column {
|
||||
background: var(--gray-bg);
|
||||
padding: var(--space);
|
||||
border-radius: var(--radius);
|
||||
border-left: 4px solid var(--green);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--dark);
|
||||
text-align: center;
|
||||
margin-bottom: calc(var(--space) * 1.5);
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--gray);
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
padding-bottom: var(--space);
|
||||
margin-top: calc(var(--space) * 1.5);
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin: var(--space) 0;
|
||||
padding: var(--space);
|
||||
background: var(--gray-bg);
|
||||
border-radius: var(--radius);
|
||||
border-left: 4px solid var(--blue);
|
||||
}
|
||||
|
||||
.controls, .slider-group {
|
||||
display: flex;
|
||||
gap: var(--space);
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.controls { flex-wrap: wrap; }
|
||||
.slider-group { align-items: center; }
|
||||
.feature-grid { display: grid; gap: var(--space); margin: var(--space) 0; }
|
||||
|
||||
button {
|
||||
background: var(--blue);
|
||||
color: var(--white);
|
||||
border: none;
|
||||
padding: var(--space) var(--space);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover { filter: brightness(0.9); }
|
||||
button:disabled { background: #ccc; cursor: not-allowed; }
|
||||
input[type="range"] { flex: 1; max-width: 200px; }
|
||||
|
||||
.status, .code-example {
|
||||
font-family: monospace;
|
||||
padding: var(--space);
|
||||
border-radius: var(--radius);
|
||||
margin: var(--space) 0;
|
||||
}
|
||||
|
||||
.status {
|
||||
background: var(--dark);
|
||||
color: #0f0;
|
||||
white-space: pre-wrap;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.code-example {
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
padding: 15px;
|
||||
overflow-x: auto;
|
||||
margin: var(--space) 0;
|
||||
}
|
||||
|
||||
.highlight { color: #68d391; }
|
||||
.keyword { color: #fbb6ce; }
|
||||
.string { color: #fbd38d; }
|
||||
|
||||
.template-visual {
|
||||
height: 100px;
|
||||
background: linear-gradient(45deg, #667eea, #764ba2);
|
||||
border-radius: var(--radius);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--white);
|
||||
font-weight: bold;
|
||||
margin: 15px 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.template-animation {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.3;
|
||||
background: repeating-linear-gradient(90deg, transparent 0 10px, rgba(255,255,255,0.2) 10px 20px);
|
||||
animation: slide 3s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes slide { to { transform: translateX(20px); } }
|
||||
|
||||
.api-section { margin: 15px 0; }
|
||||
.api-section h3 {
|
||||
color: var(--green);
|
||||
margin: 0 0 var(--space) 0;
|
||||
}
|
||||
|
||||
.api-method {
|
||||
font-family: monospace;
|
||||
margin: 3px 0;
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.api-method .method-name { color: var(--blue); font-weight: bold; }
|
||||
.api-method .return-type { color: #6f42c1; }
|
||||
.api-method .param { color: #e83e8c; }
|
||||
|
||||
.api-description {
|
||||
margin: var(--space) 0;
|
||||
}
|
||||
|
||||
.api-options {
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.template-code {
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
padding: 15px;
|
||||
border-radius: var(--radius);
|
||||
margin: var(--space) 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.output {
|
||||
background: var(--white);
|
||||
border: 2px solid var(--green);
|
||||
padding: var(--space);
|
||||
border-radius: var(--radius);
|
||||
margin: var(--space) 0;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.code-comment {
|
||||
color: #68d391;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: var(--space);
|
||||
border: 1px solid #ccc;
|
||||
border-radius: var(--radius);
|
||||
font-family: monospace;
|
||||
margin: var(--space) 0;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.interactive-editor {
|
||||
background: var(--white);
|
||||
padding: calc(var(--space) * 1.5);
|
||||
border-radius: var(--radius);
|
||||
border: 2px solid var(--blue);
|
||||
}
|
||||
|
||||
.help-text {
|
||||
color: var(--gray);
|
||||
margin: var(--space) 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.feature-grid button {
|
||||
padding: calc(var(--space) * 1.5) var(--space);
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.feature-grid button:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 122, 204, 0.3);
|
||||
}
|
||||
|
||||
.feature-grid button:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.main-layout { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body onload="initializeDemos();">
|
||||
<div class="container">
|
||||
<h1>U-Macrobars.js Demo</h1>
|
||||
|
||||
<div class="main-layout">
|
||||
<div class="demo-column">
|
||||
<h2>Basic Field Output</h2>
|
||||
<div class="demo-section">
|
||||
<div class="template-code">{{name}} is {{age}} years old and works as {{job or "unemployed"}}</div>
|
||||
<div class="output" id="basic-output"></div>
|
||||
<div class="controls">
|
||||
<button onclick="runBasicDemo()">Run Basic Demo</button>
|
||||
<button onclick="runBasicDemoVariant()">Try Different Data</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Number Formatting</h3>
|
||||
<div class="template-code">Price: ${{%price}} | Large Number: {{~bigNumber}}</div>
|
||||
<div class="output" id="number-output"></div>
|
||||
<div class="controls">
|
||||
<button onclick="runNumberDemo()">Format Numbers</button>
|
||||
<button onclick="runNumberVariants()">Try Different Numbers</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Default Values & Safety</h3>
|
||||
<div class="template-code">{{username or "Guest"}} | {{profile.bio or "No bio available"}}</div>
|
||||
<div class="output" id="default-output"></div>
|
||||
<div class="controls">
|
||||
<button onclick="runDefaultDemo()">Test Defaults</button>
|
||||
<button onclick="runSafetyDemo()">HTML Safety Demo</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-column">
|
||||
<h2>Field Output API</h2>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Basic Syntax</h3>
|
||||
<div class="api-method"><span class="method-name">{{field}}</span> → <span class="return-type">Safe HTML output</span></div>
|
||||
<div class="api-method"><span class="method-name">{{{field}}}</span> → <span class="return-type">Raw HTML output</span></div>
|
||||
<div class="api-method"><span class="method-name">{{:variable}}</span> → <span class="return-type">Direct variable</span></div>
|
||||
<div class="api-method"><span class="method-name">{{field or "default"}}</span> → <span class="return-type">With fallback</span></div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Number Formatting</h3>
|
||||
<div class="api-method"><span class="method-name">{{%number}}</span> → <span class="return-type">2 decimal places</span></div>
|
||||
<div class="api-method"><span class="method-name">{{~number}}</span> → <span class="return-type">Rounded (1k, 1M)</span></div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Examples</h3>
|
||||
<pre class="code-example">
|
||||
<span class="code-comment">// Basic field output</span>
|
||||
<span class="keyword">const</span> template = Macrobars.compile(<span class="string">'{{name}}'</span>);
|
||||
<span class="keyword">const</span> result = template({name: <span class="string">'John'</span>});
|
||||
|
||||
<span class="code-comment">// With defaults</span>
|
||||
<span class="keyword">const</span> withDefault = <span class="string">'{{title or "Untitled"}}'</span>;
|
||||
|
||||
<span class="code-comment">// Number formatting</span>
|
||||
<span class="keyword">const</span> price = <span class="string">'${{%cost}}'</span>; <span class="code-comment">// $12.34</span>
|
||||
<span class="keyword">const</span> count = <span class="string">'{{~views}}'</span>; <span class="code-comment">// 1.2k</span>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-layout">
|
||||
<div class="demo-column">
|
||||
<h2>Control Structures</h2>
|
||||
<div class="demo-section">
|
||||
<h3>Conditionals</h3>
|
||||
<div class="template-code">{{#if isLoggedIn}}
|
||||
Welcome back, {{username}}!
|
||||
{{#else}}
|
||||
Please log in to continue.
|
||||
{{/if}}</div>
|
||||
<div class="output" id="conditional-output"></div>
|
||||
<div class="controls">
|
||||
<button onclick="runConditionalDemo(true)">👤 Logged In</button>
|
||||
<button onclick="runConditionalDemo(false)">🚪 Not Logged In</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Loops & Iteration</h3>
|
||||
<div class="template-code">{{#each items}}
|
||||
• {{number}}. {{name}} - ${{price}}
|
||||
{{/each}}</div>
|
||||
<div class="output" id="loop-output"></div>
|
||||
<div class="controls">
|
||||
<button onclick="runLoopDemo()">🔄 Process List</button>
|
||||
<button onclick="runNamedLoopDemo()">📝 Named Iteration</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Equality & Lookup</h3>
|
||||
<div class="template-code">{{#eq status "active"}}User is active{{/eq}}
|
||||
{{lookup user "permissions"}}</div>
|
||||
<div class="output" id="equality-output"></div>
|
||||
<div class="controls">
|
||||
<button onclick="runEqualityDemo()">⚖️ Test Equality</button>
|
||||
<button onclick="runLookupDemo()">🔍 Dynamic Lookup</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-column">
|
||||
<h2>Control Flow API</h2>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Conditionals</h3>
|
||||
<div class="api-method"><span class="method-name">{{#if condition}}</span>...{{/if}}</div>
|
||||
<div class="api-method"><span class="method-name">{{#else}}</span> → <span class="return-type">Alternative branch</span></div>
|
||||
<div class="api-description">
|
||||
Conditional rendering based on truthy values. Supports nested conditions.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Loops</h3>
|
||||
<div class="api-method"><span class="method-name">{{#each items}}</span>...{{/each}}</div>
|
||||
<div class="api-method"><span class="method-name">{{#each items as item}}</span>...{{/each}}</div>
|
||||
<div class="api-description">
|
||||
• Standard: <strong>data</strong> becomes current item<br>
|
||||
• Named: <strong>item</strong> becomes current item, data preserved
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Comparison & Lookup</h3>
|
||||
<div class="api-method"><span class="method-name">{{#eq val1 val2}}</span>...{{/eq}}</div>
|
||||
<div class="api-method"><span class="method-name">{{eq val1 val2}}</span> → <span class="return-type">"true" or ""</span></div>
|
||||
<div class="api-method"><span class="method-name">{{#lookup obj key}}</span>...{{/lookup}}</div>
|
||||
<div class="api-method"><span class="method-name">{{lookup obj key}}</span> → <span class="return-type">value or ""</span></div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Examples</h3>
|
||||
<pre class="code-example">
|
||||
<span class="code-comment">// Conditionals</span>
|
||||
<span class="string">'{{#if user.isAdmin}}Admin Panel{{/if}}'</span>
|
||||
|
||||
<span class="code-comment">// Named loops</span>
|
||||
<span class="string">'{{#each products as product}}'</span>
|
||||
<span class="string">'{{product.name}} - {{data.storeName}}'</span>
|
||||
<span class="string">'{{/each}}'</span>
|
||||
|
||||
<span class="code-comment">// Equality & lookup</span>
|
||||
<span class="string">'{{#eq user.role "admin"}}Secret{{/eq}}'</span>
|
||||
<span class="string">'{{lookup config "theme"}}'</span>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-layout">
|
||||
<div class="demo-column">
|
||||
<h2>Advanced Features</h2>
|
||||
<div class="demo-section">
|
||||
<h3>Event Binding</h3>
|
||||
<div class="template-code"><button {{@click="handleClick"}}>Click Count: {{clickCount}}</button></div>
|
||||
<div class="output" id="event-output"></div>
|
||||
<div class="controls">
|
||||
<button onclick="runEventDemo()">🎯 Setup Interactive Button</button>
|
||||
<button onclick="runMultiEventDemo()">⚡ Multiple Events</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Code Blocks</h3>
|
||||
<div class="template-code"><script>var computed = data.value * 2;</script>
|
||||
Result: {{:computed}}</div>
|
||||
<div class="output" id="code-output"></div>
|
||||
<div class="controls">
|
||||
<button onclick="runCodeDemo()">💻 Execute Code</button>
|
||||
<button onclick="runDeferDemo()">⏰ Deferred Execution</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Components</h3>
|
||||
<div class="template-code">{{#component userCard}}</div>
|
||||
<div class="output" id="component-output"></div>
|
||||
<div class="controls">
|
||||
<button onclick="runComponentDemo()">🧩 Load Component</button>
|
||||
<button onclick="createCustomComponent()">✨ Create Custom</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-column">
|
||||
<h2>Advanced API</h2>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Event Binding</h3>
|
||||
<div class="api-method"><span class="method-name">{{@event="handler"}}</span> → <span class="return-type">DOM attribute</span></div>
|
||||
<div class="api-method"><span class="method-name">template.renderTo</span>(<span class="param">container, data</span>)</div>
|
||||
<div class="api-description">
|
||||
Events are automatically bound when using renderTo(). Handler can reference data properties or global functions.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Code Execution</h3>
|
||||
<div class="api-method"><span class="method-name"><script></span>...</script></div>
|
||||
<div class="api-method"><span class="method-name"><defer></span>...</defer></div>
|
||||
<div class="api-method"><span class="method-name"><?</span> code <span class="method-name">?></span></div>
|
||||
<div class="api-method"><span class="method-name"><?=</span> expression <span class="method-name">?></span></div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Components & Compilation</h3>
|
||||
<div class="api-method"><span class="method-name">Macrobars.compile</span>(<span class="param">template, options</span>)</div>
|
||||
<div class="api-method"><span class="method-name">Macrobars.createComponents</span>(<span class="param">definitions</span>)</div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Examples</h3>
|
||||
<pre class="code-example">
|
||||
<span class="code-comment">// Event binding with renderTo</span>
|
||||
<span class="keyword">const</span> template = Macrobars.compile(
|
||||
<span class="string">'<button {{@click="increment"}}>{{count}}</button>'</span>
|
||||
);
|
||||
template.renderTo(<span class="string">'#container'</span>, {
|
||||
count: <span class="highlight">0</span>,
|
||||
increment: <span class="keyword">function</span>() { <span class="keyword">this</span>.count++; }
|
||||
});
|
||||
|
||||
<span class="code-comment">// Components</span>
|
||||
<span class="keyword">const</span> components = Macrobars.createComponents({
|
||||
userCard: <span class="string">'<div>{{name}} - {{email}}</div>'</span>
|
||||
});
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-layout">
|
||||
<div class="demo-column">
|
||||
<h2>Template Editor</h2>
|
||||
<div class="interactive-editor">
|
||||
|
||||
<label for="template-input"><strong>Template:</strong></label>
|
||||
<textarea id="template-input" rows="6" placeholder="Enter your template here">Hello {{name}}!
|
||||
{{#if age}}You are {{age}} years old.{{/if}}
|
||||
{{#each hobbies as hobby}}
|
||||
• {{:hobby}}
|
||||
{{/each}}
|
||||
Total score: {{~score}}</textarea>
|
||||
|
||||
<label for="data-input"><strong>Data (JSON):</strong></label>
|
||||
<textarea id="data-input" rows="6" placeholder="Enter JSON data here">{
|
||||
"name": "Alice",
|
||||
"age": 25,
|
||||
"score": 98750,
|
||||
"hobbies": ["reading", "coding", "gaming"]
|
||||
}</textarea>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="runCustomTemplate()">Render Template</button>
|
||||
<button onclick="loadExampleTemplate('basic')">Load Basic Example</button>
|
||||
<button onclick="loadExampleTemplate('advanced')">Load Advanced Example</button>
|
||||
<button onclick="clearEditor()">Clear All</button>
|
||||
</div>
|
||||
|
||||
<div class="output" id="dynamic-content" style="white-space: pre;">Click "Render Template"</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Template Information</h3>
|
||||
<div class="status" id="template-info">Ready.</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-column">
|
||||
<h2>Compilation & Debugging</h2>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Compilation Options</h3>
|
||||
<div class="api-method"><span class="method-name">decimals</span>: <span class="param">number</span> → <span class="return-type">Number precision</span></div>
|
||||
<div class="api-method"><span class="method-name">strict</span>: <span class="param">boolean</span> → <span class="return-type">Strict mode</span></div>
|
||||
<div class="api-method"><span class="method-name">components</span>: <span class="param">object</span> → <span class="return-type">Reusable templates</span></div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Debugging Properties</h3>
|
||||
<div class="api-method"><span class="method-name">template.tokens</span> → <span class="return-type">Parsed tokens</span></div>
|
||||
<div class="api-method"><span class="method-name">template.gensource</span> → <span class="return-type">Generated JS</span></div>
|
||||
<div class="api-method"><span class="method-name">template.event_bindings</span> → <span class="return-type">Event data</span></div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Utility Functions</h3>
|
||||
<div class="api-method"><span class="method-name">Macrobars.safe_out</span>(<span class="param">text, default</span>)</div>
|
||||
<div class="api-method"><span class="method-name">Macrobars.num_out</span>(<span class="param">number, decimals</span>)</div>
|
||||
<div class="api-method"><span class="method-name">Macrobars.num_out_round</span>(<span class="param">number</span>)</div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Example Templates</h3>
|
||||
<pre class="code-example">
|
||||
<span class="code-comment">// Complete example</span>
|
||||
<span class="keyword">const</span> template = Macrobars.compile(<span class="string">`
|
||||
<div class="user-card">
|
||||
<h3>{{name or "Unknown User"}}</h3>
|
||||
{{#if profile.verified}}✅ Verified{{/if}}
|
||||
<p>Balance: ${{%balance}}</p>
|
||||
{{#each achievements}}
|
||||
<span class="badge">{{data}}</span>
|
||||
{{/each}}
|
||||
</div>
|
||||
`</span>, { decimals: <span class="highlight">2</span> });
|
||||
|
||||
<span class="keyword">const</span> result = template(userData);
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="u-macrobars.js"></script>
|
||||
<script>
|
||||
let currentTemplate = null;
|
||||
let clickCount = 0;
|
||||
let components = {};
|
||||
|
||||
// Initialize demos on page load
|
||||
function initializeDemos() {
|
||||
runBasicDemo();
|
||||
runNumberDemo();
|
||||
updateTemplateInfo('Ready.');
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function log(message) {
|
||||
//console.log(`Macrobars Demo: ${message}`);
|
||||
}
|
||||
|
||||
function updateTemplateInfo(info) {
|
||||
const element = document.getElementById('template-info');
|
||||
if (element) {
|
||||
element.textContent = info;
|
||||
}
|
||||
}
|
||||
|
||||
function updateVisualAnimation(playing) {
|
||||
const animations = document.querySelectorAll('.template-animation');
|
||||
animations.forEach(anim => {
|
||||
anim.style.animationPlayState = playing ? 'running' : 'paused';
|
||||
});
|
||||
}
|
||||
|
||||
// Demo 1: Basic field output
|
||||
function runBasicDemo() {
|
||||
try {
|
||||
const template = Macrobars.compile('{{name}} is {{age}} years old and works as {{job or "unemployed"}}');
|
||||
const data = { name: "John Doe", age: 30 };
|
||||
const result = template(data);
|
||||
document.getElementById('basic-output').innerHTML = result;
|
||||
updateVisualAnimation(true);
|
||||
setTimeout(() => updateVisualAnimation(false), 1000);
|
||||
} catch (error) {
|
||||
document.getElementById('basic-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function runBasicDemoVariant() {
|
||||
try {
|
||||
const template = Macrobars.compile('{{name}} is {{age}} years old and works as {{job or "unemployed"}}');
|
||||
const data = { name: "Jane Smith", age: 28, job: "Software Engineer" };
|
||||
const result = template(data);
|
||||
document.getElementById('basic-output').innerHTML = result;
|
||||
updateVisualAnimation(true);
|
||||
setTimeout(() => updateVisualAnimation(false), 1000);
|
||||
} catch (error) {
|
||||
document.getElementById('basic-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// Demo 2: Number formatting
|
||||
function runNumberDemo() {
|
||||
try {
|
||||
const template = Macrobars.compile('Price: ${{%price}} | Large Number: {{~bigNumber}}');
|
||||
const data = { price: 123.456, bigNumber: 1234567 };
|
||||
const result = template(data);
|
||||
document.getElementById('number-output').innerHTML = result;
|
||||
} catch (error) {
|
||||
document.getElementById('number-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function runNumberVariants() {
|
||||
try {
|
||||
const template = Macrobars.compile('Price: ${{%price}} | Large Number: {{~bigNumber}} | Small: {{%small}}');
|
||||
const data = { price: 999.99, bigNumber: 42850000, small: 1.2345 };
|
||||
const result = template(data);
|
||||
document.getElementById('number-output').innerHTML = result;
|
||||
} catch (error) {
|
||||
document.getElementById('number-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// Demo 3: Default values & safety
|
||||
function runDefaultDemo() {
|
||||
try {
|
||||
const template = Macrobars.compile('{{username or "Guest"}} | {{profile.bio or "No bio available"}}');
|
||||
const data = { username: "", profile: {} }; // Empty data to test defaults
|
||||
const result = template(data);
|
||||
document.getElementById('default-output').innerHTML = result;
|
||||
} catch (error) {
|
||||
document.getElementById('default-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function runSafetyDemo() {
|
||||
try {
|
||||
const template = Macrobars.compile('Safe: {{userInput}} | Unsafe: {{{userInput}}}');
|
||||
const data = { userInput: '<script>alert("XSS")</' + 'script><b>Bold Text</b>' };
|
||||
const result = template(data);
|
||||
document.getElementById('default-output').innerHTML = result;
|
||||
} catch (error) {
|
||||
document.getElementById('default-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// Demo 4: Conditionals
|
||||
function runConditionalDemo(isLoggedIn) {
|
||||
try {
|
||||
const template = Macrobars.compile('{{#if isLoggedIn}}Welcome back, {{username}}! 🎉{{#else}}Please log in to continue. 🔐{{/if}}');
|
||||
const data = {
|
||||
isLoggedIn: isLoggedIn,
|
||||
username: "Alice"
|
||||
};
|
||||
const result = template(data);
|
||||
document.getElementById('conditional-output').innerHTML = result;
|
||||
} catch (error) {
|
||||
document.getElementById('conditional-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// Demo 5: Loops
|
||||
function runLoopDemo() {
|
||||
try {
|
||||
const template = Macrobars.compile(
|
||||
'{{#each items}}' +
|
||||
'• {{number}}. {{name}} - ${{price}}<br>' +
|
||||
'{{/each}}'
|
||||
);
|
||||
const data = {
|
||||
items: [
|
||||
{ number: 1, name: "Magic Widget", price: 19.99 },
|
||||
{ number: 2, name: "Super Gadget", price: 29.50 },
|
||||
{ number: 3, name: "Ultra Tool", price: 15.75 }
|
||||
]
|
||||
};
|
||||
const result = template(data);
|
||||
document.getElementById('loop-output').innerHTML = result;
|
||||
} catch (error) {
|
||||
document.getElementById('loop-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function runNamedLoopDemo() {
|
||||
try {
|
||||
const template = Macrobars.compile('Store: {{storeName}}<br>{{#each products as product}}→ {{product.name}} ({{product.category}}) - Available at {{storeName}}<br>{{/each}}');
|
||||
const data = {
|
||||
storeName: "TechMart",
|
||||
products: [
|
||||
{ name: "Laptop", category: "Electronics" },
|
||||
{ name: "Mouse", category: "Accessories" },
|
||||
{ name: "Keyboard", category: "Accessories" }
|
||||
]
|
||||
};
|
||||
const result = template(data);
|
||||
document.getElementById('loop-output').innerHTML = result;
|
||||
} catch (error) {
|
||||
document.getElementById('loop-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// Demo 6: Equality & Lookup
|
||||
function runEqualityDemo() {
|
||||
try {
|
||||
const template = Macrobars.compile('{{#eq status "active"}}✅ User is active{{/eq}}{{#eq status "inactive"}}❌ User is inactive{{/eq}}{{#eq status "pending"}}⏳ User is pending{{/eq}}<br>Inline check: {{eq role "admin"}}');
|
||||
const data = { status: "active", role: "admin" };
|
||||
const result = template(data);
|
||||
document.getElementById('equality-output').innerHTML = result;
|
||||
} catch (error) {
|
||||
document.getElementById('equality-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function runLookupDemo() {
|
||||
try {
|
||||
const template = Macrobars.compile('Theme: {{lookup user "theme"}}<br>{{#lookup user "permissions"}}🔑 Has permissions{{/lookup}}{{#lookup user "missing"}}This won\'t show{{/lookup}}');
|
||||
const data = {
|
||||
user: {
|
||||
theme: "dark",
|
||||
permissions: ["read", "write"]
|
||||
}
|
||||
};
|
||||
const result = template(data);
|
||||
document.getElementById('equality-output').innerHTML = result;
|
||||
} catch (error) {
|
||||
document.getElementById('equality-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// Demo 7: Event binding
|
||||
function runEventDemo() {
|
||||
try {
|
||||
clickCount = 0;
|
||||
const template = Macrobars.compile('<button {{@click="handleClick"}} style="padding: 10px;">🖱️ Click Count: {{clickCount}}</button>');
|
||||
|
||||
const data = {
|
||||
clickCount: clickCount,
|
||||
handleClick: function() {
|
||||
clickCount++;
|
||||
runEventDemo(); // Re-render with new count
|
||||
}
|
||||
};
|
||||
|
||||
const container = document.getElementById('event-output');
|
||||
template.renderTo(container, data);
|
||||
} catch (error) {
|
||||
document.getElementById('event-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function runMultiEventDemo() {
|
||||
try {
|
||||
var templateStr = '<div style="padding: 10px; border: 1px solid #ccc; border-radius: 5px;">';
|
||||
templateStr += '<button {{@click="increment"}} style="margin: 5px; padding: 8px;">➕ Add</button>';
|
||||
templateStr += '<button {{@click="decrement"}} style="margin: 5px; padding: 8px;">➖ Subtract</button>';
|
||||
templateStr += '<button {{@click="reset"}} style="margin: 5px; padding: 8px;">🔄 Reset</button>';
|
||||
templateStr += '<br><br>';
|
||||
templateStr += '<strong>Counter: {{counter}}</strong>';
|
||||
templateStr += '</div>';
|
||||
|
||||
const template = Macrobars.compile(templateStr);
|
||||
|
||||
let counter = 0;
|
||||
const data = {
|
||||
counter: counter,
|
||||
increment: function() {
|
||||
counter++;
|
||||
data.counter = counter;
|
||||
template.renderTo(document.getElementById('event-output'), data);
|
||||
},
|
||||
decrement: function() {
|
||||
counter--;
|
||||
data.counter = counter;
|
||||
template.renderTo(document.getElementById('event-output'), data);
|
||||
},
|
||||
reset: function() {
|
||||
counter = 0;
|
||||
data.counter = counter;
|
||||
template.renderTo(document.getElementById('event-output'), data);
|
||||
}
|
||||
};
|
||||
|
||||
template.renderTo(document.getElementById('event-output'), data);
|
||||
} catch (error) {
|
||||
document.getElementById('event-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// Demo 8: Code blocks
|
||||
function runCodeDemo() {
|
||||
try {
|
||||
var templateStr = '';
|
||||
templateStr += '<' + 'script>';
|
||||
templateStr += 'var computed = data.value * 2;';
|
||||
templateStr += 'var message = "Computed: " + computed;';
|
||||
templateStr += '<' + '/script>';
|
||||
templateStr += 'Input: {{value}}<br>';
|
||||
templateStr += 'Result: {{:computed}}<br>';
|
||||
templateStr += 'Message: {{:message}}';
|
||||
|
||||
const template = Macrobars.compile(templateStr);
|
||||
const data = { value: 21 };
|
||||
const result = template(data);
|
||||
document.getElementById('code-output').innerHTML = result;
|
||||
} catch (error) {
|
||||
document.getElementById('code-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function runDeferDemo() {
|
||||
try {
|
||||
var templateStr = 'Value: {{value}}<br>' +
|
||||
'<' + 'defer>' +
|
||||
'alert("Value was: " + data.value);' +
|
||||
'<' + '/defer>' +
|
||||
'<em>Check browser console and alert!</em>';
|
||||
|
||||
const template = Macrobars.compile(templateStr);
|
||||
const data = { value: 42 };
|
||||
const result = template(data);
|
||||
document.getElementById('code-output').innerHTML = result;
|
||||
} catch (error) {
|
||||
document.getElementById('code-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// Demo 9: Components
|
||||
function runComponentDemo() {
|
||||
try {
|
||||
var userCardTemplate = '<div style="border: 1px solid #ddd; padding: 15px; border-radius: 5px; margin: 5px 0;">';
|
||||
userCardTemplate += '<h4 style="margin: 0 0 10px 0;">👤 {{name}}</h4>';
|
||||
userCardTemplate += '<p style="margin: 5px 0;"><strong>Email:</strong> {{email}}</p>';
|
||||
userCardTemplate += '<p style="margin: 5px 0;"><strong>Role:</strong> {{role}}</p>';
|
||||
userCardTemplate += '{{#if isActive}}<span style="color: green;">✅ Active</span>{{#else}}<span style="color: red;">❌ Inactive</span>{{/if}}';
|
||||
userCardTemplate += '</div>';
|
||||
|
||||
components = Macrobars.createComponents({
|
||||
userCard: userCardTemplate
|
||||
});
|
||||
|
||||
const template = Macrobars.compile('{{#component userCard}}', { components });
|
||||
const data = { name: "John Doe", email: "john@example.com", role: "Developer", isActive: true };
|
||||
const result = template(data);
|
||||
document.getElementById('component-output').innerHTML = result;
|
||||
} catch (error) {
|
||||
document.getElementById('component-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function createCustomComponent() {
|
||||
try {
|
||||
var productCardTemplate = '<div style="border: 2px solid #007acc; padding: 15px; border-radius: 8px; margin: 5px 0; background: #f8f9fa;">';
|
||||
productCardTemplate += '<h4 style="color: #007acc; margin: 0 0 10px 0;">🛍️ {{productName}}</h4>';
|
||||
productCardTemplate += '<p><strong>Price:</strong> ${{%price}}</p>';
|
||||
productCardTemplate += '<p><strong>Stock:</strong> {{stock}} units</p>';
|
||||
productCardTemplate += '{{#if onSale}}<div style="background: #28a745; color: white; padding: 5px; border-radius: 3px; text-align: center;">🏷️ ON SALE!</div>{{/if}}';
|
||||
productCardTemplate += '</div>';
|
||||
|
||||
components = Macrobars.createComponents({
|
||||
productCard: productCardTemplate
|
||||
});
|
||||
|
||||
const template = Macrobars.compile('{{#component productCard}}', { components });
|
||||
const data = { productName: "Magic Widget Pro", price: 299.99, stock: 15, onSale: true };
|
||||
const result = template(data);
|
||||
document.getElementById('component-output').innerHTML = result;
|
||||
} catch (error) {
|
||||
document.getElementById('component-output').innerHTML = '<span style="color: red;">Error: ' + error.message + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// Demo 10: Custom template editor
|
||||
function runCustomTemplate() {
|
||||
try {
|
||||
const templateStr = document.getElementById('template-input').value;
|
||||
const dataStr = document.getElementById('data-input').value;
|
||||
const data = JSON.parse(dataStr);
|
||||
|
||||
currentTemplate = Macrobars.compile(templateStr);
|
||||
const result = currentTemplate(data);
|
||||
|
||||
document.getElementById('dynamic-content').innerHTML = result;
|
||||
|
||||
// Update template info
|
||||
var infoText = 'Compiled successfully!\n';
|
||||
infoText += 'Tokens: ' + (currentTemplate.tokens ? currentTemplate.tokens.length : 0) + '\n';
|
||||
infoText += 'Event bindings: ' + (currentTemplate.event_bindings ? currentTemplate.event_bindings.length : 0) + '\n';
|
||||
infoText += 'Template size: ' + templateStr.length + ' characters';
|
||||
updateTemplateInfo(infoText);
|
||||
|
||||
} catch (error) {
|
||||
document.getElementById('dynamic-content').innerHTML = '<div style="color: red; padding: 10px; background: #ffe6e6; border-radius: 5px;"><strong>❌ Error:</strong> ' + error.message + '</div>';
|
||||
updateTemplateInfo('Compilation failed: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function loadExampleTemplate(type) {
|
||||
const templates = {
|
||||
basic: {
|
||||
template: '<h3>Welcome {{name}}!</h3>\n<p>{{#if isVip}}🌟 VIP Member{{#else}}Regular Member{{/if}}</p>\n<p>Account Balance: ${{%balance}}</p>\n{{#each notifications}}\n• {{data}}\n{{/each}}',
|
||||
data: '{\n "name": "Sarah Connor",\n "isVip": true,\n "balance": 1247.50,\n "notifications": ["New message", "System update", "Payment received"]\n}'
|
||||
},
|
||||
advanced: {
|
||||
template: '<div class="dashboard">\n' +
|
||||
' <h2>{{company}} Dashboard</h2>\n' +
|
||||
' \n' +
|
||||
' <script>\n' +
|
||||
' var totalRevenue = 0;\n' +
|
||||
' data.quarters.forEach(q => totalRevenue += q.revenue);\n' +
|
||||
' </script>\n' +
|
||||
' \n' +
|
||||
' <p><strong>Total Revenue:</strong> ${{:totalRevenue}}</p>\n' +
|
||||
' \n' +
|
||||
' {{#each quarters as quarter}}\n' +
|
||||
' <div style="margin: 10px 0; padding: 10px; border-left: 4px solid #007acc;">\n' +
|
||||
' <h4>{{quarter.name}}</h4>\n' +
|
||||
' <p>Revenue: ${{%quarter.revenue}} {{#eq quarter.trend "up"}}📈{{/eq}}{{#eq quarter.trend "down"}}📉{{/eq}}</p>\n' +
|
||||
' {{#lookup quarter "bonus"}}\n' +
|
||||
' <span style="color: green;">🎯 Bonus: ${{quarter.bonus}}</span>\n' +
|
||||
' {{/lookup}}\n' +
|
||||
' </div>\n' +
|
||||
' {{/each}}\n' +
|
||||
' \n' +
|
||||
' <button {{@click="generateReport"}} style="padding: 10px; background: #28a745; color: white; border: none; border-radius: 5px;">\n' +
|
||||
' 📊 Generate Report\n' +
|
||||
' </button>\n' +
|
||||
'</div>',
|
||||
data: '{\n "company": "TechCorp Industries",\n "quarters": [\n {"name": "Q1 2024", "revenue": 125000, "trend": "up", "bonus": 5000},\n {"name": "Q2 2024", "revenue": 138000, "trend": "up"},\n {"name": "Q3 2024", "revenue": 142000, "trend": "up", "bonus": 7500},\n {"name": "Q4 2024", "revenue": 128000, "trend": "down"}\n ],\n "generateReport": "function() { alert(\'Report generated for \' + this.company); }"\n}'
|
||||
}
|
||||
};
|
||||
|
||||
const example = templates[type];
|
||||
if (example) {
|
||||
document.getElementById('template-input').value = example.template;
|
||||
document.getElementById('data-input').value = example.data;
|
||||
runCustomTemplate();
|
||||
}
|
||||
}
|
||||
|
||||
function clearEditor() {
|
||||
document.getElementById('template-input').value = '';
|
||||
document.getElementById('data-input').value = '{}';
|
||||
document.getElementById('dynamic-content').innerHTML = 'Editor cleared. Enter a template and click "Render Template".';
|
||||
updateTemplateInfo('Ready.');
|
||||
}
|
||||
|
||||
// Initialize demos on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeDemos();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,84 @@
|
||||
(function (root, factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD
|
||||
define(['jquery'], factory);
|
||||
} else if (typeof module === 'object' && module.exports) {
|
||||
// CommonJS
|
||||
module.exports = factory(require('jquery'));
|
||||
} else {
|
||||
// Browser globals
|
||||
root.showPopupMenu = factory(root.$);
|
||||
}
|
||||
}(typeof self !== 'undefined' ? self : this, function ($) {
|
||||
'use strict';
|
||||
|
||||
let showPopupMenu = (x, y, items, prop = {}) => {
|
||||
document.querySelectorAll('.vp-popup-menu').forEach(menu => {
|
||||
if(menu.parentNode) menu.parentNode.removeChild(menu);
|
||||
});
|
||||
|
||||
let container = prop.container || document.body;
|
||||
let menu = $('<div class="vp-popup-menu">').css({
|
||||
position: 'absolute',
|
||||
left: (x|0) + 'px',
|
||||
top: (y|0) + 'px'
|
||||
})[0];
|
||||
|
||||
let elems = [];
|
||||
if(!items || !items.length) {
|
||||
$(menu).append(`<div class="vp-popup-empty">${prop.emptyText || 'No items'}</div>`);
|
||||
} else {
|
||||
items.forEach((it, idx) => {
|
||||
let label = typeof it === 'string' ? it : (it.label || it.screen || it.name || JSON.stringify(it));
|
||||
let $item = $(`<div class="vp-popup-item" tabindex="0">${label}</div>`)
|
||||
.attr('data-idx', idx)
|
||||
.on('click', ev => { ev.stopPropagation(); try { (prop.onSelect || (()=>{}))(it); } catch(e){ console.error(e); } removeMenu(); })
|
||||
.on('keydown', e => { if(e.key === 'Enter') { e.preventDefault(); $item[0].click(); } });
|
||||
$(menu).append($item[0]);
|
||||
elems.push({el: $item[0], data: it});
|
||||
});
|
||||
}
|
||||
|
||||
let focusedIndex = elems.length ? 0 : -1;
|
||||
let focusAt = i => {
|
||||
elems.forEach((it, idx) => { $(it.el).removeClass('focused'); if(idx === i) { $(it.el).addClass('focused'); try{ it.el.focus(); }catch(e){} } });
|
||||
focusedIndex = i;
|
||||
};
|
||||
|
||||
let removeMenu = () => {
|
||||
if(menu.parentNode) menu.parentNode.removeChild(menu);
|
||||
$(document).off('mousedown', onDoc).off('keydown', onKey);
|
||||
$(window).off('resize', onResize);
|
||||
};
|
||||
let onDoc = ev => { if(!menu.contains(ev.target)) removeMenu(); };
|
||||
let onResize = () => reposition();
|
||||
let onKey = ev => {
|
||||
if(!elems.length) { if(ev.key === 'Escape') removeMenu(); return; }
|
||||
if(ev.key === 'Escape') { ev.preventDefault(); removeMenu(); return; }
|
||||
if(ev.key === 'ArrowDown') { ev.preventDefault(); focusAt((focusedIndex + 1) % elems.length); return; }
|
||||
if(ev.key === 'ArrowUp') { ev.preventDefault(); focusAt((focusedIndex - 1 + elems.length) % elems.length); return; }
|
||||
if(ev.key === 'Home') { ev.preventDefault(); focusAt(0); return; }
|
||||
if(ev.key === 'End') { ev.preventDefault(); focusAt(elems.length - 1); return; }
|
||||
if(ev.key === 'Enter') { ev.preventDefault(); if(focusedIndex >= 0) elems[focusedIndex].el.click(); }
|
||||
};
|
||||
|
||||
container.appendChild(menu);
|
||||
let reposition = () => {
|
||||
let mRect = menu.getBoundingClientRect();
|
||||
let winW = window.innerWidth, winH = window.innerHeight;
|
||||
let left = parseInt($(menu).css('left'),10) || 0, top = parseInt($(menu).css('top'),10) || 0;
|
||||
if(mRect.right > winW) left = Math.max(4, winW - Math.ceil(mRect.width) - 8);
|
||||
if(mRect.bottom > winH) top = Math.max(4, winH - Math.ceil(mRect.height) - 8);
|
||||
if(left < 4) left = 4; if(top < 4) top = 4;
|
||||
$(menu).css({left: left + 'px', top: top + 'px'});
|
||||
};
|
||||
|
||||
$(document).on('mousedown', onDoc).on('keydown', onKey);
|
||||
$(window).on('resize', onResize);
|
||||
if(elems.length) { focusAt(0); }
|
||||
setTimeout(reposition, 0);
|
||||
return menu;
|
||||
};
|
||||
|
||||
return showPopupMenu;
|
||||
}));
|
||||
Executable
+976
@@ -0,0 +1,976 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>U-Query.js Demo</title>
|
||||
<style>
|
||||
:root {
|
||||
--space: 8px;
|
||||
--radius: 5px;
|
||||
|
||||
--gray: #6c757d;
|
||||
--gray-bg: #f5f5f5;
|
||||
--blue: #007acc;
|
||||
--green: #28a745;
|
||||
--white: white;
|
||||
--dark: #333;
|
||||
|
||||
--max-width: 1400px;
|
||||
--sidebar: 400px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: var(--space);
|
||||
background: var(--gray-bg);
|
||||
}
|
||||
|
||||
.container {
|
||||
background: var(--white);
|
||||
padding: calc(var(--space) * 1.5);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.main-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: calc(var(--space) * 1.5);
|
||||
}
|
||||
|
||||
.api-column {
|
||||
background: var(--gray-bg);
|
||||
padding: var(--space);
|
||||
border-radius: var(--radius);
|
||||
border-left: 4px solid var(--green);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--dark);
|
||||
text-align: center;
|
||||
margin-bottom: calc(var(--space) * 1.5);
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--gray);
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
padding-bottom: var(--space);
|
||||
margin-top: calc(var(--space) * 1.5);
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin: var(--space) 0;
|
||||
padding: var(--space);
|
||||
background: var(--gray-bg);
|
||||
border-radius: var(--radius);
|
||||
border-left: 4px solid var(--blue);
|
||||
}
|
||||
|
||||
.controls, .slider-group {
|
||||
display: flex;
|
||||
gap: var(--space);
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.controls { flex-wrap: wrap; }
|
||||
.slider-group { align-items: center; }
|
||||
.feature-grid { display: grid; gap: var(--space); margin: var(--space) 0; }
|
||||
|
||||
button {
|
||||
background: var(--blue);
|
||||
color: var(--white);
|
||||
border: none;
|
||||
padding: var(--space) var(--space);
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover { filter: brightness(0.9); }
|
||||
button:disabled { background: #ccc; cursor: not-allowed; }
|
||||
input[type="range"] { flex: 1; max-width: 200px; }
|
||||
input[type="text"] {
|
||||
padding: var(--space);
|
||||
border: 1px solid #ddd;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.status, .code-example {
|
||||
font-family: monospace;
|
||||
padding: var(--space);
|
||||
border-radius: var(--radius);
|
||||
margin: var(--space) 0;
|
||||
}
|
||||
|
||||
.status {
|
||||
background: var(--dark);
|
||||
color: #0f0;
|
||||
white-space: pre-wrap;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.code-example {
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
padding: 15px;
|
||||
overflow-x: auto;
|
||||
margin: var(--space) 0;
|
||||
}
|
||||
|
||||
.highlight { color: #68d391; }
|
||||
.keyword { color: #fbb6ce; }
|
||||
.string { color: #fbd38d; }
|
||||
|
||||
.test-element {
|
||||
padding: var(--space);
|
||||
margin: 5px;
|
||||
border: 2px solid #ddd;
|
||||
background: var(--white);
|
||||
border-radius: var(--radius);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.highlighted {
|
||||
background: #ffd700 !important;
|
||||
border-color: #ffcc00 !important;
|
||||
}
|
||||
|
||||
.hidden { display: none; }
|
||||
|
||||
.output {
|
||||
background: var(--gray-bg);
|
||||
padding: var(--space);
|
||||
border-radius: var(--radius);
|
||||
margin: var(--space) 0;
|
||||
min-height: 40px;
|
||||
border-left: 3px solid var(--green);
|
||||
}
|
||||
|
||||
.ajax-visual {
|
||||
height: 100px;
|
||||
background: linear-gradient(45deg, #1e3c72, #2a5298);
|
||||
border-radius: var(--radius);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--white);
|
||||
font-weight: bold;
|
||||
margin: 15px 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loading-animation {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.3;
|
||||
background: repeating-linear-gradient(90deg, transparent 0 10px, rgba(255,255,255,0.2) 10px 20px);
|
||||
animation: wave 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes wave { to { transform: translateX(20px); } }
|
||||
|
||||
.api-section { margin: 15px 0; }
|
||||
.api-section h3 {
|
||||
color: var(--green);
|
||||
margin: 0 0 var(--space) 0;
|
||||
}
|
||||
|
||||
.api-method {
|
||||
font-family: monospace;
|
||||
margin: 3px 0;
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.api-method .method-name { color: var(--blue); font-weight: bold; }
|
||||
.api-method .return-type { color: #6f42c1; }
|
||||
.api-method .param { color: #e83e8c; }
|
||||
|
||||
.api-description {
|
||||
margin: var(--space) 0;
|
||||
}
|
||||
|
||||
.api-options {
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.code-comment {
|
||||
color: #68d391;
|
||||
}
|
||||
|
||||
.demo-visual {
|
||||
background: var(--gray-bg);
|
||||
padding: calc(var(--space) * 2);
|
||||
border-radius: var(--radius);
|
||||
margin: var(--space) 0;
|
||||
border: 2px dashed #ddd;
|
||||
text-align: center;
|
||||
min-height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.demo-visual.active {
|
||||
background: #e8f5e8;
|
||||
border-color: var(--green);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.main-layout { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>U-Query.js Demo</h1>
|
||||
|
||||
<div class="main-layout">
|
||||
<div class="demo-column">
|
||||
<h2>Element Selection & DOM Manipulation</h2>
|
||||
<div class="demo-section">
|
||||
<h3>DOM Queries</h3>
|
||||
<div class="demo-visual" id="selection-visual">
|
||||
<span>Select elements to see them highlighted</span>
|
||||
</div>
|
||||
<div class="test-element" id="test-1">Test Element 1</div>
|
||||
<div class="test-element" id="test-2">Test Element 2</div>
|
||||
<div class="test-element special" id="test-3">Test Element 3 (special)</div>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="selectAllElements()">Select All .test-element</button>
|
||||
<button onclick="selectSpecial()">Select .special</button>
|
||||
<button onclick="selectById()">Select by ID</button>
|
||||
<button onclick="clearSelection()">Clear Selection</button>
|
||||
</div>
|
||||
|
||||
<div class="status" id="selection-output"></div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>DOM Manipulation</h3>
|
||||
<div class="demo-visual" id="manipulation-target">Original content</div>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="htmlDemo()">Change HTML</button>
|
||||
<button onclick="textDemo()">Change Text</button>
|
||||
<button onclick="appendDemo()">Append Content</button>
|
||||
<button onclick="resetManipulation()">Reset</button>
|
||||
</div>
|
||||
|
||||
<div class="status" id="manipulation-output"></div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>CSS & Classes</h3>
|
||||
<div class="test-element" id="css-target">CSS Target Element</div>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="cssDemo()">Apply CSS Styles</button>
|
||||
<button onclick="addClassDemo()">Add Highlight Class</button>
|
||||
<button onclick="removeClassDemo()">Remove Class</button>
|
||||
<button onclick="resetCSS()">Reset Styles</button>
|
||||
</div>
|
||||
|
||||
<div class="status" id="css-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-column">
|
||||
<h2>Selection API</h2>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Core Selectors</h3>
|
||||
<div class="api-method"><span class="method-name">$</span>(<span class="param">selector</span>) → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">$</span>(<span class="param">'.class'</span>) → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">$</span>(<span class="param">'#id'</span>) → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">$</span>(<span class="param">'tag'</span>) → <span class="return-type">NodeList</span></div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>DOM Manipulation</h3>
|
||||
<div class="api-method"><span class="method-name">html</span>(<span class="param">content?</span>) → <span class="return-type">NodeList|string</span></div>
|
||||
<div class="api-method"><span class="method-name">text</span>(<span class="param">content?</span>) → <span class="return-type">NodeList|string</span></div>
|
||||
<div class="api-method"><span class="method-name">append</span>(<span class="param">content</span>) → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">prepend</span>(<span class="param">content</span>) → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">remove</span>() → <span class="return-type">NodeList</span></div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>CSS & Classes</h3>
|
||||
<div class="api-method"><span class="method-name">css</span>(<span class="param">styles</span>) → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">addClass</span>(<span class="param">className</span>) → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">removeClass</span>(<span class="param">className</span>) → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">toggleClass</span>(<span class="param">className</span>) → <span class="return-type">NodeList</span></div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Examples</h3>
|
||||
<pre class="code-example">
|
||||
<span class="code-comment">// Element selection</span>
|
||||
<span class="keyword">const</span> elements = <span class="highlight">$</span>(<span class="string">'.my-class'</span>);
|
||||
<span class="keyword">const</span> byId = <span class="highlight">$</span>(<span class="string">'#my-id'</span>);
|
||||
|
||||
<span class="highlight">$</span>(<span class="string">'.target'</span>).html(<span class="string">'<b>New content</b>'</span>);
|
||||
<span class="highlight">$</span>(<span class="string">'.target'</span>).text(<span class="string">'Plain text'</span>);
|
||||
<span class="highlight">$</span>(<span class="string">'.container'</span>).append(<span class="string">'<p>Added</p>'</span>);
|
||||
|
||||
<span class="highlight">$</span>(<span class="string">'.element'</span>).css({
|
||||
<span class="string">'color'</span>: <span class="string">'red'</span>,
|
||||
<span class="string">'background'</span>: <span class="string">'blue'</span>
|
||||
});
|
||||
<span class="highlight">$</span>(<span class="string">'.element'</span>).addClass(<span class="string">'active'</span>);
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-layout">
|
||||
<div class="demo-column">
|
||||
<h2>Event Handling & Visibility</h2>
|
||||
<div class="demo-section">
|
||||
<h3>Event System</h3>
|
||||
<button id="event-button">Click Me!</button>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="addEventHandler()">Add Click Handler</button>
|
||||
<button onclick="removeEventHandler()">Remove Handler</button>
|
||||
<button onclick="addMultipleEvents()">Add Multiple Events</button>
|
||||
</div>
|
||||
|
||||
<div class="status" id="event-output"></div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Show/Hide/Toggle</h3>
|
||||
<div class="test-element" id="toggle-target">Toggle me!</div>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="$('#toggle-target').hide()">Hide</button>
|
||||
<button onclick="$('#toggle-target').show()">Show</button>
|
||||
<button onclick="$('#toggle-target').toggle()">Toggle</button>
|
||||
<button onclick="animateVisibility()">Animate Toggle</button>
|
||||
</div>
|
||||
|
||||
<div class="status" id="toggle-output"></div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Method Chaining</h3>
|
||||
<div class="demo-visual" id="chaining-demo">
|
||||
<div class="test-element chain-target">Chain Target 1</div>
|
||||
<div class="test-element chain-target">Chain Target 2</div>
|
||||
<div class="test-element chain-target">Chain Target 3</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="chainingDemo()">Demo Method Chaining</button>
|
||||
<button onclick="resetChaining()">Reset</button>
|
||||
</div>
|
||||
|
||||
<div class="status" id="chaining-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-column">
|
||||
<h2>Events & Visibility API</h2>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Event Methods</h3>
|
||||
<div class="api-method"><span class="method-name">on</span>(<span class="param">event, handler</span>) → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">off</span>(<span class="param">event, handler?</span>) → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">trigger</span>(<span class="param">event, data?</span>) → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">once</span>(<span class="param">event, handler</span>) → <span class="return-type">NodeList</span></div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Visibility Control</h3>
|
||||
<div class="api-method"><span class="method-name">show</span>() → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">hide</span>() → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">toggle</span>() → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">fadeIn</span>(<span class="param">duration?</span>) → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">fadeOut</span>(<span class="param">duration?</span>) → <span class="return-type">NodeList</span></div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Method Chaining</h3>
|
||||
<div class="api-description">
|
||||
All u-query methods return the NodeList, enabling method chaining.
|
||||
</div>
|
||||
<pre class="code-example">
|
||||
<span class="highlight">$</span>(<span class="string">'.button'</span>).on(<span class="string">'click'</span>, <span class="keyword">function</span>() {
|
||||
<span class="highlight">console</span>.log(<span class="string">'Clicked!'</span>);
|
||||
});
|
||||
|
||||
<span class="highlight">$</span>(<span class="string">'.element'</span>)
|
||||
.addClass(<span class="string">'active'</span>)
|
||||
.css({<span class="string">'color'</span>: <span class="string">'red'</span>})
|
||||
.show()
|
||||
.on(<span class="string">'click'</span>, handler);
|
||||
|
||||
<span class="highlight">$</span>(<span class="string">'.modal'</span>).fadeIn(<span class="highlight">300</span>);
|
||||
<span class="highlight">$</span>(<span class="string">'.tooltip'</span>).fadeOut(<span class="highlight">200</span>);
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-layout">
|
||||
<div class="demo-column">
|
||||
<h2>AJAX & Global Events</h2>
|
||||
<div class="demo-section">
|
||||
<h3>AJAX Utilities</h3>
|
||||
<div class="ajax-visual" id="ajax-visual">
|
||||
<div class="loading-animation" id="loading-animation" style="animation-play-state: paused;"></div>
|
||||
<span>AJAX Request Status</span>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="ajaxDemo()">GET Request</button>
|
||||
<button onclick="postDemo()">POST Request</button>
|
||||
<button onclick="asyncDemo()">Async/Await Demo</button>
|
||||
</div>
|
||||
|
||||
<div class="status" id="ajax-output"></div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Global Event System</h3>
|
||||
<input type="text" id="global-event-data" placeholder="Event data" value="Hello global events!">
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="addGlobalListener()">Add Global Listener</button>
|
||||
<button onclick="emitGlobalEvent()">Emit Global Event</button>
|
||||
<button onclick="removeGlobalListener()">Remove Listener</button>
|
||||
</div>
|
||||
|
||||
<div class="status" id="global-output"></div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>Utility Functions</h3>
|
||||
<div class="test-element util-target">Utility Element 1</div>
|
||||
<div class="test-element util-target">Utility Element 2</div>
|
||||
<div class="test-element util-target">Utility Element 3</div>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="eachDemo()">Test $.each</button>
|
||||
<button onclick="readyDemo()">Test $.ready</button>
|
||||
<button onclick="utilityChain()">Utility Chain Demo</button>
|
||||
</div>
|
||||
|
||||
<div class="status" id="utility-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-column">
|
||||
<h2>AJAX & Utilities API</h2>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>AJAX Methods</h3>
|
||||
<div class="api-method"><span class="method-name">$.get</span>(<span class="param">url, callback</span>) → <span class="return-type">Promise</span></div>
|
||||
<div class="api-method"><span class="method-name">$.post</span>(<span class="param">url, data, callback</span>) → <span class="return-type">Promise</span></div>
|
||||
<div class="api-method"><span class="method-name">$.ajax</span>(<span class="param">options</span>) → <span class="return-type">Promise</span></div>
|
||||
<div class="api-method"><span class="method-name">$.json</span>(<span class="param">url</span>) → <span class="return-type">Promise</span></div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Global Event System</h3>
|
||||
<div class="api-method"><span class="method-name">$.on</span>(<span class="param">event, handler</span>) → <span class="return-type">$</span></div>
|
||||
<div class="api-method"><span class="method-name">$.off</span>(<span class="param">event, handler?</span>) → <span class="return-type">$</span></div>
|
||||
<div class="api-method"><span class="method-name">$.emit</span>(<span class="param">event, data?</span>) → <span class="return-type">number</span></div>
|
||||
<div class="api-method"><span class="method-name">$.trigger</span>(<span class="param">event, data?</span>) → <span class="return-type">number</span></div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Utility Functions</h3>
|
||||
<div class="api-method"><span class="method-name">$.each</span>(<span class="param">selector, callback</span>) → <span class="return-type">$</span></div>
|
||||
<div class="api-method"><span class="method-name">$.ready</span>(<span class="param">callback</span>) → <span class="return-type">$</span></div>
|
||||
<div class="api-method"><span class="method-name">$.extend</span>(<span class="param">target, ...sources</span>) → <span class="return-type">object</span></div>
|
||||
<div class="api-method"><span class="method-name">$.map</span>(<span class="param">selector, callback</span>) → <span class="return-type">Array</span></div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Examples</h3>
|
||||
<pre class="code-example">
|
||||
<span class="keyword">const</span> data = <span class="keyword">await</span> <span class="highlight">$.get</span>(<span class="string">'/api/users'</span>);
|
||||
<span class="highlight">$.post</span>(<span class="string">'/api/users'</span>, {name: <span class="string">'John'</span>})
|
||||
.then(<span class="param">response</span> => <span class="highlight">console</span>.log(<span class="param">response</span>));
|
||||
|
||||
<span class="highlight">$.on</span>(<span class="string">'user-login'</span>, <span class="param">user</span> => {
|
||||
<span class="highlight">console</span>.log(<span class="string">'User logged in:'</span>, <span class="param">user</span>);
|
||||
});
|
||||
<span class="highlight">$.emit</span>(<span class="string">'user-login'</span>, {id: <span class="highlight">123</span>});
|
||||
|
||||
<span class="highlight">$.each</span>(<span class="string">'.item'</span>, (<span class="param">el</span>, <span class="param">i</span>) => {
|
||||
<span class="param">el</span>.textContent = <span class="string">`Item ${</span><span class="param">i</span><span class="string">}`</span>;
|
||||
});
|
||||
|
||||
<span class="highlight">$.ready</span>(() => {
|
||||
<span class="highlight">console</span>.log(<span class="string">'DOM ready!'</span>);
|
||||
});
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-layout">
|
||||
<div class="demo-column">
|
||||
<h2>Browser Info</h2>
|
||||
<div class="demo-section">
|
||||
<div class="status" id="library-info">Loading library information...</div>
|
||||
|
||||
<h3>This browser supports</h3>
|
||||
<div id="feature-support" class="status"></div>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="testCompatibility()">Test Browser Compatibility</button>
|
||||
<button onclick="performanceBenchmark()">Performance Benchmark</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="api-column">
|
||||
<h2>Advanced Features</h2>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Advanced Selectors</h3>
|
||||
<div class="api-method"><span class="method-name">$</span>(<span class="param">':visible'</span>) → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">$</span>(<span class="param">':hidden'</span>) → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">$</span>(<span class="param">':checked'</span>) → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">$</span>(<span class="param">':first'</span>) → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">$</span>(<span class="param">':last'</span>) → <span class="return-type">NodeList</span></div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Data Attributes</h3>
|
||||
<div class="api-method"><span class="method-name">data</span>(<span class="param">key, value?</span>) → <span class="return-type">NodeList|any</span></div>
|
||||
<div class="api-method"><span class="method-name">attr</span>(<span class="param">name, value?</span>) → <span class="return-type">NodeList|string</span></div>
|
||||
<div class="api-method"><span class="method-name">prop</span>(<span class="param">name, value?</span>) → <span class="return-type">NodeList|any</span></div>
|
||||
<div class="api-method"><span class="method-name">val</span>(<span class="param">value?</span>) → <span class="return-type">NodeList|string</span></div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>Traversal Methods</h3>
|
||||
<div class="api-method"><span class="method-name">parent</span>() → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">children</span>() → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">siblings</span>() → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">find</span>(<span class="param">selector</span>) → <span class="return-type">NodeList</span></div>
|
||||
<div class="api-method"><span class="method-name">closest</span>(<span class="param">selector</span>) → <span class="return-type">NodeList</span></div>
|
||||
</div>
|
||||
|
||||
<div class="api-section">
|
||||
<h3>DOM Diff Updates</h3>
|
||||
<div class="api-description">
|
||||
u-query can make use of morphdom.js for efficient DOM diff updates.
|
||||
</div>
|
||||
<div class="api-method"><span class="method-name">$.options.alwaysDoDifferentialUpdate</span> = <span class="param">true</span></div>
|
||||
<div class="api-description">
|
||||
Global flag for morphdom.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="u-query.js"></script>
|
||||
<script>
|
||||
let clickHandler, globalHandler;
|
||||
|
||||
// Initialize library info on load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
updateLibraryInfo();
|
||||
updateFeatureSupport();
|
||||
});
|
||||
|
||||
function updateLibraryInfo() {
|
||||
const info = document.getElementById('library-info');
|
||||
const version = typeof $ !== 'undefined' ? 'Loaded' : 'Not Available';
|
||||
const selectors = document.querySelectorAll('*').length;
|
||||
|
||||
info.textContent = `
|
||||
Library Status: ${version}
|
||||
DOM Elements: ${selectors}
|
||||
Browser: ${navigator.userAgent.split(' ')[0]}
|
||||
Features: ES6+, Promises, DOM API
|
||||
Build: Development
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function updateFeatureSupport() {
|
||||
const features = [
|
||||
{ name: 'Promises', supported: typeof Promise !== 'undefined' },
|
||||
{ name: 'Arrow Functions', supported: true },
|
||||
{ name: 'Template Literals', supported: true },
|
||||
{ name: 'Fetch API', supported: typeof fetch !== 'undefined' },
|
||||
{ name: 'querySelector', supported: typeof document.querySelector !== 'undefined' },
|
||||
{ name: 'addEventListener', supported: typeof document.addEventListener !== 'undefined' }
|
||||
];
|
||||
|
||||
const support = features.map(feature =>
|
||||
`${feature.name}: ${feature.supported ? 'Supported' : 'Not Supported'}`
|
||||
).join('\n');
|
||||
|
||||
document.getElementById('feature-support').textContent = support;
|
||||
}
|
||||
|
||||
function log(elementId, message) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.textContent += new Date().toLocaleTimeString() + ': ' + message + '\n';
|
||||
element.scrollTop = element.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced Selection Demos
|
||||
function selectAllElements() {
|
||||
clearSelection();
|
||||
const elements = $('.test-element');
|
||||
elements.forEach(el => el.classList.add('highlighted'));
|
||||
updateSelectionVisual(`Selected ${elements.length} .test-element items`);
|
||||
log('selection-output', `Found ${elements.length} elements with class 'test-element'`);
|
||||
}
|
||||
|
||||
function selectSpecial() {
|
||||
clearSelection();
|
||||
const elements = $('.special');
|
||||
elements.forEach(el => el.classList.add('highlighted'));
|
||||
updateSelectionVisual(`Selected ${elements.length} .special items`);
|
||||
log('selection-output', `Found ${elements.length} elements with class 'special'`);
|
||||
}
|
||||
|
||||
function selectById() {
|
||||
clearSelection();
|
||||
const elements = $('#test-2');
|
||||
elements.forEach(el => el.classList.add('highlighted'));
|
||||
updateSelectionVisual(`Selected element by ID: #test-2`);
|
||||
log('selection-output', `Found element by ID: ${elements.length > 0 ? elements[0].textContent : 'none'}`);
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
$('.test-element').forEach(el => el.classList.remove('highlighted'));
|
||||
updateSelectionVisual('Selection cleared');
|
||||
}
|
||||
|
||||
function updateSelectionVisual(message) {
|
||||
const visual = document.getElementById('selection-visual');
|
||||
visual.textContent = message;
|
||||
visual.classList.add('active');
|
||||
setTimeout(() => visual.classList.remove('active'), 2000);
|
||||
}
|
||||
|
||||
// DOM Manipulation
|
||||
function htmlDemo() {
|
||||
$('#manipulation-target').html('<strong>HTML content changed</strong>');
|
||||
log('manipulation-output', 'HTML content updated with formatting');
|
||||
}
|
||||
|
||||
function textDemo() {
|
||||
$('#manipulation-target').text('Text content changed');
|
||||
log('manipulation-output', 'Text content updated (plain text)');
|
||||
}
|
||||
|
||||
function appendDemo() {
|
||||
$('#manipulation-target').append(' <em>Appended content</em>');
|
||||
log('manipulation-output', 'Content appended to existing content');
|
||||
}
|
||||
|
||||
function resetManipulation() {
|
||||
$('#manipulation-target').html('Original content');
|
||||
log('manipulation-output', 'Content reset to original state');
|
||||
}
|
||||
|
||||
// CSS and Classes
|
||||
function cssDemo() {
|
||||
$('#css-target').css({
|
||||
'background': 'linear-gradient(45deg, #ff6b6b, #4ecdc4)',
|
||||
'color': 'white',
|
||||
'border-radius': '10px',
|
||||
'padding': '15px',
|
||||
'transform': 'scale(1.05)',
|
||||
'transition': 'all 0.3s ease'
|
||||
});
|
||||
log('css-output', 'Applied gradient background and transformation');
|
||||
}
|
||||
|
||||
function addClassDemo() {
|
||||
$('#css-target').addClass('highlighted');
|
||||
log('css-output', 'Added "highlighted" class');
|
||||
}
|
||||
|
||||
function removeClassDemo() {
|
||||
$('#css-target').removeClass('highlighted');
|
||||
log('css-output', 'Removed "highlighted" class');
|
||||
}
|
||||
|
||||
function resetCSS() {
|
||||
$('#css-target').css({
|
||||
'background': '',
|
||||
'color': '',
|
||||
'border-radius': '',
|
||||
'padding': '',
|
||||
'transform': '',
|
||||
'transition': ''
|
||||
}).removeClass('highlighted');
|
||||
log('css-output', 'All styles and classes reset');
|
||||
}
|
||||
|
||||
function addEventHandler() {
|
||||
clickHandler = function(e) {
|
||||
log('event-output', `Button clicked! Event type: ${e.type}`);
|
||||
e.target.style.transform = 'scale(0.95)';
|
||||
setTimeout(() => e.target.style.transform = '', 150);
|
||||
};
|
||||
$('#event-button').on('click', clickHandler);
|
||||
log('event-output', 'Click handler added with visual feedback');
|
||||
}
|
||||
|
||||
function removeEventHandler() {
|
||||
if (clickHandler) {
|
||||
$('#event-button').off('click', clickHandler);
|
||||
log('event-output', 'Click handler removed');
|
||||
}
|
||||
}
|
||||
|
||||
function addMultipleEvents() {
|
||||
const button = $('#event-button')[0];
|
||||
if (button) {
|
||||
const events = {
|
||||
mouseenter: () => log('event-output', 'Mouse entered button'),
|
||||
mouseleave: () => log('event-output', 'Mouse left button'),
|
||||
focus: () => log('event-output', 'Button focused'),
|
||||
blur: () => log('event-output', 'Button lost focus')
|
||||
};
|
||||
|
||||
Object.entries(events).forEach(([event, handler]) => {
|
||||
button.addEventListener(event, handler);
|
||||
});
|
||||
|
||||
log('event-output', 'Added multiple event listeners (hover, focus, blur)');
|
||||
}
|
||||
}
|
||||
|
||||
function animateVisibility() {
|
||||
const target = $('#toggle-target')[0];
|
||||
if (target) {
|
||||
target.style.transition = 'all 0.5s ease';
|
||||
target.style.transform = target.style.display === 'none' ? 'scale(1)' : 'scale(0)';
|
||||
setTimeout(() => {
|
||||
$('#toggle-target').toggle();
|
||||
if (target.style.display !== 'none') {
|
||||
target.style.transform = 'scale(1)';
|
||||
}
|
||||
}, 250);
|
||||
log('toggle-output', 'Animated visibility toggle with scaling effect');
|
||||
}
|
||||
|
||||
function ajaxDemo() {
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced AJAX with visual feedback
|
||||
function ajaxDemo() {
|
||||
updateAjaxVisual(true);
|
||||
log('ajax-output', 'Initiating GET request...');
|
||||
|
||||
$.get('https://jsonplaceholder.typicode.com/posts/1', function(data) {
|
||||
updateAjaxVisual(false);
|
||||
log('ajax-output', `GET Success: "${data.title.substring(0, 50)}..."`);
|
||||
log('ajax-output', `Response body: ${data.body.substring(0, 100)}...`);
|
||||
}).catch(error => {
|
||||
updateAjaxVisual(false);
|
||||
log('ajax-output', `GET Error: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
function postDemo() {
|
||||
updateAjaxVisual(true);
|
||||
log('ajax-output', 'Initiating POST request...');
|
||||
|
||||
$.post('https://jsonplaceholder.typicode.com/posts', {
|
||||
title: 'u-query demo post',
|
||||
body: 'This is a test post from u-query demonstration',
|
||||
userId: 1
|
||||
}, function(data) {
|
||||
updateAjaxVisual(false);
|
||||
log('ajax-output', `POST Success: Created post with ID ${data.id}`);
|
||||
log('ajax-output', `Posted title: "${data.title}"`);
|
||||
}).catch(error => {
|
||||
updateAjaxVisual(false);
|
||||
log('ajax-output', `POST Error: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function asyncDemo() {
|
||||
try {
|
||||
updateAjaxVisual(true);
|
||||
log('ajax-output', 'Using async/await pattern...');
|
||||
|
||||
const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
|
||||
const user = await response.json();
|
||||
|
||||
updateAjaxVisual(false);
|
||||
log('ajax-output', `Async Success: User "${user.name}" from ${user.address.city}`);
|
||||
log('ajax-output', `Email: ${user.email}, Phone: ${user.phone}`);
|
||||
} catch (error) {
|
||||
updateAjaxVisual(false);
|
||||
log('ajax-output', `Async Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function updateAjaxVisual(loading) {
|
||||
const visual = document.getElementById('ajax-visual');
|
||||
const animation = document.getElementById('loading-animation');
|
||||
const text = visual.querySelector('span');
|
||||
|
||||
if (loading) {
|
||||
visual.style.background = 'linear-gradient(45deg, #ff6b6b, #4ecdc4)';
|
||||
animation.style.animationPlayState = 'running';
|
||||
text.textContent = 'Loading...';
|
||||
} else {
|
||||
visual.style.background = 'linear-gradient(45deg, #1e3c72, #2a5298)';
|
||||
animation.style.animationPlayState = 'paused';
|
||||
text.textContent = 'Request Complete';
|
||||
}
|
||||
}
|
||||
|
||||
function addGlobalListener() {
|
||||
globalHandler = function(data) {
|
||||
log('global-output', `Global event received: "${data}"`);
|
||||
};
|
||||
$.on('demo-event', globalHandler);
|
||||
log('global-output', 'Global listener added for "demo-event"');
|
||||
}
|
||||
|
||||
function emitGlobalEvent() {
|
||||
const data = $('#global-event-data')[0].value;
|
||||
const count = $.emit('demo-event', data);
|
||||
log('global-output', `Event emitted to ${count} listeners with data: "${data}"`);
|
||||
}
|
||||
|
||||
function removeGlobalListener() {
|
||||
if (globalHandler) {
|
||||
$.off('demo-event', globalHandler);
|
||||
log('global-output', 'Global listener removed');
|
||||
}
|
||||
}
|
||||
|
||||
function eachDemo() {
|
||||
document.getElementById('utility-output').textContent = '';
|
||||
$.each('.util-target', function(element, index) {
|
||||
element.style.background = `hsl(${index * 60}, 70%, 90%)`;
|
||||
element.style.transform = `translateX(${index * 10}px)`;
|
||||
log('utility-output', `Element ${index + 1}: "${element.textContent}" - styled`);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
$('.util-target').forEach(el => {
|
||||
el.style.background = '';
|
||||
el.style.transform = '';
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function readyDemo() {
|
||||
$.ready(function() {
|
||||
log('utility-output', '$.ready() callback executed - DOM fully loaded');
|
||||
log('utility-output', `Current timestamp: ${new Date().toLocaleString()}`);
|
||||
});
|
||||
}
|
||||
|
||||
function utilityChain() {
|
||||
$.each('.util-target', (el, i) => {
|
||||
$(el).css({'color': 'blue'}).addClass('highlighted');
|
||||
});
|
||||
log('utility-output', 'Utility chain: each + css + addClass applied');
|
||||
|
||||
setTimeout(() => {
|
||||
$('.util-target').css({'color': ''}).removeClass('highlighted');
|
||||
log('utility-output', 'Utility chain effects reset');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function chainingDemo() {
|
||||
$('.chain-target')
|
||||
.addClass('highlighted')
|
||||
.css({
|
||||
'color': 'white',
|
||||
'background': 'linear-gradient(45deg, #667eea, #764ba2)',
|
||||
'transform': 'translateY(-5px) rotate(2deg)',
|
||||
'transition': 'all 0.5s ease'
|
||||
})
|
||||
.show();
|
||||
|
||||
log('chaining-output', 'Method chain applied: addClass → css → show');
|
||||
log('chaining-output', 'Added highlight, gradient, and transform');
|
||||
}
|
||||
|
||||
function resetChaining() {
|
||||
$('.chain-target')
|
||||
.removeClass('highlighted')
|
||||
.css({
|
||||
'color': '',
|
||||
'background': '',
|
||||
'transform': '',
|
||||
'transition': ''
|
||||
});
|
||||
|
||||
log('chaining-output', 'Method chain reset: removeClass → css clear');
|
||||
}
|
||||
|
||||
// Performance and Compatibility
|
||||
function testCompatibility() {
|
||||
const tests = [
|
||||
{ name: 'CSS Selectors', test: () => !!document.querySelector },
|
||||
{ name: 'Event Listeners', test: () => !!document.addEventListener },
|
||||
{ name: 'Fetch API', test: () => !!window.fetch },
|
||||
{ name: 'Promises', test: () => !!window.Promise },
|
||||
{ name: 'Arrow Functions', test: () => { try { eval('() => {}'); return true; } catch { return false; } } },
|
||||
{ name: 'Template Literals', test: () => { try { eval('`test`'); return true; } catch { return false; } } }
|
||||
];
|
||||
|
||||
document.getElementById('library-info').textContent = 'Running compatibility tests...\n';
|
||||
|
||||
tests.forEach((test, i) => {
|
||||
setTimeout(() => {
|
||||
const result = test.test();
|
||||
document.getElementById('library-info').textContent +=
|
||||
`${test.name}: ${result ? 'Supported' : 'Not Supported'}\n`;
|
||||
}, i * 200);
|
||||
});
|
||||
}
|
||||
|
||||
function performanceBenchmark() {
|
||||
const iterations = 1000;
|
||||
const start = performance.now();
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
$('.test-element').css({'color': 'red'}).addClass('test-class').removeClass('test-class');
|
||||
}
|
||||
|
||||
const end = performance.now();
|
||||
const duration = (end - start).toFixed(2);
|
||||
|
||||
document.getElementById('library-info').textContent =
|
||||
`Performance Benchmark Complete\n` +
|
||||
`Operations: ${iterations} selection + css + addClass + removeClass\n` +
|
||||
`Duration: ${duration}ms\n` +
|
||||
`Avg per operation: ${(duration / iterations).toFixed(4)}ms\n` +
|
||||
`Operations per second: ${(iterations / (duration / 1000)).toFixed(0)}`;
|
||||
}
|
||||
|
||||
$.ready(function() {
|
||||
log('utility-output', 'Demo initialized successfully');
|
||||
});
|
||||
|
||||
setInterval(updateLibraryInfo, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+589
@@ -0,0 +1,589 @@
|
||||
(function (root, factory) {
|
||||
// this is the stupid UMD pattern, but eh we lost that battle a long time ago
|
||||
if (typeof exports === 'object' && typeof module !== 'undefined') {
|
||||
var exports_obj = factory();
|
||||
module.exports = exports_obj.$;
|
||||
// Also export individual components
|
||||
for (var key in exports_obj) {
|
||||
if (key !== '$' && exports_obj.hasOwnProperty(key)) {
|
||||
exports[key] = exports_obj[key];
|
||||
}
|
||||
}
|
||||
} else if (typeof define === 'function' && define.amd) {
|
||||
define([], factory);
|
||||
} else {
|
||||
var exports_obj = factory();
|
||||
root.$ = exports_obj.$;
|
||||
for (var key in exports_obj) {
|
||||
if (key !== '$' && exports_obj.hasOwnProperty(key)) {
|
||||
root[key] = exports_obj[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}(typeof self !== 'undefined' ? self : this, function () {
|
||||
|
||||
// a collection of DOM utility functions that I thought were cool in jQuery
|
||||
|
||||
class EventEmitter {
|
||||
constructor() {
|
||||
/** Map<eventName, Map<slotKey|Symbol, Function>> */
|
||||
this._topics = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to `topic`. If `slot_key` is provided (truthy),
|
||||
* it dedupes by that key; otherwise a unique Symbol is used.
|
||||
* Returns an unsubscribe fn.
|
||||
*/
|
||||
on(topic, handler, slot_key = null) {
|
||||
let map = this._topics.get(topic);
|
||||
if (!map) {
|
||||
map = new Map();
|
||||
this._topics.set(topic, map);
|
||||
}
|
||||
// use the provided slot_key or a fresh Symbol()
|
||||
const key = slot_key != null ? slot_key : Symbol();
|
||||
map.set(key, handler);
|
||||
return () => this.off(topic, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe by handler function or by slot_key.
|
||||
*/
|
||||
off(topic, handlerOrSlotKey) {
|
||||
const map = this._topics.get(topic);
|
||||
if (!map) return;
|
||||
|
||||
// if it matches a slotKey directly, remove it
|
||||
if (map.has(handlerOrSlotKey)) {
|
||||
map.delete(handlerOrSlotKey);
|
||||
} else {
|
||||
// otherwise assume it's a function: remove all matching fns
|
||||
for (const [key, fn] of map.entries()) {
|
||||
if (fn === handlerOrSlotKey) {
|
||||
map.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (map.size === 0) {
|
||||
this._topics.delete(topic);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit to all handlers on `topic`. Handlers returning
|
||||
* 'remove_handler' are auto-removed.
|
||||
* Returns the number of handlers invoked.
|
||||
*/
|
||||
emit(topic, ...args) {
|
||||
let count = 0;
|
||||
const map = this._topics.get(topic);
|
||||
if (!map) return count;
|
||||
|
||||
for (const [key, fn] of Array.from(map.entries())) {
|
||||
const res = fn(...args);
|
||||
count++;
|
||||
if (res === 'remove_handler') {
|
||||
map.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (map.size === 0) {
|
||||
this._topics.delete(topic);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
class QueryWrapper extends Array {
|
||||
constructor(elements) {
|
||||
super();
|
||||
if (elements) {
|
||||
this.push(...(Array.isArray(elements) ? elements : [elements]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$ = function(selector_or_element) {
|
||||
let elements;
|
||||
if (selector_or_element instanceof Element || selector_or_element === document || selector_or_element === window) {
|
||||
elements = [selector_or_element];
|
||||
} else if (typeof selector_or_element === 'string') {
|
||||
// Check if string looks like HTML (starts with < and ends with >)
|
||||
if (selector_or_element.trim().charAt(0) === '<' && selector_or_element.trim().charAt(selector_or_element.trim().length - 1) === '>') {
|
||||
// Create element from HTML string
|
||||
let temp = document.createElement('div');
|
||||
temp.innerHTML = selector_or_element.trim();
|
||||
elements = Array.from(temp.children);
|
||||
} else {
|
||||
// Treat as CSS selector
|
||||
elements = Array.from(document.querySelectorAll(selector_or_element));
|
||||
}
|
||||
} else if (selector_or_element instanceof NodeList || Array.isArray(selector_or_element)) {
|
||||
elements = Array.from(selector_or_element);
|
||||
} else {
|
||||
elements = [selector_or_element];
|
||||
}
|
||||
return new QueryWrapper(elements);
|
||||
};
|
||||
|
||||
$.options = {
|
||||
alwaysDoDifferentialUpdate: true,
|
||||
};
|
||||
|
||||
$.events = new EventEmitter();
|
||||
$.on = $.events.on.bind($.events);
|
||||
$.off = $.events.off.bind($.events);
|
||||
$.emit = $.events.emit.bind($.events);
|
||||
|
||||
$.each = function(selector, callback) {
|
||||
let elements;
|
||||
if (typeof selector === 'string') {
|
||||
elements = document.querySelectorAll(selector);
|
||||
} else if (selector instanceof QueryWrapper) {
|
||||
elements = selector;
|
||||
} else {
|
||||
elements = selector;
|
||||
}
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
callback(elements[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
$.post = function(url, data, callback) {
|
||||
return $.ajax({
|
||||
method: 'POST',
|
||||
url: url,
|
||||
data: data,
|
||||
success: callback,
|
||||
error: function(xhr) {
|
||||
console.error('POST request failed:', xhr);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$.get = function(url, callback) {
|
||||
return $.ajax({
|
||||
method: 'GET',
|
||||
url: url,
|
||||
success: callback,
|
||||
error: function(xhr) {
|
||||
console.error('GET request failed:', xhr);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$.ajax = function(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open(options.method || 'GET', options.url, true);
|
||||
|
||||
// Set default headers
|
||||
if (options.method === 'POST' || options.data) {
|
||||
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
|
||||
}
|
||||
|
||||
// Set custom headers
|
||||
if (options.headers) {
|
||||
for (var key in options.headers) {
|
||||
xhr.setRequestHeader(key, options.headers[key]);
|
||||
}
|
||||
}
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4) {
|
||||
var response;
|
||||
try {
|
||||
response = JSON.parse(xhr.responseText);
|
||||
} catch (e) {
|
||||
// If JSON parsing fails, use raw response
|
||||
response = xhr.responseText;
|
||||
}
|
||||
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
if (options.success) options.success(response);
|
||||
resolve(response);
|
||||
} else {
|
||||
if (options.error) options.error(xhr);
|
||||
reject(xhr);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var data = null;
|
||||
if (options.data) {
|
||||
data = typeof options.data === 'string' ? options.data : JSON.stringify(options.data);
|
||||
}
|
||||
|
||||
xhr.send(data);
|
||||
});
|
||||
}
|
||||
|
||||
$.ready = function(callback) {
|
||||
if (document.readyState !== 'loading') {
|
||||
callback();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', callback);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to execute scripts in HTML content
|
||||
function executeScripts(htmlContent) {
|
||||
// Create a temporary container to parse the HTML
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = htmlContent;
|
||||
|
||||
// Find all script tags and disable them temporarily
|
||||
const scripts = tempDiv.querySelectorAll('script');
|
||||
const scriptData = [];
|
||||
|
||||
scripts.forEach((script) => {
|
||||
// Store script data for later execution
|
||||
scriptData.push({
|
||||
content: script.textContent || script.innerText || '',
|
||||
src: script.src,
|
||||
type: script.type,
|
||||
nonce: script.nonce,
|
||||
async: script.async,
|
||||
defer: script.defer
|
||||
});
|
||||
|
||||
// Disable the script by changing its type
|
||||
script.type = 'text/disabled-script';
|
||||
});
|
||||
|
||||
// Return the modified HTML and script data
|
||||
return {
|
||||
html: tempDiv.innerHTML,
|
||||
scripts: scriptData
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to execute a single script
|
||||
function executeScript(scriptInfo) {
|
||||
if (scriptInfo.src) {
|
||||
// External script
|
||||
const newScript = document.createElement('script');
|
||||
if (scriptInfo.type && scriptInfo.type !== 'text/disabled-script') {
|
||||
newScript.type = scriptInfo.type;
|
||||
}
|
||||
if (scriptInfo.nonce) newScript.nonce = scriptInfo.nonce;
|
||||
if (scriptInfo.async) newScript.async = scriptInfo.async;
|
||||
if (scriptInfo.defer) newScript.defer = scriptInfo.defer;
|
||||
newScript.src = scriptInfo.src;
|
||||
|
||||
document.head.appendChild(newScript);
|
||||
} else if (scriptInfo.content.trim()) {
|
||||
// Inline script
|
||||
const newScript = document.createElement('script');
|
||||
if (scriptInfo.type && scriptInfo.type !== 'text/disabled-script') {
|
||||
newScript.type = scriptInfo.type;
|
||||
}
|
||||
if (scriptInfo.nonce) newScript.nonce = scriptInfo.nonce;
|
||||
newScript.text = scriptInfo.content;
|
||||
|
||||
// Execute by inserting and immediately removing
|
||||
document.head.appendChild(newScript);
|
||||
document.head.removeChild(newScript);
|
||||
}
|
||||
}
|
||||
|
||||
// Add all the jQuery-like methods to QueryWrapper
|
||||
QueryWrapper.prototype.parent = function() {
|
||||
const parents = Array.from(this).map(el => el.parentNode).filter(el => el);
|
||||
return new QueryWrapper(parents);
|
||||
}
|
||||
|
||||
QueryWrapper.prototype.load = function(url, opt = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ajaxOptions = {
|
||||
method: opt.method || (opt.postData ? 'POST' : 'GET'),
|
||||
url: url,
|
||||
success: (response) => {
|
||||
if(opt.replace)
|
||||
this.replaceWith(response, opt);
|
||||
else
|
||||
this.html(response, opt);
|
||||
if (opt.onLoad) opt.onLoad(response, null);
|
||||
resolve(this);
|
||||
},
|
||||
error: (xhr) => {
|
||||
if (opt.onError) opt.onError(xhr);
|
||||
reject(new Error('Failed to load: ' + url));
|
||||
}
|
||||
};
|
||||
|
||||
if (opt.postData) {
|
||||
ajaxOptions.data = opt.postData;
|
||||
}
|
||||
|
||||
$.ajax(ajaxOptions);
|
||||
});
|
||||
}
|
||||
|
||||
QueryWrapper.prototype.html = function(opt_content = false, prop = {}) {
|
||||
if (opt_content === false) {
|
||||
return Array.from(this).map(el => el.innerHTML).join('');
|
||||
}
|
||||
if(typeof prop.diff == 'undefined')
|
||||
prop.diff = $.options.alwaysDoDifferentialUpdate;
|
||||
if(prop.diff === true) {
|
||||
const temp = document.createElement('div');
|
||||
temp.innerHTML = opt_content;
|
||||
this.forEach(el => {
|
||||
morphdom(el, temp, { childrenOnly: true });
|
||||
});
|
||||
} else {
|
||||
this.forEach(el => {
|
||||
const result = executeScripts(opt_content);
|
||||
el.innerHTML = result.html;
|
||||
result.scripts.forEach(executeScript);
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
QueryWrapper.prototype.text = function(opt_content = false) {
|
||||
if (opt_content === false) {
|
||||
return Array.from(this).map(el => el.textContent).join('');
|
||||
}
|
||||
this.forEach(el => {
|
||||
el.textContent = opt_content;
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
QueryWrapper.prototype.replaceWith = function(content, prop = {}) {
|
||||
if(typeof prop.diff == 'undefined')
|
||||
prop.diff = $.options.alwaysDoDifferentialUpdate;
|
||||
if(prop.diff === true) {
|
||||
this.forEach(el => {
|
||||
morphdom(el, content);
|
||||
});
|
||||
} else {
|
||||
if(typeof content === 'string') {
|
||||
this.forEach(el => {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = content;
|
||||
el.parentNode.replaceChild(tempDiv.firstChild, el);
|
||||
});
|
||||
} else if(content instanceof Element) {
|
||||
this.forEach(el => {
|
||||
el.parentNode.replaceChild(content.cloneNode(true), el);
|
||||
});
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
QueryWrapper.prototype.query = function(selector) {
|
||||
const found = Array.from(this).reduce((acc, el) => {
|
||||
const results = el.querySelectorAll(selector);
|
||||
return acc.concat(Array.from(results));
|
||||
}, []);
|
||||
return new QueryWrapper(found);
|
||||
}
|
||||
|
||||
QueryWrapper.prototype.find = function(selector) {
|
||||
const found = Array.from(this).reduce((acc, el) => {
|
||||
const results = el.querySelectorAll(selector);
|
||||
return acc.concat(Array.from(results));
|
||||
}, []);
|
||||
return new QueryWrapper(found);
|
||||
}
|
||||
|
||||
QueryWrapper.prototype.on = function(event, handler) {
|
||||
this.forEach(function(el) {
|
||||
el.addEventListener(event, handler);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
QueryWrapper.prototype.off = function(event, handler) {
|
||||
this.forEach(function(el) {
|
||||
el.removeEventListener(event, handler);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
QueryWrapper.prototype.hide = function() {
|
||||
return this.css({ display: 'none' });
|
||||
}
|
||||
|
||||
QueryWrapper.prototype.show = function() {
|
||||
return this.css({ display: '' });
|
||||
}
|
||||
|
||||
QueryWrapper.prototype.toggle = function() {
|
||||
this.forEach(function(el) {
|
||||
el.style.display = (el.style.display === 'none' || el.style.display === '') ? '' : 'none';
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
QueryWrapper.prototype.attr = function(name, value) {
|
||||
if (value === undefined) {
|
||||
return this.length > 0 ? this[0].getAttribute(name) : null;
|
||||
}
|
||||
this.forEach(function(el) {
|
||||
el.setAttribute(name, value);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
QueryWrapper.prototype.addClass = function(classNameOrList) {
|
||||
var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList];
|
||||
this.forEach(function(el) {
|
||||
classList.forEach(function(classNames) {
|
||||
// Split space-separated class names
|
||||
var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; });
|
||||
classes.forEach(function(className) {
|
||||
el.classList.add(className);
|
||||
});
|
||||
});
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
QueryWrapper.prototype.removeClass = function(classNameOrList) {
|
||||
var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList];
|
||||
this.forEach(function(el) {
|
||||
classList.forEach(function(classNames) {
|
||||
// Split space-separated class names
|
||||
var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; });
|
||||
classes.forEach(function(className) {
|
||||
el.classList.remove(className);
|
||||
});
|
||||
});
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
QueryWrapper.prototype.toggleClass = function(classNameOrList, force) {
|
||||
var classList = Array.isArray(classNameOrList) ? classNameOrList : [classNameOrList];
|
||||
this.forEach(function(el) {
|
||||
classList.forEach(function(classNames) {
|
||||
// Split space-separated class names
|
||||
var classes = classNames.toString().split(/\s+/).filter(function(name) { return name.length > 0; });
|
||||
classes.forEach(function(className) {
|
||||
if (typeof force !== 'undefined') {
|
||||
el.classList.toggle(className, force);
|
||||
} else {
|
||||
el.classList.toggle(className);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
QueryWrapper.prototype.empty = function() {
|
||||
this.forEach(function(el) {
|
||||
el.innerHTML = '';
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
QueryWrapper.prototype.css = function(styles, optOrValue) {
|
||||
// If no arguments or first argument is a string (getter mode)
|
||||
if (arguments.length === 0 || (typeof styles === 'string' && arguments.length === 1)) {
|
||||
if (this.length === 0) return undefined;
|
||||
var el = this[0];
|
||||
if (typeof styles === 'string') {
|
||||
// Get computed style for a specific property
|
||||
return window.getComputedStyle(el)[styles];
|
||||
} else {
|
||||
// Return computed styles object (not commonly used)
|
||||
return window.getComputedStyle(el);
|
||||
}
|
||||
}
|
||||
|
||||
// Setter mode
|
||||
this.forEach(function(el) {
|
||||
if (typeof styles === 'string' && arguments.length >= 2) {
|
||||
// Single property setter: .css('prop', 'value')
|
||||
el.style[styles] = optOrValue;
|
||||
} else if (typeof styles === 'object') {
|
||||
// Multiple properties setter: .css({prop1: 'value1', prop2: 'value2'})
|
||||
for (var key in styles) {
|
||||
el.style[key] = styles[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
QueryWrapper.prototype.each = function(callback) {
|
||||
this.forEach(function(el, index) {
|
||||
callback(el, index);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
QueryWrapper.prototype.append = function(child_or_html) {
|
||||
this.forEach(function(el) {
|
||||
if (typeof child_or_html === 'string' && el.insertAdjacentHTML) {
|
||||
if (child_or_html.includes('<script')) {
|
||||
const result = executeScripts(child_or_html);
|
||||
el.insertAdjacentHTML('beforeend', result.html);
|
||||
result.scripts.forEach(executeScript);
|
||||
} else {
|
||||
el.insertAdjacentHTML('beforeend', child_or_html);
|
||||
}
|
||||
} else if (el.appendChild) {
|
||||
if (typeof child_or_html === 'string') {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = child_or_html;
|
||||
el.appendChild(tempDiv.firstChild);
|
||||
} else {
|
||||
el.appendChild(child_or_html);
|
||||
}
|
||||
} else if(el.hasOwnProperty('innerHTML')) {
|
||||
el.innerHTML += child_or_html;
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
QueryWrapper.prototype.remove = function() {
|
||||
this.forEach(function(el) {
|
||||
el.parentNode.removeChild(el);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
function first(...args) {
|
||||
for (const arg of args) {
|
||||
if (arg !== undefined && arg !== null && arg !== '') {
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function clamp(v, min, max) {
|
||||
if (v < min) return min;
|
||||
if (v > max) return max;
|
||||
return v;
|
||||
}
|
||||
|
||||
function pick_entry_from_range(array, value) {
|
||||
if (!array) return {};
|
||||
let result = {};
|
||||
for (const [pv] of Object.entries(array)) {
|
||||
if (value >= pv.from && value <= pv.to) result = pv;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return {
|
||||
$: $,
|
||||
EventEmitter: EventEmitter,
|
||||
QueryWrapper: QueryWrapper,
|
||||
first: first,
|
||||
clamp: clamp,
|
||||
pick_entry_from_range: pick_entry_from_range
|
||||
};
|
||||
|
||||
}));
|
||||
@@ -0,0 +1,165 @@
|
||||
:root {
|
||||
--vp-color-primary: rgba(255, 125, 55, 1);
|
||||
--vp-color-primary-focus: rgba(255, 120, 60, 0.4);
|
||||
--vp-background: rgba(0,0,0,0.2);
|
||||
|
||||
--vp-color-black: #000;
|
||||
--vp-color-gray: #666;
|
||||
--vp-color-white: #fff;
|
||||
--vp-space-s: 4px;
|
||||
--vp-space-l: 6px;
|
||||
|
||||
--vp-size-icon: 12px;
|
||||
--vp-size-nav-height: 48px;
|
||||
|
||||
--vp-opacity: 0.4;
|
||||
--vp-opacity-active: 0.9;
|
||||
|
||||
--vp-z-pane: 2;
|
||||
--vp-z-toolbar: 3;
|
||||
--vp-z-popup: 9999;
|
||||
--vp-duration: 0.25s;
|
||||
--vp-easing: ease;
|
||||
}
|
||||
|
||||
#fabric {
|
||||
position: absolute;
|
||||
top: var(--vp-size-nav-height);
|
||||
left: 0; right: 0; bottom: 0;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vp-split {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vp-split.row { flex-direction: row; }
|
||||
.vp-split.col { flex-direction: column; }
|
||||
|
||||
.viewport-pane {
|
||||
flex: 1 1 0;
|
||||
position: relative;
|
||||
border: var(--vp-space-s) solid var(--color-border);
|
||||
background: var(--vp-background);
|
||||
padding: var(--vp-space-l);
|
||||
overflow: auto;
|
||||
font-family: console;
|
||||
transition:
|
||||
border-color var(--vp-duration) var(--vp-easing),
|
||||
box-shadow var(--vp-duration) var(--vp-easing),
|
||||
background var(--vp-duration) var(--vp-easing);
|
||||
}
|
||||
|
||||
.vp-split > .viewport-pane {
|
||||
transition:
|
||||
flex var(--vp-duration) var(--vp-easing),
|
||||
opacity var(--vp-duration) var(--vp-easing);
|
||||
}
|
||||
|
||||
.vp-no-transition,
|
||||
.vp-no-transition * {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.viewport-pane:before {
|
||||
content: attr(data-title);
|
||||
position: absolute;
|
||||
top: var(--vp-space-s);
|
||||
right: var(--vp-space-s);
|
||||
opacity: var(--vp-opacity-medium);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.viewport-pane.focused {
|
||||
border-color: var(--vp-color-primary-focus);
|
||||
z-index: var(--vp-z-pane);
|
||||
}
|
||||
|
||||
.viewport-pane.focused:before {
|
||||
opacity: var(--vp-opacity-active);
|
||||
color: var(--color-highlight);
|
||||
}
|
||||
|
||||
.vp-divider {
|
||||
background: var(--color-border);
|
||||
opacity: var(--vp-opacity);
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.vp-divider.row {
|
||||
width: var(--vp-space-l);
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.vp-divider.col {
|
||||
height: var(--vp-space-l);
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.vp-divider:after {
|
||||
content:'';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transition:
|
||||
background var(--vp-duration) var(--vp-easing),
|
||||
opacity var(--vp-duration) var(--vp-easing);
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.vp-divider.row:after {
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.vp-divider:hover:after {
|
||||
background: var(--color-highlight);
|
||||
opacity: var(--vp-opacity);
|
||||
}
|
||||
|
||||
.vp-divider.dragging:after {
|
||||
background: var(--color-highlight);
|
||||
opacity: var(--vp-opacity-active);
|
||||
}
|
||||
|
||||
.vp-help-overlay {
|
||||
position: absolute;
|
||||
top: var(--vp-space-s);
|
||||
left: var(--vp-space-s);
|
||||
opacity: var(--vp-opacity);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.vp-pane-toolbar {
|
||||
position: absolute;
|
||||
top: var(--vp-space-s);
|
||||
right: var(--vp-space-s);
|
||||
display: flex;
|
||||
gap: var(--vp-space-s);
|
||||
z-index: var(--vp-z-toolbar);
|
||||
}
|
||||
|
||||
.vp-pane-toolbar button {
|
||||
padding: 0 var(--vp-space-l);
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--vp-background);
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.vp-pane-toolbar button:hover {
|
||||
background: var(--color-highlight);
|
||||
color: var(--vp-color-black);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.vp-icon {
|
||||
width: var(--vp-size-icon);
|
||||
height: var(--vp-size-icon);
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
(function (root, factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD
|
||||
define(['jquery'], factory);
|
||||
} else if (typeof module === 'object' && module.exports) {
|
||||
// CommonJS
|
||||
module.exports = factory(require('jquery'));
|
||||
} else {
|
||||
// Browser globals
|
||||
root.TilingViewportManager = factory(root.$);
|
||||
}
|
||||
}(typeof self !== 'undefined' ? self : this, function ($) {
|
||||
'use strict';
|
||||
|
||||
// Note: This module expects showPopupMenu to be available globally
|
||||
// or you may need to adjust the dependency list above to include it
|
||||
|
||||
let TilingViewportManager = function(container, options = {}) {
|
||||
|
||||
let idCounter = 1, rootNode = null, focusedPane = null, containerEl = null;
|
||||
let nextId = () => 'vp_' + (idCounter++);
|
||||
let paneMeta = new Map();
|
||||
let pendingViewLoads = [];
|
||||
let defaultoptions = {
|
||||
minPaneSize: 100,
|
||||
keyboard: true,
|
||||
showHelp: true,
|
||||
closeTransitionTimeout: 400,
|
||||
toolbarButtons: ['split-v','split-h','views','close'],
|
||||
standardViewList: [],
|
||||
cssVars: { '--vp-duration': '.22s', '--vp-easing': 'ease' }
|
||||
};
|
||||
options = Object.assign({}, defaultoptions, options);
|
||||
|
||||
let createPane = (content, props = {}) => {
|
||||
let id = nextId();
|
||||
let $el = $('<div class="viewport-pane">').attr('data-id', id);
|
||||
if(props.title) $el.attr('data-title', props.title);
|
||||
$el.html(content || `<div style="opacity:.65;">Use Alt+<H V O X ↑ ← → ↓></div>`)
|
||||
.on('mousedown', () => focusPaneByEl($el[0]));
|
||||
attachPaneToolbar($el[0]);
|
||||
paneMeta.set(id, {id, canClose: true, canSplit: true, title: props.title, ...props});
|
||||
if(props.view?.screen) pendingViewLoads.push({id, view: props.view});
|
||||
return {type: 'pane', id, el: $el[0]};
|
||||
};
|
||||
|
||||
let buildInitial = () => { rootNode = createPane(); $(containerEl).html('').append(rootNode.el); focusPane(rootNode); };
|
||||
let focusPane = node => { if(!node || node.type !== 'pane') return; if(focusedPane?.el) $(focusedPane.el).removeClass('focused'); focusedPane = node; $(node.el).addClass('focused'); };
|
||||
let focusPaneByEl = el => focusPane(findNodeById(rootNode, $(el).attr('data-id')));
|
||||
let findNodeById = (node, id) => !node ? null : node.type === 'pane' ? (node.id === id ? node : null) : node.children.map(ch => findNodeById(ch, id)).find(r=>r);
|
||||
let getNodeById = id => findNodeById(rootNode, id) || null;
|
||||
let findParentPath = (node, id, path=[]) => !node ? null : node.type === 'pane' ? (node.id === id ? path : null) : node.children.map(ch => findParentPath(ch, id, path.concat(node))).find(r=>r);
|
||||
let paneList = (node, acc=[]) => !node ? acc : node.type === 'pane' ? (acc.push(node), acc) : (node.children.forEach(ch => paneList(ch, acc)), acc);
|
||||
let renormalizeSizes = splitNode => { let total = splitNode.sizes.reduce((a,b)=>a+b,0)||1; splitNode.sizes = splitNode.sizes.map(s=>s/total); };
|
||||
let replaceChild = (splitNode, oldChild, newChild) => { let i = splitNode.children.indexOf(oldChild); if(i>=0) splitNode.children.splice(i,1,newChild); };
|
||||
|
||||
let split = (direction, newPaneProps = {}) => {
|
||||
if(!focusedPane) return;
|
||||
let meta = paneMeta.get(focusedPane.id);
|
||||
if(meta?.canSplit === false) return;
|
||||
|
||||
let paneRect = focusedPane.el.getBoundingClientRect();
|
||||
let availableSpace = direction === 'row' ? paneRect.width : paneRect.height;
|
||||
let requiredSpace = 2 * options.minPaneSize;
|
||||
if(availableSpace < requiredSpace) {
|
||||
//console.log(`Cannot split: available space ${availableSpace}px < required ${requiredSpace}px`);
|
||||
return;
|
||||
}
|
||||
|
||||
let parentPath = findParentPath(rootNode, focusedPane.id);
|
||||
let newPane = createPane(undefined, newPaneProps);
|
||||
let animatedSplit = null, newIndex = -1;
|
||||
if(!parentPath?.length) {
|
||||
rootNode = {type:'split', direction, children:[focusedPane, newPane], sizes:[1,0]};
|
||||
animatedSplit = rootNode; newIndex = 1;
|
||||
} else {
|
||||
let parent = parentPath.at(-1);
|
||||
if(parent.type === 'split' && parent.direction === direction) {
|
||||
let idx = parent.children.indexOf(focusedPane);
|
||||
parent.children.splice(idx+1, 0, newPane);
|
||||
parent.sizes = parent.sizes || parent.children.map(()=>1);
|
||||
parent.sizes.splice(idx+1, 0, 0);
|
||||
renormalizeSizes(parent);
|
||||
animatedSplit = parent; newIndex = idx+1;
|
||||
} else if(parent.type === 'split' && parent.direction !== direction && parent.children.length === 1) {
|
||||
parent.direction = direction; parent.children.push(newPane); parent.sizes = [1,0];
|
||||
animatedSplit = parent; newIndex = parent.children.length-1;
|
||||
} else {
|
||||
let idx = parent.children.indexOf(focusedPane);
|
||||
let created = {type:'split', direction, children:[focusedPane, newPane], sizes:[1,0]};
|
||||
parent.children.splice(idx, 1, created);
|
||||
animatedSplit = created; newIndex = 1;
|
||||
}
|
||||
}
|
||||
render();
|
||||
if(animatedSplit && newIndex >= 0) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
animatedSplit.sizes = animatedSplit.children.map(()=>1);
|
||||
renormalizeSizes(animatedSplit);
|
||||
animatedSplit.children.forEach((ch,i) => ch.el && (ch.el.style.flex = `${animatedSplit.sizes[i]} 1 0`));
|
||||
}));
|
||||
}
|
||||
focusPane(newPane); return newPane;
|
||||
};
|
||||
|
||||
let splitVertical = () => split('row');
|
||||
let splitHorizontal = () => split('col');
|
||||
|
||||
let closeFocused = () => {
|
||||
if(!focusedPane) return;
|
||||
let meta = paneMeta.get(focusedPane.id);
|
||||
if(meta?.canClose === false) return;
|
||||
if(rootNode === focusedPane) { buildInitial(); return; }
|
||||
if(typeof meta?.onUnload === 'function') meta.onUnload(focusedPane.id);
|
||||
if(typeof meta?.view?.onUnload === 'function') meta.view.onUnload(focusedPane.id);
|
||||
let parentPath = findParentPath(rootNode, focusedPane.id);
|
||||
if(!parentPath?.length) return;
|
||||
let parent = parentPath.at(-1);
|
||||
if(parent.type !== 'split') return;
|
||||
let idx = parent.children.indexOf(focusedPane);
|
||||
|
||||
let oldSizes = (parent.sizes || parent.children.map(()=>1)).slice();
|
||||
let oldSize = oldSizes[idx] || 0;
|
||||
let remaining = 1 - oldSize;
|
||||
let newSizes = oldSizes.slice();
|
||||
if(remaining > 0) {
|
||||
for(let i=0;i<newSizes.length;i++) if(i!==idx) newSizes[i] = newSizes[i] / remaining; else newSizes[i] = 0;
|
||||
} else {
|
||||
let count = parent.children.length - 1 || 1;
|
||||
for(let i=0;i<newSizes.length;i++) newSizes[i] = (i===idx) ? 0 : 1/count;
|
||||
}
|
||||
parent.sizes = newSizes;
|
||||
parent.children.forEach((ch,i) => ch.el && (ch.el.style.flex = `${parent.sizes[i]} 1 0`));
|
||||
if(focusedPane.el) {
|
||||
focusedPane.el.style.transition = (focusedPane.el.style.transition || '') + ', opacity .18s ease';
|
||||
focusedPane.el.style.opacity = '0';
|
||||
}
|
||||
let done = false;
|
||||
let cleanup = () => {
|
||||
if(done) return; done = true;
|
||||
parent.children.splice(idx,1);
|
||||
parent.sizes?.splice(idx,1);
|
||||
paneMeta.delete(focusedPane.id);
|
||||
if(parent.children.length === 1) {
|
||||
let only = parent.children[0];
|
||||
parentPath.length === 1 ? rootNode = only : replaceChild(parentPath.at(-2), parent, only);
|
||||
} else if(parent.sizes) renormalizeSizes(parent);
|
||||
render(); focusPane(paneList(rootNode)[0]);
|
||||
};
|
||||
let onEnd = ev => { if(ev.target === focusedPane.el && (ev.propertyName === 'opacity' || ev.propertyName === 'flex')) { focusedPane.el.removeEventListener('transitionend', onEnd); cleanup(); } };
|
||||
if(focusedPane.el) focusedPane.el.addEventListener('transitionend', onEnd);
|
||||
setTimeout(() => cleanup(), 350);
|
||||
};
|
||||
|
||||
let nextPane = (node, currentId, options={direction:1}) => { let list = paneList(node); if(!list.length) return null; let idx = list.findIndex(p=>p.id===(currentId||(focusedPane?.id)))||0; return list[(idx+options.direction+list.length)%list.length]; };
|
||||
let focusNext = () => { let n = nextPane(rootNode); if(n) focusPane(n); };
|
||||
let focusPrev = () => { let n = nextPane(rootNode, null, {direction:-1}); if(n) focusPane(n); };
|
||||
|
||||
let serialize = (node=rootNode) => {
|
||||
let ser = n => !n ? null : n.type==='pane' ?
|
||||
{type:'pane', id:n.id, title:paneMeta.get(n.id)?.title, props:{canClose:paneMeta.get(n.id)?.canClose!==false, canSplit:paneMeta.get(n.id)?.canSplit!==false}, view:paneMeta.get(n.id)?.view||null} :
|
||||
{type:'split', direction:n.direction, sizes:(n.sizes||[]).slice(), children:n.children.map(ser)};
|
||||
return {focus: focusedPane?.id, layout: ser(node)};
|
||||
};
|
||||
|
||||
let restore = data => {
|
||||
if(!data) return;
|
||||
let wrapper = data.layout ? data : {layout:data};
|
||||
paneMeta.clear(); idCounter = 1; pendingViewLoads.length = 0;
|
||||
let idNums = [];
|
||||
let build = l => l && l.type==='pane' ?
|
||||
(() => { let pane = createPane(undefined, {title:l.title, canClose:l.props?.canClose, canSplit:l.props?.canSplit, view:l.view});
|
||||
if(l.id) { pane.el.dataset.id = pane.id = l.id; let n=parseInt(l.id.split('_')[1],10); if(!isNaN(n)) idNums.push(n); }
|
||||
let meta = paneMeta.get(pane.id); if(meta) { Object.assign(meta, l.props||{}, {title:l.title, view:l.view}); if(pane.el && l.title) pane.el.dataset.title = l.title; paneMeta.set(pane.id, meta); }
|
||||
return pane; })() :
|
||||
l && l.type==='split' ? {type:'split', direction:l.direction||'row', children:(l.children||[]).map(build).filter(Boolean), sizes:(l.sizes||[]).slice()} : null;
|
||||
rootNode = build(wrapper.layout) || createPane();
|
||||
if(idNums.length) idCounter = Math.max(...idNums) + 1;
|
||||
render();
|
||||
pendingViewLoads.forEach(v => v.view && loadView(v.id, v.view)); pendingViewLoads.length = 0;
|
||||
focusPane(wrapper.focus ? findNodeById(rootNode, wrapper.focus) || paneList(rootNode)[0] : paneList(rootNode)[0]);
|
||||
};
|
||||
|
||||
let render = () => { if(!containerEl) return; $(containerEl).empty().append(renderNode(rootNode)); injectHelpOverlay(); };
|
||||
|
||||
let renderNode = node => {
|
||||
if(node.type==='pane') return node.el;
|
||||
node.el = node.el || $('<div>')[0];
|
||||
$(node.el).empty().addClass(`vp-split ${node.direction}`);
|
||||
if(!node.sizes || node.sizes.length !== node.children.length) { node.sizes = node.children.map(()=>1); renormalizeSizes(node); }
|
||||
node.children.forEach((ch,i) => {
|
||||
let chEl = renderNode(ch); $(chEl).css('flex', `${node.sizes[i]} 1 0`); node.el.appendChild(chEl);
|
||||
if(i < node.children.length-1) { let div = $('<div>').addClass(`vp-divider ${node.direction}`)[0]; setupDivider(div, node, i); node.el.appendChild(div); }
|
||||
});
|
||||
return node.el;
|
||||
};
|
||||
|
||||
let setupDivider = (div, splitNode, leftIdx) => {
|
||||
$(div).on('mousedown', e => {
|
||||
e.preventDefault(); $(div).addClass('dragging');
|
||||
$(containerEl).addClass('vp-no-transition');
|
||||
let isRow = splitNode.direction === 'row';
|
||||
let startPos = isRow ? e.clientX : e.clientY;
|
||||
let childRects = splitNode.children.map(ch => ch.el.getBoundingClientRect());
|
||||
let prop = isRow ? 'width' : 'height';
|
||||
let startPixels = childRects.map(r => r[prop]);
|
||||
let [aStartPx, bStartPx] = [startPixels[leftIdx], startPixels[leftIdx+1]];
|
||||
let totalPixels = startPixels.reduce((a,b)=>a+b,0);
|
||||
let onMove = ev => {
|
||||
let curPos = isRow ? ev.clientX : ev.clientY;
|
||||
let [newApx, newBpx] = [aStartPx + curPos - startPos, bStartPx - curPos + startPos];
|
||||
if(newApx < options.minPaneSize) [newApx, newBpx] = [options.minPaneSize, aStartPx + bStartPx - options.minPaneSize];
|
||||
if(newBpx < options.minPaneSize) [newApx, newBpx] = [aStartPx + bStartPx - options.minPaneSize, options.minPaneSize];
|
||||
startPixels[leftIdx] = newApx; startPixels[leftIdx+1] = newBpx;
|
||||
splitNode.sizes = startPixels.map(p => p / totalPixels);
|
||||
splitNode.children.forEach((ch,i)=> {
|
||||
if(ch.el) {
|
||||
let flexValue = `${splitNode.sizes[i]} 1 0`;
|
||||
$(ch.el).css('flex', flexValue);
|
||||
}
|
||||
});
|
||||
};
|
||||
let onUp = () => {
|
||||
$(div).removeClass('dragging');
|
||||
$(containerEl).removeClass('vp-no-transition');
|
||||
$(document).off('mousemove', onMove).off('mouseup', onUp);
|
||||
};
|
||||
$(document).on('mousemove', onMove).on('mouseup', onUp);
|
||||
});
|
||||
$(div).on('dblclick', () => {
|
||||
$(containerEl).addClass('vp-no-transition');
|
||||
let len = splitNode.sizes.length; splitNode.sizes = splitNode.sizes.map(()=>1/len);
|
||||
splitNode.children.forEach((ch,i)=> ch.el && $(ch.el).css('flex', `${splitNode.sizes[i]} 1 0`));
|
||||
setTimeout(() => $(containerEl).removeClass('vp-no-transition'), 0);
|
||||
});
|
||||
};
|
||||
|
||||
let attachPaneToolbar = el => {
|
||||
let bar = $('<div>').addClass('vp-pane-toolbar').html(`
|
||||
<button title="Split Vert" data-act="split-v" aria-label="Split vertically">
|
||||
<svg class="vp-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<rect x="1" y="4" width="8" height="16" rx="1" fill="currentColor" />
|
||||
<rect x="15" y="4" width="8" height="16" rx="1" fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Split Horiz" data-act="split-h" aria-label="Split horizontally">
|
||||
<svg class="vp-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<rect x="3" y="1" width="18" height="8" rx="1" fill="currentColor" />
|
||||
<rect x="3" y="15" width="18" height="8" rx="1" fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<button title="Views" data-act="views">O</button>
|
||||
<button title="Close" data-act="close">X</button>`)[0];
|
||||
el.appendChild(bar);
|
||||
$(bar).on('mousedown', ev => ev.stopPropagation());
|
||||
$(bar).on('click', ev => { ev.stopPropagation(); focusPaneByEl(el);
|
||||
let button = ev.target.closest('button[data-act]');
|
||||
let act = button ? button.getAttribute('data-act') : null;
|
||||
if(act==='split-v') splitVertical(); else if(act==='split-h') splitHorizontal(); else if(act==='close') closeFocused();
|
||||
else if(act==='views') {
|
||||
// compute button position
|
||||
let rect = button.getBoundingClientRect();
|
||||
let items = options.standardViewList.slice();
|
||||
showPopupMenu(rect.left, rect.bottom, items, { onSelect: it => {
|
||||
// support string or object {screen}
|
||||
let view = typeof it === 'string' ? {screen: it} : (it.screen ? it : {screen: it});
|
||||
loadView(el.dataset.id, view);
|
||||
}, container: document.body, emptyText: 'No views available' });
|
||||
}
|
||||
});
|
||||
refreshToolbarState(el.dataset.id);
|
||||
};
|
||||
|
||||
let refreshToolbarState = paneId => {
|
||||
let meta = paneMeta.get(paneId); let el = $(containerEl).find(`.viewport-pane[data-id="${paneId}"]`)[0];
|
||||
if(!el) return; let bar = $(el).find('.vp-pane-toolbar')[0]; if(!bar) return;
|
||||
let [canSplit, canClose] = [!meta||meta.canSplit!==false, !meta||meta.canClose!==false];
|
||||
['split-v','split-h'].forEach(act => { let btn = $(bar).find(`[data-act="${act}"]`)[0]; if(btn) { btn.disabled = !canSplit; $(btn).toggleClass('disabled', !canSplit); } });
|
||||
let btnC = $(bar).find('[data-act="close"]')[0]; if(btnC) { btnC.disabled = !canClose; $(btnC).toggleClass('disabled', !canClose); }
|
||||
};
|
||||
|
||||
let setPaneProps = (paneId, props) => { let meta = paneMeta.get(paneId); if(!meta) return; Object.assign(meta, props);
|
||||
if(props.title) { let el = $(containerEl).find(`.viewport-pane[data-id="${paneId}"]`)[0]; if(el) el.dataset.title = props.title; }
|
||||
if(typeof props.onUnload === 'function') meta.onUnload = props.onUnload;
|
||||
paneMeta.set(paneId, meta); refreshToolbarState(paneId); };
|
||||
let getPaneProps = paneId => paneMeta.get(paneId);
|
||||
|
||||
let focusDirection = (dx, dy) => {
|
||||
if(!focusedPane) return;
|
||||
let panes = Array.from(containerEl.querySelectorAll('.viewport-pane'));
|
||||
let cur = focusedPane.el.getBoundingClientRect();
|
||||
let candidates = panes.filter(p => p !== focusedPane.el).map(p => {
|
||||
let r = p.getBoundingClientRect();
|
||||
if(dx) {
|
||||
if(dx > 0 && r.left < cur.right-1 || dx < 0 && r.right > cur.left+1) return null;
|
||||
let overlap = Math.max(0, Math.min(cur.bottom, r.bottom) - Math.max(cur.top, r.top));
|
||||
let primaryDist = dx > 0 ? r.left - cur.right : cur.left - r.right;
|
||||
if(primaryDist < -1) return null;
|
||||
return {el:p, score: primaryDist*1000 + (cur.height - overlap)};
|
||||
}
|
||||
if(dy > 0 && r.top < cur.bottom-1 || dy < 0 && r.bottom > cur.top+1) return null;
|
||||
let overlap = Math.max(0, Math.min(cur.right, r.right) - Math.max(cur.left, r.left));
|
||||
let primaryDist = dy > 0 ? r.top - cur.bottom : cur.top - r.bottom;
|
||||
if(primaryDist < -1) return null;
|
||||
return {el:p, score: primaryDist*1000 + (cur.width - overlap)};
|
||||
}).filter(Boolean);
|
||||
if(candidates.length) return focusPaneByEl(candidates.sort((a,b) => a.score - b.score)[0].el);
|
||||
let [cx, cy] = [cur.left + cur.width/2, cur.top + cur.height/2];
|
||||
let fallback = panes.filter(p => p !== focusedPane.el).map(p => {
|
||||
let r = p.getBoundingClientRect(); let [px, py] = [r.left + r.width/2, r.top + r.height/2];
|
||||
let [vx, vy] = [px-cx, py-cy];
|
||||
if(dx && Math.sign(vx) !== Math.sign(dx) || dy && Math.sign(vy) !== Math.sign(dy)) return null;
|
||||
return {el:p, score: (dx ? Math.abs(vx) : Math.abs(vy))*2 + (dx ? Math.abs(vy) : Math.abs(vx))};
|
||||
}).filter(Boolean);
|
||||
if(fallback.length) focusPaneByEl(fallback.sort((a,b) => a.score - b.score)[0].el);
|
||||
};
|
||||
|
||||
let injectHelpOverlay = () => {
|
||||
if(!options.showHelp) return;
|
||||
if(!containerEl.querySelector('.vp-help-overlay')) containerEl.appendChild(Object.assign(document.createElement('div'), {className:'vp-help-overlay', innerHTML:options.hint_text || ``}));
|
||||
};
|
||||
|
||||
let getPaneEl = id => findNodeById(rootNode, id)?.el || null;
|
||||
|
||||
let openViewsPopupForPane = (paneId) => {
|
||||
let node = findNodeById(rootNode, paneId || focusedPane?.id);
|
||||
if(!node || !node.el) return;
|
||||
let rect = node.el.getBoundingClientRect();
|
||||
let x = Math.max(8, rect.right - 80);
|
||||
let y = rect.top + 24;
|
||||
let items = options.standardViewList.slice();
|
||||
showPopupMenu(x, y, items, { onSelect: it => {
|
||||
let view = typeof it === 'string' ? {screen: it} : (it.screen ? it : {screen: it});
|
||||
loadView(node.id, view);
|
||||
}, container: document.body, emptyText: 'No views available' });
|
||||
};
|
||||
|
||||
let loadView = (paneId, view) => {
|
||||
let node = findNodeById(rootNode, paneId);
|
||||
if(!node || !node.el) return; // nothing to load into
|
||||
let meta = paneMeta.get(paneId);
|
||||
try { if(meta && typeof meta.onUnload === 'function') meta.onUnload(paneId); if(meta && meta.view && typeof meta.view.onUnload === 'function') meta.view.onUnload(paneId); } catch(e){ console.error('onUnload error', e); }
|
||||
if(meta) meta.view = view;
|
||||
node.el.innerHTML = `<div style="opacity:.4;">Loading ${view.screen||'...'}...</div>`;
|
||||
$(node.el).load(`screens/${view.screen}.html?v=${Date.now()}`, {diff:true, onLoad:()=>$.emit('view:loaded',{paneId,view})});
|
||||
};
|
||||
|
||||
let handleKeys = e => {
|
||||
if(!e.altKey || !options.keyboard) return;
|
||||
let actions = {
|
||||
'KeyV':()=>splitVertical(),'v':()=>splitVertical(),'V':()=>splitVertical(),
|
||||
'KeyH':()=>splitHorizontal(),'h':()=>splitHorizontal(),'H':()=>splitHorizontal(),
|
||||
'KeyX':()=>closeFocused(),'x':()=>closeFocused(),'X':()=>closeFocused(),
|
||||
'KeyO':()=>openViewsPopupForPane(),'o':()=>openViewsPopupForPane(), 'O':()=>openViewsPopupForPane(),
|
||||
'ArrowRight':()=>focusDirection(1,0),'ArrowLeft':()=>focusDirection(-1,0),'ArrowUp':()=>focusDirection(0,-1),'ArrowDown':()=>focusDirection(0,1),'Tab':()=>focusNext()
|
||||
};
|
||||
if(actions[e.code] || actions[e.key]) {
|
||||
(actions[e.code] || actions[e.key])();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
containerEl = typeof container === 'string' ? document.querySelector(container) : container;
|
||||
if(!containerEl) throw new Error('TilingViewportManager: container not found');
|
||||
if(options.cssVars) Object.keys(options.cssVars).forEach(k => containerEl.style.setProperty(k, options.cssVars[k]));
|
||||
buildInitial();
|
||||
if(options.keyboard) document.addEventListener('keydown', handleKeys);
|
||||
|
||||
this.splitVertical = splitVertical;
|
||||
this.splitHorizontal = splitHorizontal;
|
||||
this.closeFocused = closeFocused;
|
||||
this.focusNext = focusNext;
|
||||
this.focusPrev = focusPrev;
|
||||
this.serialize = serialize;
|
||||
this.restore = restore;
|
||||
this.setPaneProps = setPaneProps;
|
||||
this.getPaneProps = getPaneProps;
|
||||
this.options = options;
|
||||
this.loadView = loadView;
|
||||
this.getPaneEl = getPaneEl;
|
||||
this.getNodeById = getNodeById;
|
||||
|
||||
Object.defineProperty(this, 'layout', { get: () => serialize(), set: d => restore(d) });
|
||||
Object.defineProperty(this, 'focused', { get: () => focusedPane?.id });
|
||||
};
|
||||
|
||||
return TilingViewportManager;
|
||||
}));
|
||||
@@ -0,0 +1,68 @@
|
||||
(function (root, factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define([], factory);
|
||||
} else if (typeof module === 'object' && module.exports) {
|
||||
module.exports = factory();
|
||||
} else {
|
||||
root.UWorkspaceShell = factory();
|
||||
}
|
||||
}(typeof self !== 'undefined' ? self : this, function () {
|
||||
'use strict';
|
||||
const globalScope = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this);
|
||||
|
||||
function getElement(target) {
|
||||
if (!target) return null;
|
||||
if (typeof target === 'string') return document.getElementById(target);
|
||||
return target && target.nodeType === 1 ? target : null;
|
||||
}
|
||||
|
||||
function bindShell(options) {
|
||||
const sidebar = getElement(options.sidebarId || options.sidebar);
|
||||
const overlay = getElement(options.overlayId || options.overlay);
|
||||
const toggle = getElement(options.toggleButtonId || options.toggle);
|
||||
const closeOnNav = options.closeOnNav !== false;
|
||||
|
||||
if (!sidebar || !overlay) return null;
|
||||
|
||||
function setOpen(open) {
|
||||
sidebar.classList.toggle('is-open', !!open);
|
||||
overlay.classList.toggle('is-open', !!open);
|
||||
}
|
||||
|
||||
function toggleOpen() {
|
||||
setOpen(!sidebar.classList.contains('is-open'));
|
||||
}
|
||||
|
||||
if (toggle) {
|
||||
toggle.addEventListener('click', toggleOpen);
|
||||
}
|
||||
|
||||
overlay.addEventListener('click', function () {
|
||||
setOpen(false);
|
||||
});
|
||||
|
||||
if (closeOnNav) {
|
||||
sidebar.querySelectorAll('a').forEach(function (link) {
|
||||
link.addEventListener('click', function () {
|
||||
setOpen(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
globalScope.addEventListener('resize', function () {
|
||||
if (globalScope.innerWidth > 860) {
|
||||
setOpen(false);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
open: function () { setOpen(true); },
|
||||
close: function () { setOpen(false); },
|
||||
toggle: toggleOpen,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
init: bindShell,
|
||||
};
|
||||
}));
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
(function (root, factory) {
|
||||
if (typeof exports === 'object' && typeof module !== 'undefined') {
|
||||
module.exports = factory();
|
||||
} else if (typeof define === 'function' && define.amd) {
|
||||
define([], factory);
|
||||
} else {
|
||||
root.Connection = factory();
|
||||
}
|
||||
}(typeof self !== 'undefined' ? self : this, function () {
|
||||
|
||||
var Connection = {
|
||||
|
||||
auto_reconnect : false,
|
||||
established : false,
|
||||
|
||||
update_indicator : (status) => {
|
||||
var status_colors = {
|
||||
'offline' : 'red',
|
||||
'online' : 'lightgreen',
|
||||
'error' : 'DarkOrange',
|
||||
};
|
||||
$('#connection-status').text(status).css('color', status_colors[status] || 'gray');
|
||||
},
|
||||
|
||||
debug : true,
|
||||
auto_reconnect : true,
|
||||
cmd_waiting_rels : {},
|
||||
|
||||
server_url : '',
|
||||
|
||||
init : () => {
|
||||
new EventSystem(Connection);
|
||||
},
|
||||
|
||||
start : (url = null) => {
|
||||
|
||||
if(url) Connection.server_url = url;
|
||||
|
||||
Connection.update_indicator('offline');
|
||||
if(Connection.socket) Connection.socket.close();
|
||||
Connection.socket = new WebSocket(Connection.server_url);
|
||||
Connection.socket.onmessage = function(rawmsg) {
|
||||
var msg = JSON.parse(rawmsg.data);
|
||||
if(Connection.debug) console.log('CONNECTION MSG', msg);
|
||||
if(msg.type) Connection.trigger(msg.type, msg);
|
||||
Connection.trigger('message', msg);
|
||||
}
|
||||
Connection.socket.onclose = function() {
|
||||
Connection.update_indicator('offline');
|
||||
if(Connection.debug) console.log('CONNECTION CLOSED');
|
||||
Connection.established = false;
|
||||
Connection.trigger('close', {});
|
||||
}
|
||||
Connection.socket.onerror = function(error) {
|
||||
Connection.update_indicator('error');
|
||||
console.error('CONNECTION', error);
|
||||
Connection.established = false;
|
||||
}
|
||||
Connection.socket.onopen = function() {
|
||||
Connection.update_indicator('online');
|
||||
if(Connection.debug) console.log('CONNECTION ESTABLISHED');
|
||||
Connection.established = true;
|
||||
Connection.trigger('open', {});
|
||||
}
|
||||
setTimeout(Connection.reconnect, 2000);
|
||||
|
||||
},
|
||||
|
||||
deauth : () => {
|
||||
Game.session = {};
|
||||
},
|
||||
|
||||
reconnect : () => {
|
||||
if(!Connection.established && Connection.auto_reconnect)
|
||||
Connection.start();
|
||||
else
|
||||
setTimeout(Connection.reconnect, 2000);
|
||||
},
|
||||
|
||||
queue : [],
|
||||
|
||||
dequeue : () => {
|
||||
var dq = Connection.queue;
|
||||
Connection.queue = [];
|
||||
dq.forEach(function(fm) {
|
||||
Connection.send(fm);
|
||||
});
|
||||
},
|
||||
|
||||
send : (msg) => {
|
||||
if(Connection.established) {
|
||||
if(typeof msg == 'function')
|
||||
msg();
|
||||
else
|
||||
Connection.socket.send(JSON.stringify(msg));
|
||||
} else {
|
||||
Connection.queue.push(msg);
|
||||
}
|
||||
},
|
||||
|
||||
close : () => {
|
||||
Connection.auto_reconnect = false;
|
||||
Connection.socket.close();
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
return Connection;
|
||||
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user