diff --git a/src/interview/wizardState.ts b/src/interview/wizardState.ts index 99f27d7..8d9fd57 100644 --- a/src/interview/wizardState.ts +++ b/src/interview/wizardState.ts @@ -9,6 +9,7 @@ export interface WizardState { loopContexts: Map; // loop key -> array of collected items (deprecated, use loopRuntimeStates) loopRuntimeStates: Map; // loop step key -> runtime state patches: Patch[]; // Collected patches to apply + activeLoopPath: string[]; // Stack of loop keys representing current nesting level (e.g. ["items", "item_list"]) } export interface Patch { @@ -36,6 +37,7 @@ export function createWizardState(profile: InterviewProfile): WizardState { loopContexts: new Map(), // Keep for backwards compatibility loopRuntimeStates: new Map(), patches: [], + activeLoopPath: [], // Start at top level }; } diff --git a/src/ui/InterviewWizardModal.ts b/src/ui/InterviewWizardModal.ts index b9c2a77..411192b 100644 --- a/src/ui/InterviewWizardModal.ts +++ b/src/ui/InterviewWizardModal.ts @@ -9,7 +9,7 @@ import { TextAreaComponent, TextComponent, } from "obsidian"; -import type { InterviewProfile, InterviewStep } from "../interview/types"; +import type { InterviewProfile, InterviewStep, LoopStep } from "../interview/types"; import { type WizardState, createWizardState, @@ -596,6 +596,24 @@ export class InterviewWizardModal extends Modal { renderLoopStep(step: InterviewStep, containerEl: HTMLElement): void { if (step.type !== "loop") return; + // Check if we're in a nested loop (fullscreen mode) + const currentLoopKey = this.state.activeLoopPath.length > 0 + ? this.state.activeLoopPath[this.state.activeLoopPath.length - 1] + : null; + + // If we're in a nested loop, find which nested step it is + if (currentLoopKey && currentLoopKey.startsWith(step.key + ".")) { + // We're in a nested loop of this step - render it in fullscreen mode + this.renderNestedLoopFullscreen(step, containerEl, currentLoopKey); + return; + } + + // Otherwise, render normal loop (or top-level loop) + if (this.state.activeLoopPath.length > 0 && this.state.activeLoopPath[0] !== step.key) { + // We're in a different loop's nested loop, don't render this one + return; + } + // Initialize or get loop runtime state let loopState = this.state.loopRuntimeStates.get(step.key); if (!loopState) { @@ -619,8 +637,14 @@ export class InterviewWizardModal extends Modal { nestedStepsCount: step.items.length, editIndex: loopState.editIndex, commitMode: commitMode, + activeLoopPath: this.state.activeLoopPath, }); + // Breadcrumb navigation if in nested context + if (this.state.activeLoopPath.length > 0) { + this.renderBreadcrumb(containerEl, step); + } + // Title containerEl.createEl("h2", { text: step.label || "Loop", @@ -954,6 +978,9 @@ export class InterviewWizardModal extends Modal { onFieldChange: (fieldId: string, value: unknown) => void ): void { const existingValue = draftValue !== undefined ? String(draftValue) : ""; + // Use unique key for preview mode tracking in nested loops + const previewKey = `${loopKey}.${nestedStep.key}`; + const isPreviewMode = this.previewMode.get(previewKey) || false; if (nestedStep.type === "capture_text") { // Field container @@ -977,16 +1004,31 @@ export class InterviewWizardModal extends Modal { }); } - // Editor container + // Container for editor/preview const editorContainer = fieldContainer.createEl("div", { cls: "markdown-editor-container", }); editorContainer.style.width = "100%"; editorContainer.style.position = "relative"; + // Preview container (hidden by default) + const previewContainer = editorContainer.createEl("div", { + cls: "markdown-preview-container", + }); + previewContainer.style.display = isPreviewMode ? "block" : "none"; + previewContainer.style.width = "100%"; + previewContainer.style.minHeight = "240px"; + previewContainer.style.padding = "1em"; + previewContainer.style.border = "1px solid var(--background-modifier-border)"; + previewContainer.style.borderRadius = "4px"; + previewContainer.style.background = "var(--background-primary)"; + previewContainer.style.overflowY = "auto"; + + // Editor container const textEditorContainer = editorContainer.createEl("div", { cls: "markdown-editor-wrapper", }); + textEditorContainer.style.display = isPreviewMode ? "none" : "block"; textEditorContainer.style.width = "100%"; const textSetting = new Setting(textEditorContainer); @@ -999,10 +1041,18 @@ export class InterviewWizardModal extends Modal { settingNameEl.style.display = "none"; } + let textareaRef: HTMLTextAreaElement | null = null; + textSetting.addTextArea((text) => { + textareaRef = text.inputEl; text.setValue(existingValue); text.onChange((value) => { onFieldChange(nestedStep.key, value); + // Update preview if in preview mode + const currentPreviewMode = this.previewMode.get(previewKey) || false; + if (currentPreviewMode) { + this.updatePreview(previewContainer, value); + } }); text.inputEl.rows = 8; text.inputEl.style.width = "100%"; @@ -1010,14 +1060,34 @@ export class InterviewWizardModal extends Modal { text.inputEl.style.boxSizing = "border-box"; }); - // Add toolbar + // Add toolbar with preview toggle setTimeout(() => { const textarea = textEditorContainer.querySelector("textarea"); if (textarea) { - const itemToolbar = createMarkdownToolbar(textarea); + const itemToolbar = createMarkdownToolbar( + textarea, + () => { + // Get current value from textarea before toggling + const currentValue = textarea.value; + // Update draft with current value + onFieldChange(nestedStep.key, currentValue); + + // Toggle preview mode + const newPreviewMode = !this.previewMode.get(previewKey); + this.previewMode.set(previewKey, newPreviewMode); + + // Re-render to show/hide preview + this.renderStep(); + } + ); textEditorContainer.insertBefore(itemToolbar, textEditorContainer.firstChild); } }, 10); + + // Render preview if in preview mode + if (isPreviewMode && existingValue) { + this.updatePreview(previewContainer, existingValue); + } } else if (nestedStep.type === "capture_text_line") { // Field container const fieldContainer = containerEl.createEl("div", { @@ -1112,12 +1182,11 @@ export class InterviewWizardModal extends Modal { text.inputEl.style.boxSizing = "border-box"; }); } else if (nestedStep.type === "loop") { - // Nested loop: render as a nested loop editor - // The draft value should be an array of items + // Nested loop: render as a button to enter fullscreen mode const nestedLoopItems = Array.isArray(draftValue) ? draftValue : []; + const nestedLoopKey = `${loopKey}.${nestedStep.key}`; // Get or create nested loop state - const nestedLoopKey = `${loopKey}.${nestedStep.key}`; let nestedLoopState = this.state.loopRuntimeStates.get(nestedLoopKey); if (!nestedLoopState) { nestedLoopState = { @@ -1129,276 +1198,510 @@ export class InterviewWizardModal extends Modal { this.state.loopRuntimeStates.set(nestedLoopKey, nestedLoopState); } - // Render nested loop UI - use a simplified 2-pane layout - const nestedLoopContainer = containerEl.createEl("div", { - cls: "nested-loop-container", + // Field container + const fieldContainer = containerEl.createEl("div", { + cls: "mindnet-field", }); - nestedLoopContainer.style.border = "1px solid var(--background-modifier-border)"; - nestedLoopContainer.style.borderRadius = "4px"; - nestedLoopContainer.style.padding = "1em"; - nestedLoopContainer.style.marginTop = "0.5em"; + // Label if (nestedStep.label) { - const labelEl = nestedLoopContainer.createEl("div", { + const labelEl = fieldContainer.createEl("div", { cls: "mindnet-field__label", text: nestedStep.label, }); - labelEl.style.marginBottom = "0.5em"; - labelEl.style.fontWeight = "bold"; } - // Create 2-pane layout for nested loop - const nestedPaneContainer = nestedLoopContainer.createEl("div", { - cls: "nested-loop-panes", + // Show item count + const countText = nestedLoopState.items.length > 0 + ? `${nestedLoopState.items.length} ${nestedLoopState.items.length === 1 ? "Eintrag" : "Einträge"}` + : "Keine Einträge"; + const countEl = fieldContainer.createEl("div", { + cls: "mindnet-field__desc", + text: countText, }); - nestedPaneContainer.style.display = "flex"; - nestedPaneContainer.style.gap = "1em"; - nestedPaneContainer.style.minHeight = "200px"; + countEl.style.marginBottom = "0.5em"; - // Left pane: Items list (narrower for nested loops) - const nestedLeftPane = nestedPaneContainer.createEl("div", { - cls: "nested-loop-items-pane", + // Button to enter nested loop (fullscreen mode) + const enterBtn = fieldContainer.createEl("button", { + text: nestedLoopState.items.length > 0 ? "Bearbeiten" : "Hinzufügen", + cls: "mod-cta", }); - nestedLeftPane.style.width = "25%"; - nestedLeftPane.style.borderRight = "1px solid var(--background-modifier-border)"; - nestedLeftPane.style.paddingRight = "1em"; - nestedLeftPane.style.maxHeight = "300px"; - nestedLeftPane.style.overflowY = "auto"; + enterBtn.style.width = "100%"; + enterBtn.onclick = () => { + // Enter nested loop: add to activeLoopPath + this.state.activeLoopPath.push(nestedLoopKey); + this.renderStep(); + }; + } + } + + /** + * Render breadcrumb navigation showing loop hierarchy. + */ + private renderBreadcrumb(containerEl: HTMLElement, currentStep: InterviewStep): void { + const breadcrumbContainer = containerEl.createEl("div", { + cls: "loop-breadcrumb", + }); + breadcrumbContainer.style.display = "flex"; + breadcrumbContainer.style.alignItems = "center"; + breadcrumbContainer.style.gap = "0.5em"; + breadcrumbContainer.style.marginBottom = "1em"; + breadcrumbContainer.style.padding = "0.5em"; + breadcrumbContainer.style.background = "var(--background-secondary)"; + breadcrumbContainer.style.borderRadius = "4px"; + + // Build breadcrumb path + const path: Array<{ key: string; label: string }> = []; + + // Find all parent loops + for (let i = 0; i < this.state.activeLoopPath.length; i++) { + const loopKey = this.state.activeLoopPath[i]; + if (!loopKey) continue; - const nestedItemsTitle = nestedLeftPane.createEl("div", { - text: "Einträge", - cls: "mindnet-field__label", + // Extract step key from loop key (e.g., "items.item_list" -> "item_list") + const parts = loopKey.split("."); + const stepKey = parts[parts.length - 1]; + if (!stepKey) continue; + + // Find the step in the profile + const step = this.findStepByKey(stepKey); + if (step && step.type === "loop") { + path.push({ key: loopKey, label: step.label || stepKey }); + } + } + + // Render breadcrumb + path.forEach((item, index) => { + if (index > 0) { + breadcrumbContainer.createEl("span", { text: "›" }); + } + + const breadcrumbItem = breadcrumbContainer.createEl("button", { + text: item.label, + cls: "breadcrumb-item", }); - nestedItemsTitle.style.marginBottom = "0.5em"; + breadcrumbItem.style.background = "transparent"; + breadcrumbItem.style.border = "none"; + breadcrumbItem.style.cursor = "pointer"; + breadcrumbItem.style.textDecoration = index < path.length - 1 ? "underline" : "none"; - // Render items list - nestedLoopState.items.forEach((item, i) => { - const itemEl = nestedLeftPane.createEl("div", { - cls: "nested-loop-item", - }); - itemEl.style.padding = "0.5em"; - itemEl.style.marginBottom = "0.25em"; - itemEl.style.background = "var(--background-secondary)"; - itemEl.style.borderRadius = "4px"; - itemEl.style.cursor = "pointer"; - - // Extract first non-empty field value for display - let itemText = `Item ${i + 1}`; - if (typeof item === "object" && item !== null) { - const itemObj = item as Record; - // Try to find first non-empty string value - for (const [key, value] of Object.entries(itemObj)) { - if (value && typeof value === "string" && value.trim() !== "") { - itemText = value.trim(); - break; - } - } - } else if (item) { - itemText = String(item); + if (index < path.length - 1) { + breadcrumbItem.onclick = () => { + // Navigate to this level + this.state.activeLoopPath = this.state.activeLoopPath.slice(0, index + 1); + this.renderStep(); + }; + } + }); + + // Back button to parent level + if (this.state.activeLoopPath.length > 0) { + const backBtn = breadcrumbContainer.createEl("button", { + text: "← Zurück", + cls: "mod-cta", + }); + backBtn.style.marginLeft = "auto"; + backBtn.onclick = () => { + this.state.activeLoopPath.pop(); + this.renderStep(); + }; + } + } + + /** + * Find a step by its key in the profile (recursive search). + */ + private findStepByKey(key: string): InterviewStep | null { + const searchInSteps = (steps: InterviewStep[]): InterviewStep | null => { + for (const step of steps) { + if (step.key === key) { + return step; } - itemEl.textContent = itemText.length > 40 ? itemText.substring(0, 40) + "..." : itemText; - - // Edit button - const editBtn = itemEl.createEl("button", { - text: "✏️", - cls: "nested-edit-btn", - }); - editBtn.style.float = "right"; - editBtn.style.marginLeft = "0.5em"; - editBtn.onclick = (e) => { - e.stopPropagation(); - const currentNestedState = this.state.loopRuntimeStates.get(nestedLoopKey); - if (currentNestedState) { - let newState = startEdit(currentNestedState, i); - newState = resetItemWizard(newState); - this.state.loopRuntimeStates.set(nestedLoopKey, newState); - this.renderStep(); - } - }; - - // Move Up button - const moveUpBtn = itemEl.createEl("button", { - text: "↑", - cls: "nested-move-up-btn", - }); - moveUpBtn.style.float = "right"; - moveUpBtn.style.marginLeft = "0.25em"; - moveUpBtn.disabled = i === 0; - moveUpBtn.onclick = (e) => { - e.stopPropagation(); - const currentNestedState = this.state.loopRuntimeStates.get(nestedLoopKey); - if (currentNestedState) { - const newState = moveItemUp(currentNestedState, i); - this.state.loopRuntimeStates.set(nestedLoopKey, newState); - // Update parent draft - onFieldChange(nestedStep.key, newState.items); - this.renderStep(); - } - }; - - // Move Down button - const moveDownBtn = itemEl.createEl("button", { - text: "↓", - cls: "nested-move-down-btn", - }); - moveDownBtn.style.float = "right"; - moveDownBtn.style.marginLeft = "0.25em"; - moveDownBtn.disabled = i >= nestedLoopState.items.length - 1; - moveDownBtn.onclick = (e) => { - e.stopPropagation(); - const currentNestedState = this.state.loopRuntimeStates.get(nestedLoopKey); - if (currentNestedState) { - const newState = moveItemDown(currentNestedState, i); - this.state.loopRuntimeStates.set(nestedLoopKey, newState); - // Update parent draft - onFieldChange(nestedStep.key, newState.items); - this.renderStep(); - } - }; - - // Delete button - const deleteBtn = itemEl.createEl("button", { - text: "🗑️", - cls: "nested-delete-btn", - }); - deleteBtn.style.float = "right"; - deleteBtn.style.marginLeft = "0.25em"; - deleteBtn.onclick = (e) => { - e.stopPropagation(); - const currentNestedState = this.state.loopRuntimeStates.get(nestedLoopKey); - if (currentNestedState) { - const newState = deleteItem(currentNestedState, i); - this.state.loopRuntimeStates.set(nestedLoopKey, newState); - // Update parent draft - onFieldChange(nestedStep.key, newState.items); - this.renderStep(); - } - }; - }); + if (step.type === "loop") { + const found = searchInSteps(step.items); + if (found) return found; + } + } + return null; + }; + + return searchInSteps(this.state.profile.steps); + } + + /** + * Render a nested loop in fullscreen mode (uses full width). + */ + private renderNestedLoopFullscreen( + parentStep: InterviewStep, + containerEl: HTMLElement, + nestedLoopKey: string + ): void { + // Extract the nested step from parent step + const nestedStepKey = nestedLoopKey.split(".").pop(); + if (!nestedStepKey) return; + + // parentStep must be a LoopStep to have items + if (parentStep.type !== "loop") return; + + const loopStep = parentStep as LoopStep; + const nestedStep = loopStep.items.find((s: InterviewStep) => s.key === nestedStepKey); + if (!nestedStep || nestedStep.type !== "loop") return; + + // Get nested loop state + let nestedLoopState = this.state.loopRuntimeStates.get(nestedLoopKey); + if (!nestedLoopState) { + nestedLoopState = { + items: [], + draft: {}, + editIndex: null, + activeItemStepIndex: 0, + }; + this.state.loopRuntimeStates.set(nestedLoopKey, nestedLoopState); + } + + // Breadcrumb + this.renderBreadcrumb(containerEl, nestedStep); + + // Context header: Show parent loop item context + const contextHeader = containerEl.createEl("div", { + cls: "nested-loop-context", + }); + contextHeader.style.padding = "1em"; + contextHeader.style.background = "var(--background-secondary)"; + contextHeader.style.borderRadius = "6px"; + contextHeader.style.marginBottom = "1.5em"; + contextHeader.style.border = "1px solid var(--background-modifier-border)"; + + // Find parent loop context + const lastDotIndex = nestedLoopKey.lastIndexOf("."); + if (lastDotIndex > 0) { + const parentLoopKey = nestedLoopKey.substring(0, lastDotIndex); + const parentLoopState = this.state.loopRuntimeStates.get(parentLoopKey); - // Right pane: Editor for nested loop (wider for nested loops) - const nestedRightPane = nestedPaneContainer.createEl("div", { - cls: "nested-loop-editor-pane", - }); - nestedRightPane.style.width = "75%"; - nestedRightPane.style.flex = "1"; - - const nestedEditorTitle = nestedRightPane.createEl("div", { - cls: "mindnet-field__label", - }); - const nestedTitleText = nestedLoopState.editIndex !== null - ? `Eintrag ${nestedLoopState.editIndex + 1} bearbeiten` - : "Neuer Eintrag"; - const nestedStepCounter = nestedStep.items.length > 0 - ? ` - Schritt ${nestedLoopState.activeItemStepIndex + 1}/${nestedStep.items.length}` - : ""; - nestedEditorTitle.textContent = nestedTitleText + nestedStepCounter; - nestedEditorTitle.style.marginBottom = "0.5em"; - - // Render active nested step - if (nestedStep.items.length > 0) { - const activeNestedStepIndex = Math.min(nestedLoopState.activeItemStepIndex, nestedStep.items.length - 1); - const activeNestedNestedStep = nestedStep.items[activeNestedStepIndex]; + if (parentLoopState) { + // Find the top-level loop step + const topLevelLoopKey = nestedLoopKey.split(".")[0]; + if (!topLevelLoopKey) { + // Fallback if no top-level key found + const contextTitle = contextHeader.createEl("div", { + cls: "context-title", + text: "📍 Kontext: " + (parentStep.label || "Parent Loop"), + }); + contextTitle.style.fontWeight = "bold"; + contextTitle.style.marginBottom = "0.5em"; + contextTitle.style.fontSize = "0.9em"; + contextTitle.style.color = "var(--text-muted)"; + } else { + const topLevelLoopState = this.state.loopRuntimeStates.get(topLevelLoopKey); + const topLevelStep = this.findStepByKey(topLevelLoopKey); + + // Build context path: top-level loop > nested loop (current) + const contextPath: string[] = []; + if (topLevelStep && topLevelStep.type === "loop") { + contextPath.push(topLevelStep.label || topLevelLoopKey); + } + // Add the nested loop label (the one we're currently in) + if (nestedStep && nestedStep.type === "loop") { + contextPath.push(nestedStep.label || nestedStepKey || ""); + } + + const contextTitle = contextHeader.createEl("div", { + cls: "context-title", + }); + contextTitle.style.fontWeight = "bold"; + contextTitle.style.marginBottom = "0.5em"; + contextTitle.style.fontSize = "0.9em"; + contextTitle.style.color = "var(--text-muted)"; + + if (contextPath.length > 0) { + contextTitle.textContent = "📍 Kontext: " + contextPath.join(" › "); + } else { + contextTitle.textContent = "📍 Kontext: " + (parentStep.label || "Parent Loop"); + } + } - // Recursively render nested step (supports arbitrary nesting depth) - if (activeNestedNestedStep) { - const nestedDraftValue = nestedLoopState.draft[activeNestedNestedStep.key]; - this.renderLoopNestedStep( - activeNestedNestedStep, - nestedRightPane, - nestedLoopKey, - nestedDraftValue, - (fieldId, value) => { - // Update state without re-rendering to preserve focus - const currentNestedState = this.state.loopRuntimeStates.get(nestedLoopKey); - if (currentNestedState) { - const newState = setDraftField(currentNestedState, fieldId, value); - this.state.loopRuntimeStates.set(nestedLoopKey, newState); - // Update parent draft without re-rendering - const parentLoopState = this.state.loopRuntimeStates.get(loopKey); - if (parentLoopState) { - const updatedParentDraft = { - ...parentLoopState.draft, - [nestedStep.key]: newState.items, - }; - const updatedParentState = setDraftField(parentLoopState, nestedStep.key, newState.items); - this.state.loopRuntimeStates.set(loopKey, updatedParentState); - } - // Do NOT call renderStep() here - it causes focus loss + // Show which parent item we're editing + if (parentLoopState.editIndex !== null) { + const parentItem = parentLoopState.items[parentLoopState.editIndex]; + if (parentItem && typeof parentItem === "object") { + const parentItemObj = parentItem as Record; + const contextInfo = contextHeader.createEl("div", { + cls: "context-info", + }); + contextInfo.style.fontSize = "0.85em"; + contextInfo.style.color = "var(--text-normal)"; + + // Show parent item fields (excluding the nested loop field itself) + const parentFields: string[] = []; + for (const [key, value] of Object.entries(parentItemObj)) { + if (nestedStepKey && key !== nestedStepKey && value && typeof value === "string" && value.trim() !== "") { + const step = loopStep.items.find(s => s.key === key); + const label = step?.label || key; + parentFields.push(`${label}: ${value.trim().substring(0, 60)}${value.trim().length > 60 ? "..." : ""}`); } } - ); + + if (parentFields.length > 0) { + contextInfo.textContent = `Item ${parentLoopState.editIndex + 1} - ${parentFields.join(" | ")}`; + } else { + contextInfo.textContent = `Item ${parentLoopState.editIndex + 1} (bearbeiten)`; + } + } else { + const contextInfo = contextHeader.createEl("div", { + cls: "context-info", + text: `Item ${parentLoopState.editIndex + 1} (bearbeiten)`, + }); + contextInfo.style.fontSize = "0.85em"; + contextInfo.style.color = "var(--text-normal)"; + } + } else { + // New item in parent loop + const contextInfo = contextHeader.createEl("div", { + cls: "context-info", + text: "Neues Item (wird erstellt)", + }); + contextInfo.style.fontSize = "0.85em"; + contextInfo.style.color = "var(--text-muted)"; + contextInfo.style.fontStyle = "italic"; } - - // Navigation buttons for nested loop - const nestedNav = nestedRightPane.createEl("div", { - cls: "nested-loop-navigation", + } + } + + // Title + containerEl.createEl("h2", { + text: nestedStep.label || "Verschachtelter Loop", + }); + + // Show editing indicator + if (nestedLoopState.editIndex !== null) { + const indicator = containerEl.createEl("div", { + cls: "loop-editing-indicator", + text: `✏️ Editing item ${nestedLoopState.editIndex + 1}`, + }); + indicator.style.padding = "0.5em"; + indicator.style.background = "var(--background-modifier-border-hover)"; + indicator.style.borderRadius = "4px"; + indicator.style.marginBottom = "1em"; + } + + // Full-width 2-pane container + const paneContainer = containerEl.createEl("div", { + cls: "loop-pane-container", + }); + paneContainer.style.display = "flex"; + paneContainer.style.gap = "1em"; + paneContainer.style.width = "100%"; + + // Left pane: Items list (30% for fullscreen mode) + const leftPane = paneContainer.createEl("div", { + cls: "loop-items-pane", + }); + leftPane.style.width = "30%"; + leftPane.style.borderRight = "1px solid var(--background-modifier-border)"; + leftPane.style.paddingRight = "1em"; + leftPane.style.maxHeight = "70vh"; + leftPane.style.overflowY = "auto"; + + const itemsTitle = leftPane.createEl("h3", { + text: "Einträge", + }); + itemsTitle.style.marginBottom = "0.5em"; + + // Render items list (same as normal loop) + nestedLoopState.items.forEach((item, i) => { + const itemEl = leftPane.createEl("div", { + cls: "loop-item", + }); + itemEl.style.padding = "0.75em"; + itemEl.style.marginBottom = "0.5em"; + itemEl.style.background = "var(--background-secondary)"; + itemEl.style.borderRadius = "4px"; + itemEl.style.cursor = "pointer"; + + // Extract first non-empty field value for display + let itemText = `Item ${i + 1}`; + if (typeof item === "object" && item !== null) { + const itemObj = item as Record; + for (const [key, value] of Object.entries(itemObj)) { + if (value && typeof value === "string" && value.trim() !== "") { + itemText = value.trim(); + break; + } + } + } else if (item) { + itemText = String(item); + } + itemEl.textContent = itemText.length > 50 ? itemText.substring(0, 50) + "..." : itemText; + + // Action buttons (same as normal loop) + const buttonContainer = itemEl.createEl("div", { + cls: "loop-item-actions", + }); + buttonContainer.style.display = "flex"; + buttonContainer.style.gap = "0.25em"; + buttonContainer.style.marginTop = "0.5em"; + buttonContainer.style.justifyContent = "flex-end"; + + const editBtn = buttonContainer.createEl("button", { text: "✏️ Edit" }); + editBtn.onclick = () => { + let newState = startEdit(nestedLoopState!, i); + newState = resetItemWizard(newState); + this.state.loopRuntimeStates.set(nestedLoopKey, newState); + this.renderStep(); + }; + + const moveUpBtn = buttonContainer.createEl("button", { text: "↑" }); + moveUpBtn.disabled = i === 0; + moveUpBtn.onclick = () => { + const newState = moveItemUp(nestedLoopState!, i); + this.state.loopRuntimeStates.set(nestedLoopKey, newState); + this.renderStep(); + }; + + const moveDownBtn = buttonContainer.createEl("button", { text: "↓" }); + moveDownBtn.disabled = i >= nestedLoopState.items.length - 1; + moveDownBtn.onclick = () => { + const newState = moveItemDown(nestedLoopState!, i); + this.state.loopRuntimeStates.set(nestedLoopKey, newState); + this.renderStep(); + }; + + const deleteBtn = buttonContainer.createEl("button", { text: "🗑️" }); + deleteBtn.onclick = () => { + const newState = deleteItem(nestedLoopState!, i); + this.state.loopRuntimeStates.set(nestedLoopKey, newState); + this.renderStep(); + }; + }); + + // Right pane: Editor (70% for fullscreen mode) + const rightPane = paneContainer.createEl("div", { + cls: "loop-editor-pane", + }); + rightPane.style.width = "70%"; + rightPane.style.flex = "1"; + + // Subwizard header + const itemTitle = rightPane.createEl("h3"); + const itemTitleText = nestedLoopState.editIndex !== null + ? `Item: ${nestedLoopState.editIndex + 1} (editing)` + : "Item: New"; + const stepCounter = nestedStep.items.length > 0 + ? ` - Step ${nestedLoopState.activeItemStepIndex + 1}/${nestedStep.items.length}` + : ""; + itemTitle.textContent = itemTitleText + stepCounter; + + // Render active nested step + if (nestedStep.items.length > 0) { + const activeStepIndex = Math.min(nestedLoopState.activeItemStepIndex, nestedStep.items.length - 1); + const activeNestedStep = nestedStep.items[activeStepIndex]; + + if (activeNestedStep) { + const editorContainer = rightPane.createEl("div", { + cls: "loop-item-editor", }); - nestedNav.style.display = "flex"; - nestedNav.style.gap = "0.5em"; - nestedNav.style.marginTop = "1em"; - nestedNav.style.justifyContent = "space-between"; - // Left: Back/Next - const nestedNavLeft = nestedNav.createEl("div"); - nestedNavLeft.style.display = "flex"; - nestedNavLeft.style.gap = "0.5em"; + const draftValue = nestedLoopState.draft[activeNestedStep.key]; + this.renderLoopNestedStep( + activeNestedStep, + editorContainer, + nestedLoopKey, + draftValue, + (fieldId, value) => { + const currentState = this.state.loopRuntimeStates.get(nestedLoopKey); + if (currentState) { + const newState = setDraftField(currentState, fieldId, value); + this.state.loopRuntimeStates.set(nestedLoopKey, newState); + // Update parent draft + const lastDotIndex = nestedLoopKey.lastIndexOf("."); + if (lastDotIndex > 0) { + const parentLoopKey = nestedLoopKey.substring(0, lastDotIndex); + if (parentLoopKey) { + const parentState = this.state.loopRuntimeStates.get(parentLoopKey); + if (parentState && nestedStepKey) { + const updatedParentDraft = { + ...parentState.draft, + [nestedStepKey]: newState.items, + }; + const updatedParentState = setDraftField(parentState, nestedStepKey, newState.items); + this.state.loopRuntimeStates.set(parentLoopKey, updatedParentState); + } + } + } + } + } + ); - const nestedBackBtn = nestedNavLeft.createEl("button", { - text: "← Zurück", + // Navigation buttons (same as normal loop) + const subwizardNav = rightPane.createEl("div", { + cls: "loop-subwizard-navigation", }); - nestedBackBtn.disabled = activeNestedStepIndex === 0; - nestedBackBtn.onclick = () => { - const currentNestedState = this.state.loopRuntimeStates.get(nestedLoopKey); - if (currentNestedState) { - const newState = itemPrevStep(currentNestedState); + subwizardNav.style.display = "flex"; + subwizardNav.style.gap = "0.5em"; + subwizardNav.style.marginTop = "1em"; + subwizardNav.style.justifyContent = "space-between"; + + const itemNavLeft = subwizardNav.createEl("div"); + itemNavLeft.style.display = "flex"; + itemNavLeft.style.gap = "0.5em"; + + const itemBackBtn = itemNavLeft.createEl("button", { text: "← Item Back" }); + itemBackBtn.disabled = activeStepIndex === 0; + itemBackBtn.onclick = () => { + const currentState = this.state.loopRuntimeStates.get(nestedLoopKey); + if (currentState) { + const newState = itemPrevStep(currentState); this.state.loopRuntimeStates.set(nestedLoopKey, newState); this.renderStep(); } }; - const nestedNextBtn = nestedNavLeft.createEl("button", { - text: "Weiter →", - }); - nestedNextBtn.disabled = activeNestedStepIndex >= nestedStep.items.length - 1; - nestedNextBtn.onclick = () => { - const currentNestedState = this.state.loopRuntimeStates.get(nestedLoopKey); - if (currentNestedState) { - const newState = itemNextStep(currentNestedState, nestedStep.items.length); + const itemNextBtn = itemNavLeft.createEl("button", { text: "Item Next →" }); + itemNextBtn.disabled = activeStepIndex >= nestedStep.items.length - 1; + itemNextBtn.onclick = () => { + const currentState = this.state.loopRuntimeStates.get(nestedLoopKey); + if (currentState) { + const newState = itemNextStep(currentState, nestedStep.items.length); this.state.loopRuntimeStates.set(nestedLoopKey, newState); this.renderStep(); } }; - // Right: Save/Clear - const nestedNavRight = nestedNav.createEl("div"); - nestedNavRight.style.display = "flex"; - nestedNavRight.style.gap = "0.5em"; + const itemNavRight = subwizardNav.createEl("div"); + itemNavRight.style.display = "flex"; + itemNavRight.style.gap = "0.5em"; - const nestedSaveBtn = nestedNavRight.createEl("button", { - text: nestedLoopState.editIndex !== null ? "Speichern" : "Hinzufügen", + const doneBtn = itemNavRight.createEl("button", { + text: nestedLoopState.editIndex !== null ? "Save Item" : "Done", cls: "mod-cta", }); - nestedSaveBtn.onclick = () => { - const currentNestedState = this.state.loopRuntimeStates.get(nestedLoopKey); - if (currentNestedState && isDraftDirty(currentNestedState.draft)) { - const newState = commitDraft(currentNestedState); + doneBtn.onclick = () => { + const currentState = this.state.loopRuntimeStates.get(nestedLoopKey); + if (currentState && isDraftDirty(currentState.draft)) { + const newState = commitDraft(currentState); this.state.loopRuntimeStates.set(nestedLoopKey, newState); // Update parent draft - onFieldChange(nestedStep.key, newState.items); - // Re-render to show updated item list + const lastDotIndex = nestedLoopKey.lastIndexOf("."); + if (lastDotIndex > 0 && nestedStepKey) { + const parentLoopKey = nestedLoopKey.substring(0, lastDotIndex); + if (parentLoopKey) { + const parentState = this.state.loopRuntimeStates.get(parentLoopKey); + if (parentState) { + const updatedParentState = setDraftField(parentState, nestedStepKey, newState.items); + this.state.loopRuntimeStates.set(parentLoopKey, updatedParentState); + } + } + } this.renderStep(); } }; if (nestedLoopState.editIndex !== null || isDraftDirty(nestedLoopState.draft)) { - const nestedClearBtn = nestedNavRight.createEl("button", { - text: "Löschen", - }); - nestedClearBtn.onclick = () => { - const currentNestedState = this.state.loopRuntimeStates.get(nestedLoopKey); - if (currentNestedState) { - const newState = clearDraft(currentNestedState); + const clearBtn = itemNavRight.createEl("button", { text: "Clear" }); + clearBtn.onclick = () => { + const currentState = this.state.loopRuntimeStates.get(nestedLoopKey); + if (currentState) { + const newState = clearDraft(currentState); this.state.loopRuntimeStates.set(nestedLoopKey, newState); this.renderStep(); }