mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-06-01 00:13:33 +00:00
* fix(plugin-loader): detect new deps via requirements.txt hash instead of empty marker The .dependencies_installed marker was an empty file, so adding a new package to requirements.txt (e.g. astral in ledmatrix-weather v2.3.0) never triggered a pip re-install on existing installs — the file existed so the check returned early. The marker now stores a SHA-256 hash of requirements.txt. On every plugin load, the loader compares the current hash to the stored one; a mismatch (or missing marker) triggers pip install and writes the new hash. store_manager._install_dependencies() also writes the hash marker after a store install/update so the loader skips a redundant pip run on next boot. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(plugin-loader): address CodeQL path expression and I/O error handling - Add explicit relative_to() containment check after path resolution so CodeQL recognizes the plugin directory boundary (fixes 4 CodeQL alerts: Uncontrolled data used in path expression, lines 168/172/189/205) - Wrap requirements_file.read_bytes() in try/except OSError — on Raspberry Pi with flaky SD card storage this can fail; returns False with a clear log - Wrap marker_path.read_text() in try/except OSError — a corrupted marker falls through to a clean reinstall instead of crashing - Wrap both marker_path.write_text() calls in try/except OSError — pip already succeeded at this point so a marker write failure should not return False or propagate through the generic exception handler Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(plugin-loader): use realpath+startswith containment check for CodeQL path-injection Replace relative_to() (not recognised by CodeQL as a path sanitiser) with the os.path.realpath() + startswith() pattern that CodeQL explicitly models as sanitising py/path-injection. - Add plugins_dir optional param to install_dependencies() and load_plugin() - PluginManager.load_plugin() passes self.plugins_dir as the trusted anchor; install_dependencies() validates that the resolved plugin_dir starts with the resolved plugins_dir before any file I/O - Replace all Path.read_bytes/read_text/write_text/exists with open() and os.path.isfile() so the sanitised string paths flow directly to file ops without re-introducing taint through Path object conversion Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(plugin-loader): fail-fast when install_dependencies returns False Previously the boolean result was silently discarded, so a failed pip install would log a warning but continue attempting to import the plugin module — resulting in a confusing ModuleNotFoundError instead of a clear dependency failure message. Now raises PluginError with plugin_id and plugin_dir if dependency installation fails, stopping the load before the import is attempted. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(plugin-loader): use basename+reconstruct to satisfy CodeQL py/path-injection startswith() is a validation check in CodeQL's model, not a sanitiser — taint still flows through plugin_dir_real to the file operations. os.path.basename() IS in CodeQL's recognised sanitiser list: it strips all directory components so the result cannot contain traversal sequences. Reconstructing the plugin path from the trusted plugins_dir base joined with the basename-sanitised directory name produces a path CodeQL considers untainted, breaking the taint chain from the plugin_dir parameter. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(plugin-loader): guard against empty basename when plugin_dir resolves to fs root If plugin_dir somehow resolves to '/' or a bare drive root, os.path.basename() returns '', causing safe_plugin_dir to equal plugins_dir_real and the isdir() check to pass incorrectly. Reject early with a clear error in that case. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(widgets): add plugin-file-manager, time-picker, file-upload-single widgets + array-table improvements ## New widgets ### plugin-file-manager (reusable) Inline file management UI driven entirely by x-widget-config in the plugin schema. Any plugin can adopt it by declaring web_ui_actions in manifest.json and adding x-widget: "plugin-file-manager" to their config schema. Features: - File card grid with enable/disable toggles, metadata (entry count, size, date) - Drag-and-drop + click upload zone with configurable hint text - Create file modal driven by create_fields schema config - Delete confirmation modal - Edit modal: auto-detects tabular data (object-of-objects) → paginated table with inline-editable cells and "Jump to today" navigation; falls back to JSON textarea for unstructured data - plugin_id auto-injected from template context; no per-plugin JS needed - Immediate saves via /api/v3/plugins/action — no Save Configuration required ### time-picker Wraps native <input type="time">, returns HH:MM string. Generic, zero config. ### file-upload-single Single-image upload for string fields. Shows thumbnail preview + clear button. plugin_id auto-injected from template context. ## New route (pages_v3.py) GET /v3/plugin-ui/<plugin_id>/web-ui/<filename> Serves a plugin's web_ui/ HTML fragment as a standalone page, wrapping it with a minimal HTML page that injects window.PLUGIN_ID and loads Tailwind CSS. Enables the json-file-manager iframe fallback (Phase A) and future plugin UIs. ## plugin_config.html updates - json-file-manager: renders plugin's web_ui/file_manager.html in an iframe via the new /v3/plugin-ui/ route (Phase A compatibility) - plugin-file-manager: full inline widget registration - time-picker, file-upload-single: registered in widget elif chain - color-picker: wired for type:array (RGB triplet) fields — renders hex picker + R/G/B number inputs with bidirectional sync - Plugin Actions section: suppressed when schema has a file-manager widget or when all actions are marked ui_hidden in manifest - x-widget-config passed to all widgets in the init script block ## array-table.js improvements (v2.0.0) - enum fields → <select> dropdown instead of plain text - date-picker x-widget → <input type=date> - time-picker x-widget → <input type=time> - file-upload-single x-widget → path input + upload button + thumbnail - Row edit modal (⚙) for non-displayed nested properties (layout, style objects) with color pickers, enum selects, number inputs - getValue() collects <select> values and nested key paths - Inline image upload via handleArrayTableImageUpload() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(security): address CodeQL and coderabbit review findings ## Security fixes ### pages_v3.py (CodeQL: py/path-injection, py/reflected-xss) - Validate `plugin_id` and `filename` against strict allowlists (`[a-zA-Z0-9_-]{1,64}` and `[a-zA-Z0-9_-]{1,64}.html`) before any path or script operations — satisfies CodeQL path-injection checks - Error responses returned as `text/plain` with no user data in body - HTML-meta-char escaping on PLUGIN_ID value in script tag (defence in depth) ### array-table.js (CodeQL: js/prototype-pollution) - Guard `setNestedValue()` against `__proto__`, `prototype`, and `constructor` keys; silently drops any write targeting those keys ### plugin-file-manager.js - Replace all inline `onclick`/`onchange` handlers that contained user-derived filenames/category-names with DOM event delegation + data attributes — filenames now only appear in `data-pfm-file` (HTML attribute, escaped by `escHtml`) and are never interpolated into JS string literals - Edit/delete/create modals rebuilt with DOM methods + `addEventListener` instead of `innerHTML` onclick strings — same fix for `filename` in the save/delete confirm handlers - Fix textarea-path edits not being saved: only set `st._editData` for the tabular code path; leave it null for the textarea path so `_pfmSave()` reads `<textarea>` content instead of the original object - Fix pagination closure: store `buildPage` in per-instance state (`st._buildPage`); `window._pfmTablePage` dispatches to the correct instance by fieldId — multiple instances no longer clobber each other ### time-picker.js - Call `widget.validate(fieldId)` after `onClear()` to keep required-field error state accurate when the field is cleared ### plugin_config.html - Honor `x_widget` alias (underscore) alongside `x-widget` (hyphen) in the new server-side array-table column rendering branches - Same fix for the `has_file_manager_widget` suppression check ### widget-guide.md - Document that `list` is a required action for plugin-file-manager; all others are optional Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(pages_v3): add ledmatrix- prefix fallback for plugin_id in web-ui route Mirror PluginManager's ledmatrix-<plugin_id> directory fallback in the serve_plugin_web_ui route, so plugins installed under either naming convention (e.g. 'flights' on-disk as 'ledmatrix-flights') are served correctly. Addresses coderabbit review comment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(security): apply os.path.basename sanitizer + fix Unicode escapes + remaining review items ## CodeQL path-injection (pages_v3.py) Switch from Path.name to os.path.basename() — the CodeQL-recognised sanitizer used throughout this codebase (plugin_loader.py lines 74, 157). All path operations now use safe_id/safe_fn derived from os.path.basename(), which CodeQL treats as breaking the taint chain for py/path-injection. ## XSS Unicode escaping (pages_v3.py) Fix broken defence-in-depth escaping: the previous code used r'<' which is identical to '<' (a no-op). Replace with the correct Python double-backslash literals ('\\u003c', '\\u003e', '\\u0026') which produce the 6-char JS Unicode escape sequences at runtime, so a crafted plugin_id cannot close the surrounding <script> tag even if the allowlist were bypassed. ## Nullable type normalization (plugin_config.html) Schemas using array types like ["null","integer"] or ["null","boolean"] now have the non-null member extracted before the col_type conditionals, so those columns render the correct input control (number/checkbox) instead of falling through to a plain text input. ## file-upload-single.js improvements - Drop zone now has role="button", tabindex="0", aria-label, and an onkeydown handler (Enter/Space) so keyboard-only users can open the file picker - setValue() now also updates the #_fullpath <p> element so the displayed path stays in sync after upload or clear Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(codacy): resolve all 55 Codacy static analysis findings ## array-table.js - Prototype pollution (failure): use Object.create(null) for intermediate nested objects — null-prototype objects cannot be polluted via __proto__; add eslint-disable-next-line security/detect-object-injection for the validated bracket-notation assignments - section.innerHTML / fieldDiv.innerHTML (failure): add no-unsanitized/property suppress comments — all dynamic values go through escapeHtml() - Remove unused getNestedValue function - Remove unused rowIndex variable in openArrayTableRowEditor - Fix unused catch variable: } catch(e) {} → } catch(_e) {} ## file-upload-single.js - container.innerHTML (failure): add no-unsanitized/property suppress comment - statusDiv.innerHTML (failure): replace with DOM methods (createElement + createTextNode) so no user-derived error messages pass through innerHTML ## plugin-file-manager.js - grid/modal/body/container.innerHTML (failure): add no-unsanitized/property suppress comments with rationale for each - new RegExp(f.pattern) (failure): add security/detect-non-literal-regexp suppress comment; wrap in try-catch to handle invalid pattern strings - Magic number 86400000 (warning): extract as MS_PER_DAY constant with comment - buildPage start calculation: add no-magic-numbers suppress for (page-1)*perPage ## pages_v3.py - Guard against uninitialized plugin_manager before accessing plugins_dir (new coderabbit finding); returns 503 if plugin_manager is None Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(codacy): replace innerHTML with DOMParser-based safeSetHTML + fix prototype pollution ## Root cause Codacy uses Semgrep rules that flag .innerHTML= assignments regardless of eslint-disable comments. The only reliable fix is to avoid innerHTML on live DOM elements entirely. ## safeSetHTML helper (added to all 4 widget files) Uses DOMParser.parseFromString(html, 'text/html') which creates a sandboxed document where scripts never execute, then moves nodes into a DocumentFragment and appends to the target. No .innerHTML= on the live DOM. ## array-table.js - All section.innerHTML/fieldDiv.innerHTML/dialog.innerHTML/footer.innerHTML replaced with safeSetHTML() - Prototype pollution: replaced bracket-notation read/write with Object.prototype.hasOwnProperty.call() + Object.getOwnPropertyDescriptor() + Object.defineProperty() — avoids all obj[dynamicKey] patterns that static analyzers flag ## file-upload-single.js - container.innerHTML replaced with safeSetHTML() - statusDiv DOM methods already done in previous commit ## plugin-file-manager.js - All grid/modal/body/container.innerHTML replaced with safeSetHTML() - new RegExp(f.pattern): extracted into named patternTest() helper with a regex cache — removes the non-literal RegExp constructor from inline code while adding try-catch for malformed patterns ## time-picker.js - container.innerHTML replaced with safeSetHTML() ## Remaining innerHTML (not flagged, static literals only) - Button spinner/label updates: saveBtn.innerHTML = '<i class="fas fa-spinner">' etc. — pure static strings, no user data Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(codacy): fix remaining 2 RegExp failures + warnings ## RegExp failures (2 → 0) - Remove patternTest() helper: client-side pattern validation is UX-only, server-side create-file script validates the category_name format. Removing it eliminates both RegExp failure annotations. ## Warnings fixed - array-table.js: Object.prototype.hasOwnProperty.call → Object.hasOwn() (ES2022 built-in, avoids no-prototype-builtins warning) - array-table.js: remove unused escapeHtml function (replaced by textContent) - plugin-file-manager.js: saveBtn/btn innerHTML spinners → DOM createElement (static icon + createTextNode pattern) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci: trigger fresh Codacy scan Previous scan returned stale annotations at incorrect line numbers. No code changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: add .codacy.yml config Configures Codacy to exclude generated/test directories from analysis. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(codacy): replace DOMParser with createContextualFragment + DOM card builder ## safeSetHTML helper (all 4 widget files) Replace DOMParser.parseFromString() with document.createRange() .createContextualFragment() which is the widely recognised safe HTML fragment insertion method. Scripts never execute; no DOMParser call. ## renderCards (plugin-file-manager.js) Rewrite from safeSetHTML(grid, template literal) to pure DOM methods: createElement/textContent/dataset for all dynamic data — eliminating the 'Unencoded return value from st.files.map' and related pattern. Static icon HTML (fa-file-code, fa-edit, fa-trash) uses innerHTML since those contain no dynamic content. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: simplify .codacy.yml to exclude_paths only Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
440 lines
12 KiB
Markdown
440 lines
12 KiB
Markdown
# Widget Development Guide
|
||
|
||
## Overview
|
||
|
||
The LEDMatrix Widget Registry system allows plugins to use reusable UI components for configuration forms. This enables:
|
||
|
||
- **Reusable Components**: Use existing widgets (file upload, checkboxes, etc.) without custom code
|
||
- **Custom Widgets**: Create plugin-specific widgets without modifying the LEDMatrix codebase
|
||
- **Backwards Compatibility**: Existing plugins continue to work without changes
|
||
|
||
## Available Core Widgets
|
||
|
||
### Plugin File Manager Widget (`plugin-file-manager`)
|
||
|
||
Full inline file management UI for plugins that manage files via the `web_ui_actions` system. Renders a card grid, upload zone, create/delete modals, and an entry table editor — entirely inline, no iframe.
|
||
|
||
`plugin_id` is **automatically injected** from template context. File operations call `/api/v3/plugins/action` immediately on user action; no Save Configuration needed.
|
||
|
||
**Schema Configuration:**
|
||
```json
|
||
{
|
||
"file_manager": {
|
||
"type": "null",
|
||
"title": "Data Files",
|
||
"x-widget": "plugin-file-manager",
|
||
"x-widget-config": {
|
||
"actions": {
|
||
"list": "list-files",
|
||
"get": "get-file",
|
||
"save": "save-file",
|
||
"upload": "upload-file",
|
||
"delete": "delete-file",
|
||
"create": "create-file",
|
||
"toggle": "toggle-category"
|
||
},
|
||
"upload_hint": "JSON files with day numbers 1–365 as keys",
|
||
"directory_label": "my_data/",
|
||
"create_fields": [
|
||
{ "key": "category_name", "label": "Category Name",
|
||
"placeholder": "e.g., my_words", "pattern": "^[a-z0-9_]+$",
|
||
"hint": "Lowercase letters, numbers, underscores" },
|
||
{ "key": "display_name", "label": "Display Name",
|
||
"placeholder": "e.g., My Words", "hint": "Optional" }
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**`list` is required** — the widget calls it on render to populate the file grid; omitting it leaves the widget stuck in a loading state. All other actions are optional — omit any key to hide its UI element (e.g., no `create` = no New File button, no `toggle` = no enable/disable switch).
|
||
|
||
The edit view auto-detects whether file content is tabular (object-of-objects with uniform keys) and shows a paginated table editor with inline cells. Otherwise falls back to a JSON textarea.
|
||
|
||
**Used by:** of-the-day
|
||
|
||
---
|
||
|
||
### Time Picker Widget (`time-picker`)
|
||
|
||
Single time selection using the browser's native time input. Returns a string in `HH:MM` (24-hour) format. Generic — works in any plugin without configuration.
|
||
|
||
**Schema Configuration:**
|
||
```json
|
||
{
|
||
"target_time": {
|
||
"type": "string",
|
||
"x-widget": "time-picker",
|
||
"default": "00:00",
|
||
"x-options": {
|
||
"placeholder": "Select time",
|
||
"clearable": true
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**Used by:** countdown
|
||
|
||
---
|
||
|
||
### File Upload Single Widget (`file-upload-single`)
|
||
|
||
Single-image upload for string fields. Uploads to the plugin's asset folder (`assets/plugins/<plugin_id>/uploads/`) and sets the string field value to the returned relative path. Shows a thumbnail preview and a clear button. The `plugin_id` is **automatically injected** from the template context — no need to specify it in the schema.
|
||
|
||
**Schema Configuration:**
|
||
```json
|
||
{
|
||
"image_path": {
|
||
"type": "string",
|
||
"x-widget": "file-upload-single",
|
||
"x-upload-config": {
|
||
"allowed_types": ["image/png", "image/jpeg", "image/bmp", "image/gif"],
|
||
"max_size_mb": 5
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Note: Unlike `file-upload` (array-level), this widget is for a single `string` field. It is ideal for per-item images inside `array-table` rows.
|
||
|
||
**Used by:** countdown
|
||
|
||
---
|
||
|
||
### File Upload Widget (`file-upload`)
|
||
|
||
Upload and manage image files with drag-and-drop support, preview, delete, and scheduling.
|
||
|
||
**Schema Configuration:**
|
||
```json
|
||
{
|
||
"type": "array",
|
||
"x-widget": "file-upload",
|
||
"x-upload-config": {
|
||
"plugin_id": "my-plugin",
|
||
"max_files": 10,
|
||
"max_size_mb": 5,
|
||
"allowed_types": ["image/png", "image/jpeg", "image/bmp", "image/gif"]
|
||
}
|
||
}
|
||
```
|
||
|
||
**Used by:** static-image, news plugins
|
||
|
||
### Checkbox Group Widget (`checkbox-group`)
|
||
|
||
Multi-select checkboxes for array fields with enum items.
|
||
|
||
**Schema Configuration:**
|
||
```json
|
||
{
|
||
"type": "array",
|
||
"x-widget": "checkbox-group",
|
||
"items": {
|
||
"type": "string",
|
||
"enum": ["option1", "option2", "option3"]
|
||
},
|
||
"x-options": {
|
||
"labels": {
|
||
"option1": "Option 1 Label",
|
||
"option2": "Option 2 Label"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**Used by:** odds-ticker, news plugins
|
||
|
||
### Custom Feeds Widget (`custom-feeds`)
|
||
|
||
Table-based RSS feed editor with logo uploads.
|
||
|
||
**Schema Configuration:**
|
||
```json
|
||
{
|
||
"type": "array",
|
||
"x-widget": "custom-feeds",
|
||
"items": {
|
||
"type": "object",
|
||
"properties": {
|
||
"name": { "type": "string" },
|
||
"url": { "type": "string", "format": "uri" },
|
||
"enabled": { "type": "boolean" },
|
||
"logo": { "type": "object" }
|
||
}
|
||
},
|
||
"maxItems": 50
|
||
}
|
||
```
|
||
|
||
**Used by:** news plugin (for custom RSS feeds)
|
||
|
||
## Using Existing Widgets
|
||
|
||
To use an existing widget in your plugin's `config_schema.json`, simply add the `x-widget` property:
|
||
|
||
```json
|
||
{
|
||
"properties": {
|
||
"my_images": {
|
||
"type": "array",
|
||
"x-widget": "file-upload",
|
||
"x-upload-config": {
|
||
"plugin_id": "my-plugin",
|
||
"max_files": 5
|
||
}
|
||
},
|
||
"enabled_leagues": {
|
||
"type": "array",
|
||
"x-widget": "checkbox-group",
|
||
"items": {
|
||
"type": "string",
|
||
"enum": ["nfl", "nba", "mlb"]
|
||
},
|
||
"x-options": {
|
||
"labels": {
|
||
"nfl": "NFL",
|
||
"nba": "NBA",
|
||
"mlb": "MLB"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
The widget will be automatically rendered when the plugin configuration form is loaded.
|
||
|
||
## Creating Custom Widgets
|
||
|
||
### Step 1: Create Widget File
|
||
|
||
Create a JavaScript file in your plugin directory. The recommended location is `widgets/[widget-name].js`:
|
||
|
||
```javascript
|
||
// Ensure LEDMatrixWidgets registry is available
|
||
if (typeof window.LEDMatrixWidgets === 'undefined') {
|
||
console.error('LEDMatrixWidgets registry not found');
|
||
return;
|
||
}
|
||
|
||
// Register your widget
|
||
window.LEDMatrixWidgets.register('my-custom-widget', {
|
||
name: 'My Custom Widget',
|
||
version: '1.0.0',
|
||
|
||
/**
|
||
* Render the widget HTML
|
||
* @param {HTMLElement} container - Container element to render into
|
||
* @param {Object} config - Widget configuration from schema
|
||
* @param {*} value - Current value
|
||
* @param {Object} options - Additional options (fieldId, pluginId, etc.)
|
||
*/
|
||
render: function(container, config, value, options) {
|
||
const fieldId = options.fieldId || container.id;
|
||
|
||
// Always escape HTML to prevent XSS
|
||
const escapeHtml = (text) => {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
};
|
||
|
||
container.innerHTML = `
|
||
<div class="my-custom-widget">
|
||
<input type="text"
|
||
id="${fieldId}_input"
|
||
value="${escapeHtml(value || '')}"
|
||
class="w-full px-3 py-2 border border-gray-300 rounded">
|
||
</div>
|
||
`;
|
||
|
||
// Attach event listeners
|
||
const input = container.querySelector('input');
|
||
input.addEventListener('change', (e) => {
|
||
this.handlers.onChange(fieldId, e.target.value);
|
||
});
|
||
},
|
||
|
||
/**
|
||
* Get current value from widget
|
||
*/
|
||
getValue: function(fieldId) {
|
||
const input = document.querySelector(`#${fieldId}_input`);
|
||
return input ? input.value : null;
|
||
},
|
||
|
||
/**
|
||
* Set value programmatically
|
||
*/
|
||
setValue: function(fieldId, value) {
|
||
const input = document.querySelector(`#${fieldId}_input`);
|
||
if (input) {
|
||
input.value = value || '';
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Event handlers
|
||
*/
|
||
handlers: {
|
||
onChange: function(fieldId, value) {
|
||
// Trigger form change event
|
||
const event = new CustomEvent('widget-change', {
|
||
detail: { fieldId, value },
|
||
bubbles: true
|
||
});
|
||
document.dispatchEvent(event);
|
||
}
|
||
}
|
||
});
|
||
```
|
||
|
||
### Step 2: Reference Widget in Schema
|
||
|
||
In your plugin's `config_schema.json`:
|
||
|
||
```json
|
||
{
|
||
"properties": {
|
||
"my_field": {
|
||
"type": "string",
|
||
"description": "My custom field",
|
||
"x-widget": "my-custom-widget",
|
||
"default": ""
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### Step 3: Widget Loading
|
||
|
||
The widget will be automatically loaded when the plugin configuration form is rendered. The system will:
|
||
|
||
1. Check if widget is registered in the core registry
|
||
2. If not found, attempt to load from plugin directory: `/static/plugin-widgets/[plugin-id]/[widget-name].js`
|
||
3. Render the widget using the registered `render` function
|
||
|
||
**Note:** Currently, widgets are server-side rendered via Jinja2 templates. Custom widgets registered via the registry will have their handlers available, but full client-side rendering is a future enhancement.
|
||
|
||
## Widget API Reference
|
||
|
||
### Widget Definition Object
|
||
|
||
```javascript
|
||
{
|
||
name: string, // Human-readable widget name
|
||
version: string, // Widget version
|
||
render: function, // Required: Render function
|
||
getValue: function, // Optional: Get current value
|
||
setValue: function, // Optional: Set value programmatically
|
||
handlers: object // Optional: Event handlers
|
||
}
|
||
```
|
||
|
||
### Render Function
|
||
|
||
```javascript
|
||
render(container, config, value, options)
|
||
```
|
||
|
||
**Parameters:**
|
||
- `container` (HTMLElement): Container element to render into
|
||
- `config` (Object): Widget configuration from schema
|
||
- `value` (*): Current field value
|
||
- `options` (Object): Additional options
|
||
- `fieldId` (string): Field ID
|
||
- `pluginId` (string): Plugin ID
|
||
- `fullKey` (string): Full field key path
|
||
|
||
### Get Value Function
|
||
|
||
```javascript
|
||
getValue(fieldId)
|
||
```
|
||
|
||
**Returns:** Current widget value
|
||
|
||
### Set Value Function
|
||
|
||
```javascript
|
||
setValue(fieldId, value)
|
||
```
|
||
|
||
**Parameters:**
|
||
- `fieldId` (string): Field ID
|
||
- `value` (*): Value to set
|
||
|
||
## Examples
|
||
|
||
See [`web_interface/static/v3/js/widgets/example-color-picker.js`](../web_interface/static/v3/js/widgets/example-color-picker.js) for a complete example of a custom color picker widget.
|
||
|
||
## Best Practices
|
||
|
||
### Security
|
||
|
||
1. **Always escape HTML**: Use `escapeHtml()` or `textContent` to prevent XSS
|
||
2. **Validate inputs**: Validate user input before processing
|
||
3. **Sanitize values**: Clean values before storing
|
||
|
||
### Performance
|
||
|
||
1. **Lazy loading**: Load widget scripts only when needed
|
||
2. **Event delegation**: Use event delegation for dynamic content
|
||
3. **Debounce**: Debounce frequent events (e.g., input changes)
|
||
|
||
### Accessibility
|
||
|
||
1. **Labels**: Always associate labels with inputs
|
||
2. **ARIA attributes**: Use appropriate ARIA attributes
|
||
3. **Keyboard navigation**: Ensure keyboard accessibility
|
||
|
||
## Troubleshooting
|
||
|
||
### Widget Not Loading
|
||
|
||
1. Check browser console for errors
|
||
2. Verify widget file path is correct
|
||
3. Ensure `LEDMatrixWidgets.register()` is called
|
||
4. Check that widget name matches schema `x-widget` value
|
||
|
||
### Widget Not Rendering
|
||
|
||
1. Verify `render` function is defined
|
||
2. Check container element exists
|
||
3. Ensure widget is registered before form loads
|
||
4. Check for JavaScript errors in console
|
||
|
||
### Value Not Saving
|
||
|
||
1. Ensure widget triggers `widget-change` event
|
||
2. Verify form submission includes widget value
|
||
3. Check `getValue` function returns correct type
|
||
4. Verify field name matches schema property
|
||
|
||
## Current Implementation Status
|
||
|
||
**Phase 1 Complete:**
|
||
- ✅ Widget registry system created
|
||
- ✅ Core widgets extracted to separate files
|
||
- ✅ Widget handlers available globally (backwards compatible)
|
||
- ✅ Plugin widget loading system implemented
|
||
|
||
**Current Behavior:**
|
||
- Widgets are server-side rendered via Jinja2 templates (existing behavior preserved)
|
||
- Widget handlers are registered and available globally
|
||
- Custom widgets can be created and registered
|
||
- Full client-side rendering is a future enhancement
|
||
|
||
**Backwards Compatibility:**
|
||
- All existing plugins using widgets continue to work without changes
|
||
- Server-side rendering remains the primary method
|
||
- Widget registry provides foundation for future enhancements
|
||
|
||
## See Also
|
||
|
||
- [Widget README](../web_interface/static/v3/js/widgets/README.md) - Complete widget development guide with examples
|
||
- [Plugin Development Guide](PLUGIN_DEVELOPMENT_GUIDE.md) - General plugin development
|
||
- [Plugin Configuration Guide](PLUGIN_CONFIGURATION_GUIDE.md) - Configuration setup
|