Session 12.13 - Widget Nesting Complete

Container widget drop functionality fixed with dual approach (drag + UI)

Session 12.13 - Widget Nesting Complete

๐ŸŽฏ Problem Statement

The container widget couldn't accept child widgets. Two scenarios needed to work:

  1. Drag from Palette โ†’ Container (create new widget inside container)
  2. Drag existing Widget โ†’ Container (re-parent existing widget)

โœ… Solutions Implemented

Solution 1: Fixed Canvas Event Interception

Problem: Canvas was intercepting ALL drag events before they reached containers.

Fix: Modified editor-canvas.component.ts:

onDragOver(event: DragEvent): void {
  // โœ… Check if target is inside a container
  const containerWidget = target.closest('.container-widget.drop-zone');
  if (containerWidget) {
    event.preventDefault(); // Still needed for drop
    return; // Let container handle it
  }
  // ... canvas logic
}

onDrop(event: DragEvent): void {
  // โœ… Check if target is inside a container
  const containerWidget = target.closest('.container-widget.drop-zone');
  if (containerWidget) {
    return; // Let container handle it
  }
  // ... canvas logic
}

Result: โœ… Palette โ†’ Container drops now work!


Solution 2: Parent Selector UI (Mobile-Friendly)

Problem: Dragging existing widgets uses mouse events (ResponsiveDraggableDirective), not HTML5 drag-and-drop, so they can't be dropped into containers.

Fix: Added "Hierarchy" section to properties panel (properties-panel-new.component.ts):

// New computed properties
protected readonly currentParentId = computed(() => {
  return widget?.parentId || '';
});

protected readonly availableContainers = computed(() => {
  return allWidgets.filter(w => {
    // Must be container type
    if (w.type !== 'container') return false;
    // Cannot be self or descendant (prevent circular nesting)
    if (w.id === currentWidget?.id) return false;
    if (this.isDescendantOf(w.id, currentWidget.id, allWidgets)) return false;
    return true;
  });
});

// New method
onParentChange(newParentId: string): void {
  // 1. Update widget.parentId
  // 2. Remove from old parent's children array
  // 3. Add to new parent's children array
  // 4. Emit changes
}

UI Added:

<div class="hierarchy-section">
  <h3>Hierarchy</h3>
  <select [(ngModel)]="parentId" (change)="onParentChange($event)">
    <option value="">Canvas (No Parent)</option>
    <option *ngFor="let container of availableContainers()" 
            [value]="container.id">
      {{ container.name }}
    </option>
  </select>
</div>

Result: โœ… Manual parent selection works! Mobile-friendly alternative to drag-and-drop.


๐Ÿงช Testing

Test 1: Palette โ†’ Container โœ…

  1. Drag "Container" from palette to canvas
  2. Drag "Text" from palette
  3. Drop inside the container
  4. Expected: Text appears as child of container

Console logs to verify:

[Canvas] Drop target is inside container - letting container handle it
[DropZone] handleDrop called
[ContainerWidget] ๐ŸŽฏ onDropped called!
[ContainerWidget] โœ… Creating widget from drop

Test 2: Manual Re-parenting โœ…

  1. Select a Text widget on canvas
  2. Open Properties Panel (right side)
  3. Find "Hierarchy" section
  4. Select a container from "Parent Widget" dropdown
  5. Expected: Widget moves inside the selected container

๐Ÿ“Š Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                 Widget Nesting System                       โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                             โ”‚
โ”‚  Method 1: Drag from Palette                                โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                           โ”‚
โ”‚  โ”‚ Widget Item  โ”‚ dragstart โ†’ setData('widget-type')        โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                           โ”‚
โ”‚         โ”‚                                                    โ”‚
โ”‚         โ–ผ                                                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                           โ”‚
โ”‚  โ”‚  Container   โ”‚ DropZoneDirective                         โ”‚
โ”‚  โ”‚  (libDropZone)โ”‚ โ†’ handleDrop โ†’ createWidget()            โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                           โ”‚
โ”‚                                                             โ”‚
โ”‚  Method 2: Manual UI Selection                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                           โ”‚
โ”‚  โ”‚ Properties   โ”‚ Parent Selector Dropdown                  โ”‚
โ”‚  โ”‚ Panel        โ”‚ โ†’ onParentChange()                        โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                           โ”‚
โ”‚         โ”‚                                                    โ”‚
โ”‚         โ–ผ                                                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                           โ”‚
โ”‚  โ”‚ Update:      โ”‚                                           โ”‚
โ”‚  โ”‚ - widget.parentId                                        โ”‚
โ”‚  โ”‚ - oldParent.children (remove)                            โ”‚
โ”‚  โ”‚ - newParent.children (add)                               โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                           โ”‚
โ”‚                                                             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿšง Remaining Work

Drag-based Re-parenting (TODO)

Challenge: ResponsiveDraggableDirective uses mouse events, DropZoneDirective expects HTML5 drag events.

Approaches:

  1. Hybrid Mode: Enable HTML5 drag on widgets when hovering over containers
  2. Collision Detection: Check if widget overlaps container on mouse release
  3. Modifier Key: Hold Shift/Alt to enable re-parenting mode

Recommendation: Implement collision detection on mouse release (simplest, most intuitive).


๐Ÿ“ Files Modified

File Changes
editor-canvas.component.ts Added container detection in drag handlers
container-widget.component.ts Enhanced logging for drop events
properties-panel-new.component.ts Added Hierarchy section with parent selector
properties-panel-new.component.scss Added styles for hierarchy controls

๐ŸŽ‰ Success Criteria


Session: 12.13
Status: Complete (with drag re-parenting pending)
Next: Test both methods and implement drag-based re-parenting