feat(ui): Implement inline widget controls and drag value adjustments
- Added 'InlineControlFactory' to create inline toggle switches and combo boxes for supported widget types, replacing the click-to-edit modal for these types. - Added 'DragValueController' to enable adjusting numeric values by dragging on the value display. - Updated 'UIManager' to integrate these new controls, manage their lifecycle, and properly clean them up. - Enhanced 'WidgetEditorFactory' to handle Enter key for confirming edits. - Removed the copy button from string values to reduce clutter. - Updated 'index.ts' to export the new components.
This commit is contained in:
+4
-1
@@ -43,4 +43,7 @@ DEBUG_SUMMARY.md
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.local
|
||||
|
||||
# Images
|
||||
pasted file.png
|
||||
+311
-15
@@ -46,10 +46,12 @@
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(20px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.magnify-info-panel * {
|
||||
@@ -165,10 +167,44 @@
|
||||
|
||||
.magnify-info-panel.panel-minimized .panel-content {
|
||||
max-height: 0 !important;
|
||||
height: 0 !important;
|
||||
overflow: hidden;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Hover-expand: Show ALL content at once - no scrollbar, full expansion */
|
||||
.magnify-info-panel.is-expanded:not(.panel-minimized) {
|
||||
max-height: none !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.magnify-info-panel.is-expanded:not(.panel-minimized) .panel-content {
|
||||
max-height: none !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* STICKY MODE (PERSIST) OVERRIDES
|
||||
* When sticky mode is active, we enforce the max-height and enable scrolling.
|
||||
* This ensures the panel respects user-configured height and can scroll.
|
||||
*/
|
||||
.magnify-info-panel.persist-active:not(.panel-minimized) {
|
||||
/* Use the CSS variable set by JS for max-height */
|
||||
max-height: var(--panel-max-height, 600px) !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.magnify-info-panel.persist-active:not(.panel-minimized) .panel-content {
|
||||
/* Allow scrolling inside the content area */
|
||||
overflow-y: auto !important;
|
||||
/* Reset max-height so it fills the parent (which is constrained) */
|
||||
max-height: none !important;
|
||||
/* Ensure it takes available space */
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.floating-controls {
|
||||
position: fixed;
|
||||
display: none;
|
||||
@@ -310,8 +346,9 @@
|
||||
|
||||
.panel-content {
|
||||
padding: 0;
|
||||
max-height: calc(100% - 60px);
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: visible;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
@@ -853,16 +890,33 @@
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
/* Match the blue text theme style */
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
color: var(--info-panel-accent-color, #74b9ff);
|
||||
font-size: 14px;
|
||||
padding: 4px 24px 4px 10px;
|
||||
border-radius: 6px;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2374b9ff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
background-size: 14px;
|
||||
padding-right: 28px;
|
||||
background-position: right 6px center;
|
||||
background-size: 12px;
|
||||
}
|
||||
|
||||
.widget-editor-select:hover {
|
||||
background-color: rgba(116, 185, 255, 0.12);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.widget-editor-select:focus {
|
||||
background-color: rgba(116, 185, 255, 0.15);
|
||||
border-color: var(--info-panel-accent-color, #74b9ff);
|
||||
box-shadow: 0 0 0 2px rgba(116, 185, 255, 0.2);
|
||||
}
|
||||
|
||||
.widget-editor-select option {
|
||||
background: #2a2a2a;
|
||||
color: #e0e0e0;
|
||||
background: #1a1a1f;
|
||||
color: var(--info-panel-accent-color, #74b9ff);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
@@ -1049,18 +1103,31 @@
|
||||
|
||||
/* Theme-specific adjustments for editors */
|
||||
.magnify-info-panel.theme-light .widget-editor-input,
|
||||
.magnify-info-panel.theme-light .widget-editor-textarea,
|
||||
.magnify-info-panel.theme-light .widget-editor-select {
|
||||
.magnify-info-panel.theme-light .widget-editor-textarea {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-light .widget-editor-input:focus,
|
||||
.magnify-info-panel.theme-light .widget-editor-textarea:focus,
|
||||
.magnify-info-panel.theme-light .widget-editor-select:focus {
|
||||
.magnify-info-panel.theme-light .widget-editor-textarea:focus {
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-light .widget-editor-select {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--info-panel-accent-color, #0066cc);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-light .widget-editor-select:hover {
|
||||
background-color: rgba(0, 102, 204, 0.08);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-light .widget-editor-select option {
|
||||
background: #ffffff;
|
||||
color: var(--info-panel-accent-color, #0066cc);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-light .widget-editor-stepper {
|
||||
border-color: rgba(0, 0, 0, 0.15);
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
@@ -1072,14 +1139,243 @@
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-github .widget-editor-input,
|
||||
.magnify-info-panel.theme-github .widget-editor-textarea,
|
||||
.magnify-info-panel.theme-github .widget-editor-select {
|
||||
.magnify-info-panel.theme-github .widget-editor-textarea {
|
||||
background: #f6f8fa;
|
||||
border-color: #d1d5db;
|
||||
color: #24292e;
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-github .widget-editor-select {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--info-panel-accent-color, #0366d6);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-github .widget-editor-select:hover {
|
||||
background-color: rgba(3, 102, 214, 0.08);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-github .widget-editor-select option {
|
||||
background: #ffffff;
|
||||
color: #24292e;
|
||||
color: var(--info-panel-accent-color, #0366d6);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Inline Controls - Compact controls that render directly in rows
|
||||
========================================================================== */
|
||||
|
||||
.inline-control-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.inline-control {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Inline Toggle - Compact version */
|
||||
.inline-toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.inline-toggle-input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.inline-toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 20px;
|
||||
transition: all 0.25s ease;
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.inline-toggle-slider::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: all 0.25s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.inline-toggle-input:checked+.inline-toggle-slider {
|
||||
background: var(--info-panel-accent-color, #74b9ff);
|
||||
border-color: var(--info-panel-accent-color, #74b9ff);
|
||||
}
|
||||
|
||||
.inline-toggle-input:checked+.inline-toggle-slider::before {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
.inline-toggle-input:focus+.inline-toggle-slider {
|
||||
box-shadow: 0 0 0 2px rgba(116, 185, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Inline Dropdown - Compact version matching blue text theme */
|
||||
.inline-dropdown {
|
||||
min-width: 100px;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.inline-dropdown-select {
|
||||
width: 100%;
|
||||
padding: 4px 24px 4px 10px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 14px;
|
||||
/* Match the blue text theme style */
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
color: var(--info-panel-accent-color, #74b9ff);
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2374b9ff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 6px center;
|
||||
background-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.inline-dropdown-select:hover {
|
||||
background-color: rgba(116, 185, 255, 0.12);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.inline-dropdown-select:focus {
|
||||
outline: none;
|
||||
background-color: rgba(116, 185, 255, 0.15);
|
||||
border-color: var(--info-panel-accent-color, #74b9ff);
|
||||
box-shadow: 0 0 0 2px rgba(116, 185, 255, 0.2);
|
||||
}
|
||||
|
||||
.inline-dropdown-select option {
|
||||
background: #1a1a1f;
|
||||
color: var(--info-panel-accent-color, #74b9ff);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Drag Gesture Indicators
|
||||
========================================================================== */
|
||||
|
||||
.info-row.draggable {
|
||||
position: relative;
|
||||
cursor: ew-resize;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Value container in draggable rows should show text cursor for click-to-edit */
|
||||
.info-row.draggable .info-value {
|
||||
cursor: text;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.info-row.draggable .info-value:hover {
|
||||
background: rgba(116, 185, 255, 0.18);
|
||||
border-color: rgba(116, 185, 255, 0.4);
|
||||
}
|
||||
|
||||
.info-row.draggable::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(116, 185, 255, 0.3) 20%,
|
||||
rgba(116, 185, 255, 0.3) 80%,
|
||||
transparent 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.info-row.draggable:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.info-row.dragging {
|
||||
background: rgba(116, 185, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
.info-row.dragging .info-value {
|
||||
color: var(--info-panel-accent-color, #74b9ff);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.drag-indicator {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 10px;
|
||||
opacity: 0;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.info-row.draggable:hover .drag-indicator {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.info-row.dragging .drag-indicator {
|
||||
opacity: 1;
|
||||
color: var(--info-panel-accent-color, #74b9ff);
|
||||
}
|
||||
|
||||
/* Theme-specific adjustments for inline controls */
|
||||
.magnify-info-panel.theme-light .inline-toggle-slider {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
border-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-light .inline-dropdown-select {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--info-panel-accent-color, #0066cc);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-light .inline-dropdown-select:hover {
|
||||
background-color: rgba(0, 102, 204, 0.08);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-light .inline-dropdown-select option {
|
||||
background: #ffffff;
|
||||
color: var(--info-panel-accent-color, #0066cc);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-github .inline-dropdown-select {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--info-panel-accent-color, #0366d6);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-github .inline-dropdown-select:hover {
|
||||
background-color: rgba(3, 102, 214, 0.08);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-github .inline-dropdown-select option {
|
||||
background: #ffffff;
|
||||
color: var(--info-panel-accent-color, #0366d6);
|
||||
}
|
||||
+112
-41
@@ -20,7 +20,14 @@ import {
|
||||
type ParameterItem
|
||||
} from './NodeDataExtractor';
|
||||
import { NodeSelector, type NodeListEntry, type NodeExecOrderEntry } from './NodeSelector';
|
||||
import { WidgetEditorFactory, WidgetSyncManager, type WidgetEditorInstance } from './widget-editors';
|
||||
import {
|
||||
WidgetEditorFactory,
|
||||
WidgetSyncManager,
|
||||
InlineControlFactory,
|
||||
DragValueController,
|
||||
type WidgetEditorInstance,
|
||||
type InlineControlInstance
|
||||
} from './widget-editors';
|
||||
|
||||
interface InfoPanelElements {
|
||||
panel: HTMLDivElement | null;
|
||||
@@ -41,6 +48,10 @@ export class UIManager {
|
||||
onNodeSelected: ((nodeId: number) => void) | null = null;
|
||||
// Track active widget editors for cleanup
|
||||
private activeEditors: Map<string, WidgetEditorInstance> = new Map();
|
||||
// Track active inline controls for cleanup
|
||||
private activeInlineControls: Map<string, InlineControlInstance> = new Map();
|
||||
// Track active drag controllers for cleanup
|
||||
private activeDragControllers: Map<string, DragValueController> = new Map();
|
||||
|
||||
constructor(stateManager: StateManager) {
|
||||
this.stateManager = stateManager;
|
||||
@@ -718,9 +729,17 @@ export class UIManager {
|
||||
|
||||
/**
|
||||
* Display information in the panel.
|
||||
* @param info
|
||||
* @param info
|
||||
*/
|
||||
displayInfo(info: any): void {
|
||||
// Skip re-rendering if user is actively editing a value
|
||||
// This prevents the editor from being destroyed mid-edit
|
||||
if (this.activeEditors.size > 0) {
|
||||
// Only update the header subtitle, don't re-render content
|
||||
this.updateHeaderSubtitle(info);
|
||||
return;
|
||||
}
|
||||
|
||||
const sections = this.buildSections(info);
|
||||
this.renderSections(sections);
|
||||
this.updateSectionStates();
|
||||
@@ -920,22 +939,12 @@ export class UIManager {
|
||||
|
||||
// Only show dropdown arrow for actual dropdowns, not actions like zoom
|
||||
const dropdownIcon = (item.clickable && item.clickable !== 'zoom') ? '<span class="dropdown-indicator" style="margin-left: 4px; opacity: 0.6; font-size: 10px;">▼</span>' : '';
|
||||
// Show copy button for string values that are long enough to be worth copying
|
||||
const rawValue = String(item.value || '');
|
||||
const showCopyBtn = !item.clickable && rawValue.length > 3 && typeof item.value === 'string';
|
||||
const copyBtnHtml = showCopyBtn ? `
|
||||
<button class="copy-btn" data-copy-value="${escapeHtml(rawValue)}" title="Copy to clipboard" aria-label="Copy to clipboard">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
</button>` : '';
|
||||
return `
|
||||
<div class="info-row ${clickableClass} ${editableClass}" ${clickableAttr} ${nodeIdAttr} ${editableAttrs} style="${item.clickable ? 'cursor: pointer;' : ''}">
|
||||
<span class="info-label">${escapeHtml(item.label)}</span>
|
||||
<span class="info-value ${valueClass} original" ${valueAttributes}>${value}${dropdownIcon}</span>
|
||||
<div class="inline-control-container" style="display: none;"></div>
|
||||
<div class="widget-editor-container" style="display: none;"></div>
|
||||
${copyBtnHtml}
|
||||
</div>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
@@ -952,7 +961,7 @@ export class UIManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup active widget editors.
|
||||
* Cleanup active widget editors, inline controls, and drag controllers.
|
||||
*/
|
||||
private cleanupEditors(): void {
|
||||
this.activeEditors.forEach(editor => {
|
||||
@@ -963,10 +972,29 @@ export class UIManager {
|
||||
}
|
||||
});
|
||||
this.activeEditors.clear();
|
||||
|
||||
this.activeInlineControls.forEach(control => {
|
||||
try {
|
||||
control.destroy();
|
||||
} catch (e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
});
|
||||
this.activeInlineControls.clear();
|
||||
|
||||
this.activeDragControllers.forEach(controller => {
|
||||
try {
|
||||
controller.destroy();
|
||||
} catch (e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
});
|
||||
this.activeDragControllers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach click handlers to editable widget rows.
|
||||
* Also creates inline controls for toggle/combo widgets and drag controllers.
|
||||
*/
|
||||
private attachEditableRowHandlers(): void {
|
||||
if (!this.elements.content) return;
|
||||
@@ -976,14 +1004,78 @@ export class UIManager {
|
||||
const rowEl = row as HTMLElement;
|
||||
const valueEl = rowEl.querySelector('.info-value.original') as HTMLElement;
|
||||
const editorContainer = rowEl.querySelector('.widget-editor-container') as HTMLElement;
|
||||
const inlineContainer = rowEl.querySelector('.inline-control-container') as HTMLElement;
|
||||
|
||||
if (!valueEl || !editorContainer) return;
|
||||
if (!valueEl) return;
|
||||
|
||||
// Click on value to enter edit mode
|
||||
valueEl.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.enterEditMode(rowEl, valueEl, editorContainer);
|
||||
});
|
||||
const nodeId = parseInt(rowEl.dataset.nodeId || '0', 10);
|
||||
const widgetName = rowEl.dataset.widgetName || '';
|
||||
const widgetType = rowEl.dataset.widgetType || 'text';
|
||||
const rawValue = rowEl.dataset.rawValue;
|
||||
|
||||
// Parse constraints
|
||||
let constraints: any = {};
|
||||
try {
|
||||
const constraintsStr = rowEl.dataset.constraints;
|
||||
if (constraintsStr) {
|
||||
constraints = JSON.parse(constraintsStr);
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parse errors
|
||||
}
|
||||
|
||||
if (isNaN(nodeId) || !widgetName) return;
|
||||
|
||||
const controlKey = `${nodeId}:${widgetName}`;
|
||||
|
||||
// Check if this widget type should use inline controls
|
||||
if (InlineControlFactory.shouldUseInlineControl(widgetType) && inlineContainer) {
|
||||
// Create inline control
|
||||
const control = InlineControlFactory.createControl({
|
||||
nodeId,
|
||||
widgetName,
|
||||
widgetType,
|
||||
currentValue: widgetType.toLowerCase() === 'boolean' || widgetType.toLowerCase() === 'toggle'
|
||||
? rawValue === 'true'
|
||||
: rawValue,
|
||||
constraints,
|
||||
onChange: (value) => {
|
||||
rowEl.dataset.rawValue = String(value);
|
||||
}
|
||||
});
|
||||
|
||||
if (control) {
|
||||
this.activeInlineControls.set(controlKey, control);
|
||||
inlineContainer.appendChild(control.element);
|
||||
inlineContainer.style.display = 'flex';
|
||||
// Hide the text value display
|
||||
valueEl.style.display = 'none';
|
||||
}
|
||||
} else if (editorContainer) {
|
||||
// Fall back to click-to-edit for text/number types
|
||||
valueEl.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.enterEditMode(rowEl, valueEl, editorContainer);
|
||||
});
|
||||
}
|
||||
|
||||
// Attach drag controller if widget type supports it and we don't have an inline control
|
||||
if (DragValueController.isTypeSupported(widgetType) &&
|
||||
!InlineControlFactory.shouldUseInlineControl(widgetType)) {
|
||||
const dragController = new DragValueController(rowEl, {
|
||||
nodeId,
|
||||
widgetName,
|
||||
widgetType,
|
||||
currentValue: rawValue,
|
||||
constraints,
|
||||
onChange: (value) => {
|
||||
rowEl.dataset.rawValue = String(value);
|
||||
// Update displayed value
|
||||
valueEl.textContent = formatWidgetValue(value);
|
||||
}
|
||||
});
|
||||
this.activeDragControllers.set(controlKey, dragController);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1122,27 +1214,6 @@ export class UIManager {
|
||||
(row as HTMLElement).style.background = '';
|
||||
});
|
||||
});
|
||||
|
||||
// Add click handlers for copy buttons
|
||||
const copyButtons = this.elements.content.querySelectorAll('.copy-btn');
|
||||
copyButtons.forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const value = (btn as HTMLElement).dataset.copyValue || '';
|
||||
navigator.clipboard.writeText(value).then(() => {
|
||||
// Show success feedback
|
||||
btn.classList.add('copied');
|
||||
const originalHtml = btn.innerHTML;
|
||||
btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = originalHtml;
|
||||
btn.classList.remove('copied');
|
||||
}, 1500);
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy:', err);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* ComfyUI MagnifyGlass - Drag Value Controller
|
||||
*
|
||||
* Handles horizontal drag gestures on inspector rows for quick value changes.
|
||||
* Allows users to drag left/right to adjust numbers or cycle through options.
|
||||
*/
|
||||
|
||||
import { WidgetSyncManager, WidgetConstraints } from './WidgetSyncManager';
|
||||
import { Logger } from '../../shared/logger';
|
||||
|
||||
/**
|
||||
* Configuration for drag behavior
|
||||
*/
|
||||
export interface DragConfig {
|
||||
nodeId: number;
|
||||
widgetName: string;
|
||||
widgetType: string;
|
||||
currentValue: unknown;
|
||||
constraints?: WidgetConstraints;
|
||||
onChange?: (value: unknown) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default sensitivity settings
|
||||
*/
|
||||
const DRAG_SENSITIVITY = {
|
||||
// Pixels per unit change for integers
|
||||
intPixelsPerUnit: 8,
|
||||
// Pixels per 0.1 change for floats
|
||||
floatPixelsPerTenth: 10,
|
||||
// Minimum pixels to trigger option cycle for combos
|
||||
comboThreshold: 40,
|
||||
};
|
||||
|
||||
/**
|
||||
* Drag Value Controller
|
||||
* Manages drag interactions for inline value editing
|
||||
*/
|
||||
export class DragValueController {
|
||||
private element: HTMLElement;
|
||||
private config: DragConfig;
|
||||
private isDragging: boolean = false;
|
||||
private startX: number = 0;
|
||||
private startValue: number = 0;
|
||||
private startOptionIndex: number = 0;
|
||||
private accumulatedDelta: number = 0;
|
||||
|
||||
// Bound event handlers for cleanup
|
||||
private boundPointerMove: (e: PointerEvent) => void;
|
||||
private boundPointerUp: (e: PointerEvent) => void;
|
||||
|
||||
constructor(element: HTMLElement, config: DragConfig) {
|
||||
this.element = element;
|
||||
this.config = config;
|
||||
|
||||
this.boundPointerMove = this.onPointerMove.bind(this);
|
||||
this.boundPointerUp = this.onPointerUp.bind(this);
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize drag handling
|
||||
*/
|
||||
private init(): void {
|
||||
this.element.classList.add('draggable');
|
||||
|
||||
this.element.addEventListener('pointerdown', this.onPointerDown.bind(this));
|
||||
|
||||
// Add visual indicator
|
||||
// this.addDragIndicator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add visual drag indicator to the row
|
||||
*/
|
||||
private addDragIndicator(): void {
|
||||
// Check if already has indicator
|
||||
if (this.element.querySelector('.drag-indicator')) return;
|
||||
|
||||
const indicator = document.createElement('span');
|
||||
indicator.className = 'drag-indicator';
|
||||
indicator.innerHTML = '⟷'; // Left-right arrow
|
||||
indicator.setAttribute('aria-hidden', 'true');
|
||||
this.element.appendChild(indicator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle pointer down - start drag tracking
|
||||
*/
|
||||
private onPointerDown(e: PointerEvent): void {
|
||||
// Only handle left mouse button or touch
|
||||
if (e.button !== 0) return;
|
||||
|
||||
// Don't start drag if clicking on interactive elements or value/editor areas
|
||||
// Only the label area should initiate drag - all controls should be clickable
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.tagName === 'INPUT' ||
|
||||
target.tagName === 'SELECT' ||
|
||||
target.tagName === 'BUTTON' ||
|
||||
target.closest('.inline-control') ||
|
||||
target.closest('.inline-control-container') ||
|
||||
target.closest('.info-value') ||
|
||||
target.closest('.widget-editor') ||
|
||||
target.closest('.widget-editor-container') ||
|
||||
target.closest('.widget-editor-stepper') ||
|
||||
target.closest('.widget-editor-number') ||
|
||||
target.closest('.widget-editor-input')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isDragging = true;
|
||||
this.startX = e.clientX;
|
||||
this.accumulatedDelta = 0;
|
||||
|
||||
// Capture current value
|
||||
const type = this.config.widgetType.toLowerCase();
|
||||
if (type === 'number' || type === 'int' || type === 'float' || type === 'slider') {
|
||||
this.startValue = Number(this.config.currentValue) || 0;
|
||||
} else if (type === 'combo') {
|
||||
const options = this.config.constraints?.options ?? [];
|
||||
const currentStr = String(this.config.currentValue);
|
||||
this.startOptionIndex = options.findIndex(opt => String(opt) === currentStr);
|
||||
if (this.startOptionIndex === -1) this.startOptionIndex = 0;
|
||||
}
|
||||
|
||||
// Add dragging class
|
||||
this.element.classList.add('dragging');
|
||||
|
||||
// Capture pointer for reliable tracking
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
|
||||
// Add document-level listeners
|
||||
document.addEventListener('pointermove', this.boundPointerMove);
|
||||
document.addEventListener('pointerup', this.boundPointerUp);
|
||||
|
||||
e.preventDefault();
|
||||
Logger.debug(`[DragValue] Started drag on ${this.config.widgetName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle pointer move - update value based on drag distance
|
||||
*/
|
||||
private onPointerMove(e: PointerEvent): void {
|
||||
if (!this.isDragging) return;
|
||||
|
||||
const deltaX = e.clientX - this.startX;
|
||||
const type = this.config.widgetType.toLowerCase();
|
||||
|
||||
if (type === 'number' || type === 'int' || type === 'float' || type === 'slider') {
|
||||
this.handleNumberDrag(deltaX);
|
||||
} else if (type === 'combo') {
|
||||
this.handleComboDrag(deltaX);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle number value drag
|
||||
*/
|
||||
private handleNumberDrag(deltaX: number): void {
|
||||
const constraints = this.config.constraints;
|
||||
const isInt = this.config.widgetType.toLowerCase() === 'int';
|
||||
|
||||
// Calculate sensitivity based on type and constraints
|
||||
let sensitivity: number;
|
||||
if (isInt) {
|
||||
sensitivity = DRAG_SENSITIVITY.intPixelsPerUnit;
|
||||
} else {
|
||||
// For floats, adjust based on step
|
||||
const step = constraints?.step ?? 0.1;
|
||||
sensitivity = DRAG_SENSITIVITY.floatPixelsPerTenth / (step / 0.1);
|
||||
}
|
||||
|
||||
// Calculate new value
|
||||
const units = deltaX / sensitivity;
|
||||
const step = constraints?.step ?? (isInt ? 1 : 0.1);
|
||||
let newValue = this.startValue + (units * step);
|
||||
|
||||
// Apply constraints
|
||||
if (constraints?.min !== undefined) {
|
||||
newValue = Math.max(constraints.min, newValue);
|
||||
}
|
||||
if (constraints?.max !== undefined) {
|
||||
newValue = Math.min(constraints.max, newValue);
|
||||
}
|
||||
|
||||
// Round to step precision
|
||||
if (step) {
|
||||
newValue = Math.round(newValue / step) * step;
|
||||
}
|
||||
|
||||
// Round for display
|
||||
if (isInt) {
|
||||
newValue = Math.round(newValue);
|
||||
} else {
|
||||
const precision = constraints?.precision ?? 2;
|
||||
newValue = Number(newValue.toFixed(precision));
|
||||
}
|
||||
|
||||
// Sync the value
|
||||
WidgetSyncManager.syncWidgetValue(this.config.nodeId, this.config.widgetName, newValue);
|
||||
this.config.currentValue = newValue;
|
||||
this.config.onChange?.(newValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle combo value drag - cycle through options
|
||||
*/
|
||||
private handleComboDrag(deltaX: number): void {
|
||||
const options = this.config.constraints?.options ?? [];
|
||||
if (options.length === 0) return;
|
||||
|
||||
this.accumulatedDelta = deltaX;
|
||||
|
||||
// Calculate how many options to move
|
||||
const optionDelta = Math.floor(Math.abs(this.accumulatedDelta) / DRAG_SENSITIVITY.comboThreshold);
|
||||
|
||||
if (optionDelta > 0) {
|
||||
const direction = deltaX > 0 ? 1 : -1;
|
||||
let newIndex = this.startOptionIndex + (optionDelta * direction);
|
||||
|
||||
// Wrap around
|
||||
newIndex = ((newIndex % options.length) + options.length) % options.length;
|
||||
|
||||
const newValue = options[newIndex];
|
||||
WidgetSyncManager.syncWidgetValue(this.config.nodeId, this.config.widgetName, newValue);
|
||||
this.config.currentValue = newValue;
|
||||
this.config.onChange?.(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle pointer up - end drag
|
||||
*/
|
||||
private onPointerUp(e: PointerEvent): void {
|
||||
if (!this.isDragging) return;
|
||||
|
||||
this.isDragging = false;
|
||||
this.element.classList.remove('dragging');
|
||||
|
||||
// Release pointer capture
|
||||
try {
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
} catch {
|
||||
// Ignore if already released
|
||||
}
|
||||
|
||||
// Remove document-level listeners
|
||||
document.removeEventListener('pointermove', this.boundPointerMove);
|
||||
document.removeEventListener('pointerup', this.boundPointerUp);
|
||||
|
||||
Logger.debug(`[DragValue] Ended drag on ${this.config.widgetName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current value (call when value changes externally)
|
||||
*/
|
||||
updateValue(value: unknown): void {
|
||||
this.config.currentValue = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup and remove drag handling
|
||||
*/
|
||||
destroy(): void {
|
||||
this.element.classList.remove('draggable', 'dragging');
|
||||
|
||||
// Remove indicator
|
||||
const indicator = this.element.querySelector('.drag-indicator');
|
||||
if (indicator) {
|
||||
indicator.remove();
|
||||
}
|
||||
|
||||
// Remove document listeners if still attached
|
||||
document.removeEventListener('pointermove', this.boundPointerMove);
|
||||
document.removeEventListener('pointerup', this.boundPointerUp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a widget type supports drag editing
|
||||
*/
|
||||
static isTypeSupported(widgetType: string): boolean {
|
||||
const type = widgetType.toLowerCase();
|
||||
return [
|
||||
'number', 'int', 'float', 'slider', 'combo'
|
||||
].includes(type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* ComfyUI MagnifyGlass - Inline Control Factory
|
||||
*
|
||||
* Creates compact inline controls that render directly in inspector rows
|
||||
* for immediate value editing without entering edit mode.
|
||||
*/
|
||||
|
||||
import { WidgetSyncManager, WidgetConstraints } from './WidgetSyncManager';
|
||||
import { Logger } from '../../shared/logger';
|
||||
|
||||
/**
|
||||
* Configuration for creating an inline control
|
||||
*/
|
||||
export interface InlineControlConfig {
|
||||
nodeId: number;
|
||||
widgetName: string;
|
||||
widgetType: string;
|
||||
currentValue: unknown;
|
||||
constraints?: WidgetConstraints;
|
||||
onChange?: (value: unknown) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for inline control instances
|
||||
*/
|
||||
export interface InlineControlInstance {
|
||||
element: HTMLElement;
|
||||
getValue(): unknown;
|
||||
setValue(value: unknown): void;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline Control Factory
|
||||
* Creates compact inline controls for immediate value editing
|
||||
*/
|
||||
export class InlineControlFactory {
|
||||
/**
|
||||
* Check if a widget type should use inline controls
|
||||
*/
|
||||
static shouldUseInlineControl(widgetType: string): boolean {
|
||||
const type = widgetType.toLowerCase();
|
||||
return type === 'toggle' || type === 'boolean' || type === 'combo';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an inline control for the given configuration
|
||||
*/
|
||||
static createControl(config: InlineControlConfig): InlineControlInstance | null {
|
||||
const type = config.widgetType.toLowerCase();
|
||||
|
||||
switch (type) {
|
||||
case 'toggle':
|
||||
case 'boolean':
|
||||
return this.createInlineToggle(config);
|
||||
case 'combo':
|
||||
return this.createInlineDropdown(config);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create compact inline toggle for boolean values
|
||||
*/
|
||||
private static createInlineToggle(config: InlineControlConfig): InlineControlInstance {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'inline-control inline-toggle';
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.className = 'inline-toggle-switch';
|
||||
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.className = 'inline-toggle-input';
|
||||
// Handle boolean coercion properly
|
||||
const boolValue = config.currentValue === true || config.currentValue === 'true';
|
||||
checkbox.checked = boolValue;
|
||||
|
||||
const slider = document.createElement('span');
|
||||
slider.className = 'inline-toggle-slider';
|
||||
|
||||
// Stop propagation to prevent row click handlers
|
||||
container.addEventListener('click', (e) => e.stopPropagation());
|
||||
|
||||
// Sync on change
|
||||
checkbox.addEventListener('change', () => {
|
||||
WidgetSyncManager.syncWidgetValue(config.nodeId, config.widgetName, checkbox.checked);
|
||||
config.onChange?.(checkbox.checked);
|
||||
Logger.debug(`[InlineControl] Toggle ${config.widgetName}: ${checkbox.checked}`);
|
||||
});
|
||||
|
||||
label.appendChild(checkbox);
|
||||
label.appendChild(slider);
|
||||
container.appendChild(label);
|
||||
|
||||
return {
|
||||
element: container,
|
||||
getValue: () => checkbox.checked,
|
||||
setValue: (v) => { checkbox.checked = Boolean(v); },
|
||||
destroy: () => { container.remove(); }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create compact inline dropdown for combo values
|
||||
*/
|
||||
private static createInlineDropdown(config: InlineControlConfig): InlineControlInstance {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'inline-control inline-dropdown';
|
||||
|
||||
const select = document.createElement('select');
|
||||
select.className = 'inline-dropdown-select';
|
||||
|
||||
// Populate options
|
||||
const options = config.constraints?.options ?? [];
|
||||
const currentValueStr = String(config.currentValue);
|
||||
|
||||
for (const opt of options) {
|
||||
const option = document.createElement('option');
|
||||
const optStr = String(opt);
|
||||
option.value = optStr;
|
||||
option.textContent = optStr;
|
||||
if (optStr === currentValueStr) {
|
||||
option.selected = true;
|
||||
}
|
||||
select.appendChild(option);
|
||||
}
|
||||
|
||||
// Stop propagation to prevent row click handlers
|
||||
container.addEventListener('click', (e) => e.stopPropagation());
|
||||
|
||||
// Sync on change
|
||||
select.addEventListener('change', () => {
|
||||
WidgetSyncManager.syncWidgetValue(config.nodeId, config.widgetName, select.value);
|
||||
config.onChange?.(select.value);
|
||||
Logger.debug(`[InlineControl] Dropdown ${config.widgetName}: ${select.value}`);
|
||||
});
|
||||
|
||||
container.appendChild(select);
|
||||
|
||||
return {
|
||||
element: container,
|
||||
getValue: () => select.value,
|
||||
setValue: (v) => { select.value = String(v); },
|
||||
destroy: () => { container.remove(); }
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -150,9 +150,10 @@ export class WidgetEditorFactory {
|
||||
syncValue();
|
||||
});
|
||||
|
||||
// Handle keyboard
|
||||
// Handle keyboard - Enter to confirm, Escape to cancel
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (e.key === 'Escape' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
input.blur();
|
||||
}
|
||||
});
|
||||
@@ -282,7 +283,8 @@ export class WidgetEditorFactory {
|
||||
select.addEventListener('blur', () => config.onBlur?.());
|
||||
|
||||
select.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (e.key === 'Escape' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
select.blur();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,3 +6,5 @@
|
||||
|
||||
export { WidgetSyncManager, type WidgetConstraints, type SyncResult } from './WidgetSyncManager';
|
||||
export { WidgetEditorFactory, type WidgetEditorConfig, type WidgetEditorInstance } from './WidgetEditorFactory';
|
||||
export { InlineControlFactory, type InlineControlConfig, type InlineControlInstance } from './InlineControlFactory';
|
||||
export { DragValueController, type DragConfig } from './DragValueController';
|
||||
|
||||
+528
-1
@@ -851,4 +851,531 @@
|
||||
height: 14px;
|
||||
stroke-width: 2.5;
|
||||
/* Make icon slightly bolder */
|
||||
}
|
||||
}
|
||||
/* ==========================================================================
|
||||
Widget Editor Styles - Inspector Inline Editing
|
||||
========================================================================== */
|
||||
|
||||
/* Base widget editor container */
|
||||
.widget-editor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Shared input styles */
|
||||
.widget-editor-input,
|
||||
.widget-editor-textarea,
|
||||
.widget-editor-select {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 13px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid var(--info-panel-accent-color, #74b9ff);
|
||||
border-radius: 4px;
|
||||
color: inherit;
|
||||
padding: 6px 8px;
|
||||
outline: none;
|
||||
transition: all 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.widget-editor-input:focus,
|
||||
.widget-editor-textarea:focus,
|
||||
.widget-editor-select:focus {
|
||||
border-color: var(--info-panel-accent-color, #74b9ff);
|
||||
box-shadow: 0 0 0 2px rgba(116, 185, 255, 0.25);
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.widget-editor-input:hover,
|
||||
.widget-editor-textarea:hover,
|
||||
.widget-editor-select:hover {
|
||||
border-color: rgba(116, 185, 255, 0.7);
|
||||
}
|
||||
|
||||
/* Number editor specific */
|
||||
.widget-editor-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.widget-editor-number .widget-editor-input {
|
||||
flex: 1;
|
||||
min-width: 60px;
|
||||
max-width: 100px;
|
||||
text-align: center;
|
||||
appearance: textfield;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.widget-editor-number .widget-editor-input::-webkit-outer-spin-button,
|
||||
.widget-editor-number .widget-editor-input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.widget-editor-stepper {
|
||||
width: 24px;
|
||||
height: 28px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
transition: all 0.15s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.widget-editor-stepper:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: var(--info-panel-accent-color, #74b9ff);
|
||||
}
|
||||
|
||||
.widget-editor-stepper:active {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.widget-editor-stepper.decrement {
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
.widget-editor-stepper.increment {
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
/* Text editor specific */
|
||||
.widget-editor-text {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.widget-editor-text .widget-editor-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.widget-editor-textarea {
|
||||
width: 100%;
|
||||
min-height: 60px;
|
||||
max-height: 200px;
|
||||
resize: vertical;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.widget-editor-counter {
|
||||
font-size: 11px;
|
||||
opacity: 0.6;
|
||||
text-align: right;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
/* Combo/dropdown editor - blue text theme style */
|
||||
.widget-editor-combo {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.widget-editor-select {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
color: var(--info-panel-accent-color, #74b9ff);
|
||||
font-size: 14px;
|
||||
padding: 4px 24px 4px 10px;
|
||||
border-radius: 6px;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2374b9ff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 6px center;
|
||||
background-size: 12px;
|
||||
}
|
||||
|
||||
.widget-editor-select:hover {
|
||||
background-color: rgba(116, 185, 255, 0.12);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.widget-editor-select:focus {
|
||||
background-color: rgba(116, 185, 255, 0.15);
|
||||
border-color: var(--info-panel-accent-color, #74b9ff);
|
||||
box-shadow: 0 0 0 2px rgba(116, 185, 255, 0.2);
|
||||
}
|
||||
|
||||
.widget-editor-select option {
|
||||
background: #1a1a1f;
|
||||
color: var(--info-panel-accent-color, #74b9ff);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Boolean toggle editor */
|
||||
.widget-editor-boolean {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.widget-editor-toggle {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.widget-editor-checkbox {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.widget-editor-toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 24px;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.widget-editor-toggle-slider::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.widget-editor-checkbox:checked+.widget-editor-toggle-slider {
|
||||
background: var(--info-panel-accent-color, #74b9ff);
|
||||
border-color: var(--info-panel-accent-color, #74b9ff);
|
||||
}
|
||||
|
||||
.widget-editor-checkbox:checked+.widget-editor-toggle-slider::before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.widget-editor-checkbox:focus+.widget-editor-toggle-slider {
|
||||
box-shadow: 0 0 0 2px rgba(116, 185, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Slider editor */
|
||||
.widget-editor-slider {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.widget-editor-range {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.widget-editor-range::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--info-panel-accent-color, #74b9ff);
|
||||
cursor: pointer;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.widget-editor-range::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 0 4px rgba(116, 185, 255, 0.25);
|
||||
}
|
||||
|
||||
.widget-editor-range::-moz-range-thumb {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--info-panel-accent-color, #74b9ff);
|
||||
cursor: pointer;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.widget-editor-slider-value {
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: var(--info-panel-accent-color, #74b9ff);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Editable row styling */
|
||||
.info-row.editable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.info-row.editable:hover {
|
||||
background: rgba(116, 185, 255, 0.08);
|
||||
}
|
||||
|
||||
.info-row.editable .info-value {
|
||||
cursor: text;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.info-row.editable .info-value:hover {
|
||||
border-color: rgba(116, 185, 255, 0.3);
|
||||
}
|
||||
|
||||
.info-row.editing {
|
||||
background: rgba(116, 185, 255, 0.1);
|
||||
padding: 8px;
|
||||
margin: 4px -8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.info-row.editing .info-label {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* Hide original value when editing */
|
||||
.info-row.editing .info-value.original {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Inline Controls - Compact controls that render directly in rows
|
||||
========================================================================== */
|
||||
|
||||
.inline-control-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.inline-control {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Inline Toggle - Compact version */
|
||||
.inline-toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.inline-toggle-input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.inline-toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 20px;
|
||||
transition: all 0.25s ease;
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.inline-toggle-slider::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: all 0.25s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.inline-toggle-input:checked+.inline-toggle-slider {
|
||||
background: var(--info-panel-accent-color, #74b9ff);
|
||||
border-color: var(--info-panel-accent-color, #74b9ff);
|
||||
}
|
||||
|
||||
.inline-toggle-input:checked+.inline-toggle-slider::before {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
.inline-toggle-input:focus+.inline-toggle-slider {
|
||||
box-shadow: 0 0 0 2px rgba(116, 185, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Inline Dropdown - blue text theme style */
|
||||
.inline-dropdown {
|
||||
min-width: 100px;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.inline-dropdown-select {
|
||||
width: 100%;
|
||||
padding: 4px 24px 4px 10px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 14px;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
color: var(--info-panel-accent-color, #74b9ff);
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2374b9ff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 6px center;
|
||||
background-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.inline-dropdown-select:hover {
|
||||
background-color: rgba(116, 185, 255, 0.12);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.inline-dropdown-select:focus {
|
||||
outline: none;
|
||||
background-color: rgba(116, 185, 255, 0.15);
|
||||
border-color: var(--info-panel-accent-color, #74b9ff);
|
||||
box-shadow: 0 0 0 2px rgba(116, 185, 255, 0.2);
|
||||
}
|
||||
|
||||
.inline-dropdown-select option {
|
||||
background: #1a1a1f;
|
||||
color: var(--info-panel-accent-color, #74b9ff);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Drag Gesture Indicators
|
||||
========================================================================== */
|
||||
|
||||
.info-row.draggable {
|
||||
position: relative;
|
||||
cursor: ew-resize;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Value container in draggable rows should show text cursor for click-to-edit */
|
||||
.info-row.draggable .info-value {
|
||||
cursor: text;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.info-row.draggable .info-value:hover {
|
||||
background: rgba(116, 185, 255, 0.18);
|
||||
border-color: rgba(116, 185, 255, 0.4);
|
||||
}
|
||||
|
||||
.info-row.draggable::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(116, 185, 255, 0.3) 20%,
|
||||
rgba(116, 185, 255, 0.3) 80%,
|
||||
transparent 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.info-row.draggable:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.info-row.dragging {
|
||||
background: rgba(116, 185, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
.info-row.dragging .info-value {
|
||||
color: var(--info-panel-accent-color, #74b9ff);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.drag-indicator {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 10px;
|
||||
opacity: 0;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.info-row.draggable:hover .drag-indicator {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.info-row.dragging .drag-indicator {
|
||||
opacity: 1;
|
||||
color: var(--info-panel-accent-color, #74b9ff);
|
||||
}
|
||||
|
||||
/* Theme-specific adjustments for inline controls */
|
||||
.magnify-info-panel.theme-light .inline-toggle-slider {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
border-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-light .inline-dropdown-select {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--info-panel-accent-color, #0066cc);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-light .inline-dropdown-select:hover {
|
||||
background-color: rgba(0, 102, 204, 0.08);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-light .inline-dropdown-select option {
|
||||
background: #ffffff;
|
||||
color: var(--info-panel-accent-color, #0066cc);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-github .inline-dropdown-select {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--info-panel-accent-color, #0366d6);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-github .inline-dropdown-select:hover {
|
||||
background-color: rgba(3, 102, 214, 0.08);
|
||||
}
|
||||
|
||||
.magnify-info-panel.theme-github .inline-dropdown-select option {
|
||||
background: #ffffff;
|
||||
color: var(--info-panel-accent-color, #0366d6);
|
||||
}
|
||||
|
||||
+81
-35
@@ -10,6 +10,8 @@ import { getCheckpointInfo, getImageInfo, getTextBoxContent, getImportantNodePar
|
||||
import { NodeSelector } from "./NodeSelector.js";
|
||||
import { WidgetSyncManager } from "./widget-editors/WidgetSyncManager.js";
|
||||
import { WidgetEditorFactory } from "./widget-editors/WidgetEditorFactory.js";
|
||||
import { InlineControlFactory } from "./widget-editors/InlineControlFactory.js";
|
||||
import { DragValueController } from "./widget-editors/DragValueController.js";
|
||||
class UIManager {
|
||||
constructor(stateManager) {
|
||||
__publicField(this, "stateManager");
|
||||
@@ -19,6 +21,10 @@ class UIManager {
|
||||
__publicField(this, "onNodeSelected", null);
|
||||
// Track active widget editors for cleanup
|
||||
__publicField(this, "activeEditors", /* @__PURE__ */ new Map());
|
||||
// Track active inline controls for cleanup
|
||||
__publicField(this, "activeInlineControls", /* @__PURE__ */ new Map());
|
||||
// Track active drag controllers for cleanup
|
||||
__publicField(this, "activeDragControllers", /* @__PURE__ */ new Map());
|
||||
this.stateManager = stateManager;
|
||||
this.elements = {
|
||||
panel: null,
|
||||
@@ -516,9 +522,13 @@ class UIManager {
|
||||
}
|
||||
/**
|
||||
* Display information in the panel.
|
||||
* @param info
|
||||
* @param info
|
||||
*/
|
||||
displayInfo(info) {
|
||||
if (this.activeEditors.size > 0) {
|
||||
this.updateHeaderSubtitle(info);
|
||||
return;
|
||||
}
|
||||
const sections = this.buildSections(info);
|
||||
this.renderSections(sections);
|
||||
this.updateSectionStates();
|
||||
@@ -662,21 +672,12 @@ class UIManager {
|
||||
const rawValueStr = isEditable ? typeof item.rawValue === "boolean" ? item.rawValue ? "true" : "false" : escapeHtml(String(item.rawValue ?? "")) : "";
|
||||
const editableAttrs = isEditable ? `data-editable="true" data-widget-name="${escapeHtml(item.widgetName)}" data-widget-type="${escapeHtml(item.widgetType || "text")}" data-raw-value="${rawValueStr}" data-constraints="${constraintsJson}"` : "";
|
||||
const dropdownIcon = item.clickable && item.clickable !== "zoom" ? '<span class="dropdown-indicator" style="margin-left: 4px; opacity: 0.6; font-size: 10px;">▼</span>' : "";
|
||||
const rawValue = String(item.value || "");
|
||||
const showCopyBtn = !item.clickable && rawValue.length > 3 && typeof item.value === "string";
|
||||
const copyBtnHtml = showCopyBtn ? `
|
||||
<button class="copy-btn" data-copy-value="${escapeHtml(rawValue)}" title="Copy to clipboard" aria-label="Copy to clipboard">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||
</svg>
|
||||
</button>` : "";
|
||||
return `
|
||||
<div class="info-row ${clickableClass} ${editableClass}" ${clickableAttr} ${nodeIdAttr} ${editableAttrs} style="${item.clickable ? "cursor: pointer;" : ""}">
|
||||
<span class="info-label">${escapeHtml(item.label)}</span>
|
||||
<span class="info-value ${valueClass} original" ${valueAttributes}>${value}${dropdownIcon}</span>
|
||||
<div class="inline-control-container" style="display: none;"></div>
|
||||
<div class="widget-editor-container" style="display: none;"></div>
|
||||
${copyBtnHtml}
|
||||
</div>`;
|
||||
}).join("")}
|
||||
</div>
|
||||
@@ -688,7 +689,7 @@ class UIManager {
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Cleanup active widget editors.
|
||||
* Cleanup active widget editors, inline controls, and drag controllers.
|
||||
*/
|
||||
cleanupEditors() {
|
||||
this.activeEditors.forEach((editor) => {
|
||||
@@ -698,9 +699,24 @@ class UIManager {
|
||||
}
|
||||
});
|
||||
this.activeEditors.clear();
|
||||
this.activeInlineControls.forEach((control) => {
|
||||
try {
|
||||
control.destroy();
|
||||
} catch (e) {
|
||||
}
|
||||
});
|
||||
this.activeInlineControls.clear();
|
||||
this.activeDragControllers.forEach((controller) => {
|
||||
try {
|
||||
controller.destroy();
|
||||
} catch (e) {
|
||||
}
|
||||
});
|
||||
this.activeDragControllers.clear();
|
||||
}
|
||||
/**
|
||||
* Attach click handlers to editable widget rows.
|
||||
* Also creates inline controls for toggle/combo widgets and drag controllers.
|
||||
*/
|
||||
attachEditableRowHandlers() {
|
||||
if (!this.elements.content) return;
|
||||
@@ -709,11 +725,59 @@ class UIManager {
|
||||
const rowEl = row;
|
||||
const valueEl = rowEl.querySelector(".info-value.original");
|
||||
const editorContainer = rowEl.querySelector(".widget-editor-container");
|
||||
if (!valueEl || !editorContainer) return;
|
||||
valueEl.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this.enterEditMode(rowEl, valueEl, editorContainer);
|
||||
});
|
||||
const inlineContainer = rowEl.querySelector(".inline-control-container");
|
||||
if (!valueEl) return;
|
||||
const nodeId = parseInt(rowEl.dataset.nodeId || "0", 10);
|
||||
const widgetName = rowEl.dataset.widgetName || "";
|
||||
const widgetType = rowEl.dataset.widgetType || "text";
|
||||
const rawValue = rowEl.dataset.rawValue;
|
||||
let constraints = {};
|
||||
try {
|
||||
const constraintsStr = rowEl.dataset.constraints;
|
||||
if (constraintsStr) {
|
||||
constraints = JSON.parse(constraintsStr);
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
if (isNaN(nodeId) || !widgetName) return;
|
||||
const controlKey = `${nodeId}:${widgetName}`;
|
||||
if (InlineControlFactory.shouldUseInlineControl(widgetType) && inlineContainer) {
|
||||
const control = InlineControlFactory.createControl({
|
||||
nodeId,
|
||||
widgetName,
|
||||
widgetType,
|
||||
currentValue: widgetType.toLowerCase() === "boolean" || widgetType.toLowerCase() === "toggle" ? rawValue === "true" : rawValue,
|
||||
constraints,
|
||||
onChange: (value) => {
|
||||
rowEl.dataset.rawValue = String(value);
|
||||
}
|
||||
});
|
||||
if (control) {
|
||||
this.activeInlineControls.set(controlKey, control);
|
||||
inlineContainer.appendChild(control.element);
|
||||
inlineContainer.style.display = "flex";
|
||||
valueEl.style.display = "none";
|
||||
}
|
||||
} else if (editorContainer) {
|
||||
valueEl.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this.enterEditMode(rowEl, valueEl, editorContainer);
|
||||
});
|
||||
}
|
||||
if (DragValueController.isTypeSupported(widgetType) && !InlineControlFactory.shouldUseInlineControl(widgetType)) {
|
||||
const dragController = new DragValueController(rowEl, {
|
||||
nodeId,
|
||||
widgetName,
|
||||
widgetType,
|
||||
currentValue: rawValue,
|
||||
constraints,
|
||||
onChange: (value) => {
|
||||
rowEl.dataset.rawValue = String(value);
|
||||
valueEl.textContent = formatWidgetValue(value);
|
||||
}
|
||||
});
|
||||
this.activeDragControllers.set(controlKey, dragController);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
@@ -817,24 +881,6 @@ class UIManager {
|
||||
row.style.background = "";
|
||||
});
|
||||
});
|
||||
const copyButtons = this.elements.content.querySelectorAll(".copy-btn");
|
||||
copyButtons.forEach((btn) => {
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const value = btn.dataset.copyValue || "";
|
||||
navigator.clipboard.writeText(value).then(() => {
|
||||
btn.classList.add("copied");
|
||||
const originalHtml = btn.innerHTML;
|
||||
btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="#22c55e" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>`;
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = originalHtml;
|
||||
btn.classList.remove("copied");
|
||||
}, 1500);
|
||||
}).catch((err) => {
|
||||
console.error("Failed to copy:", err);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Update section expansion states.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,197 @@
|
||||
var __defProp = Object.defineProperty;
|
||||
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
||||
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
||||
import { WidgetSyncManager } from "./WidgetSyncManager.js";
|
||||
import { Logger } from "../../shared/logger.js";
|
||||
const DRAG_SENSITIVITY = {
|
||||
// Pixels per unit change for integers
|
||||
intPixelsPerUnit: 8,
|
||||
// Pixels per 0.1 change for floats
|
||||
floatPixelsPerTenth: 10,
|
||||
// Minimum pixels to trigger option cycle for combos
|
||||
comboThreshold: 40
|
||||
};
|
||||
class DragValueController {
|
||||
constructor(element, config) {
|
||||
__publicField(this, "element");
|
||||
__publicField(this, "config");
|
||||
__publicField(this, "isDragging", false);
|
||||
__publicField(this, "startX", 0);
|
||||
__publicField(this, "startValue", 0);
|
||||
__publicField(this, "startOptionIndex", 0);
|
||||
__publicField(this, "accumulatedDelta", 0);
|
||||
// Bound event handlers for cleanup
|
||||
__publicField(this, "boundPointerMove");
|
||||
__publicField(this, "boundPointerUp");
|
||||
this.element = element;
|
||||
this.config = config;
|
||||
this.boundPointerMove = this.onPointerMove.bind(this);
|
||||
this.boundPointerUp = this.onPointerUp.bind(this);
|
||||
this.init();
|
||||
}
|
||||
/**
|
||||
* Initialize drag handling
|
||||
*/
|
||||
init() {
|
||||
this.element.classList.add("draggable");
|
||||
this.element.addEventListener("pointerdown", this.onPointerDown.bind(this));
|
||||
}
|
||||
/**
|
||||
* Add visual drag indicator to the row
|
||||
*/
|
||||
addDragIndicator() {
|
||||
if (this.element.querySelector(".drag-indicator")) return;
|
||||
const indicator = document.createElement("span");
|
||||
indicator.className = "drag-indicator";
|
||||
indicator.innerHTML = "⟷";
|
||||
indicator.setAttribute("aria-hidden", "true");
|
||||
this.element.appendChild(indicator);
|
||||
}
|
||||
/**
|
||||
* Handle pointer down - start drag tracking
|
||||
*/
|
||||
onPointerDown(e) {
|
||||
var _a;
|
||||
if (e.button !== 0) return;
|
||||
const target = e.target;
|
||||
if (target.tagName === "INPUT" || target.tagName === "SELECT" || target.tagName === "BUTTON" || target.closest(".inline-control") || target.closest(".inline-control-container") || target.closest(".info-value") || target.closest(".widget-editor") || target.closest(".widget-editor-container") || target.closest(".widget-editor-stepper") || target.closest(".widget-editor-number") || target.closest(".widget-editor-input")) {
|
||||
return;
|
||||
}
|
||||
this.isDragging = true;
|
||||
this.startX = e.clientX;
|
||||
this.accumulatedDelta = 0;
|
||||
const type = this.config.widgetType.toLowerCase();
|
||||
if (type === "number" || type === "int" || type === "float" || type === "slider") {
|
||||
this.startValue = Number(this.config.currentValue) || 0;
|
||||
} else if (type === "combo") {
|
||||
const options = ((_a = this.config.constraints) == null ? void 0 : _a.options) ?? [];
|
||||
const currentStr = String(this.config.currentValue);
|
||||
this.startOptionIndex = options.findIndex((opt) => String(opt) === currentStr);
|
||||
if (this.startOptionIndex === -1) this.startOptionIndex = 0;
|
||||
}
|
||||
this.element.classList.add("dragging");
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
document.addEventListener("pointermove", this.boundPointerMove);
|
||||
document.addEventListener("pointerup", this.boundPointerUp);
|
||||
e.preventDefault();
|
||||
Logger.debug(`[DragValue] Started drag on ${this.config.widgetName}`);
|
||||
}
|
||||
/**
|
||||
* Handle pointer move - update value based on drag distance
|
||||
*/
|
||||
onPointerMove(e) {
|
||||
if (!this.isDragging) return;
|
||||
const deltaX = e.clientX - this.startX;
|
||||
const type = this.config.widgetType.toLowerCase();
|
||||
if (type === "number" || type === "int" || type === "float" || type === "slider") {
|
||||
this.handleNumberDrag(deltaX);
|
||||
} else if (type === "combo") {
|
||||
this.handleComboDrag(deltaX);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handle number value drag
|
||||
*/
|
||||
handleNumberDrag(deltaX) {
|
||||
var _a, _b;
|
||||
const constraints = this.config.constraints;
|
||||
const isInt = this.config.widgetType.toLowerCase() === "int";
|
||||
let sensitivity;
|
||||
if (isInt) {
|
||||
sensitivity = DRAG_SENSITIVITY.intPixelsPerUnit;
|
||||
} else {
|
||||
const step2 = (constraints == null ? void 0 : constraints.step) ?? 0.1;
|
||||
sensitivity = DRAG_SENSITIVITY.floatPixelsPerTenth / (step2 / 0.1);
|
||||
}
|
||||
const units = deltaX / sensitivity;
|
||||
const step = (constraints == null ? void 0 : constraints.step) ?? (isInt ? 1 : 0.1);
|
||||
let newValue = this.startValue + units * step;
|
||||
if ((constraints == null ? void 0 : constraints.min) !== void 0) {
|
||||
newValue = Math.max(constraints.min, newValue);
|
||||
}
|
||||
if ((constraints == null ? void 0 : constraints.max) !== void 0) {
|
||||
newValue = Math.min(constraints.max, newValue);
|
||||
}
|
||||
if (step) {
|
||||
newValue = Math.round(newValue / step) * step;
|
||||
}
|
||||
if (isInt) {
|
||||
newValue = Math.round(newValue);
|
||||
} else {
|
||||
const precision = (constraints == null ? void 0 : constraints.precision) ?? 2;
|
||||
newValue = Number(newValue.toFixed(precision));
|
||||
}
|
||||
WidgetSyncManager.syncWidgetValue(this.config.nodeId, this.config.widgetName, newValue);
|
||||
this.config.currentValue = newValue;
|
||||
(_b = (_a = this.config).onChange) == null ? void 0 : _b.call(_a, newValue);
|
||||
}
|
||||
/**
|
||||
* Handle combo value drag - cycle through options
|
||||
*/
|
||||
handleComboDrag(deltaX) {
|
||||
var _a, _b, _c;
|
||||
const options = ((_a = this.config.constraints) == null ? void 0 : _a.options) ?? [];
|
||||
if (options.length === 0) return;
|
||||
this.accumulatedDelta = deltaX;
|
||||
const optionDelta = Math.floor(Math.abs(this.accumulatedDelta) / DRAG_SENSITIVITY.comboThreshold);
|
||||
if (optionDelta > 0) {
|
||||
const direction = deltaX > 0 ? 1 : -1;
|
||||
let newIndex = this.startOptionIndex + optionDelta * direction;
|
||||
newIndex = (newIndex % options.length + options.length) % options.length;
|
||||
const newValue = options[newIndex];
|
||||
WidgetSyncManager.syncWidgetValue(this.config.nodeId, this.config.widgetName, newValue);
|
||||
this.config.currentValue = newValue;
|
||||
(_c = (_b = this.config).onChange) == null ? void 0 : _c.call(_b, newValue);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handle pointer up - end drag
|
||||
*/
|
||||
onPointerUp(e) {
|
||||
if (!this.isDragging) return;
|
||||
this.isDragging = false;
|
||||
this.element.classList.remove("dragging");
|
||||
try {
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
} catch {
|
||||
}
|
||||
document.removeEventListener("pointermove", this.boundPointerMove);
|
||||
document.removeEventListener("pointerup", this.boundPointerUp);
|
||||
Logger.debug(`[DragValue] Ended drag on ${this.config.widgetName}`);
|
||||
}
|
||||
/**
|
||||
* Update the current value (call when value changes externally)
|
||||
*/
|
||||
updateValue(value) {
|
||||
this.config.currentValue = value;
|
||||
}
|
||||
/**
|
||||
* Cleanup and remove drag handling
|
||||
*/
|
||||
destroy() {
|
||||
this.element.classList.remove("draggable", "dragging");
|
||||
const indicator = this.element.querySelector(".drag-indicator");
|
||||
if (indicator) {
|
||||
indicator.remove();
|
||||
}
|
||||
document.removeEventListener("pointermove", this.boundPointerMove);
|
||||
document.removeEventListener("pointerup", this.boundPointerUp);
|
||||
}
|
||||
/**
|
||||
* Check if a widget type supports drag editing
|
||||
*/
|
||||
static isTypeSupported(widgetType) {
|
||||
const type = widgetType.toLowerCase();
|
||||
return [
|
||||
"number",
|
||||
"int",
|
||||
"float",
|
||||
"slider",
|
||||
"combo"
|
||||
].includes(type);
|
||||
}
|
||||
}
|
||||
export {
|
||||
DragValueController
|
||||
};
|
||||
//# sourceMappingURL=DragValueController.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,106 @@
|
||||
import { WidgetSyncManager } from "./WidgetSyncManager.js";
|
||||
import { Logger } from "../../shared/logger.js";
|
||||
class InlineControlFactory {
|
||||
/**
|
||||
* Check if a widget type should use inline controls
|
||||
*/
|
||||
static shouldUseInlineControl(widgetType) {
|
||||
const type = widgetType.toLowerCase();
|
||||
return type === "toggle" || type === "boolean" || type === "combo";
|
||||
}
|
||||
/**
|
||||
* Create an inline control for the given configuration
|
||||
*/
|
||||
static createControl(config) {
|
||||
const type = config.widgetType.toLowerCase();
|
||||
switch (type) {
|
||||
case "toggle":
|
||||
case "boolean":
|
||||
return this.createInlineToggle(config);
|
||||
case "combo":
|
||||
return this.createInlineDropdown(config);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Create compact inline toggle for boolean values
|
||||
*/
|
||||
static createInlineToggle(config) {
|
||||
const container = document.createElement("div");
|
||||
container.className = "inline-control inline-toggle";
|
||||
const label = document.createElement("label");
|
||||
label.className = "inline-toggle-switch";
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.className = "inline-toggle-input";
|
||||
const boolValue = config.currentValue === true || config.currentValue === "true";
|
||||
checkbox.checked = boolValue;
|
||||
const slider = document.createElement("span");
|
||||
slider.className = "inline-toggle-slider";
|
||||
container.addEventListener("click", (e) => e.stopPropagation());
|
||||
checkbox.addEventListener("change", () => {
|
||||
var _a;
|
||||
WidgetSyncManager.syncWidgetValue(config.nodeId, config.widgetName, checkbox.checked);
|
||||
(_a = config.onChange) == null ? void 0 : _a.call(config, checkbox.checked);
|
||||
Logger.debug(`[InlineControl] Toggle ${config.widgetName}: ${checkbox.checked}`);
|
||||
});
|
||||
label.appendChild(checkbox);
|
||||
label.appendChild(slider);
|
||||
container.appendChild(label);
|
||||
return {
|
||||
element: container,
|
||||
getValue: () => checkbox.checked,
|
||||
setValue: (v) => {
|
||||
checkbox.checked = Boolean(v);
|
||||
},
|
||||
destroy: () => {
|
||||
container.remove();
|
||||
}
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Create compact inline dropdown for combo values
|
||||
*/
|
||||
static createInlineDropdown(config) {
|
||||
var _a;
|
||||
const container = document.createElement("div");
|
||||
container.className = "inline-control inline-dropdown";
|
||||
const select = document.createElement("select");
|
||||
select.className = "inline-dropdown-select";
|
||||
const options = ((_a = config.constraints) == null ? void 0 : _a.options) ?? [];
|
||||
const currentValueStr = String(config.currentValue);
|
||||
for (const opt of options) {
|
||||
const option = document.createElement("option");
|
||||
const optStr = String(opt);
|
||||
option.value = optStr;
|
||||
option.textContent = optStr;
|
||||
if (optStr === currentValueStr) {
|
||||
option.selected = true;
|
||||
}
|
||||
select.appendChild(option);
|
||||
}
|
||||
container.addEventListener("click", (e) => e.stopPropagation());
|
||||
select.addEventListener("change", () => {
|
||||
var _a2;
|
||||
WidgetSyncManager.syncWidgetValue(config.nodeId, config.widgetName, select.value);
|
||||
(_a2 = config.onChange) == null ? void 0 : _a2.call(config, select.value);
|
||||
Logger.debug(`[InlineControl] Dropdown ${config.widgetName}: ${select.value}`);
|
||||
});
|
||||
container.appendChild(select);
|
||||
return {
|
||||
element: container,
|
||||
getValue: () => select.value,
|
||||
setValue: (v) => {
|
||||
select.value = String(v);
|
||||
},
|
||||
destroy: () => {
|
||||
container.remove();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
export {
|
||||
InlineControlFactory
|
||||
};
|
||||
//# sourceMappingURL=InlineControlFactory.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -100,7 +100,8 @@ class WidgetEditorFactory {
|
||||
syncValue();
|
||||
});
|
||||
input.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
if (e.key === "Escape" || e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
input.blur();
|
||||
}
|
||||
});
|
||||
@@ -231,7 +232,8 @@ class WidgetEditorFactory {
|
||||
return (_a2 = config.onBlur) == null ? void 0 : _a2.call(config);
|
||||
});
|
||||
select.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
if (e.key === "Escape" || e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
select.blur();
|
||||
}
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user