mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 22:35:08 +00:00
refactor(web): delete dead routes, JS files and duplicate definitions (#609)
* refactor(web): drop validators nothing calls escape_html, validate_image_url, validate_font_awesome_class, validate_mime_type, validate_numeric_range, validate_string_length and sanitize_plugin_config had no callers outside their own tests. Only validate_file_upload (fonts upload) is imported by the web interface. dedup_unique_arrays is kept: its one caller in save_plugin_config was removed by the unrelated sync PR (#330), which looks accidental. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(api): remove the music-auth and of-the-day JSON routes POST /plugins/authenticate/spotify and /plugins/authenticate/ytm had no caller but their tests: the music plugin authenticates through its web_ui_actions (authenticate_spotify.py / authenticate_ytm.py) via /plugins/action. POST /plugins/of-the-day/json/upload and /json/delete looked the plugin up by the id ledmatrix-of-the-day (its manifest id is of-the-day), were reachable only from a file_type "json" upload field that no schema declares, and put the plugin directory on sys.path per request to import scripts.update_config. of-the-day manages its files through plugin-file-manager and its own web_ui_actions. The of-the-day branch of GET /plugins/config stays: it matches the real manifest id and still merges the on-disk category files into the form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(api): read managers only from the blueprints api_v3/__init__.py and pages_v3.py declared module globals (plugin_store_manager, saved_repositories_manager, schema_manager, operation_queue, plugin_state_manager, operation_history, sync_manager, config_manager, plugin_manager) that nothing assigns: app.py sets the managers as attributes on the Blueprint objects, and every route reads them there. The one reader, backup restore's fallback to the module plugin_store_manager, could only ever fall back to None. _ensure_cache_manager() built a second CacheManager in the web process instead of using the one app.py puts on api_v3. The display routes now read api_v3.cache_manager, creating it on the blueprint only when nothing set it (the same None handling as the /cache routes). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(web): drop run.sh and the unused log_config_change web_interface/run.sh was referenced only by web_interface/README.md; the service starts the UI through scripts/utils/start_web_conditionally.py and the README already documents `python3 web_interface/start.py`. log_config_change() in web_interface/logging_config.py was never called. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(web): delete unreferenced store_manager.js, diff_viewer.js, htmx-sse.js - js/plugins/store_manager.js (window.PluginStoreManager) and js/config/diff_viewer.js (window.ConfigDiffViewer) were loaded on every page but nothing reads either global. - htmx-sse.js (plus its CDN fallback) was loaded after HTMX, but no template or plugin page uses sse-connect / hx-ext="sse": the live streams run through LEDStreams in app-shell.js. js/plugins/state_manager.js stays: install_manager.js's updateAll() reads and refreshes window.PluginStateManager. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(web): remove app.js helpers nothing calls - hexToRgb, rgbToHex, validateForm, uploadFont and switchTab (whose 'switch-tab' event had no listener) have no caller in the templates, static JS or the plugin monorepo. - installPlugin: plugins_manager.js (loaded last) assigns window.installPlugin, and its own store cards are the only callers. - The showNotification fallback could never install: app-shell.js is deferred ahead of app.js and defines the same fallback at top level. - performanceMonitor only logged with ?debug=perf and read an unset this.measures; the marks it took on every load had no reader. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(web): drop app-shell.js refreshPlugin A top-level function in app-shell.js, so a window global, but nothing calls it (no inline handler, no window lookup, no string-built name). The other plugin actions in that block stay. updatePlugin is the live window.updatePlugin: plugins_manager.js only installs its own copy when none exists. uninstallPlugin/pollUninstallOperation, updateAllPlugins, executePluginAction and toggleNestedSection are replaced by later deferred scripts, but a click that lands while those scripts are still downloading reaches the app-shell copies, so removing them is not a pure no-op. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(web): remove definitions plugins_manager.js always overrides All of these are replaced before anything can call them, checked against the load order in base.html and the live window.* values: - openOnDemandModal/requestOnDemandStop stubs: the IIFE later in the same script assigns the real functions synchronously. - updatePlugin and uninstallPlugin stubs (`window.X || stub`): app-shell.js already defined both, so the fallback never installed. Same for the later updatePlugin override, gated on the live function containing '[UPDATE]', which app-shell.js's never does. - The first addArrayObjectItem/removeArrayObjectItem: reassigned by the top-level copies after the IIFE. - The first `function formatDate` in the IIFE: a later declaration of the same name in the same scope wins. - deleteUploadedImage, getCurrentImages, showUploadProgress, formatFileSize and getScheduleSummary: character-for-character copies of js/widgets/file-upload.js, which stays the owner. - `typeof X === 'undefined'` fallbacks and `typeof X !== 'undefined'` re-exports after the IIFE: always false, or a self-assignment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(web): render the shell directly and delete index.html index.html extended base.html with {% block content %}, but base.html defines no blocks, so none of index.html ever rendered: rendering both with jinja2 gives byte-identical output. index() still loaded the config, read config.json and config_secrets.json raw and json.dumps'd them on every page load for variables base.html never reads, and flashed errors that base.html never shows. It now renders base.html with no context. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): stop htmx-config.js replacing console.error and console.warn It swapped both globals for filters that dropped any error mentioning insertBefore / "Cannot read properties of null" when "htmx" appeared in the message or stack, and a list of Permissions-Policy warnings. That hid real errors from every script on the page, and made every logged error and warning report htmx-config.js as its source. The beforeSwap target validation above it, which prevents the insertBefore errors in the first place, stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(web): quiet the widget load announcements and debug logs About 30 lines hit the console on every page load: one "... widget registered" per widget file, one "[WidgetRegistry] Registered widget: X" per registration, plus the registry, base widget and plugin loader announcing themselves. The load-time announcements are removed; the per-call ones (registry register, plugin widget loads, "Render called") now go through the page's debugLog switch (localStorage.pluginDebug), guarded because the widgets also load in node tests without it. fonts.html and wifi.html debug logging goes through debugLog as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(api): drop the removed music-auth and of-the-day JSON routes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2572,23 +2572,6 @@
|
||||
// Assign to window for global access
|
||||
window.uninstallPlugin = uninstallPlugin;
|
||||
|
||||
async function refreshPlugin(pluginId) {
|
||||
try {
|
||||
// Switch to the plugin manager tab briefly to refresh
|
||||
const originalTab = app.activeTab;
|
||||
app.activeTab = 'plugins';
|
||||
|
||||
// Wait a moment then switch back
|
||||
setTimeout(() => {
|
||||
app.activeTab = originalTab;
|
||||
app.showNotification(`Refreshed ${pluginId}`, 'success');
|
||||
}, 100);
|
||||
|
||||
} catch (error) {
|
||||
app.showNotification('Error refreshing plugin: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Format commit information for display
|
||||
function formatCommitInfo(commit, branch) {
|
||||
if (!commit && !branch) return 'Unknown';
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
/**
|
||||
* Configuration diff viewer.
|
||||
*
|
||||
* Shows what changed in configuration before saving.
|
||||
*/
|
||||
|
||||
const ConfigDiffViewer = {
|
||||
/**
|
||||
* Original configuration state (before changes).
|
||||
*/
|
||||
originalConfigs: new Map(),
|
||||
|
||||
/**
|
||||
* Store original configuration for a plugin.
|
||||
*
|
||||
* @param {string} pluginId - Plugin identifier
|
||||
* @param {Object} config - Original configuration
|
||||
*/
|
||||
storeOriginal(pluginId, config) {
|
||||
this.originalConfigs.set(pluginId, JSON.parse(JSON.stringify(config)));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get original configuration for a plugin.
|
||||
*
|
||||
* @param {string} pluginId - Plugin identifier
|
||||
* @returns {Object|null} Original configuration
|
||||
*/
|
||||
getOriginal(pluginId) {
|
||||
return this.originalConfigs.get(pluginId) || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear stored original configuration.
|
||||
*
|
||||
* @param {string} pluginId - Plugin identifier
|
||||
*/
|
||||
clearOriginal(pluginId) {
|
||||
this.originalConfigs.delete(pluginId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Compare two configuration objects and return differences.
|
||||
*
|
||||
* @param {Object} oldConfig - Old configuration
|
||||
* @param {Object} newConfig - New configuration
|
||||
* @returns {Object} Differences object with added, removed, and changed keys
|
||||
*/
|
||||
compare(oldConfig, newConfig) {
|
||||
const differences = {
|
||||
added: {},
|
||||
removed: {},
|
||||
changed: {},
|
||||
unchanged: {}
|
||||
};
|
||||
|
||||
// Get all keys from both configs
|
||||
const allKeys = new Set([
|
||||
...Object.keys(oldConfig || {}),
|
||||
...Object.keys(newConfig || {})
|
||||
]);
|
||||
|
||||
for (const key of allKeys) {
|
||||
const oldValue = oldConfig?.[key];
|
||||
const newValue = newConfig?.[key];
|
||||
|
||||
if (!(key in (oldConfig || {}))) {
|
||||
// Key was added
|
||||
differences.added[key] = newValue;
|
||||
} else if (!(key in (newConfig || {}))) {
|
||||
// Key was removed
|
||||
differences.removed[key] = oldValue;
|
||||
} else if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
|
||||
// Key was changed
|
||||
differences.changed[key] = {
|
||||
old: oldValue,
|
||||
new: newValue
|
||||
};
|
||||
} else {
|
||||
// Key unchanged
|
||||
differences.unchanged[key] = oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
return differences;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if there are any differences.
|
||||
*
|
||||
* @param {Object} differences - Differences object from compare()
|
||||
* @returns {boolean} True if there are changes
|
||||
*/
|
||||
hasChanges(differences) {
|
||||
return Object.keys(differences.added).length > 0 ||
|
||||
Object.keys(differences.removed).length > 0 ||
|
||||
Object.keys(differences.changed).length > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Format differences for display.
|
||||
*
|
||||
* @param {Object} differences - Differences object
|
||||
* @returns {string} HTML formatted diff
|
||||
*/
|
||||
formatDiff(differences) {
|
||||
const parts = [];
|
||||
|
||||
// Added keys
|
||||
if (Object.keys(differences.added).length > 0) {
|
||||
parts.push(`
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-semibold text-green-800 mb-2">
|
||||
<i class="fas fa-plus-circle mr-1"></i>Added
|
||||
</h4>
|
||||
<div class="bg-green-50 border border-green-200 rounded p-3">
|
||||
${Object.entries(differences.added).map(([key, value]) => `
|
||||
<div class="mb-2">
|
||||
<code class="text-xs font-mono bg-green-100 px-1 py-0.5 rounded">${this.escapeHtml(key)}</code>
|
||||
<span class="text-sm text-gray-700 ml-2">=</span>
|
||||
<pre class="mt-1 text-xs bg-white p-2 rounded border border-green-200 overflow-auto">${this.escapeHtml(JSON.stringify(value, null, 2))}</pre>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Removed keys
|
||||
if (Object.keys(differences.removed).length > 0) {
|
||||
parts.push(`
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-semibold text-red-800 mb-2">
|
||||
<i class="fas fa-minus-circle mr-1"></i>Removed
|
||||
</h4>
|
||||
<div class="bg-red-50 border border-red-200 rounded p-3">
|
||||
${Object.entries(differences.removed).map(([key, value]) => `
|
||||
<div class="mb-2">
|
||||
<code class="text-xs font-mono bg-red-100 px-1 py-0.5 rounded">${this.escapeHtml(key)}</code>
|
||||
<span class="text-sm text-gray-700 ml-2">=</span>
|
||||
<pre class="mt-1 text-xs bg-white p-2 rounded border border-red-200 overflow-auto line-through text-gray-500">${this.escapeHtml(JSON.stringify(value, null, 2))}</pre>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Changed keys
|
||||
if (Object.keys(differences.changed).length > 0) {
|
||||
parts.push(`
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-semibold text-yellow-800 mb-2">
|
||||
<i class="fas fa-edit mr-1"></i>Changed
|
||||
</h4>
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded p-3">
|
||||
${Object.entries(differences.changed).map(([key, change]) => `
|
||||
<div class="mb-3">
|
||||
<code class="text-xs font-mono bg-yellow-100 px-1 py-0.5 rounded">${this.escapeHtml(key)}</code>
|
||||
<div class="mt-2 grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<div class="text-xs font-medium text-red-700 mb-1">Old Value:</div>
|
||||
<pre class="text-xs bg-white p-2 rounded border border-red-200 overflow-auto">${this.escapeHtml(JSON.stringify(change.old, null, 2))}</pre>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs font-medium text-green-700 mb-1">New Value:</div>
|
||||
<pre class="text-xs bg-white p-2 rounded border border-green-200 overflow-auto">${this.escapeHtml(JSON.stringify(change.new, null, 2))}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
if (parts.length === 0) {
|
||||
return '<div class="text-sm text-gray-600 text-center py-4">No changes detected</div>';
|
||||
}
|
||||
|
||||
return parts.join('');
|
||||
},
|
||||
|
||||
/**
|
||||
* Show diff modal before saving.
|
||||
*
|
||||
* @param {string} pluginId - Plugin identifier
|
||||
* @param {Object} newConfig - New configuration
|
||||
* @returns {Promise<boolean>} Promise resolving to true if user confirms, false if cancelled
|
||||
*/
|
||||
async showDiffModal(pluginId, newConfig) {
|
||||
return new Promise((resolve) => {
|
||||
const original = this.getOriginal(pluginId);
|
||||
if (!original) {
|
||||
// No original to compare, proceed without diff
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const differences = this.compare(original, newConfig);
|
||||
|
||||
if (!this.hasChanges(differences)) {
|
||||
// No changes, proceed without showing modal
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create modal
|
||||
const modalContainer = document.createElement('div');
|
||||
modalContainer.id = 'config-diff-modal-container';
|
||||
modalContainer.className = 'fixed inset-0 z-50 overflow-y-auto';
|
||||
modalContainer.innerHTML = `
|
||||
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" data-diff-backdrop aria-hidden="true"></div>
|
||||
|
||||
<div id="config-diff-modal-panel" class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full" style="position:relative;">
|
||||
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10" aria-hidden="true">
|
||||
<i class="fas fa-code-branch text-blue-600"></i>
|
||||
</div>
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left flex-1">
|
||||
<h3 id="config-diff-modal-title" class="text-lg leading-6 font-medium text-gray-900">Review Configuration Changes</h3>
|
||||
<div class="mt-4 max-h-96 overflow-y-auto" tabindex="0" role="region" aria-label="Changes to save">
|
||||
${this.formatDiff(differences)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<button type="button" id="config-diff-confirm-btn"
|
||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
<i class="fas fa-check mr-2" aria-hidden="true"></i>Save Changes
|
||||
</button>
|
||||
<button type="button" id="config-diff-cancel-btn"
|
||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modalContainer);
|
||||
|
||||
let release = null;
|
||||
let settled = false;
|
||||
// Single close path: remove the modal, release the focus trap
|
||||
// (returning focus to where the user was) and settle the promise.
|
||||
function finish(result) {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
modalContainer.remove();
|
||||
if (window.__configDiffResolve === finish) window.__configDiffResolve = undefined;
|
||||
if (release) release();
|
||||
resolve(result);
|
||||
}
|
||||
|
||||
// Kept for backwards compatibility with code that calls it directly.
|
||||
window.__configDiffResolve = finish;
|
||||
|
||||
// Attach event listeners
|
||||
const confirmBtn = modalContainer.querySelector('#config-diff-confirm-btn');
|
||||
const cancelBtn = modalContainer.querySelector('#config-diff-cancel-btn');
|
||||
const backdrop = modalContainer.querySelector('[data-diff-backdrop]');
|
||||
|
||||
confirmBtn.addEventListener('click', () => finish(true));
|
||||
cancelBtn.addEventListener('click', () => finish(false));
|
||||
if (backdrop) backdrop.addEventListener('click', () => finish(false));
|
||||
|
||||
if (window.LEDDialog) {
|
||||
release = window.LEDDialog.trap(modalContainer.querySelector('#config-diff-modal-panel'), {
|
||||
labelledBy: 'config-diff-modal-title',
|
||||
initialFocus: confirmBtn,
|
||||
onEscape: () => finish(false)
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS.
|
||||
*/
|
||||
escapeHtml(text) {
|
||||
if (typeof text !== 'string') {
|
||||
text = String(text);
|
||||
}
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
};
|
||||
|
||||
// Export
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = ConfigDiffViewer;
|
||||
} else {
|
||||
window.ConfigDiffViewer = ConfigDiffViewer;
|
||||
}
|
||||
|
||||
@@ -53,67 +53,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Suppress HTMX insertBefore errors and other noisy errors - they're harmless but noisy
|
||||
const originalError = console.error;
|
||||
const originalWarn = console.warn;
|
||||
|
||||
console.error = function(...args) {
|
||||
const errorStr = args.join(' ');
|
||||
const errorStack = args.find(arg => arg && typeof arg === 'string' && arg.includes('htmx')) || '';
|
||||
|
||||
// Suppress HTMX insertBefore errors (comprehensive check)
|
||||
// These occur when HTMX tries to swap content but the target element is null
|
||||
// Usually happens due to timing/race conditions and is harmless
|
||||
if (errorStr.includes("insertBefore") ||
|
||||
errorStr.includes("Cannot read properties of null") ||
|
||||
errorStr.includes("reading 'insertBefore'")) {
|
||||
// Check if it's from HTMX by looking at stack trace or error string
|
||||
// Also check the call stack if available
|
||||
const isHtmxError = errorStr.includes('htmx') ||
|
||||
errorStack.includes('htmx') ||
|
||||
args.some(arg => {
|
||||
if (typeof arg === 'string') {
|
||||
return arg.includes('htmx');
|
||||
}
|
||||
// Check error objects for stack traces
|
||||
if (arg && typeof arg === 'object' && arg.stack) {
|
||||
return arg.stack.includes('htmx');
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (isHtmxError) {
|
||||
return; // Suppress - this is a harmless HTMX timing/race condition issue
|
||||
}
|
||||
}
|
||||
|
||||
// Suppress script execution errors from malformed HTML
|
||||
if (errorStr.includes("Failed to execute 'appendChild' on 'Node'") ||
|
||||
errorStr.includes("Failed to execute 'insertBefore' on 'Node'")) {
|
||||
if (errorStr.includes('Unexpected token')) {
|
||||
return; // Suppress malformed HTML errors
|
||||
}
|
||||
}
|
||||
originalError.apply(console, args);
|
||||
};
|
||||
|
||||
console.warn = function(...args) {
|
||||
const warnStr = args.join(' ');
|
||||
// Suppress Permissions-Policy warnings (harmless browser warnings)
|
||||
if (warnStr.includes('Permissions-Policy header') ||
|
||||
warnStr.includes('Unrecognized feature') ||
|
||||
warnStr.includes('Origin trial controlled feature') ||
|
||||
warnStr.includes('browsing-topics') ||
|
||||
warnStr.includes('run-ad-auction') ||
|
||||
warnStr.includes('join-ad-interest-group') ||
|
||||
warnStr.includes('private-state-token') ||
|
||||
warnStr.includes('private-aggregation') ||
|
||||
warnStr.includes('attribution-reporting')) {
|
||||
return; // Suppress - these are harmless browser feature warnings
|
||||
}
|
||||
originalWarn.apply(console, args);
|
||||
};
|
||||
|
||||
// Handle HTMX errors gracefully with detailed logging
|
||||
document.body.addEventListener('htmx:responseError', function(event) {
|
||||
const detail = event.detail;
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
/* global htmx */
|
||||
/*
|
||||
Server Sent Events Extension
|
||||
============================
|
||||
This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions.
|
||||
|
||||
*/
|
||||
|
||||
(function() {
|
||||
|
||||
/** @type {import("../htmx").HtmxInternalApi} */
|
||||
var api;
|
||||
|
||||
htmx.defineExtension("sse", {
|
||||
|
||||
/**
|
||||
* Init saves the provided reference to the internal HTMX API.
|
||||
*
|
||||
* @param {import("../htmx").HtmxInternalApi} api
|
||||
* @returns void
|
||||
*/
|
||||
init: function(apiRef) {
|
||||
// store a reference to the internal API.
|
||||
api = apiRef;
|
||||
|
||||
// set a function in the public API for creating new EventSource objects
|
||||
if (htmx.createEventSource == undefined) {
|
||||
htmx.createEventSource = createEventSource;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* onEvent handles all events passed to this extension.
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {Event} evt
|
||||
* @returns void
|
||||
*/
|
||||
onEvent: function(name, evt) {
|
||||
|
||||
switch (name) {
|
||||
|
||||
case "htmx:beforeCleanupElement":
|
||||
var internalData = api.getInternalData(evt.target)
|
||||
// Try to remove remove an EventSource when elements are removed
|
||||
if (internalData.sseEventSource) {
|
||||
internalData.sseEventSource.close();
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
// Try to create EventSources when elements are processed
|
||||
case "htmx:afterProcessNode":
|
||||
ensureEventSourceOnElement(evt.target);
|
||||
registerSSE(evt.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// HELPER FUNCTIONS
|
||||
///////////////////////////////////////////////
|
||||
|
||||
|
||||
/**
|
||||
* createEventSource is the default method for creating new EventSource objects.
|
||||
* it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
|
||||
*
|
||||
* @param {string} url
|
||||
* @returns EventSource
|
||||
*/
|
||||
function createEventSource(url) {
|
||||
return new EventSource(url, { withCredentials: true });
|
||||
}
|
||||
|
||||
function splitOnWhitespace(trigger) {
|
||||
return trigger.trim().split(/\s+/);
|
||||
}
|
||||
|
||||
function getLegacySSEURL(elt) {
|
||||
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
|
||||
if (legacySSEValue) {
|
||||
var values = splitOnWhitespace(legacySSEValue);
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var value = values[i].split(/:(.+)/);
|
||||
if (value[0] === "connect") {
|
||||
return value[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLegacySSESwaps(elt) {
|
||||
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
|
||||
var returnArr = [];
|
||||
if (legacySSEValue != null) {
|
||||
var values = splitOnWhitespace(legacySSEValue);
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var value = values[i].split(/:(.+)/);
|
||||
if (value[0] === "swap") {
|
||||
returnArr.push(value[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return returnArr;
|
||||
}
|
||||
|
||||
/**
|
||||
* registerSSE looks for attributes that can contain sse events, right
|
||||
* now hx-trigger and sse-swap and adds listeners based on these attributes too
|
||||
* the closest event source
|
||||
*
|
||||
* @param {HTMLElement} elt
|
||||
*/
|
||||
function registerSSE(elt) {
|
||||
// Find closest existing event source
|
||||
var sourceElement = api.getClosestMatch(elt, hasEventSource);
|
||||
if (sourceElement == null) {
|
||||
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
|
||||
return null; // no eventsource in parentage, orphaned element
|
||||
}
|
||||
|
||||
// Set internalData and source
|
||||
var internalData = api.getInternalData(sourceElement);
|
||||
var source = internalData.sseEventSource;
|
||||
|
||||
// Add message handlers for every `sse-swap` attribute
|
||||
queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) {
|
||||
|
||||
var sseSwapAttr = api.getAttributeValue(child, "sse-swap");
|
||||
if (sseSwapAttr) {
|
||||
var sseEventNames = sseSwapAttr.split(",");
|
||||
} else {
|
||||
var sseEventNames = getLegacySSESwaps(child);
|
||||
}
|
||||
|
||||
for (var i = 0; i < sseEventNames.length; i++) {
|
||||
var sseEventName = sseEventNames[i].trim();
|
||||
var listener = function(event) {
|
||||
|
||||
// If the source is missing then close SSE
|
||||
if (maybeCloseSSESource(sourceElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the body no longer contains the element, remove the listener
|
||||
if (!api.bodyContains(child)) {
|
||||
source.removeEventListener(sseEventName, listener);
|
||||
}
|
||||
|
||||
// swap the response into the DOM and trigger a notification
|
||||
swap(child, event.data);
|
||||
api.triggerEvent(elt, "htmx:sseMessage", event);
|
||||
};
|
||||
|
||||
// Register the new listener
|
||||
api.getInternalData(child).sseEventListener = listener;
|
||||
source.addEventListener(sseEventName, listener);
|
||||
}
|
||||
});
|
||||
|
||||
// Add message handlers for every `hx-trigger="sse:*"` attribute
|
||||
queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) {
|
||||
|
||||
var sseEventName = api.getAttributeValue(child, "hx-trigger");
|
||||
if (sseEventName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only process hx-triggers for events with the "sse:" prefix
|
||||
if (sseEventName.slice(0, 4) != "sse:") {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the sse: prefix from here on out
|
||||
sseEventName = sseEventName.substr(4);
|
||||
|
||||
var listener = function() {
|
||||
if (maybeCloseSSESource(sourceElement)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!api.bodyContains(child)) {
|
||||
source.removeEventListener(sseEventName, listener);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ensureEventSourceOnElement creates a new EventSource connection on the provided element.
|
||||
* If a usable EventSource already exists, then it is returned. If not, then a new EventSource
|
||||
* is created and stored in the element's internalData.
|
||||
* @param {HTMLElement} elt
|
||||
* @param {number} retryCount
|
||||
* @returns {EventSource | null}
|
||||
*/
|
||||
function ensureEventSourceOnElement(elt, retryCount) {
|
||||
|
||||
if (elt == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// handle extension source creation attribute
|
||||
queryAttributeOnThisOrChildren(elt, "sse-connect").forEach(function(child) {
|
||||
var sseURL = api.getAttributeValue(child, "sse-connect");
|
||||
if (sseURL == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureEventSource(child, sseURL, retryCount);
|
||||
});
|
||||
|
||||
// handle legacy sse, remove for HTMX2
|
||||
queryAttributeOnThisOrChildren(elt, "hx-sse").forEach(function(child) {
|
||||
var sseURL = getLegacySSEURL(child);
|
||||
if (sseURL == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureEventSource(child, sseURL, retryCount);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ensureEventSource(elt, url, retryCount) {
|
||||
var source = htmx.createEventSource(url);
|
||||
|
||||
source.onerror = function(err) {
|
||||
|
||||
// Log an error event
|
||||
api.triggerErrorEvent(elt, "htmx:sseError", { error: err, source: source });
|
||||
|
||||
// If parent no longer exists in the document, then clean up this EventSource
|
||||
if (maybeCloseSSESource(elt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, try to reconnect the EventSource
|
||||
if (source.readyState === EventSource.CLOSED) {
|
||||
retryCount = retryCount || 0;
|
||||
var timeout = Math.random() * (2 ^ retryCount) * 500;
|
||||
window.setTimeout(function() {
|
||||
ensureEventSourceOnElement(elt, Math.min(7, retryCount + 1));
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
|
||||
source.onopen = function(evt) {
|
||||
api.triggerEvent(elt, "htmx:sseOpen", { source: source });
|
||||
}
|
||||
|
||||
api.getInternalData(elt).sseEventSource = source;
|
||||
}
|
||||
|
||||
/**
|
||||
* maybeCloseSSESource confirms that the parent element still exists.
|
||||
* If not, then any associated SSE source is closed and the function returns true.
|
||||
*
|
||||
* @param {HTMLElement} elt
|
||||
* @returns boolean
|
||||
*/
|
||||
function maybeCloseSSESource(elt) {
|
||||
if (!api.bodyContains(elt)) {
|
||||
var source = api.getInternalData(elt).sseEventSource;
|
||||
if (source != undefined) {
|
||||
source.close();
|
||||
// source = null
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
|
||||
*
|
||||
* @param {HTMLElement} elt
|
||||
* @param {string} attributeName
|
||||
*/
|
||||
function queryAttributeOnThisOrChildren(elt, attributeName) {
|
||||
|
||||
var result = [];
|
||||
|
||||
// If the parent element also contains the requested attribute, then add it to the results too.
|
||||
if (api.hasAttribute(elt, attributeName)) {
|
||||
result.push(elt);
|
||||
}
|
||||
|
||||
// Search all child nodes that match the requested attribute
|
||||
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "]").forEach(function(node) {
|
||||
result.push(node);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} elt
|
||||
* @param {string} content
|
||||
*/
|
||||
function swap(elt, content) {
|
||||
|
||||
api.withExtensions(elt, function(extension) {
|
||||
content = extension.transformResponse(content, null, elt);
|
||||
});
|
||||
|
||||
var swapSpec = api.getSwapSpecification(elt);
|
||||
var target = api.getTarget(elt);
|
||||
var settleInfo = api.makeSettleInfo(elt);
|
||||
|
||||
api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
|
||||
|
||||
settleInfo.elts.forEach(function(elt) {
|
||||
if (elt.classList) {
|
||||
elt.classList.add(htmx.config.settlingClass);
|
||||
}
|
||||
api.triggerEvent(elt, 'htmx:beforeSettle');
|
||||
});
|
||||
|
||||
// Handle settle tasks (with delay if requested)
|
||||
if (swapSpec.settleDelay > 0) {
|
||||
setTimeout(doSettle(settleInfo), swapSpec.settleDelay);
|
||||
} else {
|
||||
doSettle(settleInfo)();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* doSettle mirrors much of the functionality in htmx that
|
||||
* settles elements after their content has been swapped.
|
||||
* TODO: this should be published by htmx, and not duplicated here
|
||||
* @param {import("../htmx").HtmxSettleInfo} settleInfo
|
||||
* @returns () => void
|
||||
*/
|
||||
function doSettle(settleInfo) {
|
||||
|
||||
return function() {
|
||||
settleInfo.tasks.forEach(function(task) {
|
||||
task.call();
|
||||
});
|
||||
|
||||
settleInfo.elts.forEach(function(elt) {
|
||||
if (elt.classList) {
|
||||
elt.classList.remove(htmx.config.settlingClass);
|
||||
}
|
||||
api.triggerEvent(elt, 'htmx:afterSettle');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function hasEventSource(node) {
|
||||
return api.getInternalData(node).sseEventSource != null;
|
||||
}
|
||||
|
||||
})();
|
||||
@@ -1,101 +0,0 @@
|
||||
/**
|
||||
* Plugin store management.
|
||||
*
|
||||
* Handles plugin store browsing, searching, and installation.
|
||||
*/
|
||||
|
||||
const PluginStoreManager = {
|
||||
/**
|
||||
* Cache for plugin store data.
|
||||
*/
|
||||
cache: null,
|
||||
cacheTimestamp: null,
|
||||
CACHE_DURATION: 5 * 60 * 1000, // 5 minutes
|
||||
|
||||
/**
|
||||
* Load plugin store.
|
||||
*
|
||||
* @param {boolean} useCache - Whether to use cached data
|
||||
* @returns {Promise<Array>} List of plugins
|
||||
*/
|
||||
async loadStore(useCache = true) {
|
||||
// Check cache
|
||||
if (useCache && this.cache && this.cacheTimestamp) {
|
||||
const age = Date.now() - this.cacheTimestamp;
|
||||
if (age < this.CACHE_DURATION) {
|
||||
return this.cache;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const plugins = await window.PluginAPI.getPluginStore();
|
||||
this.cache = plugins;
|
||||
this.cacheTimestamp = Date.now();
|
||||
return plugins;
|
||||
} catch (error) {
|
||||
if (window.errorHandler) {
|
||||
window.errorHandler.displayError(error, 'Failed to load plugin store');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Search plugin store.
|
||||
*
|
||||
* @param {string} query - Search query
|
||||
* @returns {Promise<Array>} Filtered list of plugins
|
||||
*/
|
||||
async searchStore(query) {
|
||||
const plugins = await this.loadStore();
|
||||
|
||||
if (!query || query.trim() === '') {
|
||||
return plugins;
|
||||
}
|
||||
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return plugins.filter(plugin => {
|
||||
const name = (plugin.name || '').toLowerCase();
|
||||
const description = (plugin.description || '').toLowerCase();
|
||||
const author = (plugin.author || '').toLowerCase();
|
||||
const category = (plugin.category || '').toLowerCase();
|
||||
|
||||
return name.includes(lowerQuery) ||
|
||||
description.includes(lowerQuery) ||
|
||||
author.includes(lowerQuery) ||
|
||||
category.includes(lowerQuery);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Install plugin from store.
|
||||
*
|
||||
* @param {string} pluginId - Plugin identifier
|
||||
* @param {string} branch - Optional branch name to install from
|
||||
* @returns {Promise<Object>} Installation result
|
||||
*/
|
||||
async installPlugin(pluginId, branch = null) {
|
||||
try {
|
||||
const result = await window.PluginAPI.installPlugin(pluginId, branch);
|
||||
|
||||
// Clear cache
|
||||
this.cache = null;
|
||||
this.cacheTimestamp = null;
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (window.errorHandler) {
|
||||
window.errorHandler.displayError(error, `Failed to install plugin ${pluginId}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Export
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = PluginStoreManager;
|
||||
} else {
|
||||
window.PluginStoreManager = PluginStoreManager;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
version: '2.0.0',
|
||||
|
||||
render: function(container, config, value, options) {
|
||||
console.log('[ArrayTableWidget] Render called (server-side rendered)');
|
||||
if (window.debugLog) window.debugLog('[ArrayTableWidget] Render called (server-side rendered)');
|
||||
},
|
||||
|
||||
getValue: function(fieldId) {
|
||||
@@ -918,6 +918,4 @@
|
||||
} else {
|
||||
initArrayTableButtons();
|
||||
}
|
||||
|
||||
console.log('[ArrayTableWidget] Array table widget registered (v2.0.0)');
|
||||
})();
|
||||
|
||||
@@ -201,6 +201,4 @@
|
||||
if (typeof window !== 'undefined') {
|
||||
window.BaseWidget = BaseWidget;
|
||||
}
|
||||
|
||||
console.log('[BaseWidget] Base widget class loaded');
|
||||
})();
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
render: function(container, config, value, options) {
|
||||
// For now, widgets are server-side rendered
|
||||
// This function is a placeholder for future client-side rendering
|
||||
console.log('[CheckboxGroupWidget] Render called (server-side rendered)');
|
||||
if (window.debugLog) window.debugLog('[CheckboxGroupWidget] Render called (server-side rendered)');
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -116,6 +116,4 @@
|
||||
});
|
||||
hiddenInput.dispatchEvent(event);
|
||||
};
|
||||
|
||||
console.log('[CheckboxGroupWidget] Checkbox group widget registered');
|
||||
})();
|
||||
|
||||
@@ -258,6 +258,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[ColorPickerWidget] Color picker widget registered');
|
||||
})();
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
render: function(container, config, value, options) {
|
||||
// For now, widgets are server-side rendered
|
||||
// This function is a placeholder for future client-side rendering
|
||||
console.log('[CustomFeedsWidget] Render called (server-side rendered)');
|
||||
if (window.debugLog) window.debugLog('[CustomFeedsWidget] Render called (server-side rendered)');
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -523,6 +523,4 @@
|
||||
event.target.value = '';
|
||||
});
|
||||
};
|
||||
|
||||
console.log('[CustomFeedsWidget] Custom feeds widget registered');
|
||||
})();
|
||||
|
||||
@@ -189,6 +189,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[DatePickerWidget] Date picker widget registered');
|
||||
})();
|
||||
|
||||
@@ -254,6 +254,4 @@
|
||||
// Expose DAYS constant for external use
|
||||
window.LEDMatrixWidgets.get('day-selector').DAYS = DAYS;
|
||||
window.LEDMatrixWidgets.get('day-selector').DAY_LABELS = DAY_LABELS;
|
||||
|
||||
console.log('[DaySelectorWidget] Day selector widget registered');
|
||||
})();
|
||||
|
||||
@@ -167,6 +167,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[EmailInputWidget] Email input widget registered');
|
||||
})();
|
||||
|
||||
@@ -197,6 +197,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[ColorPickerWidget] Color picker widget registered (example)');
|
||||
})();
|
||||
|
||||
@@ -286,6 +286,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[FileUploadSingleWidget] File upload single widget registered');
|
||||
})();
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
render: function(container, config, value, options) {
|
||||
// For now, widgets are server-side rendered
|
||||
// This function is a placeholder for future client-side rendering
|
||||
console.log('[FileUploadWidget] Render called (server-side rendered)');
|
||||
if (window.debugLog) window.debugLog('[FileUploadWidget] Render called (server-side rendered)');
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1136,6 +1136,4 @@
|
||||
window.updateImageList(fieldId, currentImages);
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[FileUploadWidget] File upload widget registered');
|
||||
})();
|
||||
|
||||
@@ -311,6 +311,4 @@
|
||||
generateDisplayName: generateDisplayName
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[FontSelectorWidget] Font selector widget registered');
|
||||
})();
|
||||
|
||||
@@ -192,6 +192,4 @@
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
console.log('[GoogleCalendarPickerWidget] registered');
|
||||
})();
|
||||
|
||||
@@ -828,8 +828,5 @@
|
||||
getValue() { return null; },
|
||||
setValue() {}
|
||||
});
|
||||
console.log('[JsonFileManager] Registered with LEDMatrixWidgets');
|
||||
} else {
|
||||
console.log('[JsonFileManager] Loaded (LEDMatrixWidgets registry not available)');
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -472,6 +472,4 @@
|
||||
} else {
|
||||
flushPending();
|
||||
}
|
||||
|
||||
console.log('[NotificationWidget] Notification widget registered');
|
||||
})();
|
||||
|
||||
@@ -239,6 +239,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[NumberInputWidget] Number input widget registered');
|
||||
})();
|
||||
|
||||
@@ -316,6 +316,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[PasswordInputWidget] Password input widget registered');
|
||||
})();
|
||||
|
||||
@@ -866,6 +866,4 @@
|
||||
getValue: function () { return null; }, // file ops are immediate; nothing to submit
|
||||
setValue: function (fieldId) { loadFiles(fieldId); }
|
||||
});
|
||||
|
||||
console.log('[PluginFileManager] plugin-file-manager widget registered');
|
||||
})();
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
// Check if widget is already registered
|
||||
if (this.has(widgetName)) {
|
||||
console.log(`[PluginWidgetLoader] Widget ${widgetName} already registered`);
|
||||
if (window.debugLog) window.debugLog(`[PluginWidgetLoader] Widget ${widgetName} already registered`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
try {
|
||||
// Dynamic import of plugin widget
|
||||
await import(widgetPath);
|
||||
console.log(`[PluginWidgetLoader] Loaded plugin widget: ${pluginId}/${widgetName} from ${widgetPath}`);
|
||||
if (window.debugLog) window.debugLog(`[PluginWidgetLoader] Loaded plugin widget: ${pluginId}/${widgetName} from ${widgetPath}`);
|
||||
|
||||
// Verify widget was registered
|
||||
if (this.has(widgetName)) {
|
||||
@@ -124,6 +124,4 @@
|
||||
|
||||
return loadedWidgets;
|
||||
};
|
||||
|
||||
console.log('[PluginWidgetLoader] Plugin widget loader initialized');
|
||||
})();
|
||||
|
||||
@@ -144,6 +144,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[RadioGroupWidget] Radio group widget registered');
|
||||
})();
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
this._handlers.set(widgetName, definition.handlers);
|
||||
}
|
||||
|
||||
console.log(`[WidgetRegistry] Registered widget: ${widgetName}`);
|
||||
if (window.debugLog) window.debugLog(`[WidgetRegistry] Registered widget: ${widgetName}`);
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -212,6 +212,4 @@
|
||||
})));
|
||||
};
|
||||
}
|
||||
|
||||
console.log('[WidgetRegistry] Widget registry initialized');
|
||||
})();
|
||||
|
||||
@@ -128,6 +128,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[SelectDropdownWidget] Select dropdown widget registered');
|
||||
})();
|
||||
|
||||
@@ -173,6 +173,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[SliderWidget] Slider widget registered');
|
||||
})();
|
||||
|
||||
@@ -768,6 +768,4 @@
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[StyleEditor] widget registered');
|
||||
})();
|
||||
|
||||
@@ -239,6 +239,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[TextInputWidget] Text input widget registered');
|
||||
})();
|
||||
|
||||
@@ -175,6 +175,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[TextareaWidget] Textarea widget registered');
|
||||
})();
|
||||
|
||||
@@ -166,6 +166,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[TimePickerWidget] Time picker widget registered');
|
||||
})();
|
||||
|
||||
@@ -370,6 +370,4 @@
|
||||
// Expose utility functions for external use
|
||||
window.LEDMatrixWidgets.get('time-range').parseTimeToMinutes = parseTimeToMinutes;
|
||||
window.LEDMatrixWidgets.get('time-range').calculateDuration = calculateDuration;
|
||||
|
||||
console.log('[TimeRangeWidget] Time range widget registered');
|
||||
})();
|
||||
|
||||
@@ -414,6 +414,4 @@
|
||||
}, 50);
|
||||
});
|
||||
})();
|
||||
|
||||
console.log('[TimezoneSelectorWidget] Timezone selector widget registered');
|
||||
})();
|
||||
|
||||
@@ -220,6 +220,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[ToggleSwitchWidget] Toggle switch widget registered');
|
||||
})();
|
||||
|
||||
@@ -284,6 +284,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[UrlInputWidget] URL input widget registered');
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user