trying to port web app starter from PHP

This commit is contained in:
udo
2026-04-19 09:38:23 +00:00
parent 46d98a092f
commit be514d63d6
546 changed files with 76910 additions and 2807 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+141
View File
@@ -0,0 +1,141 @@
# morphdom.js
A fast and lightweight DOM diffing/patching library for efficiently updating the DOM with minimal changes.
## Basic Usage
```javascript
morphdom(fromNode, toNode, options) // Transform fromNode to match toNode
```
Returns the morphed node (may be a new node if root element changes).
Instead of replacing entire DOM trees, morphdom intelligently compares and patches only the differences:
```javascript
const container = document.getElementById('container');
const newHTML = '<div><p>Updated content</p></div>';
// Efficiently update DOM - only changed parts are modified
morphdom(container, newHTML);
```
## Options
```javascript
morphdom(fromNode, toNode, {
childrenOnly: true, // Only morph children, not root element
getNodeKey: node => node.id, // Custom key function for matching nodes
onBeforeNodeAdded: node => true, // Called before adding new nodes
onNodeAdded: node => {}, // Called after nodes are added
onBeforeElUpdated: (from, to) => true, // Called before updating elements
onElUpdated: node => {}, // Called after elements are updated
onBeforeNodeDiscarded: node => true, // Called before removing nodes
onNodeDiscarded: node => {}, // Called after nodes are removed
onBeforeElChildrenUpdated: (from, to) => true, // Called before updating children
skipFromChildren: (from, to) => false, // Skip children comparison
addChild: (parent, child) => parent.appendChild(child) // Custom child addition
});
```
## Key Matching
Use `getNodeKey` for efficient list updates:
```javascript
// HTML with keyed elements
const html = `
<ul>
<li id="item-1">Item 1</li>
<li id="item-2">Item 2</li>
<li id="item-3">Item 3</li>
</ul>
`;
morphdom(list, html, {
getNodeKey: node => node.getAttribute('id')
});
```
## Event Handlers
```javascript
morphdom(container, newHTML, {
onBeforeElUpdated: (fromEl, toEl) => {
// Preserve event listeners
if (fromEl.hasEventListeners) {
return false; // Skip this element
}
return true;
},
onNodeAdded: (node) => {
// Initialize new components
if (node.classList?.contains('widget')) {
initializeWidget(node);
}
}
});
```
## Form Element Handling
Morphdom has special handling for form elements:
- **INPUT**: Preserves checked, disabled, and value properties
- **TEXTAREA**: Syncs value and text content
- **SELECT**: Maintains selectedIndex and option states
- **OPTION**: Handles selected state in select boxes
## Common Patterns
### Template Updates
```javascript
function updateTemplate(container, data) {
const template = `<div class="user">${data.name}</div>`;
morphdom(container, template);
}
```
### List Management
```javascript
function updateList(listEl, items) {
const html = items.map(item =>
`<li id="item-${item.id}">${item.name}</li>`
).join('');
morphdom(listEl, `<ul>${html}</ul>`, {
childrenOnly: true,
getNodeKey: node => node.id
});
}
```
### Component Preservation
```javascript
morphdom(container, newHTML, {
onBeforeElUpdated: (from, to) => {
// Preserve components that haven't changed
if (from.dataset.component === to.dataset.component) {
return false; // Skip update
}
return true;
}
});
```
## Integration with uquery.js
This library is used by uquery's `html()` method when differential updates are enabled:
```javascript
$.options.alwaysDoDifferentialUpdate = true;
$('#container').html(newContent); // Uses morphdom internally
```
## Browser Support
- Modern browsers with DOM Level 2+ support
- Automatic fallbacks for older browsers
- Template element support detection
- Range API support detection
+65
View File
@@ -0,0 +1,65 @@
var enable_debug = true;
var UI = {
smoothScrollToNamedAnchors: function() {
$('a[href^="#"]').each(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() {
$(document.body).append(`<style>
::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; }
}</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');
},
}
$.ready(UI.init);
+897
View File
@@ -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>
+92
View File
@@ -0,0 +1,92 @@
(function (root, factory) {
if (typeof exports === 'object' && typeof module !== 'undefined') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define([], factory);
} else {
root.EventEmitter = factory();
}
}(typeof self !== 'undefined' ? self : this, function () {
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;
}
clear(topic) {
if (topic) {
this._topics.delete(topic);
}
return this;
}
}
return EventEmitter;
}));
+136
View File
@@ -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,
};
}));
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+977
View File
@@ -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">&lt;button {{@click="handleClick"}}&gt;Click Count: {{clickCount}}&lt;/button&gt;</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">&lt;script&gt;var computed = data.value * 2;&lt;/script&gt;
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">&lt;script&gt;</span>...&lt;/script&gt;</div>
<div class="api-method"><span class="method-name">&lt;defer&gt;</span>...&lt;/defer&gt;</div>
<div class="api-method"><span class="method-name">&lt;?</span> code <span class="method-name">?&gt;</span></div>
<div class="api-method"><span class="method-name">&lt;?=</span> expression <span class="method-name">?&gt;</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">'&lt;button {{@click="increment"}}&gt;{{count}}&lt;/button&gt;'</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">'&lt;div&gt;{{name}} - {{email}}&lt;/div&gt;'</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">`
&lt;div class="user-card"&gt;
&lt;h3&gt;{{name or "Unknown User"}}&lt;/h3&gt;
{{#if profile.verified}}✅ Verified{{/if}}
&lt;p&gt;Balance: ${{%balance}}&lt;/p&gt;
{{#each achievements}}
&lt;span class="badge"&gt;{{data}}&lt;/span&gt;
{{/each}}
&lt;/div&gt;
`</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: '&lt;div class="dashboard"&gt;\n' +
' &lt;h2&gt;{{company}} Dashboard&lt;/h2&gt;\n' +
' \n' +
' &lt;script&gt;\n' +
' var totalRevenue = 0;\n' +
' data.quarters.forEach(q =&gt; totalRevenue += q.revenue);\n' +
' &lt;/script&gt;\n' +
' \n' +
' &lt;p&gt;&lt;strong&gt;Total Revenue:&lt;/strong&gt; ${{:totalRevenue}}&lt;/p&gt;\n' +
' \n' +
' {{#each quarters as quarter}}\n' +
' &lt;div style="margin: 10px 0; padding: 10px; border-left: 4px solid #007acc;"&gt;\n' +
' &lt;h4&gt;{{quarter.name}}&lt;/h4&gt;\n' +
' &lt;p&gt;Revenue: ${{%quarter.revenue}} {{#eq quarter.trend "up"}}📈{{/eq}}{{#eq quarter.trend "down"}}📉{{/eq}}&lt;/p&gt;\n' +
' {{#lookup quarter "bonus"}}\n' +
' &lt;span style="color: green;"&gt;🎯 Bonus: ${{quarter.bonus}}&lt;/span&gt;\n' +
' {{/lookup}}\n' +
' &lt;/div&gt;\n' +
' {{/each}}\n' +
' \n' +
' &lt;button {{@click="generateReport"}} style="padding: 10px; background: #28a745; color: white; border: none; border-radius: 5px;"&gt;\n' +
' 📊 Generate Report\n' +
' &lt;/button&gt;\n' +
'&lt;/div&gt;',
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>
+411
View File
@@ -0,0 +1,411 @@
(function (root, factory) { // I really hate this convoluted bullshit
if (typeof exports === 'object' && typeof module !== 'undefined') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define([], factory);
} else {
root.Macrobars = factory();
}
}(typeof self !== 'undefined' ? self : this, function () {
if(!String.prototype.replaceAll) String.prototype.replaceAll = function(search, replacement) {
var target = this;
return target.split(search).join(replacement);
};
var safe_out = (s, defaultValue = '') => {
if(typeof s == 'undefined' || s === null || s === false || s === '') s = defaultValue;
return((''+s).replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;'));
};
var num_out = (n, decimals = 2) => {
if(typeof n == 'undefined' || n === false) n = 0;
if(decimals == 0) return(Math.round(n));
return(n.toFixed(decimals));
}
var num_out_round = (n) => {
if(typeof n == 'undefined' || n === false || n === null) n = 0;
if(typeof n === 'string') {
n = parseFloat(n);
if(isNaN(n)) return '0';
}
if(Math.abs(n) >= 1e15) {
return n.toExponential(2);
}
if(Math.abs(n) > 0 && Math.abs(n) < 0.001) {
return n.toExponential(2);
}
if(Math.abs(n) >= 1e12) {
return (n/1e12).toFixed(1) + 'T';
} else if(Math.abs(n) >= 1e9) {
return (n/1e9).toFixed(1) + 'B';
} else if(Math.abs(n) >= 1e6) {
return (n/1e6).toFixed(1) + 'M';
} else if(Math.abs(n) >= 1e3) {
return (n/1e3).toFixed(1) + 'k';
} else if(Math.abs(n) >= 1) {
return Math.round(n);
} else if(Math.abs(n) >= 0.01) {
return n.toFixed(2);
} else if(Math.abs(n) >= 0.001) {
return n.toFixed(3);
} else if(n === 0) {
return 0;
}
return n.toString();
}
var debug_out = (ee) => {
var es = ee.stack.split("\n");
var loc = es[1];
var p0 = loc.indexOf('<anonymous>');
if(p0 != -1) {
loc = loc.substr(p0+('<anonymous>'.length+1));
loc = loc.substr(0, loc.length-1);
}
return('Macrobars '+es[0]+' ('+loc+')');
}
var signposts = [
{ start : '<script>', end : '</script>', type : 'code' },
{ start : '<defer>', end : '</defer>', type : 'defer' },
{ start : '<?=', end : '?>', type : 'field' },
{ start : '<?:', end : '?>', type : 'var_out' },
{ start : '<?', end : '?>', type : 'code' },
{ start : '<!--?', end : '?-->', type : 'code' },
{ start : '{{:', end : '}}', type : 'var_out' },
{ start : '{{#eq ', end : '}}', type : 'eq_block_start' },
{ start : '{{#lookup ', end : '}}', type : 'lookup_block_start' },
{ start : '{{eq ', end : '}}', type : 'eq_inline' },
{ start : '{{lookup ', end : '}}', type : 'lookup_inline' },
{ start : '{{#default ', end : '}}', type : 'default_empty' },
{ start : '{{#if ', end : '}}', type : 'if_start' },
{ start : '{{#else}}', end : '', type : 'else' },
{ start : '{{#component ', end : '}}', type : 'component' },
{ start : '{{#each ', end : '}}', type : 'each_start' },
{ start : '{{/each}}', end : '', type : 'each_end' },
{ start : '{{/if}}', end : '', type : 'if_end' },
{ start : '{{/eq}}', end : '', type : 'eq_block_end' },
{ start : '{{/lookup}}', end : '', type : 'lookup_block_end' },
{ start : '{{/', end : '}}', type : 'block_end' },
{ start : '{{@', end : '}}', type : 'event_bind' },
{ start : '{{{', end : '}}}', type : 'field_unsafe' },
{ start : '{{%', end : '}}', type : 'field_number' },
{ start : '{{~', end : '}}', type : 'field_number_round' },
{ start : '{{', end : '}}', type : 'field' },
];
if(!each) function each(o, f) {
if(!o)
return;
if(o.forEach) {
o.forEach(f);
} else {
for(var prop in o) if(o.hasOwnProperty(prop)) {
f(o[prop], prop);
}
}
}
var data_prefix = (identifier) => {
if(identifier.substr(0, 1) == ':')
return(identifier.substr(1));
else
return('data.'+identifier);
}
var parse_field_with_default = (fieldText) => {
var orMatch = fieldText.match(/^(.+?)\s+or\s+(['"])(.*?)\2$/);
if (orMatch) {
return {
field: orMatch[1].trim(),
default: orMatch[3]
};
}
return {
field: fieldText.trim(),
default: null
};
}
var compile = function(text, options = {}) {
var crash_counter = 0;
var tokens = [];
var gensource = [];
var event_bindings = [];
var components = options.components || {};
var condition_stack = [];
var emit = {
text : (token) => {
gensource.push('output += '+JSON.stringify(token.text)+';');
},
code : (token) => {
gensource.push(token.text);
},
defer : (token) => {
gensource.push('output += "<script>" + '+JSON.stringify(token.text)+' + "</script>";');
},
var_out : (token) => {
var defaultVal = token.default ? JSON.stringify(token.default) : 'default_empty_field';
gensource.push('output += safe_out('+(token.text)+', '+defaultVal+');');
},
field : (token) => {
var defaultVal = token.default ? JSON.stringify(token.default) : 'default_empty_field';
gensource.push('output += safe_out('+data_prefix(token.text)+', '+defaultVal+');');
},
field_unsafe : (token) => {
if (token.default) {
gensource.push('output += ('+data_prefix(token.text)+' || '+JSON.stringify(token.default)+');');
} else {
gensource.push('output += ('+data_prefix(token.text)+' || default_empty_field);');
}
},
field_number : (token) => {
gensource.push('output += num_out('+data_prefix(token.text)+', this.decimals);');
},
field_number_round : (token) => {
gensource.push('output += num_out_round('+data_prefix(token.text)+');');
},
each_start : (token) => {
var parts = token.text.split(' as ');
var collection = parts[0].trim();
var itemName = parts.length > 1 ? parts[1].trim() : 'data';
gensource.push('each('+data_prefix(collection)+', ('+itemName+', index) => {');
},
if_start : (token) => {
condition_stack.push('if');
gensource.push('if ('+data_prefix(token.text)+') {');
},
else : (token) => {
gensource.push('} else {');
},
if_end : (token) => {
condition_stack.pop();
gensource.push('} /* if end */');
},
each_end : (token) => {
gensource.push('}); /* each end */');
},
block_end : (token) => {
var block_type = condition_stack.pop();
gensource.push('}); /* '+(block_type || 'each')+' end */');
},
component : (token) => {
var comp_name = token.text.trim();
if (components[comp_name]) {
gensource.push('output += components['+JSON.stringify(comp_name)+'](data);');
} else {
gensource.push('output += "[Component '+comp_name+' not found]";');
}
},
event_bind : (token) => {
// Parse event binding: @click="functionName" or @click="data.handler"
var parts = token.text.split('=');
if (parts.length === 2) {
var event_type = parts[0].trim();
var handler_ref = parts[1].trim().replace(/"/g, '');
var binding_id = 'mb_bind_' + event_bindings.length;
event_bindings.push({
id: binding_id,
event: event_type,
handler: handler_ref
});
gensource.push('output += " data-mb-bind=\\"'+binding_id+'\\"";');
}
},
eq_block_start : (token) => {
// Parse "value1 value2" for equality comparison block
var parts = token.text.trim().split(/\s+/);
if (parts.length >= 2) {
var val1 = parts[0].startsWith('"') || parts[0].startsWith("'") ? parts[0] : data_prefix(parts[0]);
var val2 = parts[1].startsWith('"') || parts[1].startsWith("'") ? parts[1] : data_prefix(parts[1]);
condition_stack.push('eq');
gensource.push('if ('+val1+' == '+val2+') {');
}
},
lookup_block_start : (token) => {
// Parse "object key" for dynamic property lookup block (checks if property exists and is truthy)
var parts = token.text.trim().split(/\s+/);
if (parts.length >= 2) {
var obj = data_prefix(parts[0]);
var key = parts[1].startsWith('"') || parts[1].startsWith("'") ? parts[1] : data_prefix(parts[1]);
condition_stack.push('lookup');
gensource.push('if ('+obj+' && '+obj+'['+key+']) {');
}
},
eq_inline : (token) => {
// Parse "value1 value2" for equality comparison - outputs result directly
var parts = token.text.trim().split(/\s+/);
if (parts.length >= 2) {
var val1 = parts[0].startsWith('"') || parts[0].startsWith("'") ? parts[0] : data_prefix(parts[0]);
var val2 = parts[1].startsWith('"') || parts[1].startsWith("'") ? parts[1] : data_prefix(parts[1]);
var defaultVal = token.default ? JSON.stringify(token.default) : 'default_empty_field';
gensource.push('output += safe_out(('+val1+' == '+val2+') ? "true" : "", '+defaultVal+');');
}
},
lookup_inline : (token) => {
// Parse "object key" for dynamic property lookup - outputs result directly
var parts = token.text.trim().split(/\s+/);
if (parts.length >= 2) {
var obj = data_prefix(parts[0]);
var key = parts[1].startsWith('"') || parts[1].startsWith("'") ? parts[1] : data_prefix(parts[1]);
var defaultVal = token.default ? JSON.stringify(token.default) : 'default_empty_field';
gensource.push('output += safe_out(('+obj+' && '+obj+'['+key+']) || "", '+defaultVal+');');
}
},
eq_block_end : (token) => {
condition_stack.pop();
gensource.push('} /* eq end */');
},
lookup_block_end : (token) => {
condition_stack.pop();
gensource.push('} /* lookup end */');
},
default_empty : (token) => {
gensource.push('default_empty_field = '+JSON.stringify(token.text)+';');
},
}
while(text != '' && crash_counter < 100) {
crash_counter+=1;
var p0 = -1;
var sp_found = false;
signposts.forEach((sp) => {
var ps = text.indexOf(sp.start);
if(ps != -1 && (p0 == -1 || p0 > ps)) {
sp_found = sp;
p0 = ps;
}
});
if(sp_found)
{
tokens.push({ type : 'text', text : text.substr(0, p0) });
text = text.substr(p0 + sp_found.start.length);
var p1 = text.indexOf(sp_found.end);
if(p1 == -1) p1 = text.length;
var tokenText = text.substr(0, p1);
var token = { type : sp_found.type, text : tokenText };
if (sp_found.type === 'field' || sp_found.type === 'var_out' ||
sp_found.type === 'field_unsafe' || sp_found.type === 'field_number' ||
sp_found.type === 'field_number_round') {
var parsed = parse_field_with_default(tokenText);
token.text = parsed.field;
token.default = parsed.default;
}
tokens.push(token);
text = text.substr(p1+sp_found.end.length);
}
else
{
tokens.push({ type : 'text', text : text });
text = '';
}
}
if(typeof options.decimals == 'undefined')
options.decimals = 2;
this.decimals = options.decimals;
gensource.push('(data) => { ');
if(options.strict)
gensource.push('"use strict";');
gensource.push(' try {');
gensource.push(' if(!data) data = {}; var data_root = data;');
gensource.push(' var output = ""; var default_empty_field = "";');
gensource.push(' var echo = (s) => { output += s; };');
tokens.forEach((tok) => {
emit[tok.type](tok);
});
gensource.push(' } catch (ee) { console.error(ee); output += debug_out(ee); }');
gensource.push(' return(output);');
gensource.push('}');
var f = {};
try {
f = eval('('+gensource.join("\n")+')');
} catch(ce) {
console.error('Macrobars compilation error:', ce);
console.error('Template source:', text.substring(0, 200) + (text.length > 200 ? '...' : ''));
console.error('Generated JavaScript:', gensource.join("\n"));
}
f.tokens = tokens;
f.gensource = gensource;
f.event_bindings = event_bindings;
f.options = options;
f.components = components;
f.renderTo = function(container_or_query, data) {
var container;
if (typeof container_or_query === 'string') {
container = document.querySelector(container_or_query);
if (!container) {
return null;
}
} else {
container = container_or_query;
}
try {
var html = f(data);
container.innerHTML = html;
event_bindings.forEach(binding => {
var elements = container.querySelectorAll('[data-mb-bind="' + binding.id + '"]');
elements.forEach(element => {
var handler = data[binding.handler] || eval(binding.handler);
if (typeof handler === 'function') {
element.addEventListener(binding.event, handler);
}
});
});
} catch(renderError) {
console.error('Macrobars renderTo error:', renderError);
console.error('Data passed to template:', data);
throw renderError;
}
return container;
};
return(f);
}
return({
compile : compile,
createComponents : function(definitions) {
var components = {};
each(definitions, (template, name) => {
components[name] = compile(template);
});
return components;
},
num_out : num_out,
safe_out : safe_out,
num_out_round : num_out_round,
debug_out : debug_out,
each : each,
});
}));
+137
View File
@@ -0,0 +1,137 @@
# macrobars.js
A lightweight JavaScript templating engine with PHP-style syntax and Handlebars-inspired features.
## Basic Usage
```javascript
const template = Macrobars.compile('<h1>{{title}}</h1><p>{{content}}</p>');
const html = template({title: 'Hello', content: 'World'});
```
## Field Output
```javascript
{{field}} // Safe HTML output (escaped)
{{{field}}} // Unsafe HTML output (raw)
{{field or "default"}} // With default value
{{:variable}} // Direct variable (no data. prefix)
```
## Numbers
```javascript
{{%number}} // Formatted number (2 decimals)
{{~number}} // Rounded number (1k, 1M format)
```
## Control Structures
### Conditionals
```javascript
{{#if condition}}
Content when true
{{#else}}
Content when false
{{/if}}
```
### Equality Blocks
```javascript
{{#eq value1 value2}}
Content when equal
{{/eq}}
{{eq value1 value2}} // Inline: outputs "true" or ""
```
### Loops
```javascript
{{#each items}}
{{name}} - {{index}}
{{/each}}
{{#each items as item}}
{{item.name}}
{{/each}}
```
### Property Lookup
```javascript
{{#lookup object key}}
{{value}} exists
{{/lookup}}
{{lookup object key}} // Inline: outputs value or ""
```
## Code Blocks
```javascript
<script>
var result = data.value * 2;
</script>
<defer>
// Deferred JavaScript execution
</defer>
<?= data.field ?> // PHP-style output
<? var x = data.count; ?> // PHP-style code
```
## Event Binding
```javascript
<button {{@click="handleClick"}}>Click me</button>
```
Events are automatically bound when using `renderTo()`.
## Components
```javascript
{{#component myComponent}} // Render registered component
```
## Compilation Options
```javascript
const template = Macrobars.compile(templateString, {
decimals: 2, // Number formatting precision
strict: true, // Use strict mode
components: {...} // Reusable components
});
```
## DOM Integration
```javascript
template.renderTo('#container', data); // Render to element
template.renderTo(document.getElementById('x'), data);
```
## Utility Functions
```javascript
Macrobars.safe_out(text, default) // HTML escape
Macrobars.num_out(number, decimals) // Format number
Macrobars.num_out_round(number) // Round with k/M suffix
```
## Template Debugging
Access compiled template information:
```javascript
template.tokens // Parsed tokens
template.gensource // Generated JavaScript
template.event_bindings // Event binding data
```
## Default Values
```javascript
{{#default "N/A"}} // Set default for empty fields
{{field}} // Will show "N/A" if empty
```
@@ -0,0 +1,34 @@
:root {
--vp-size-popup-min-width: 160px;
}
.vp-popup-menu {
position: absolute;
background: var(--vp-color-black);
border: 1px solid var(--color-border);
color: var(--vp-color-white);
min-width: var(--vp-size-popup-min-width);
z-index: var(--vp-z-popup);
border-radius: var(--vp-space-l);
overflow: hidden;
}
.vp-popup-item {
padding: var(--vp-space-l);
cursor: pointer;
white-space: nowrap;
}
.vp-popup-item:hover {
color: var(--color-highlight);
}
.vp-popup-item.focused {
background: var(--color-highlight);
color: var(--vp-color-black);
}
.vp-popup-empty {
padding: var(--vp-space-l);
color: var(--vp-color-gray);
}
@@ -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;
}));
+976
View File
@@ -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">'&lt;b&gt;New content&lt;/b&gt;'</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">'&lt;p&gt;Added&lt;/p&gt;'</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>
+589
View File
@@ -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,168 @@
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory();
} else {
root.USortableTable = factory();
}
}(typeof self !== 'undefined' ? self : this, function () {
'use strict';
const globalScope = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this);
function getTable(target) {
if (!target) return null;
if (typeof target === 'string') return document.getElementById(target);
return target.nodeType === 1 ? target : null;
}
function getStorageKey(table, options) {
if (options && options.storageKey) return options.storageKey;
if (table && table.id) return `u-sortable-table.${table.id}`;
return null;
}
function loadSort(table, options) {
const storageKey = getStorageKey(table, options);
if (!storageKey || !globalScope.localStorage) return null;
try {
const raw = globalScope.localStorage.getItem(storageKey);
if (!raw) return null;
const parsed = JSON.parse(raw);
if (!parsed || !Number.isInteger(parsed.column) || (parsed.direction !== 'asc' && parsed.direction !== 'desc')) {
return null;
}
return parsed;
} catch (error) {
return null;
}
}
function saveSort(table, column, direction, options) {
const storageKey = getStorageKey(table, options);
if (!storageKey || !globalScope.localStorage) return;
try {
globalScope.localStorage.setItem(storageKey, JSON.stringify({ column, direction }));
} catch (error) {
// Ignore storage errors.
}
}
function parseSortValue(text) {
const normalized = String(text || '').trim();
if (!normalized) return { type: 'text', value: '' };
if (globalScope.UFormat && typeof globalScope.UFormat.parseUnitNumber === 'function') {
const unitValue = globalScope.UFormat.parseUnitNumber(normalized);
if (unitValue != null) {
return { type: 'number', value: unitValue };
}
}
const timestamp = Date.parse(normalized);
if (!Number.isNaN(timestamp)) {
return { type: 'number', value: timestamp };
}
return { type: 'text', value: normalized.toLowerCase() };
}
function setHeaderState(table, columnIndex, direction) {
Array.from(table.querySelectorAll('thead th')).forEach((header, index) => {
header.classList.remove('sorted-asc', 'sorted-desc');
header.setAttribute('aria-sort', 'none');
if (index === columnIndex) {
header.classList.add(direction === 'asc' ? 'sorted-asc' : 'sorted-desc');
header.setAttribute('aria-sort', direction === 'asc' ? 'ascending' : 'descending');
}
});
}
function sortTable(table, columnIndex, direction, options, persist) {
const tbody = table.querySelector('tbody');
if (!tbody) return;
const rows = Array.from(tbody.querySelectorAll('tr')).filter((row) => row.children.length > 0 && !row.hasAttribute('data-empty-row'));
if (!rows.length) return;
rows.sort((rowA, rowB) => {
const cellA = rowA.children[columnIndex];
const cellB = rowB.children[columnIndex];
const rawA = cellA ? (cellA.getAttribute('data-sort-value') || cellA.textContent) : '';
const rawB = cellB ? (cellB.getAttribute('data-sort-value') || cellB.textContent) : '';
const valueA = parseSortValue(rawA);
const valueB = parseSortValue(rawB);
let comparison = 0;
if (valueA.type === 'number' && valueB.type === 'number') {
comparison = valueA.value - valueB.value;
} else {
comparison = String(valueA.value).localeCompare(String(valueB.value), undefined, {
numeric: true,
sensitivity: 'base',
});
}
return direction === 'asc' ? comparison : -comparison;
});
rows.forEach((row) => tbody.appendChild(row));
setHeaderState(table, columnIndex, direction);
if (persist !== false) {
saveSort(table, columnIndex, direction, options);
}
}
function getInitialSort(table, options) {
const saved = loadSort(table, options);
if (saved) return saved;
if (options && options.initialSort) {
return {
column: Number(options.initialSort.column || 0),
direction: options.initialSort.direction === 'desc' ? 'desc' : 'asc',
};
}
return null;
}
function attachHeader(header, table, columnIndex, options) {
if (header.getAttribute('data-sortable') === 'false') return;
header.classList.add('sortable');
header.tabIndex = 0;
header.addEventListener('click', function () {
const current = loadSort(table, options);
const nextDirection = current && current.column === columnIndex && current.direction === 'asc' ? 'desc' : 'asc';
sortTable(table, columnIndex, nextDirection, options, true);
});
header.addEventListener('keydown', function (event) {
if (event.key !== 'Enter' && event.key !== ' ') return;
event.preventDefault();
header.click();
});
}
function init(target, options = {}) {
const table = getTable(target);
if (!table || table.dataset.sortableTableInit === '1') return table;
Array.from(table.querySelectorAll('thead th')).forEach((header, index) => attachHeader(header, table, index, options));
table.dataset.sortableTableInit = '1';
const initialSort = getInitialSort(table, options);
if (initialSort) {
sortTable(table, initialSort.column, initialSort.direction, options, false);
}
return table;
}
function initAll(selector = '.u-sortable-table') {
Array.from(document.querySelectorAll(selector)).forEach((table) => init(table));
}
return {
init,
initAll,
parseSortValue,
sortTable,
};
}));
@@ -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+&lt;H V O X ↑ ← → ↓&gt;</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,354 @@
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory();
} else {
root.UTimeSeriesChart = factory();
}
}(typeof self !== 'undefined' ? self : this, function () {
'use strict';
const globalScope = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this);
class TimeSeriesChart {
constructor(canvasOrId, options = {}) {
this.canvas = typeof canvasOrId === 'string' ? document.getElementById(canvasOrId) : canvasOrId;
if (!this.canvas || !this.canvas.getContext) {
throw new Error('UTimeSeriesChart requires a canvas element');
}
this.ctx = this.canvas.getContext('2d');
const theme = this.resolveThemeColors();
this.options = Object.assign({
xAxisLabel: 'Time',
yAxisLeftLabel: 'Value',
yAxisRightLabel: '',
yAxisLeftFormat: 'number',
yAxisRightFormat: 'number',
padding: { top: 18, right: 62, bottom: 34, left: 58 },
gridColor: theme.gridColor,
axisColor: theme.axisColor,
textColor: theme.textColor,
legendBackground: theme.legendBackground,
legendTextColor: theme.legendTextColor,
tooltipBackground: theme.tooltipBackground,
tooltipBorder: theme.tooltipBorder,
tooltipTitleColor: theme.tooltipTitleColor,
tooltipTextColor: theme.tooltipTextColor,
hoverLineColor: 'rgba(255,255,255,0.35)',
}, options);
this.series = [];
this.xLabels = [];
this.hoverIndex = null;
this.hoverX = 0;
this.width = 0;
this.height = 0;
this.onMouseMove = this.onMouseMove.bind(this);
this.onMouseLeave = this.onMouseLeave.bind(this);
this.onResize = this.onResize.bind(this);
this.canvas.addEventListener('mousemove', this.onMouseMove);
this.canvas.addEventListener('mouseleave', this.onMouseLeave);
globalScope.addEventListener('resize', this.onResize);
if (typeof ResizeObserver !== 'undefined') {
this.resizeObserver = new ResizeObserver(this.onResize);
this.resizeObserver.observe(this.canvas);
}
this.resize();
}
resolveThemeColors() {
const styles = globalScope.getComputedStyle ? globalScope.getComputedStyle(document.documentElement) : null;
const pick = function (name, fallback) {
if (!styles) return fallback;
const value = styles.getPropertyValue(name).trim();
return value || fallback;
};
return {
gridColor: pick('--border', 'rgba(148, 163, 184, 0.20)'),
axisColor: pick('--border-hover', pick('--border', 'rgba(148, 163, 184, 0.42)')),
textColor: pick('--text-secondary', '#64748b'),
legendBackground: pick('--surface', 'rgba(15, 23, 42, 0.72)'),
legendTextColor: pick('--text-primary', '#0f172a'),
tooltipBackground: pick('--surface', '#0f172a'),
tooltipBorder: pick('--border', 'rgba(148, 163, 184, 0.30)'),
tooltipTitleColor: pick('--primary', '#3b82f6'),
tooltipTextColor: pick('--text-primary', '#0f172a'),
};
}
destroy() {
this.canvas.removeEventListener('mousemove', this.onMouseMove);
this.canvas.removeEventListener('mouseleave', this.onMouseLeave);
globalScope.removeEventListener('resize', this.onResize);
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
}
onResize() {
this.resize();
}
onMouseLeave() {
this.hoverIndex = null;
this.draw();
}
resize() {
const rect = this.canvas.getBoundingClientRect();
const cssWidth = Math.max(280, Math.round(rect.width || this.canvas.width || 640));
const cssHeight = Math.max(180, Math.round(rect.height || this.canvas.height || 320));
const pixelRatio = globalScope.devicePixelRatio || 1;
this.width = cssWidth;
this.height = cssHeight;
this.canvas.width = Math.round(cssWidth * pixelRatio);
this.canvas.height = Math.round(cssHeight * pixelRatio);
this.ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
this.draw();
}
setData(series, xLabels = []) {
this.series = (Array.isArray(series) ? series : []).map((item) => ({
key: item.key || '',
label: item.label || 'Series',
color: item.color || '#60a5fa',
axis: item.axis === 'right' ? 'right' : 'left',
unit: item.unit || '',
format: item.format || '',
maxHint: Number(item.maxHint || 0),
decimals: Number.isInteger(item.decimals) ? item.decimals : 2,
values: Array.isArray(item.values) ? item.values.map((value) => Number(value || 0)) : [],
}));
this.xLabels = Array.isArray(xLabels) ? xLabels : [];
this.draw();
}
getPlotRect() {
const padding = this.options.padding;
return {
x: padding.left,
y: padding.top,
w: this.width - padding.left - padding.right,
h: this.height - padding.top - padding.bottom,
};
}
getMaxForAxis(axis) {
const relevant = this.series.filter((item) => item.axis === axis);
if (!relevant.length) return 1;
let max = 1;
relevant.forEach((item) => {
const localMax = Math.max(...(item.values.length ? item.values : [0]), item.maxHint || 0, 1);
if (localMax > max) max = localMax;
});
return max;
}
xForIndex(index, pointCount, plot) {
if (pointCount <= 1) return plot.x;
return plot.x + (index / (pointCount - 1)) * plot.w;
}
yForValue(value, axisMax, plot) {
const clamped = Math.max(0, Number(value || 0));
return plot.y + plot.h - ((clamped / axisMax) * plot.h);
}
formatValue(value, format, unit, decimals) {
const number = Number(value || 0);
if (globalScope.UFormat) {
if (format === 'bytes' && globalScope.UFormat.formatBytes) return globalScope.UFormat.formatBytes(number);
if (format === 'disk-bytes' && globalScope.UFormat.formatDiskBytes) return globalScope.UFormat.formatDiskBytes(number);
if (format === 'count' && globalScope.UFormat.formatCount) return globalScope.UFormat.formatCount(number);
if (format === 'duration-ms' && globalScope.UFormat.formatDurationMs) return globalScope.UFormat.formatDurationMs(number);
}
return `${number.toFixed(decimals)}${unit || ''}`;
}
drawGridAndAxes(plot, maxLeft, maxRight) {
const ctx = this.ctx;
ctx.strokeStyle = this.options.gridColor;
ctx.lineWidth = 1;
for (let index = 0; index <= 4; index += 1) {
const y = plot.y + (plot.h / 4) * index;
ctx.beginPath();
ctx.moveTo(plot.x, y);
ctx.lineTo(plot.x + plot.w, y);
ctx.stroke();
const leftValue = maxLeft - ((maxLeft / 4) * index);
const leftTick = this.formatValue(leftValue, this.options.yAxisLeftFormat, '', 1);
ctx.fillStyle = this.options.textColor;
ctx.font = '11px Inter, sans-serif';
const leftWidth = ctx.measureText(leftTick).width;
ctx.fillText(leftTick, Math.max(4, plot.x - leftWidth - 8), y + 4);
if (this.options.yAxisRightLabel) {
const rightValue = maxRight - ((maxRight / 4) * index);
const rightTick = this.formatValue(rightValue, this.options.yAxisRightFormat, '', 1);
ctx.fillText(rightTick, plot.x + plot.w + 8, y + 4);
}
}
ctx.strokeStyle = this.options.axisColor;
ctx.beginPath();
ctx.moveTo(plot.x, plot.y);
ctx.lineTo(plot.x, plot.y + plot.h);
ctx.lineTo(plot.x + plot.w, plot.y + plot.h);
ctx.stroke();
ctx.fillStyle = this.options.textColor;
ctx.font = '12px Inter, sans-serif';
ctx.save();
ctx.translate(14, plot.y + (plot.h / 2));
ctx.rotate(-Math.PI / 2);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(this.options.yAxisLeftLabel, 0, 0);
ctx.restore();
if (this.options.yAxisRightLabel) {
ctx.save();
ctx.translate(this.width - 14, plot.y + (plot.h / 2));
ctx.rotate(Math.PI / 2);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(this.options.yAxisRightLabel, 0, 0);
ctx.restore();
}
const xLabel = this.options.xAxisLabel;
const xLabelWidth = ctx.measureText(xLabel).width;
ctx.fillText(xLabel, plot.x + plot.w - xLabelWidth, this.height - 10);
}
drawSeries(plot, maxLeft, maxRight) {
const ctx = this.ctx;
const pointCount = Math.max(...this.series.map((item) => item.values.length), 0);
if (pointCount <= 0) return;
this.series.forEach((item) => {
if (!item.values.length) return;
const axisMax = item.axis === 'right' ? maxRight : maxLeft;
ctx.beginPath();
item.values.forEach((value, index) => {
const x = this.xForIndex(index, item.values.length, plot);
const y = this.yForValue(value, axisMax, plot);
if (index === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
});
ctx.strokeStyle = item.color;
ctx.lineWidth = 2;
ctx.stroke();
});
if (this.series.length > 1) {
ctx.fillStyle = this.options.legendBackground;
ctx.fillRect(plot.x + 4, plot.y + 4, Math.min(plot.w - 8, 320), 24);
let offsetX = plot.x + 12;
this.series.forEach((item) => {
ctx.fillStyle = item.color;
ctx.fillRect(offsetX, plot.y + 13, 14, 3);
offsetX += 20;
ctx.fillStyle = this.options.legendTextColor;
ctx.font = '11px Inter, sans-serif';
ctx.fillText(item.label, offsetX, plot.y + 17);
offsetX += ctx.measureText(item.label).width + 18;
});
}
}
drawHover(plot, maxLeft, maxRight) {
if (this.hoverIndex == null) return;
const ctx = this.ctx;
const pointCount = Math.max(...this.series.map((item) => item.values.length), 0);
if (pointCount <= 0) return;
const index = Math.max(0, Math.min(pointCount - 1, this.hoverIndex));
const x = this.xForIndex(index, pointCount, plot);
ctx.strokeStyle = this.options.hoverLineColor;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x, plot.y);
ctx.lineTo(x, plot.y + plot.h);
ctx.stroke();
const lines = [];
lines.push(this.xLabels[index] || `Sample ${index + 1}`);
this.series.forEach((item) => {
if (index >= item.values.length) return;
const axisMax = item.axis === 'right' ? maxRight : maxLeft;
const value = item.values[index] ?? 0;
const y = this.yForValue(value, axisMax, plot);
ctx.fillStyle = item.color;
ctx.beginPath();
ctx.arc(x, y, 3.5, 0, Math.PI * 2);
ctx.fill();
lines.push(`${item.label}: ${this.formatValue(value, item.format || (item.axis === 'right' ? this.options.yAxisRightFormat : this.options.yAxisLeftFormat), item.unit || '', item.decimals)}`);
});
ctx.font = '11px Inter, sans-serif';
const padding = 8;
const lineHeight = 14;
const tooltipWidth = Math.max(...lines.map((line) => ctx.measureText(line).width)) + padding * 2;
const tooltipHeight = lines.length * lineHeight + padding * 2 - 2;
let tooltipX = this.hoverX + 12;
let tooltipY = plot.y + 8;
if (tooltipX + tooltipWidth > this.width - 4) tooltipX = this.hoverX - tooltipWidth - 12;
if (tooltipX < 4) tooltipX = 4;
if (tooltipY + tooltipHeight > this.height - 4) tooltipY = this.height - tooltipHeight - 4;
ctx.fillStyle = this.options.tooltipBackground;
ctx.fillRect(tooltipX, tooltipY, tooltipWidth, tooltipHeight);
ctx.strokeStyle = this.options.tooltipBorder;
ctx.strokeRect(tooltipX, tooltipY, tooltipWidth, tooltipHeight);
lines.forEach((line, lineIndex) => {
ctx.fillStyle = lineIndex === 0 ? this.options.tooltipTitleColor : this.options.tooltipTextColor;
ctx.fillText(line, tooltipX + padding, tooltipY + padding + 10 + lineIndex * lineHeight);
});
}
onMouseMove(event) {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
this.hoverX = x;
const plot = this.getPlotRect();
const pointCount = Math.max(...this.series.map((item) => item.values.length), 0);
if (pointCount <= 0) {
this.hoverIndex = null;
this.draw();
return;
}
const relative = Math.max(0, Math.min(plot.w, x - plot.x));
this.hoverIndex = pointCount <= 1 ? 0 : Math.round((relative / plot.w) * (pointCount - 1));
this.draw();
}
draw() {
const ctx = this.ctx;
ctx.clearRect(0, 0, this.width, this.height);
const plot = this.getPlotRect();
if (plot.w <= 0 || plot.h <= 0) return;
const maxLeft = this.getMaxForAxis('left');
const maxRight = this.getMaxForAxis('right');
this.drawGridAndAxes(plot, maxLeft, maxRight);
this.drawSeries(plot, maxLeft, maxRight);
this.drawHover(plot, maxLeft, maxRight);
}
}
return TimeSeriesChart;
}));
@@ -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,
};
}));
+111
View File
@@ -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;
}));