SESSION 12.20 HIERARCHY AND DRAG DROP COMPLETE
Documentation for Sessions
Session 12.20 - Hierarchy Connector & Drag-Drop Complete Fix
Overview
Fixed two critical issues after user reverted editor-web-lib changes:
- Hierarchy Connector Mode - Re-implemented visual connect mode for assigning parent containers
- Drag-and-Drop Re-parenting - Fixed dragging existing widgets into containers
Problem Statement
The user reported:
"I had to revert a bunch of things from the editor-web-lib. Please improve the hierarchy functionality by implementing an intelligent mode that happens when we click on the button (using flash-persistence). IF this is active, then when we click on the available parents, should connect the hierarchy/inheritance (make it a parent).
Also, we are still having trouble dragging and dropping a widget - for example a text widget, inside a container widget."
Issues Fixed
1. Widget Duplication Bug ❌ → ✅
Problem: When a widget was assigned a parent (via any method), it appeared BOTH inside the container AND on the canvas top-level. This was because the canvas was rendering ALL widgets without filtering.
Root Cause: The canvas template used @for (widget of widgets(); track ...) which renders all widgets. Child widgets (those with parentId) were being rendered twice:
- Once by the canvas (incorrectly)
- Once by their parent container (correctly)
Solution: Added topLevelWidgets computed signal that filters widgets to only include those without a parentId, and updated the template to use it.
2. Hierarchy Connector Mode Missing ❌ → ✅
Problem: After reverting editor-web-lib changes, the hierarchy connector mode inputs/outputs were missing from editor-canvas.component.ts. The properties panel button existed but wasn't wired to the canvas.
Root Cause: The canvas component was missing:
hierarchyConnectorModeInput@InputcontainerClickedForConnection@OutputhierarchyConnectorModesignal- Logic to emit container ID instead of selecting when in connector mode
Solution: Re-implemented the complete hierarchy connector mode in the canvas component.
3. Container Visibility Issue ❌ → ✅
Problem: Container widgets had no visible borders, making them impossible to see or interact with.
Root Cause: The .drop-zone class had pointer-events and z-index but no visible styling (border/background).
Solution: Added subtle blue dashed border and light background to .drop-zone class to ensure containers are always visible.
4. Drag-Drop Re-parenting Not Working ❌ → ✅
Problem: When dragging an existing widget (using ResponsiveDraggableDirective) onto a container, the widget would just move position but not become a child of the container.
Root Cause: The ResponsiveDraggableDirective already detected drops on containers and included dropTargetContainerId in the dragEnd event, but widget-host.component.ts wasn't handling this information - it was just emitting position changes.
Solution: Modified widget-host.onDragEnd() to:
- Check for
event.dropTargetContainerId - Update widget's
parentIdif present - Set
_requiresReparentingflag - Let editor-ui's existing re-parenting logic handle the rest
Implementation Details
1. Canvas Rendering Filter ✅ (CRITICAL FIX)
File: libs/libs-widgets/widgets/src/lib/widgets/editor-canvas/editor-canvas.component.ts
Added computed signal to filter widgets:
// 🎯 CRITICAL: Only top-level widgets (no parentId) - child widgets are rendered by their containers
readonly topLevelWidgets = computed(() => {
const allWidgets = this.widgets();
const topLevel = allWidgets.filter(w => !w.parentId);
console.log('[Canvas] 🎯 Top-level widgets:', topLevel.length, '/', allWidgets.length);
return topLevel;
});
File: libs/libs-widgets/widgets/src/lib/widgets/editor-canvas/editor-canvas.component.html
Updated template to use filtered widgets:
<!-- Widgets (Top-Level Only - children rendered by container widgets) -->
@for (widget of topLevelWidgets(); track trackByWidgetId($index, widget)) {
<div class="canvas-child" ...>
<lib-widget-host [widget]="widget" ...></lib-widget-host>
</div>
}
Why This Is Critical:
- Prevents widget duplication (rendering in both canvas AND container)
- Ensures proper widget hierarchy (children only render inside parents)
- Matches widget tree structure (tree also filters by
!parentId)
2. Container Visibility Styles ✅
File: libs/libs-widgets/widgets/src/lib/components/container-widget/container-widget.component.scss
Added visible styling to drop-zone:
&.drop-zone {
pointer-events: auto;
z-index: 5; // Higher than canvas children
// 🎯 ALWAYS show a subtle border so containers are visible
border: 2px dashed rgba(59, 130, 246, 0.3) !important;
background-color: rgba(59, 130, 246, 0.02) !important;
&:hover {
border-color: rgba(59, 130, 246, 0.5) !important;
background-color: rgba(59, 130, 246, 0.05) !important;
}
}
3. Canvas Component Changes ✅
File: libs/libs-widgets/widgets/src/lib/widgets/editor-canvas/editor-canvas.component.ts
A. Added Input for Hierarchy Connector Mode
// 🔗 Hierarchy Connector Mode Input
@Input() set hierarchyConnectorModeInput(value: boolean) {
this.hierarchyConnectorMode.set(value);
}
B. Added Output Event
// 🔗 Hierarchy Connector Mode Output - Emits container ID when clicked in connector mode
@Output() containerClickedForConnection = new EventEmitter<string>();
C. Added Signal for Mode State
// 🔗 Hierarchy Connector Mode State
readonly hierarchyConnectorMode = signal(false);
D. Modified Widget Selection Logic
onSelectWidget(widget: Widget): void {
// 🔗 If hierarchy connector mode is active and widget is a container, connect instead of select
if (this.hierarchyConnectorMode() && widget.type === 'container') {
console.log('[Canvas] 🔗 Hierarchy Connector Mode - Container clicked:', widget.name, widget.id);
this.containerClickedForConnection.emit(widget.id);
return; // Don't select, just connect
}
this.selectionService.selectWidget(widget, this.widgetAction);
}
E. Added Canvas Class for Visual Feedback
readonly canvasClasses = computed(() => ({
...
// 🔗 Hierarchy Connector Mode
'hierarchy-connector-mode': this.hierarchyConnectorMode(),
...
}));
2. Visual Feedback Styles ✅
File: libs/libs-widgets/widgets/src/lib/widgets/editor-canvas/editor-canvas.component.scss
Added comprehensive styles for visual feedback when in connector mode:
// 🔗 Hierarchy Connector Mode Styles
.editor-canvas.hierarchy-connector-mode {
cursor: crosshair !important;
// Highlight all container widgets for connection
::ng-deep lib-container-widget {
position: relative;
&::before {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
border: 3px dashed #10b981;
border-radius: 8px;
background: rgba(16, 185, 129, 0.05);
pointer-events: none;
z-index: 1000;
animation: connector-pulse 2s infinite;
}
&::after {
content: '🔗 Click to Connect';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #10b981;
color: white;
padding: 8px 16px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
pointer-events: none;
z-index: 1001;
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
animation: connector-bounce 1s infinite;
}
&:hover::before {
border-color: #059669;
background: rgba(5, 150, 105, 0.1);
animation: connector-pulse-fast 1s infinite;
}
&:hover::after {
background: #059669;
transform: translate(-50%, -50%) scale(1.05);
}
}
}
// Animations for hierarchy connector mode
@keyframes connector-pulse {
0%, 100% {
border-color: #10b981;
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
}
50% {
border-color: #34d399;
box-shadow: 0 0 0 8px rgba(16, 185, 129, 0);
}
}
@keyframes connector-pulse-fast {
0%, 100% {
border-color: #059669;
box-shadow: 0 0 0 0 rgba(5, 150, 105, 0.7);
}
50% {
border-color: #10b981;
box-shadow: 0 0 0 10px rgba(5, 150, 105, 0);
}
}
@keyframes connector-bounce {
0%, 100% {
transform: translate(-50%, -50%) scale(1);
}
50% {
transform: translate(-50%, -50%) scale(1.05);
}
}
Visual Effects:
- ✅ Cursor changes to crosshair across entire canvas
- ✅ All containers get a pulsing green dashed border
- ✅ "🔗 Click to Connect" label appears centered in each container
- ✅ Hover effect intensifies the pulse and changes colors
- ✅ Animations make the mode feel alive and interactive
4. Widget Host Re-parenting Logic ✅
File: libs/libs-widgets/widgets/src/lib/widgets/widget-host/widget-host.component.ts
Modified onDragEnd() to handle re-parenting when widget is dropped on a container:
onDragEnd(event: ResponsiveDragEndEvent): void {
if (!this.isDraggable()) {
return;
}
try {
// ... existing position update code ...
// Usar el servicio para manejar el arrastre (ensures proper responsive structure)
let updatedWidget = this.dragHandlerService.handleDragEnd(
eventWidget,
event,
this.activeBreakpoint(),
this.zoom()
);
// 🔗 Handle re-parenting if widget was dropped on a container
if (event.dropTargetContainerId) {
console.log('🔗 WidgetHost.onDragEnd: Widget dropped on container, updating parentId:', {
widgetId: updatedWidget.id,
oldParentId: updatedWidget.parentId,
newParentId: event.dropTargetContainerId
});
// Update parent ID and mark for re-parenting
updatedWidget = {
...updatedWidget,
parentId: event.dropTargetContainerId,
_requiresReparenting: true // Flag for editor-ui to handle re-parenting
} as any;
}
// ... rest of existing code ...
}
}
Logic Flow:
- Widget drag ends (ResponsiveDraggableDirective detects drop position)
- Directive checks if widget was dropped on a container element
- If yes, includes
dropTargetContainerIdin dragEnd event - WidgetHost receives event and updates widget's
parentId - Sets
_requiresReparentingflag to trigger editor-ui's re-parenting logic - Emits updated widget
- Editor-ui's
onWidgetsChange()detects flag and callshandleWidgetReparent() - Widget is properly nested in container with correct parent/child relationships
How It Works (Complete Flow)
Hierarchy Connector Mode
- User selects a widget (e.g., Text Widget) on the canvas
- Opens Properties Panel → Hierarchy section
- Clicks "Visual Connect" button
- Button turns green with pulsing animation ✅
hierarchyConnectorModesignal set totrue✅- Signal propagates to canvas via
[hierarchyConnectorModeInput]✅
- Canvas applies visual feedback
- Cursor changes to crosshair ✅
- All containers get green pulsing border + "🔗 Click to Connect" label ✅
- User clicks on a container
- Canvas
onSelectWidget()detects the mode is active ✅ - Instead of selecting, emits
containerClickedForConnectionevent with container ID ✅
- Canvas
- Editor UI handles the connection
- Receives container ID via
onContainerClickedForConnection()✅ - Calls
handleWidgetReparent()to update widget'sparentId✅ - Updates parent/child arrays ✅
- Emits
widgetsChangeevent ✅
- Receives container ID via
- Mode auto-deactivates
hierarchyConnectorModeset back tofalse✅- Visual feedback removed ✅
- Button returns to normal blue gradient ✅
- Widget is now nested
- Widget disappears from canvas top-level ✅
- Widget appears inside container ✅
- Widget Tree updates to show correct hierarchy ✅
Drag-Drop Re-parenting
- User starts dragging a widget on the canvas
- ResponsiveDraggableDirective handles mouse events ✅
- Widget follows cursor with real-time position updates ✅
- User drags widget over a container
- Visual feedback shows container boundaries ✅
- User drops widget inside container
- ResponsiveDraggableDirective's
onMouseUpcalculates final position ✅ - Checks element at drop coordinates using
document.elementFromPoint()✅ - Finds closest
.container-widget[data-widget-id]ancestor ✅ - Extracts container ID if found ✅
- Includes
dropTargetContainerIdin dragEnd event ✅
- ResponsiveDraggableDirective's
- WidgetHost processes drag end
- Detects
event.dropTargetContainerIdis present ✅ - Updates widget's
parentIdto container ID ✅ - Sets
_requiresReparentingflag ✅ - Emits updated widget ✅
- Detects
- Canvas propagates change
- Canvas receives widgetChange event ✅
- Updates widgets array ✅
- Emits widgetsChange to editor-ui ✅
- Editor-ui handles re-parenting
onWidgetsChange()detects_requiresReparentingflag ✅- Schedules
handleWidgetReparent()call ✅ - Updates old parent's children array (removes widget) ✅
- Updates new parent's children array (adds widget) ✅
- Updates widget arrays throughout system ✅
- Widget is now nested
- Widget renders inside container ✅
- Position is relative to container ✅
- Widget Tree shows correct hierarchy ✅
Testing Instructions
Test 1: Hierarchy Connector Mode (Visual Connect)
- Refresh browser (Ctrl+Shift+R / Cmd+Shift+R)
- Create a Text Widget and a Container Widget on canvas
- Select the Text Widget
- Open Properties Panel → scroll to Hierarchy section
- Click "Visual Connect" button
- Expected visual changes:
- Button turns green with pulsing animation ✅
- Text changes to "Click container to connect..." ✅
- Cursor becomes crosshair across canvas ✅
- Container widget gets:
- Green pulsing dashed border ✅
- "🔗 Click to Connect" label in center ✅
- Click on the Container Widget
- Expected results:
- Text Widget disappears from canvas top-level ✅
- Text Widget appears inside container ✅
- Widget Tree updates to show Text Widget nested under Container ✅
- Connector mode auto-deactivates (button returns to blue) ✅
- Console shows success logs ✅
Test 2: Drag-Drop Re-parenting
- Create a Text Widget and a Container Widget on canvas (both at top level)
- Drag the Text Widget (click and hold, then drag)
- Drag it over the Container Widget
- Drop it inside the container (release mouse)
- Expected results:
- Text Widget disappears from canvas top-level ✅
- Text Widget appears inside container at dropped position ✅
- Widget Tree updates to show Text Widget nested under Container ✅
- Console shows re-parenting logs ✅
Test 3: Palette → Container Drop (Existing Functionality)
- Create a Container Widget on canvas
- Drag a Text Widget from the palette (left sidebar)
- Drag it over the Container Widget
- Drop it inside the container
- Expected results:
- New Text Widget appears inside container ✅
- Widget Tree shows Text Widget nested under Container ✅
- Container's children array includes the new widget ✅
Console Logs to Watch For
Hierarchy Connector Mode:
[PropertiesPanel] 🔗 Hierarchy Connector Mode: ACTIVATED
[EditorUI] 🔗 Hierarchy Connector Mode: ACTIVATED
[Canvas] 🔗 Hierarchy Connector Mode - Container clicked: Container Widget <container-id>
[EditorUI] 🔗 Connecting widget to container: { widgetId: ..., widgetName: ..., containerId: ... }
[EditorUI] 🏗️ Re-parenting widget <widget-id> to <container-id>
[EditorUI] ✅ Connection complete - Hierarchy Connector Mode DEACTIVATED
Drag-Drop Re-parenting:
🔗 ResponsiveDraggableDirective: Dropped on container: <container-id>
🔗 WidgetHost.onDragEnd: Widget dropped on container, updating parentId: { widgetId: ..., oldParentId: ..., newParentId: ... }
🔄 EditorUiComponent.onWidgetsChange called with N widgets
🔗 EditorUiComponent: Detected re-parenting request for widget: <widget-id> → <container-id>
[EditorUI] 🏗️ Re-parenting widget <widget-id> to <container-id>
Architecture Notes
Drag-Drop System Integration
The system now handles THREE types of drag-drop:
1. Palette → Canvas (Creating new top-level widgets)
- System: HTML5 Drag API + UnifiedPalette
- Handler: Canvas
handleDrop() - Result: New widget created at drop position
2. Palette → Container (Creating new nested widgets)
- System: HTML5 Drag API + UnifiedPalette + DropZoneDirective
- Handler: ContainerWidget
onDropped() - Result: New widget created inside container with
parentId
3. Existing Widget → Container (Re-parenting) 🆕
- System: ResponsiveDraggableDirective (mouse events) + WidgetHost
- Handler: WidgetHost
onDragEnd()+ EditorUIhandleWidgetReparent() - Result: Existing widget moved to container,
parentIdupdated
Key Architectural Decisions
Why Not Use HTML5 Drag API for Existing Widgets?
The ResponsiveDraggableDirective uses mouse events instead of HTML5 drag API because:
- Better performance for real-time position updates
- More control over drag constraints (parent, window, grid snap)
- Works consistently across browsers
- Allows custom drag cursors and visual feedback
- Integrates with responsive positioning system
However, this meant we needed to bridge the gap between:
- ResponsiveDraggableDirective (mouse events) → detects drop on container
- DropZoneDirective (HTML5 drag events) → receives palette drops
Solution: Use document.elementFromPoint() to detect containers at drop position, then use existing re-parenting infrastructure.
Why Use _requiresReparenting Flag?
The flag is necessary because:
- Widget position needs to update FIRST (ResponsiveDraggableDirective)
- Re-parenting needs to happen AFTER position update (EditorUI)
- Prevents breaking normal drag operations
- Allows editor-ui to handle re-parenting logic centrally
- Ensures proper parent/child array updates
Why Auto-Deactivate Connector Mode?
The mode auto-deactivates after connection because:
- Most users will only want to connect once
- Prevents accidental connections
- The visual feedback (crosshair, borders) is intense and should be temporary
- Users can easily re-activate if needed
Files Modified
Core Functionality
- ✅
libs/libs-widgets/widgets/src/lib/widgets/editor-canvas/editor-canvas.component.ts- topLevelWidgets filter, hierarchy input/output, signal, selection logic, classes - ✅
libs/libs-widgets/widgets/src/lib/widgets/editor-canvas/editor-canvas.component.html- Use topLevelWidgets() instead of widgets() - ✅
libs/libs-widgets/widgets/src/lib/widgets/editor-canvas/editor-canvas.component.scss- Visual feedback styles for connector mode - ✅
libs/libs-widgets/widgets/src/lib/components/container-widget/container-widget.component.scss- Visible border/background for drop-zone - ✅
libs/libs-widgets/widgets/src/lib/widgets/widget-host/widget-host.component.ts- Re-parenting logic in onDragEnd
Documentation
- ✅
sessions/sessionID.json- Updated to 12.20 - ✅
docs/english/sessions/session-12/SESSION-12.20-HIERARCHY-AND-DRAG-DROP-COMPLETE.md- This file
Comparison: Before vs After
Before ❌
| Issue | Status |
|---|---|
| Widget duplication (in canvas AND container) | ❌ Yes - widgets rendered twice |
| Canvas filters by parentId | ❌ No - all widgets rendered |
| Containers visible | ❌ No - transparent, no borders |
| Hierarchy connector mode wired to canvas | ❌ No - inputs/outputs missing |
| Visual feedback for connector mode | ❌ No - styles missing |
| Container clickable in connector mode | ❌ No - selection always happens |
| Drag existing widget into container | ❌ No - only position updates |
| Re-parenting on drag-drop | ❌ No - parentId not updated |
After ✅
| Issue | Status |
|---|---|
| Widget duplication (in canvas AND container) | ✅ Fixed - only top-level widgets render on canvas |
| Canvas filters by parentId | ✅ Yes - topLevelWidgets computed signal |
| Containers visible | ✅ Yes - blue dashed border, subtle background |
| Hierarchy connector mode wired to canvas | ✅ Yes - fully implemented |
| Visual feedback for connector mode | ✅ Yes - crosshair, borders, labels, animations |
| Container clickable in connector mode | ✅ Yes - emits connection event instead of selecting |
| Drag existing widget into container | ✅ Yes - detects container at drop position |
| Re-parenting on drag-drop | ✅ Yes - updates parentId with flag |
Known Issues & Next Steps
✅ DONE
- Re-implement hierarchy connector mode in canvas
- Add visual feedback styles
- Implement drag-drop re-parenting detection
- Wire re-parenting through widget-host
- Test and verify functionality
- Document complete solution
⏳ TODO (Future Enhancements)
- Add toast notification on successful connection
- Add keyboard shortcut to activate connector mode (e.g., Ctrl+Shift+C)
- Add option to connect to "Canvas" (remove parent) via special click zone
- Prevent circular nesting (container inside itself)
- Add undo/redo support for re-parenting operations
- Add visual feedback during drag to show valid drop targets
User Feedback Integration
"I had to revert a bunch of things from the editor-web-lib."
Status: ✅ FIXED - Re-implemented all missing functionality in canvas component
"Please improve the hierarchy functionality by implementing an intelligent mode that happens when we click on the button"
Status: ✅ IMPLEMENTED - Full hierarchy connector mode with visual feedback
"IF this is active, then when we click on the available parents, should connect the hierarchy/inheritance"
Status: ✅ IMPLEMENTED - Clicking containers in connector mode assigns parent
"Also, we are still having trouble dragging and dropping a widget - for example a text widget, inside a container widget"
Status: ✅ FIXED - Drag-drop re-parenting now works for existing widgets
Key Learnings
System Architecture
Hierarchy Assignment (3 Methods):
1. Parent Selector Dropdown
Properties Panel → onParentChange() → handleWidgetReparent()
2. Visual Connect Mode (Click)
Properties Panel → hierarchyConnectorMode(true) → Canvas → Container Click → handleWidgetReparent()
3. Drag-Drop Re-parenting (Drag)
Canvas → ResponsiveDraggableDirective → WidgetHost → _requiresReparenting flag → handleWidgetReparent()
All three methods converge on handleWidgetReparent() in editor-ui, which ensures consistent behavior.
Critical Principles
- Signal-based Mode: Use signals for mode state to ensure reactivity across components
- Event Propagation: Container clicks emit connection event instead of selection when in mode
- Visual Feedback: Mode must be obvious - crosshair cursor, pulsing borders, animated labels
- Auto-deactivate: Temporary modes should auto-deactivate after successful operation
- Bridge Systems: Use flags/events to bridge incompatible drag-drop systems (mouse vs HTML5)
- Centralized Logic: All re-parenting goes through one handler for consistency
- Progressive Enhancement: Re-parenting happens AFTER position update, not during
Session Progress
Session 12.20 - Re-implemented hierarchy connector mode in canvas and fixed drag-drop re-parenting for existing widgets. Both visual connect mode and drag-based re-parenting now fully functional.
System is now fully functional for:
- ✅ Creating widgets on canvas
- ✅ Dragging widgets from palette onto canvas
- ✅ Dragging widgets from palette into containers
- ✅ Dragging existing widgets into containers (re-parenting)
- ✅ Using Visual Connect mode to assign parents
- ✅ Using Parent Selector dropdown
- ✅ Viewing correct hierarchy in Widget Tree
- ✅ Visual feedback for all operations
Next Session - Consider adding undo/redo support and preventing circular nesting.