From ce82bcaeba001c8746d6e1d4b4b9bc194ebf60bb Mon Sep 17 00:00:00 2001 From: kunalja Date: Fri, 13 Jun 2025 00:12:18 -0400 Subject: [PATCH] format doccument --- src/main.ts | 300 +++--- src/modals/TextInputModal.ts | 1710 +++++++++++++++++----------------- styles.css | 38 +- 3 files changed, 1029 insertions(+), 1019 deletions(-) diff --git a/src/main.ts b/src/main.ts index 53015b8..f6bdc31 100644 --- a/src/main.ts +++ b/src/main.ts @@ -28,36 +28,36 @@ class ClickableVariantWidget extends WidgetType { span.className = 'variant-active-option clickable-variant'; span.setAttribute('data-full-variant', this.fullVariant); span.setAttribute('data-variant-index', this.variantIndex); - + // Add click handler span.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); - + // Select the entire variant in the editor const state = view.state; const doc = state.doc; - + // Find the positions in the current state const from = this.from; const to = this.to; - + // Set the selection to the entire variant view.dispatch({ - selection: {anchor: from, head: to} + selection: { anchor: from, head: to } }); - + // Call the highlightSelection method this.plugin.highlightSelection(); }); - + return span; } eq(other: ClickableVariantWidget): boolean { - return this.text === other.text && - this.fullVariant === other.fullVariant && - this.variantIndex === other.variantIndex; + return this.text === other.text && + this.fullVariant === other.fullVariant && + this.variantIndex === other.variantIndex; } } @@ -73,13 +73,13 @@ export default class VariantEditor extends Plugin { // Store the selection range private selectionFrom: number | null = null; private selectionTo: number | null = null; - + async onload() { try { // Bind the method to ensure proper 'this' context this.highlightSelection = this.highlightSelection.bind(this); this.clearHighlight = this.clearHighlight.bind(this); - + // Register the command with a direct function reference this.addCommand({ id: 'variant-editor-highlight', @@ -87,24 +87,24 @@ export default class VariantEditor extends Plugin { hotkeys: [{ modifiers: ["Mod"], key: "h" }], callback: () => this.highlightSelection() }); - + // Register the clear command this.addCommand({ id: 'variant-editor-clear-highlight', name: 'Variant Editor: Clear Highlighting', callback: () => this.clearHighlight() }); - + // Register command to commit all variants in selection or document this.addCommand({ id: 'variant-editor-commit-all', name: 'Variant Editor: Commit All Variants in Selection/Document', editorCallback: (editor) => this.commitAllVariants(editor) }); - + // Register the editor extension for dimming this.dimExtension = this.createDimExtension(); - + // Register the editor extension for variant indicators this.registerEditorExtension(this.createVariantIndicatorExtension()); this.registerEditorExtension(this.dimExtension); @@ -116,7 +116,7 @@ export default class VariantEditor extends Plugin { onunload() { this.clearHighlight(); } - + /** * Creates an extension that styles variant syntax to show only the active variant * while keeping the original text editable @@ -124,48 +124,48 @@ export default class VariantEditor extends Plugin { private createVariantIndicatorExtension(): Extension { // Store a reference to the plugin instance for the widget to use const pluginInstance = this; - + return ViewPlugin.fromClass( class { decorations: DecorationSet; - + constructor(view: EditorView) { this.decorations = this.buildDecorations(view); } - + update(update: ViewUpdate) { if (update.docChanged || update.viewportChanged) { this.decorations = this.buildDecorations(update.view); } } - + buildDecorations(view: EditorView): DecorationSet { const builder = new RangeSetBuilder(); - - for (let {from, to} of view.visibleRanges) { + + for (let { from, to } of view.visibleRanges) { const text = view.state.doc.sliceString(from, to); const variantRegex = /\{\{([^{}]+?)\}\}\^(\d+)/g; let match; - + while ((match = variantRegex.exec(text)) !== null) { const matchStart = from + match.index; const matchEnd = matchStart + match[0].length; const fullMatch = match[0]; const variantsText = match[1]; const activeIndex = parseInt(match[2], 10); - + // Find the position of the active variant let pos = 0; if (activeIndex === 0) { pos += 2; // Skip past the opening {{ - } + } for (let i = 0; i < activeIndex; i++) { pos = fullMatch.indexOf('|', pos) + 1; } - + const activeVariantStart = matchStart + pos; const activeVariantEnd = activeVariantStart + variantsText.split('|')[activeIndex].length; - + // Add three decorations for each variant: // 1. Hide everything before the active variant builder.add( @@ -179,7 +179,7 @@ export default class VariantEditor extends Plugin { } }) ); - + // 2. Show the active variant as a clickable widget const activeVariantText = variantsText.split('|')[activeIndex]; builder.add( @@ -196,7 +196,7 @@ export default class VariantEditor extends Plugin { ) }) ); - + // 3. Hide everything after the active variant builder.add( activeVariantEnd, @@ -211,7 +211,7 @@ export default class VariantEditor extends Plugin { ); } } - + return builder.finish(); } }, @@ -220,11 +220,11 @@ export default class VariantEditor extends Plugin { } ); } - + private createDimExtension(): Extension { // Get reference to the plugin instance for the view plugin to use const pluginInstance = this; - + // Create a state field to track cursor position changes const cursorTrackingField = StateField.define({ create(state) { @@ -234,115 +234,115 @@ export default class VariantEditor extends Plugin { if (transaction.selection) { const selection = transaction.newSelection.main; const currentLine = transaction.newDoc.lineAt(selection.head).number; - + // Store the current cursor line if (pluginInstance.previousCursorLine === null) { pluginInstance.previousCursorLine = currentLine; } - + // If we have an active line and cursor moved to a different line, clear dimming - if (pluginInstance.activeLine !== null && - currentLine !== pluginInstance.activeLine && - currentLine !== pluginInstance.previousCursorLine) { + if (pluginInstance.activeLine !== null && + currentLine !== pluginInstance.activeLine && + currentLine !== pluginInstance.previousCursorLine) { // Schedule clearing on next tick to avoid update-during-update setTimeout(() => { pluginInstance.clearHighlight(); }, 0); } - + // Update previous cursor line pluginInstance.previousCursorLine = currentLine; } return value; } }); - + // Create the view plugin for decorations const dimPlugin = ViewPlugin.fromClass( class implements PluginValue { decorations: DecorationSet; - + constructor(view: EditorView) { this.decorations = this.buildDecorations(view); } - - update(update: ViewUpdate) { + + update(update: ViewUpdate) { // Update decorations if needed if (update.docChanged || update.viewportChanged || pluginInstance.activeLine !== null) { this.decorations = this.buildDecorations(update.view); } } - + buildDecorations(view: EditorView): DecorationSet { - // If no active line is set, return empty decorations - if (pluginInstance.activeLine === null) { - return Decoration.none; - } - - // Collect all decorations first, then sort and add them - const allDecorations = []; - const activeLine = pluginInstance.activeLine; - - // First pass: Collect line decorations for all lines except the active one - for (let i = 1; i <= view.state.doc.lines; i++) { - if (i !== activeLine) { - try { - const line = view.state.doc.line(i); - const decoration = Decoration.line({ - attributes: { class: "fh-dim" } - }); - allDecorations.push({ - from: line.from, - to: line.from, - decoration: decoration - }); - } catch (e) { - console.error(`Error creating decoration for line ${i}:`, e); + // If no active line is set, return empty decorations + if (pluginInstance.activeLine === null) { + return Decoration.none; + } + + // Collect all decorations first, then sort and add them + const allDecorations = []; + const activeLine = pluginInstance.activeLine; + + // First pass: Collect line decorations for all lines except the active one + for (let i = 1; i <= view.state.doc.lines; i++) { + if (i !== activeLine) { + try { + const line = view.state.doc.line(i); + const decoration = Decoration.line({ + attributes: { class: "fh-dim" } + }); + allDecorations.push({ + from: line.from, + to: line.from, + decoration: decoration + }); + } catch (e) { + console.error(`Error creating decoration for line ${i}:`, e); + } + } + } + + // Second pass: Collect highlight decoration only for the currently selected text + if (pluginInstance.selectedText && activeLine <= view.state.doc.lines && pluginInstance.selectionFrom !== null && pluginInstance.selectionTo !== null) { + try { + // Use the exact selection positions instead of searching for all instances + const start = pluginInstance.selectionFrom; + const end = pluginInstance.selectionTo; + + allDecorations.push({ + from: start, + to: end, + decoration: Decoration.mark({ + attributes: { class: "fh-highlight" } + }) + }); + } catch (e) { + console.error(`Error creating highlight decoration:`, e); + } + } + + // Sort all decorations by from position + allDecorations.sort((a, b) => a.from - b.from); + + // Now add them to the builder in sorted order + const builder = new RangeSetBuilder(); + for (const { from, to, decoration } of allDecorations) { + try { + builder.add(from, to, decoration); + } catch (e) { + console.error(`Error adding decoration at position ${from}:`, e); + } + } + + return builder.finish(); } - } - } - - // Second pass: Collect highlight decoration only for the currently selected text - if (pluginInstance.selectedText && activeLine <= view.state.doc.lines && pluginInstance.selectionFrom !== null && pluginInstance.selectionTo !== null) { - try { - // Use the exact selection positions instead of searching for all instances - const start = pluginInstance.selectionFrom; - const end = pluginInstance.selectionTo; - - allDecorations.push({ - from: start, - to: end, - decoration: Decoration.mark({ - attributes: { class: "fh-highlight" } - }) - }); - } catch (e) { - console.error(`Error creating highlight decoration:`, e); - } - } - - // Sort all decorations by from position - allDecorations.sort((a, b) => a.from - b.from); - - // Now add them to the builder in sorted order - const builder = new RangeSetBuilder(); - for (const { from, to, decoration } of allDecorations) { - try { - builder.add(from, to, decoration); - } catch (e) { - console.error(`Error adding decoration at position ${from}:`, e); - } - } - - return builder.finish(); - } - destroy() {} + destroy() { } }, { decorations: (instance) => instance.decorations } ); - + // Return both extensions return [cursorTrackingField, dimPlugin]; } @@ -355,38 +355,38 @@ export default class VariantEditor extends Plugin { const editor = view.editor; const selection = editor.listSelections()[0]; - + // Variants only work within single lines if (selection.anchor.line !== selection.head.line) { new Notice('Selection must be within a single line'); return; } - + // Get the selected text and range const from = { line: Math.min(selection.anchor.line, selection.head.line), ch: Math.min(selection.anchor.ch, selection.head.ch) }; - + const to = { line: Math.max(selection.anchor.line, selection.head.line), ch: Math.max(selection.anchor.ch, selection.head.ch) }; - + let selectedText = editor.getRange(from, to); if (!selectedText) return; - + this.selectedText = selectedText; - + // Setup for variant editing let initialText = selectedText; let initialActiveIndex = 0; let isExistingVariant = false; - + // Check if the selection is already a variant const variantRegex = /^\{\{([^{}]+?)\}\}\^(\d+)$/; const variantMatch = selectedText.match(variantRegex); - + if (variantMatch) { // Direct selection of a variant initialText = variantMatch[1]; @@ -398,27 +398,27 @@ export default class VariantEditor extends Plugin { if (line) { const fullLineRegex = /\{\{([^{}]+?)\}\}\^(\d+)/g; let match; - + while ((match = fullLineRegex.exec(line)) !== null) { const matchStart = match.index; const matchEnd = matchStart + match[0].length; - + // Check if selection overlaps with the variant at all // This handles partial selections that include any part of the variant if ((from.ch >= matchStart && from.ch < matchEnd) || // Selection starts inside variant - (to.ch > matchStart && to.ch <= matchEnd) || // Selection ends inside variant - (from.ch <= matchStart && to.ch >= matchEnd)) { // Selection contains variant - + (to.ch > matchStart && to.ch <= matchEnd) || // Selection ends inside variant + (from.ch <= matchStart && to.ch >= matchEnd)) { // Selection contains variant + // Selection overlaps with a variant - capture the entire variant initialText = match[1]; initialActiveIndex = parseInt(match[2], 10); isExistingVariant = true; - + // Expand selection to cover the entire variant from.ch = matchStart; to.ch = matchEnd; editor.setSelection(from, to); - + // Update selectedText to match the expanded selection selectedText = editor.getRange(from, to); break; @@ -426,17 +426,17 @@ export default class VariantEditor extends Plugin { } } } - + if (isExistingVariant) { new Notice('Editing existing variant'); } - + // Set active line for dimming this.activeLine = from.line + 1; - + // Store the selected text for highlighting - make sure we use the potentially updated selection this.selectedText = selectedText; - + // Convert EditorPosition to absolute character positions for highlighting const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); if (activeView) { @@ -447,7 +447,7 @@ export default class VariantEditor extends Plugin { // Convert line/ch positions to absolute character positions const fromPos = editorView.state.doc.line(from.line + 1).from + from.ch; const toPos = editorView.state.doc.line(to.line + 1).from + to.ch; - + // Store the exact selection range for highlighting this.selectionFrom = fromPos; this.selectionTo = toPos; @@ -456,10 +456,10 @@ export default class VariantEditor extends Plugin { } } } - + // Force editor refresh to apply decorations this.app.workspace.updateOptions(); - + // Open the variant editor modal new TextInputModal( this.app, @@ -468,7 +468,7 @@ export default class VariantEditor extends Plugin { // Use the updated cursor positions if provided, otherwise use the original positions const updateFrom = currentFrom || from; const updateTo = currentTo || to; - + if (modalClosed) { // Modal was closed without committing (via ESC key or clicking outside) // Clear the highlights @@ -484,18 +484,18 @@ export default class VariantEditor extends Plugin { } else { // Create or update the variant syntax (normal variant creation/update) const variants = variantText.split('|').filter(v => v); - + if (variants.length > 0) { const activeIdx = typeof activeIndex === 'number' ? activeIndex : 0; const variantSyntax = `{{${variants.join('|')}}}^${activeIdx}`; - + // Use the updated cursor positions for the replacement editor.replaceRange(variantSyntax, updateFrom, updateTo); - + // Update the original positions for future updates from.ch = updateFrom.ch; to.ch = updateFrom.ch + variantSyntax.length; - + // Update selection positions for highlighting const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); if (activeView) { @@ -505,11 +505,11 @@ export default class VariantEditor extends Plugin { // Convert updated line/ch positions to absolute character positions const fromPos = editorView.state.doc.line(updateFrom.line + 1).from + updateFrom.ch; const toPos = editorView.state.doc.line(updateFrom.line + 1).from + updateFrom.ch + variantSyntax.length; - + // Update the exact selection range for highlighting this.selectionFrom = fromPos; this.selectionTo = toPos; - + // Force editor refresh to update decorations this.app.workspace.updateOptions(); } catch (e) { @@ -517,7 +517,7 @@ export default class VariantEditor extends Plugin { } } } - + const action = isExistingVariant ? 'Updated' : 'Created'; // Only show notice on explicit user action, not on every update if (commitVariant) { @@ -548,7 +548,7 @@ export default class VariantEditor extends Plugin { let text: string; let from: EditorPosition; let to: EditorPosition; - + const selection = editor.listSelections()[0]; if (selection && (selection.anchor.line !== selection.head.line || selection.anchor.ch !== selection.head.ch)) { // Use the selection @@ -556,12 +556,12 @@ export default class VariantEditor extends Plugin { line: Math.min(selection.anchor.line, selection.head.line), ch: Math.min(selection.anchor.ch, selection.head.ch) }; - + to = { line: Math.max(selection.anchor.line, selection.head.line), ch: Math.max(selection.anchor.ch, selection.head.ch) }; - + text = editor.getRange(from, to); } else { // Use the entire document @@ -569,23 +569,23 @@ export default class VariantEditor extends Plugin { to = { line: editor.lineCount() - 1, ch: editor.getLine(editor.lineCount() - 1).length }; text = editor.getValue(); } - + // Find all variants in the text const variantRegex = /\{\{([^{}]+?)\}\}\^(\d+)/g; let match; let variantsFound = 0; let lastIndex = 0; let result = ''; - + // Process each variant while ((match = variantRegex.exec(text)) !== null) { // Add text before this variant result += text.substring(lastIndex, match.index); - + // Extract variant information const variants = match[1].split('|'); const activeIndex = parseInt(match[2], 10); - + // Add the active variant text if (activeIndex >= 0 && activeIndex < variants.length) { result += variants[activeIndex]; @@ -594,17 +594,17 @@ export default class VariantEditor extends Plugin { // If active index is invalid, keep the original text result += match[0]; } - + // Update lastIndex for next iteration lastIndex = match.index + match[0].length; } - + // Add any remaining text result += text.substring(lastIndex); - + // Replace the text in the editor editor.replaceRange(result, from, to); - + // Show a notice with the results if (variantsFound > 0) { new Notice(`Committed ${variantsFound} variant${variantsFound === 1 ? '' : 's'}`); @@ -616,7 +616,7 @@ export default class VariantEditor extends Plugin { new Notice('Error committing variants'); } } - + private clearHighlight(): void { try { // Reset state @@ -625,7 +625,7 @@ export default class VariantEditor extends Plugin { this.selectedText = null; this.selectionFrom = null; this.selectionTo = null; - + // Force editor refresh const view = this.app.workspace.getActiveViewOfType(MarkdownView); if (view) hackToRerender(view); diff --git a/src/modals/TextInputModal.ts b/src/modals/TextInputModal.ts index 5922f64..ed3423b 100644 --- a/src/modals/TextInputModal.ts +++ b/src/modals/TextInputModal.ts @@ -5,878 +5,878 @@ import { App, Modal, Setting, ButtonComponent, setTooltip, EditorPosition } from * Used to create variants for the selected text */ export class TextInputModal extends Modal { - private variants: string[] = []; - private activeVariantIndex: number = 0; - private lastNonEmptyVariantIndex: number = 0; // Track the last non-empty variant index - private onSubmit: (result: string, activeIndex?: number, commitVariant?: boolean, currentFrom?: EditorPosition | null, currentTo?: EditorPosition | null, modalClosed?: boolean) => void; - private originalText: string; - private variantContainer: HTMLElement; - private cursorPosition: EditorPosition | null; - // Track the current variant position in the editor - private currentFrom: EditorPosition | null; - private currentTo: EditorPosition | null; - - // Drag and drop properties - private draggedElement: HTMLElement | null = null; - private draggedIndex: number = -1; - private dragOverIndex: number = -1; - - // Modal dragging properties - private isDraggingModal: boolean = false; - private modalInitialX: number = 0; - private modalInitialY: number = 0; - private pointerInitialX: number = 0; - private pointerInitialY: number = 0; - - // Setup draggable header for the modal - private setupDraggableHeader(dragHeader: HTMLElement, modalEl: HTMLElement) { - // Mouse events for desktop - dragHeader.addEventListener('mousedown', (e) => { - // Only handle left mouse button - if (e.button !== 0) return; - - // Prevent text selection during drag - e.preventDefault(); - - // Start dragging - this.isDraggingModal = true; - modalEl.addClass('dragging'); - - // Store initial positions - const modalRect = modalEl.getBoundingClientRect(); - this.modalInitialX = modalRect.left; - this.modalInitialY = modalRect.top; - this.pointerInitialX = e.clientX; - this.pointerInitialY = e.clientY; - - // Setup document-level event listeners - document.addEventListener('mousemove', this.handleModalMove); - document.addEventListener('mouseup', this.handleModalRelease); - }); - - // Touch events for mobile - dragHeader.addEventListener('touchstart', (e) => { - // Prevent scrolling while dragging - e.preventDefault(); - - // Start dragging - this.isDraggingModal = true; - modalEl.addClass('dragging'); - - // Store initial positions - const modalRect = modalEl.getBoundingClientRect(); - this.modalInitialX = modalRect.left; - this.modalInitialY = modalRect.top; - this.pointerInitialX = e.touches[0].clientX; - this.pointerInitialY = e.touches[0].clientY; - - // Setup document-level event listeners - document.addEventListener('touchmove', this.handleModalMove); - document.addEventListener('touchend', this.handleModalRelease); - }); - } - - // Handle modal movement - private handleModalMove = (e: MouseEvent | TouchEvent) => { - if (!this.isDraggingModal) return; - - // Get current pointer position - let clientX: number, clientY: number; - - if (e instanceof MouseEvent) { - clientX = e.clientX; - clientY = e.clientY; - } else { // TouchEvent - clientX = e.touches[0].clientX; - clientY = e.touches[0].clientY; - } - - // Calculate the distance moved - const deltaX = clientX - this.pointerInitialX; - const deltaY = clientY - this.pointerInitialY; - - // Calculate new position - const newX = this.modalInitialX + deltaX; - const newY = this.modalInitialY + deltaY; - - // Apply the new position - const modalEl = this.modalEl; - modalEl.style.left = `${newX}px`; - modalEl.style.top = `${newY}px`; - } - - // Handle modal release - private handleModalRelease = () => { - if (!this.isDraggingModal) return; - - // Stop dragging - this.isDraggingModal = false; - this.modalEl.removeClass('dragging'); - - // Remove document-level event listeners - document.removeEventListener('mousemove', this.handleModalMove); - document.removeEventListener('mouseup', this.handleModalRelease); - document.removeEventListener('touchmove', this.handleModalMove); - document.removeEventListener('touchend', this.handleModalRelease); - } + private variants: string[] = []; + private activeVariantIndex: number = 0; + private lastNonEmptyVariantIndex: number = 0; // Track the last non-empty variant index + private onSubmit: (result: string, activeIndex?: number, commitVariant?: boolean, currentFrom?: EditorPosition | null, currentTo?: EditorPosition | null, modalClosed?: boolean) => void; + private originalText: string; + private variantContainer: HTMLElement; + private cursorPosition: EditorPosition | null; + // Track the current variant position in the editor + private currentFrom: EditorPosition | null; + private currentTo: EditorPosition | null; - constructor( - app: App, - originalText: string, - onSubmit: (result: string, activeIndex?: number, commitVariant?: boolean, currentFrom?: EditorPosition | null, currentTo?: EditorPosition | null, modalClosed?: boolean) => void, - cursorPosition: EditorPosition | null = null, - initialActiveIndex: number = 0, - currentFrom: EditorPosition | null = null, - currentTo: EditorPosition | null = null - ) { - super(app); - this.originalText = originalText; - this.onSubmit = onSubmit; - - // If the originalText contains pipe characters, it might be a variant list - if (originalText.includes('|')) { - this.variants = originalText.split('|').filter(v => v); - this.activeVariantIndex = initialActiveIndex; - this.lastNonEmptyVariantIndex = initialActiveIndex; // Initialize with the active index - } else { - this.variants = [originalText]; - this.activeVariantIndex = 0; - this.lastNonEmptyVariantIndex = 0; - } - - this.cursorPosition = cursorPosition; - this.currentFrom = currentFrom || cursorPosition; - this.currentTo = currentTo; - } + // Drag and drop properties + private draggedElement: HTMLElement | null = null; + private draggedIndex: number = -1; + private dragOverIndex: number = -1; - onOpen() { - const {contentEl, modalEl} = this; - - // Hide the modal initially to prevent flashing - if (this.cursorPosition) { - modalEl.style.opacity = '0'; - modalEl.style.transition = 'opacity 150ms ease-in-out'; + // Modal dragging properties + private isDraggingModal: boolean = false; + private modalInitialX: number = 0; + private modalInitialY: number = 0; + private pointerInitialX: number = 0; + private pointerInitialY: number = 0; + + // Setup draggable header for the modal + private setupDraggableHeader(dragHeader: HTMLElement, modalEl: HTMLElement) { + // Mouse events for desktop + dragHeader.addEventListener('mousedown', (e) => { + // Only handle left mouse button + if (e.button !== 0) return; + + // Prevent text selection during drag + e.preventDefault(); + + // Start dragging + this.isDraggingModal = true; + modalEl.addClass('dragging'); + + // Store initial positions + const modalRect = modalEl.getBoundingClientRect(); + this.modalInitialX = modalRect.left; + this.modalInitialY = modalRect.top; + this.pointerInitialX = e.clientX; + this.pointerInitialY = e.clientY; + + // Setup document-level event listeners + document.addEventListener('mousemove', this.handleModalMove); + document.addEventListener('mouseup', this.handleModalRelease); + }); + + // Touch events for mobile + dragHeader.addEventListener('touchstart', (e) => { + // Prevent scrolling while dragging + e.preventDefault(); + + // Start dragging + this.isDraggingModal = true; + modalEl.addClass('dragging'); + + // Store initial positions + const modalRect = modalEl.getBoundingClientRect(); + this.modalInitialX = modalRect.left; + this.modalInitialY = modalRect.top; + this.pointerInitialX = e.touches[0].clientX; + this.pointerInitialY = e.touches[0].clientY; + + // Setup document-level event listeners + document.addEventListener('touchmove', this.handleModalMove); + document.addEventListener('touchend', this.handleModalRelease); + }); } - - // Set the title using Obsidian's built-in functionality - this.setTitle('Manage Variants'); - - // Add classes for styling - modalEl.addClass('variant-editor-modal'); - modalEl.addClass('variant-editor-no-dim'); // Class to remove background dimming - - // Make the modal header draggable - const titleEl = modalEl.querySelector('.modal-title'); - if (titleEl) { - titleEl.addClass('variant-editor-draggable-title'); - this.setupDraggableHeader(titleEl as HTMLElement, modalEl); + + // Handle modal movement + private handleModalMove = (e: MouseEvent | TouchEvent) => { + if (!this.isDraggingModal) return; + + // Get current pointer position + let clientX: number, clientY: number; + + if (e instanceof MouseEvent) { + clientX = e.clientX; + clientY = e.clientY; + } else { // TouchEvent + clientX = e.touches[0].clientX; + clientY = e.touches[0].clientY; + } + + // Calculate the distance moved + const deltaX = clientX - this.pointerInitialX; + const deltaY = clientY - this.pointerInitialY; + + // Calculate new position + const newX = this.modalInitialX + deltaX; + const newY = this.modalInitialY + deltaY; + + // Apply the new position + const modalEl = this.modalEl; + modalEl.style.left = `${newX}px`; + modalEl.style.top = `${newY}px`; } - - // Create a flex container for all content to support column-reverse when above - const flexContainer = contentEl.createDiv({ - cls: 'variant-editor-flex-container' - }); - - // Position the modal relative to the cursor if we have cursor position - if (this.cursorPosition) { - // Position immediately in the next microtask to avoid flashing - queueMicrotask(() => { - this.positionModalRelativeToCursor(modalEl); - // Fade in the modal after positioning - modalEl.style.opacity = '1'; - }); + + // Handle modal release + private handleModalRelease = () => { + if (!this.isDraggingModal) return; + + // Stop dragging + this.isDraggingModal = false; + this.modalEl.removeClass('dragging'); + + // Remove document-level event listeners + document.removeEventListener('mousemove', this.handleModalMove); + document.removeEventListener('mouseup', this.handleModalRelease); + document.removeEventListener('touchmove', this.handleModalMove); + document.removeEventListener('touchend', this.handleModalRelease); } - - // We've removed the 'Selected variant' text as requested - - // Create container for variant inputs - this.variantContainer = flexContainer.createDiv({ - cls: 'variant-editor-container' - }); - - // Always ensure we have an empty row at the end for adding new variants - // We'll add an empty variant if there isn't one already - if (this.variants.length === 0 || this.variants[this.variants.length - 1].trim() !== '') { - this.variants.push(''); + + constructor( + app: App, + originalText: string, + onSubmit: (result: string, activeIndex?: number, commitVariant?: boolean, currentFrom?: EditorPosition | null, currentTo?: EditorPosition | null, modalClosed?: boolean) => void, + cursorPosition: EditorPosition | null = null, + initialActiveIndex: number = 0, + currentFrom: EditorPosition | null = null, + currentTo: EditorPosition | null = null + ) { + super(app); + this.originalText = originalText; + this.onSubmit = onSubmit; + + // If the originalText contains pipe characters, it might be a variant list + if (originalText.includes('|')) { + this.variants = originalText.split('|').filter(v => v); + this.activeVariantIndex = initialActiveIndex; + this.lastNonEmptyVariantIndex = initialActiveIndex; // Initialize with the active index + } else { + this.variants = [originalText]; + this.activeVariantIndex = 0; + this.lastNonEmptyVariantIndex = 0; + } + + this.cursorPosition = cursorPosition; + this.currentFrom = currentFrom || cursorPosition; + this.currentTo = currentTo; } - - // Add the first variant (original text) and the empty row - // Focus the active variant (not the empty one at the end) - this.renderVariantInputs(this.activeVariantIndex); - - // Add buttons container - const buttonsContainer = flexContainer.createDiv({ - cls: 'variant-editor-buttons' - }); - - // Define whether we're editing existing variants or creating new ones - const hasMultipleVariants = this.variants.length > 1; - - // Create/Update variants button - const updateButton = new ButtonComponent(buttonsContainer) - .setButtonText(hasMultipleVariants ? 'Update' : 'Create') - .setCta() - .onClick(() => { - this.updateVariantsInEditor(); - this.close(); - }); - - // Add icon to update button - const updateIcon = document.createElement('span'); - updateIcon.innerHTML = '👍🏾'; // Brown-skinned thumbs up - updateButton.buttonEl.prepend(updateIcon); - updateButton.buttonEl.addClass('variant-editor-button'); - - // Commit active variant button - const commitButton = new ButtonComponent(buttonsContainer) - .setButtonText('Commit') - .onClick(() => { + + onOpen() { + const { contentEl, modalEl } = this; + + // Hide the modal initially to prevent flashing + if (this.cursorPosition) { + modalEl.style.opacity = '0'; + modalEl.style.transition = 'opacity 150ms ease-in-out'; + } + + // Set the title using Obsidian's built-in functionality + this.setTitle('Manage Variants'); + + // Add classes for styling + modalEl.addClass('variant-editor-modal'); + modalEl.addClass('variant-editor-no-dim'); // Class to remove background dimming + + // Make the modal header draggable + const titleEl = modalEl.querySelector('.modal-title'); + if (titleEl) { + titleEl.addClass('variant-editor-draggable-title'); + this.setupDraggableHeader(titleEl as HTMLElement, modalEl); + } + + // Create a flex container for all content to support column-reverse when above + const flexContainer = contentEl.createDiv({ + cls: 'variant-editor-flex-container' + }); + + // Position the modal relative to the cursor if we have cursor position + if (this.cursorPosition) { + // Position immediately in the next microtask to avoid flashing + queueMicrotask(() => { + this.positionModalRelativeToCursor(modalEl); + // Fade in the modal after positioning + modalEl.style.opacity = '1'; + }); + } + + // We've removed the 'Selected variant' text as requested + + // Create container for variant inputs + this.variantContainer = flexContainer.createDiv({ + cls: 'variant-editor-container' + }); + + // Always ensure we have an empty row at the end for adding new variants + // We'll add an empty variant if there isn't one already + if (this.variants.length === 0 || this.variants[this.variants.length - 1].trim() !== '') { + this.variants.push(''); + } + + // Add the first variant (original text) and the empty row + // Focus the active variant (not the empty one at the end) + this.renderVariantInputs(this.activeVariantIndex); + + // Add buttons container + const buttonsContainer = flexContainer.createDiv({ + cls: 'variant-editor-buttons' + }); + + // Define whether we're editing existing variants or creating new ones + const hasMultipleVariants = this.variants.length > 1; + + // Create/Update variants button + const updateButton = new ButtonComponent(buttonsContainer) + .setButtonText(hasMultipleVariants ? 'Update' : 'Create') + .setCta() + .onClick(() => { + this.updateVariantsInEditor(); + this.close(); + }); + + // Add icon to update button + const updateIcon = document.createElement('span'); + updateIcon.innerHTML = '👍🏾'; // Brown-skinned thumbs up + updateButton.buttonEl.prepend(updateIcon); + updateButton.buttonEl.addClass('variant-editor-button'); + + // Commit active variant button + const commitButton = new ButtonComponent(buttonsContainer) + .setButtonText('Commit') + .onClick(() => { + // Filter out empty variants and track their original indices + const nonEmptyVariantsWithIndices = this.variants + .map((v, i) => ({ text: v, originalIndex: i })) + .filter(v => v.text.length > 0); + + // Find the active variant after filtering + const activeVariant = nonEmptyVariantsWithIndices.find(v => v.originalIndex === this.activeVariantIndex); + + if (activeVariant) { + this.close(); + // Pass the active variant text directly with commitVariant flag + this.onSubmit(activeVariant.text, undefined, true); + } + }); + + // Add icon to commit button + const commitIcon = document.createElement('span'); + commitIcon.innerHTML = '✓'; + commitButton.buttonEl.prepend(commitIcon); + commitButton.buttonEl.addClass('variant-editor-button', 'variant-editor-commit-button'); + + // Add tooltips to the buttons + if (buttonsContainer.children[0] instanceof HTMLElement) { + setTooltip(buttonsContainer.children[0] as HTMLElement, hasMultipleVariants ? 'Save all variants' : 'Create variants', { + placement: 'bottom' + }); + } + + if (buttonsContainer.children[1] instanceof HTMLElement) { + setTooltip(buttonsContainer.children[1] as HTMLElement, 'Replace with active variant only', { + placement: 'bottom' + }); + } + } + + /** + * Positions the modal relative to the cursor position + */ + private positionModalRelativeToCursor(modalEl: HTMLElement) { + if (!this.cursorPosition) return; + + // Get the active editor view + const activeLeaf = this.app.workspace.activeLeaf; + if (!activeLeaf || !activeLeaf.view) return; + + // Get the editor element + const editorEl = activeLeaf.view.containerEl.querySelector('.cm-editor'); + if (!editorEl) return; + + // Find the line element for the cursor position + const lineElements = editorEl.querySelectorAll('.cm-line'); + if (!lineElements || lineElements.length <= this.cursorPosition.line) return; + + const lineEl = lineElements[this.cursorPosition.line]; + if (!lineEl) return; + + // Get the position of the line element + const lineRect = lineEl.getBoundingClientRect(); + + // Get the modal dimensions + const modalRect = modalEl.getBoundingClientRect(); + + // Position the modal below the line with some padding + const padding = 10; + let top = lineRect.bottom + padding; + let positionAbove = false; + + // Make sure the modal doesn't go off the bottom of the screen + const viewportHeight = window.innerHeight; + if (top + modalRect.height + 114 > viewportHeight) { + // Position above the line instead + // We want the bottom of the modal to be at the top of the line + top = Math.max(10, lineRect.top - modalRect.height - 150); + positionAbove = true; + } + + // Center horizontally relative to the line + let left = lineRect.left + (lineRect.width / 2) - (modalRect.width / 2); + + // Make sure the modal doesn't go off the sides of the screen + const viewportWidth = window.innerWidth; + if (left < 10) { + left = 10; // Minimum 10px from left edge + } else if (left + modalRect.width > viewportWidth - 10) { + left = viewportWidth - modalRect.width - 10; // Minimum 10px from right edge + } + + // Apply the position + modalEl.style.position = 'fixed'; + modalEl.style.transform = 'none'; // Remove default centering + + if (positionAbove) { + // When positioned above, set explicit top position + modalEl.style.top = `${Math.max(0, top)}px`; + modalEl.style.bottom = 'auto'; + modalEl.classList.add('variant-editor-modal-above'); + } else { + // When positioned below, set explicit top position + modalEl.style.top = `${Math.max(0, top)}px`; + modalEl.style.bottom = 'auto'; + modalEl.classList.remove('variant-editor-modal-above'); + } + + // Set the left position + modalEl.style.left = `${Math.max(0, left)}px`; + } + + /** + * Updates the variants in the editor without closing the modal + * Returns the new cursor positions for tracking + */ + private updateVariantsInEditor() { // Filter out empty variants and track their original indices const nonEmptyVariantsWithIndices = this.variants - .map((v, i) => ({ text: v, originalIndex: i })) - .filter(v => v.text.length > 0); - - // Find the active variant after filtering - const activeVariant = nonEmptyVariantsWithIndices.find(v => v.originalIndex === this.activeVariantIndex); - - if (activeVariant) { - this.close(); - // Pass the active variant text directly with commitVariant flag - this.onSubmit(activeVariant.text, undefined, true); - } - }); - - // Add icon to commit button - const commitIcon = document.createElement('span'); - commitIcon.innerHTML = '✓'; - commitButton.buttonEl.prepend(commitIcon); - commitButton.buttonEl.addClass('variant-editor-button', 'variant-editor-commit-button'); - - // Add tooltips to the buttons - if (buttonsContainer.children[0] instanceof HTMLElement) { - setTooltip(buttonsContainer.children[0] as HTMLElement, hasMultipleVariants ? 'Save all variants' : 'Create variants', { - placement: 'bottom' - }); - } - - if (buttonsContainer.children[1] instanceof HTMLElement) { - setTooltip(buttonsContainer.children[1] as HTMLElement, 'Replace with active variant only', { - placement: 'bottom' - }); - } - } - - /** - * Positions the modal relative to the cursor position - */ - private positionModalRelativeToCursor(modalEl: HTMLElement) { - if (!this.cursorPosition) return; - - // Get the active editor view - const activeLeaf = this.app.workspace.activeLeaf; - if (!activeLeaf || !activeLeaf.view) return; - - // Get the editor element - const editorEl = activeLeaf.view.containerEl.querySelector('.cm-editor'); - if (!editorEl) return; - - // Find the line element for the cursor position - const lineElements = editorEl.querySelectorAll('.cm-line'); - if (!lineElements || lineElements.length <= this.cursorPosition.line) return; - - const lineEl = lineElements[this.cursorPosition.line]; - if (!lineEl) return; - - // Get the position of the line element - const lineRect = lineEl.getBoundingClientRect(); - - // Get the modal dimensions - const modalRect = modalEl.getBoundingClientRect(); - - // Position the modal below the line with some padding - const padding = 10; - let top = lineRect.bottom + padding; - let positionAbove = false; - - // Make sure the modal doesn't go off the bottom of the screen - const viewportHeight = window.innerHeight; - if (top + modalRect.height + 114 > viewportHeight) { - // Position above the line instead - // We want the bottom of the modal to be at the top of the line - top = Math.max(10, lineRect.top - modalRect.height - 150); - positionAbove = true; - } - - // Center horizontally relative to the line - let left = lineRect.left + (lineRect.width / 2) - (modalRect.width / 2); - - // Make sure the modal doesn't go off the sides of the screen - const viewportWidth = window.innerWidth; - if (left < 10) { - left = 10; // Minimum 10px from left edge - } else if (left + modalRect.width > viewportWidth - 10) { - left = viewportWidth - modalRect.width - 10; // Minimum 10px from right edge - } - - // Apply the position - modalEl.style.position = 'fixed'; - modalEl.style.transform = 'none'; // Remove default centering - - if (positionAbove) { - // When positioned above, set explicit top position - modalEl.style.top = `${Math.max(0, top)}px`; - modalEl.style.bottom = 'auto'; - modalEl.classList.add('variant-editor-modal-above'); - } else { - // When positioned below, set explicit top position - modalEl.style.top = `${Math.max(0, top)}px`; - modalEl.style.bottom = 'auto'; - modalEl.classList.remove('variant-editor-modal-above'); - } - - // Set the left position - modalEl.style.left = `${Math.max(0, left)}px`; - } - - /** - * Updates the variants in the editor without closing the modal - * Returns the new cursor positions for tracking - */ - private updateVariantsInEditor() { - // Filter out empty variants and track their original indices - const nonEmptyVariantsWithIndices = this.variants - .map((v, i) => ({ text: v, originalIndex: i })) - .filter(v => v.text.length > 0); - - // Handle both single variant and multiple variants cases - if (nonEmptyVariantsWithIndices.length >= 1) { - // Find the new index of the active variant after filtering - let newActiveIndex = 0; - let activeVariantFound = false; - - // Check if the current active variant is empty - const isActiveVariantEmpty = this.variants[this.activeVariantIndex].trim().length === 0; - - if (isActiveVariantEmpty && this.activeVariantIndex !== 0) { - // If active variant is empty (and not the original), use the last non-empty variant - // Try to find the last non-empty variant in the filtered list - for (let i = 0; i < nonEmptyVariantsWithIndices.length; i++) { - if (nonEmptyVariantsWithIndices[i].originalIndex === this.lastNonEmptyVariantIndex) { - newActiveIndex = i; - activeVariantFound = true; - break; - } - } - } else { - // Try to find the current active variant in the filtered list - for (let i = 0; i < nonEmptyVariantsWithIndices.length; i++) { - if (nonEmptyVariantsWithIndices[i].originalIndex === this.activeVariantIndex) { - newActiveIndex = i; - activeVariantFound = true; - break; - } - } - } - - // If we still haven't found an active variant (e.g., if lastNonEmptyVariantIndex is also empty now) - if (!activeVariantFound && this.activeVariantIndex === this.variants.length - 1) { - // If we were on the last (new) variant, use the last non-empty variant - newActiveIndex = nonEmptyVariantsWithIndices.length - 1; - } - - // Join variants with pipe character for the expected format and pass the corrected active index - const nonEmptyVariants = nonEmptyVariantsWithIndices.map(v => v.text); - const variantText = nonEmptyVariants.join('|'); - - // Call onSubmit with the new variant text and active index - // The third parameter (false) indicates this is not a commit operation - // We're also passing the current cursor positions for tracking - this.onSubmit(variantText, newActiveIndex, false, this.currentFrom, this.currentTo); - - // Calculate the new cursor positions based on the variant text length - if (this.currentFrom) { - // If we have a currentTo position, update it based on the new variant text length - if (this.currentTo) { - const variantSyntax = `{{${variantText}}}^${newActiveIndex}`; - const newTo = { - line: this.currentFrom.line, - ch: this.currentFrom.ch + variantSyntax.length - }; - this.currentTo = newTo; - } - } - } - } + .map((v, i) => ({ text: v, originalIndex: i })) + .filter(v => v.text.length > 0); - private renderVariantInputs(focusIndex?: number) { - // Clear existing inputs - this.variantContainer.empty(); - - // Create an input for each variant - this.variants.forEach((variant, index) => { - // Add active class to the row if it's the active variant - const isActive = this.activeVariantIndex === index; - - // Check if this is the last empty row (add variant placeholder) - const isLastEmptyRow = index === this.variants.length - 1 && variant.trim() === ''; - - // Build the row classes - let rowClasses = 'variant-editor-row'; - if (isActive) rowClasses += ' variant-editor-row-active'; - if (isLastEmptyRow) rowClasses += ' variant-editor-row-add-variant'; - - const variantRow = this.variantContainer.createDiv({ - cls: rowClasses - }); - - // Make the entire row clickable to select this variant - variantRow.addEventListener('click', (e) => { - // Don't trigger when clicking on delete button or drag handle - if (!(e.target instanceof HTMLButtonElement)) { - // Set this as the active variant - this.activeVariantIndex = index; - - // If this variant has content, update the last non-empty variant index - if (variant.trim().length > 0 || index === 0) { - this.lastNonEmptyVariantIndex = index; - } - - // If we're clicking directly on the contenteditable div, let its own handler manage focus - // Otherwise, update the UI and focus the contenteditable - if (e.target instanceof HTMLDivElement && e.target.hasAttribute('contenteditable')) { - // Just update the active state visually without re-rendering - const currentActive = this.variantContainer.querySelector('.variant-editor-row-active'); - if (currentActive && currentActive !== variantRow) { - currentActive.removeClass('variant-editor-row-active'); - variantRow.addClass('variant-editor-row-active'); - - // Only update the editor if the selected variant has content - if (variant.trim().length > 0 || index === 0) { - this.updateVariantsInEditor(); - } - } - } else { - // For clicks elsewhere in the row, do a full update and focus the input - this.renderVariantInputs(index); - - // Only update the editor if the selected variant has content - if (variant.trim().length > 0 || index === 0) { - this.updateVariantsInEditor(); - } - - // Focus the contenteditable div when clicking anywhere else on the row - setTimeout(() => { - // Find the contenteditable div in the newly rendered row - const newRow = this.variantContainer.querySelectorAll('.variant-editor-row')[index]; - if (newRow) { - const input = newRow.querySelector('.variant-editor-input'); - if (input) { - (input as HTMLElement).focus(); - // Place cursor at the end - const range = document.createRange(); - const sel = window.getSelection(); - range.selectNodeContents(input as Node); - range.collapse(false); - sel?.removeAllRanges(); - sel?.addRange(range); + // Handle both single variant and multiple variants cases + if (nonEmptyVariantsWithIndices.length >= 1) { + // Find the new index of the active variant after filtering + let newActiveIndex = 0; + let activeVariantFound = false; + + // Check if the current active variant is empty + const isActiveVariantEmpty = this.variants[this.activeVariantIndex].trim().length === 0; + + if (isActiveVariantEmpty && this.activeVariantIndex !== 0) { + // If active variant is empty (and not the original), use the last non-empty variant + // Try to find the last non-empty variant in the filtered list + for (let i = 0; i < nonEmptyVariantsWithIndices.length; i++) { + if (nonEmptyVariantsWithIndices[i].originalIndex === this.lastNonEmptyVariantIndex) { + newActiveIndex = i; + activeVariantFound = true; + break; + } } - } - }, 0); - } - } - }); - - // Drag handle icon for reordering - const dragHandle = variantRow.createEl('button', { - cls: 'variant-editor-drag-handle clickable-icon', - attr: { - 'aria-label': 'Reorder variants' - } - }); - dragHandle.innerHTML = '≡'; - - // Use Obsidian's setTooltip API to position tooltip above the button - setTooltip(dragHandle, 'Reorder variants', { - placement: 'top' - }); - - // Don't add drag functionality to the "Add a variant" row - if (!isLastEmptyRow) { - // Make the drag handle draggable - dragHandle.setAttribute('draggable', 'true'); - - // Drag start event - dragHandle.addEventListener('dragstart', (e) => { - this.draggedElement = variantRow; - this.draggedIndex = index; - - // Add dragging class for visual feedback - variantRow.classList.add('dragging'); - - // Create an invisible drag image to replace the default one - const dragGhost = document.createElement('div'); - dragGhost.classList.add('variant-editor-drag-ghost'); - document.body.appendChild(dragGhost); - - // Set the custom drag image - if (e.dataTransfer) { - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('text/plain', index.toString()); - e.dataTransfer.setDragImage(dragGhost, 0, 0); - - // Clean up the ghost element after a short delay - setTimeout(() => { - document.body.removeChild(dragGhost); - }, 0); - } - }); - - // Drag end event - dragHandle.addEventListener('dragend', () => { - if (this.draggedElement) { - this.draggedElement.classList.remove('dragging'); - this.draggedElement = null; - this.draggedIndex = -1; - - // Remove drag-over class from all rows - const allRows = this.variantContainer.querySelectorAll('.variant-editor-row'); - allRows.forEach(row => row.classList.remove('drag-over')); - } - }); - - // Drag over event for the row - variantRow.addEventListener('dragover', (e) => { - e.preventDefault(); - e.stopPropagation(); - - // Only process if we have a dragged element - if (!this.draggedElement || this.draggedIndex === index) return; - - // Remove all drag indicators from all rows - const allRows = this.variantContainer.querySelectorAll('.variant-editor-row'); - allRows.forEach(row => { - row.classList.remove('drag-over-top'); - row.classList.remove('drag-over-bottom'); - }); - - // Determine if we're in the top or bottom half of the row - const rect = variantRow.getBoundingClientRect(); - const mouseY = e.clientY; - const rowMiddleY = rect.top + rect.height / 2; - - if (mouseY < rowMiddleY) { - // Top half - show indicator above this row - variantRow.classList.add('drag-over-top'); - this.dragOverIndex = index; - } else { - // Bottom half - show indicator below this row - variantRow.classList.add('drag-over-bottom'); - this.dragOverIndex = index + 1; - } - }); - - // Drag leave event - variantRow.addEventListener('dragleave', () => { - variantRow.classList.remove('drag-over-top'); - variantRow.classList.remove('drag-over-bottom'); - }); - - // Drop event - variantRow.addEventListener('drop', (e) => { - e.preventDefault(); - e.stopPropagation(); - - // Only process if we have a dragged element - if (!this.draggedElement || this.draggedIndex === this.dragOverIndex) return; - - // Remove visual indicators - variantRow.classList.remove('drag-over-top'); - variantRow.classList.remove('drag-over-bottom'); - - // Move the variant in the array - const draggedVariant = this.variants[this.draggedIndex]; - - // Get the target index (this.dragOverIndex was set in dragover) - const targetIndex = this.dragOverIndex; - - // Adjust targetIndex if dragging from above to below - let adjustedTargetIndex = targetIndex; - if (this.draggedIndex < targetIndex) { - // When dragging from above to below, we need to adjust the target index - // because removing the original item shifts all indexes down by 1 - adjustedTargetIndex--; - } - - // Remove the dragged variant - this.variants.splice(this.draggedIndex, 1); - - // Insert at the new position - this.variants.splice(adjustedTargetIndex, 0, draggedVariant); - - // Update active index if needed - if (this.activeVariantIndex === this.draggedIndex) { - this.activeVariantIndex = adjustedTargetIndex; - } else if (this.activeVariantIndex > this.draggedIndex && this.activeVariantIndex <= adjustedTargetIndex) { - this.activeVariantIndex--; - } else if (this.activeVariantIndex < this.draggedIndex && this.activeVariantIndex >= adjustedTargetIndex) { - this.activeVariantIndex++; - } - - // Update lastNonEmptyVariantIndex if needed - if (this.lastNonEmptyVariantIndex === this.draggedIndex) { - this.lastNonEmptyVariantIndex = adjustedTargetIndex; - } else if (this.lastNonEmptyVariantIndex > this.draggedIndex && this.lastNonEmptyVariantIndex <= adjustedTargetIndex) { - this.lastNonEmptyVariantIndex--; - } else if (this.lastNonEmptyVariantIndex < this.draggedIndex && this.lastNonEmptyVariantIndex >= adjustedTargetIndex) { - this.lastNonEmptyVariantIndex++; - } - - // Re-render with the new order - this.renderVariantInputs(this.activeVariantIndex); - - // Update the editor with the new order - this.updateVariantsInEditor(); - }); - } - - // Input for the variant - placeholder text varies by index - const placeholder = index === 0 ? 'Original text' : - index === this.variants.length - 1 && variant.trim() === '' ? 'Add a variant' : - `Variant ${index}`; - - // Create contenteditable div instead of input - const variantInput = variantRow.createDiv({ - cls: 'variant-editor-input', - attr: { - contenteditable: 'true', - 'data-placeholder': placeholder, - 'role': 'textbox', - 'aria-multiline': 'false' - } - }); - - // Set the text content - variantInput.textContent = variant; - - // Add keyboard navigation with arrow keys - variantInput.addEventListener('keydown', (e) => { - // Handle arrow key navigation - if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { - e.preventDefault(); // Prevent default scrolling behavior - - // Calculate the target index based on arrow key direction - const targetIndex = e.key === 'ArrowUp' - ? Math.max(0, index - 1) // Move up (minimum index is 0) - : Math.min(this.variants.length - 1, index + 1); // Move down (maximum is last variant) - - // Only proceed if we're actually moving to a different row - if (targetIndex !== index) { - // Set the new active variant index - this.activeVariantIndex = targetIndex; - - // If the target variant has content, update the last non-empty variant index - if (this.variants[targetIndex].trim().length > 0 || targetIndex === 0) { - this.lastNonEmptyVariantIndex = targetIndex; - } - - // Focus the target input - const targetRow = this.variantContainer.querySelectorAll('.variant-editor-row')[targetIndex]; - if (targetRow) { - const targetInput = targetRow.querySelector('.variant-editor-input'); - if (targetInput) { - // Update visual active state - const currentActive = this.variantContainer.querySelector('.variant-editor-row-active'); - if (currentActive) { - currentActive.removeClass('variant-editor-row-active'); + } else { + // Try to find the current active variant in the filtered list + for (let i = 0; i < nonEmptyVariantsWithIndices.length; i++) { + if (nonEmptyVariantsWithIndices[i].originalIndex === this.activeVariantIndex) { + newActiveIndex = i; + activeVariantFound = true; + break; + } } - targetRow.addClass('variant-editor-row-active'); - - // Focus the target input - (targetInput as HTMLElement).focus(); - - // Place cursor at the end of the text - const range = document.createRange(); - const sel = window.getSelection(); - range.selectNodeContents(targetInput as Node); - range.collapse(false); // Collapse to end - sel?.removeAllRanges(); - sel?.addRange(range); - - // Only update the editor if the selected variant has content - if (this.variants[targetIndex].trim().length > 0 || targetIndex === 0) { - this.updateVariantsInEditor(); + } + + // If we still haven't found an active variant (e.g., if lastNonEmptyVariantIndex is also empty now) + if (!activeVariantFound && this.activeVariantIndex === this.variants.length - 1) { + // If we were on the last (new) variant, use the last non-empty variant + newActiveIndex = nonEmptyVariantsWithIndices.length - 1; + } + + // Join variants with pipe character for the expected format and pass the corrected active index + const nonEmptyVariants = nonEmptyVariantsWithIndices.map(v => v.text); + const variantText = nonEmptyVariants.join('|'); + + // Call onSubmit with the new variant text and active index + // The third parameter (false) indicates this is not a commit operation + // We're also passing the current cursor positions for tracking + this.onSubmit(variantText, newActiveIndex, false, this.currentFrom, this.currentTo); + + // Calculate the new cursor positions based on the variant text length + if (this.currentFrom) { + // If we have a currentTo position, update it based on the new variant text length + if (this.currentTo) { + const variantSyntax = `{{${variantText}}}^${newActiveIndex}`; + const newTo = { + line: this.currentFrom.line, + ch: this.currentFrom.ch + variantSyntax.length + }; + this.currentTo = newTo; } - } } - } } - }); - - // Add click handler specifically for the contenteditable div - variantInput.addEventListener('click', (e) => { - // Only handle the activation if this isn't already the active variant - // This allows text selection to work when clicking within the already active variant - if (this.activeVariantIndex !== index) { - // Set this as the active variant - this.activeVariantIndex = index; - - // If this variant has content, update the last non-empty variant index - if (variant.trim().length > 0 || index === 0) { - this.lastNonEmptyVariantIndex = index; - } - - // Only update the rows visually if we're changing the active variant - const currentActive = this.variantContainer.querySelector('.variant-editor-row-active'); - if (currentActive) { - currentActive.removeClass('variant-editor-row-active'); - } - variantRow.addClass('variant-editor-row-active'); - - // Only update the editor if the selected variant has content - if (variant.trim().length > 0 || index === 0) { - // Update the editor immediately when a non-empty variant is selected - this.updateVariantsInEditor(); - } - } - - // Always focus the input when clicked directly - (variantInput as HTMLElement).focus(); - - // Prevent the event from bubbling to avoid double-handling - e.stopPropagation(); - }); - - // Update the variant when the input changes - variantInput.addEventListener('input', (e) => { - this.variants[index] = variantInput.textContent || ''; - - // If this is the last row and user starts typing, add a new empty row - if (index === this.variants.length - 1 && variantInput.textContent && variantInput.textContent.trim() !== '') { - // Debounce adding a new row to avoid adding multiple rows rapidly - if (variantInput.dataset.addRowTimeout) { - clearTimeout(parseInt(variantInput.dataset.addRowTimeout)); - } - - const addRowTimeoutId = setTimeout(() => { - // Store the current active index (the one being typed in) - const currentActiveIndex = this.activeVariantIndex; - - // Add a new empty variant row - this.variants.push(''); - - // Re-render but keep focus on the current row - this.renderVariantInputs(currentActiveIndex); - }, 300); - - variantInput.dataset.addRowTimeout = addRowTimeoutId.toString(); - } - - // Debounce the update to avoid too many updates while typing - if (variantInput.dataset.updateTimeout) { - clearTimeout(parseInt(variantInput.dataset.updateTimeout)); - } - - const timeoutId = setTimeout(() => { - // Only update if we have at least two non-empty variants - const nonEmptyVariants = this.variants.filter(v => v.trim().length > 0); - if (nonEmptyVariants.length >= 2) { - // Make sure the active variant index is set to the current input's index - // This ensures the variant we're editing is the one that gets shown - if (this.variants[index].trim().length > 0) { - this.activeVariantIndex = index; - // Update the last non-empty variant index when a variant gets content - this.lastNonEmptyVariantIndex = index; + } + + private renderVariantInputs(focusIndex?: number) { + // Clear existing inputs + this.variantContainer.empty(); + + // Create an input for each variant + this.variants.forEach((variant, index) => { + // Add active class to the row if it's the active variant + const isActive = this.activeVariantIndex === index; + + // Check if this is the last empty row (add variant placeholder) + const isLastEmptyRow = index === this.variants.length - 1 && variant.trim() === ''; + + // Build the row classes + let rowClasses = 'variant-editor-row'; + if (isActive) rowClasses += ' variant-editor-row-active'; + if (isLastEmptyRow) rowClasses += ' variant-editor-row-add-variant'; + + const variantRow = this.variantContainer.createDiv({ + cls: rowClasses + }); + + // Make the entire row clickable to select this variant + variantRow.addEventListener('click', (e) => { + // Don't trigger when clicking on delete button or drag handle + if (!(e.target instanceof HTMLButtonElement)) { + // Set this as the active variant + this.activeVariantIndex = index; + + // If this variant has content, update the last non-empty variant index + if (variant.trim().length > 0 || index === 0) { + this.lastNonEmptyVariantIndex = index; + } + + // If we're clicking directly on the contenteditable div, let its own handler manage focus + // Otherwise, update the UI and focus the contenteditable + if (e.target instanceof HTMLDivElement && e.target.hasAttribute('contenteditable')) { + // Just update the active state visually without re-rendering + const currentActive = this.variantContainer.querySelector('.variant-editor-row-active'); + if (currentActive && currentActive !== variantRow) { + currentActive.removeClass('variant-editor-row-active'); + variantRow.addClass('variant-editor-row-active'); + + // Only update the editor if the selected variant has content + if (variant.trim().length > 0 || index === 0) { + this.updateVariantsInEditor(); + } + } + } else { + // For clicks elsewhere in the row, do a full update and focus the input + this.renderVariantInputs(index); + + // Only update the editor if the selected variant has content + if (variant.trim().length > 0 || index === 0) { + this.updateVariantsInEditor(); + } + + // Focus the contenteditable div when clicking anywhere else on the row + setTimeout(() => { + // Find the contenteditable div in the newly rendered row + const newRow = this.variantContainer.querySelectorAll('.variant-editor-row')[index]; + if (newRow) { + const input = newRow.querySelector('.variant-editor-input'); + if (input) { + (input as HTMLElement).focus(); + // Place cursor at the end + const range = document.createRange(); + const sel = window.getSelection(); + range.selectNodeContents(input as Node); + range.collapse(false); + sel?.removeAllRanges(); + sel?.addRange(range); + } + } + }, 0); + } + } + }); + + // Drag handle icon for reordering + const dragHandle = variantRow.createEl('button', { + cls: 'variant-editor-drag-handle clickable-icon', + attr: { + 'aria-label': 'Reorder variants' + } + }); + dragHandle.innerHTML = '≡'; + + // Use Obsidian's setTooltip API to position tooltip above the button + setTooltip(dragHandle, 'Reorder variants', { + placement: 'top' + }); + + // Don't add drag functionality to the "Add a variant" row + if (!isLastEmptyRow) { + // Make the drag handle draggable + dragHandle.setAttribute('draggable', 'true'); + + // Drag start event + dragHandle.addEventListener('dragstart', (e) => { + this.draggedElement = variantRow; + this.draggedIndex = index; + + // Add dragging class for visual feedback + variantRow.classList.add('dragging'); + + // Create an invisible drag image to replace the default one + const dragGhost = document.createElement('div'); + dragGhost.classList.add('variant-editor-drag-ghost'); + document.body.appendChild(dragGhost); + + // Set the custom drag image + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', index.toString()); + e.dataTransfer.setDragImage(dragGhost, 0, 0); + + // Clean up the ghost element after a short delay + setTimeout(() => { + document.body.removeChild(dragGhost); + }, 0); + } + }); + + // Drag end event + dragHandle.addEventListener('dragend', () => { + if (this.draggedElement) { + this.draggedElement.classList.remove('dragging'); + this.draggedElement = null; + this.draggedIndex = -1; + + // Remove drag-over class from all rows + const allRows = this.variantContainer.querySelectorAll('.variant-editor-row'); + allRows.forEach(row => row.classList.remove('drag-over')); + } + }); + + // Drag over event for the row + variantRow.addEventListener('dragover', (e) => { + e.preventDefault(); + e.stopPropagation(); + + // Only process if we have a dragged element + if (!this.draggedElement || this.draggedIndex === index) return; + + // Remove all drag indicators from all rows + const allRows = this.variantContainer.querySelectorAll('.variant-editor-row'); + allRows.forEach(row => { + row.classList.remove('drag-over-top'); + row.classList.remove('drag-over-bottom'); + }); + + // Determine if we're in the top or bottom half of the row + const rect = variantRow.getBoundingClientRect(); + const mouseY = e.clientY; + const rowMiddleY = rect.top + rect.height / 2; + + if (mouseY < rowMiddleY) { + // Top half - show indicator above this row + variantRow.classList.add('drag-over-top'); + this.dragOverIndex = index; + } else { + // Bottom half - show indicator below this row + variantRow.classList.add('drag-over-bottom'); + this.dragOverIndex = index + 1; + } + }); + + // Drag leave event + variantRow.addEventListener('dragleave', () => { + variantRow.classList.remove('drag-over-top'); + variantRow.classList.remove('drag-over-bottom'); + }); + + // Drop event + variantRow.addEventListener('drop', (e) => { + e.preventDefault(); + e.stopPropagation(); + + // Only process if we have a dragged element + if (!this.draggedElement || this.draggedIndex === this.dragOverIndex) return; + + // Remove visual indicators + variantRow.classList.remove('drag-over-top'); + variantRow.classList.remove('drag-over-bottom'); + + // Move the variant in the array + const draggedVariant = this.variants[this.draggedIndex]; + + // Get the target index (this.dragOverIndex was set in dragover) + const targetIndex = this.dragOverIndex; + + // Adjust targetIndex if dragging from above to below + let adjustedTargetIndex = targetIndex; + if (this.draggedIndex < targetIndex) { + // When dragging from above to below, we need to adjust the target index + // because removing the original item shifts all indexes down by 1 + adjustedTargetIndex--; + } + + // Remove the dragged variant + this.variants.splice(this.draggedIndex, 1); + + // Insert at the new position + this.variants.splice(adjustedTargetIndex, 0, draggedVariant); + + // Update active index if needed + if (this.activeVariantIndex === this.draggedIndex) { + this.activeVariantIndex = adjustedTargetIndex; + } else if (this.activeVariantIndex > this.draggedIndex && this.activeVariantIndex <= adjustedTargetIndex) { + this.activeVariantIndex--; + } else if (this.activeVariantIndex < this.draggedIndex && this.activeVariantIndex >= adjustedTargetIndex) { + this.activeVariantIndex++; + } + + // Update lastNonEmptyVariantIndex if needed + if (this.lastNonEmptyVariantIndex === this.draggedIndex) { + this.lastNonEmptyVariantIndex = adjustedTargetIndex; + } else if (this.lastNonEmptyVariantIndex > this.draggedIndex && this.lastNonEmptyVariantIndex <= adjustedTargetIndex) { + this.lastNonEmptyVariantIndex--; + } else if (this.lastNonEmptyVariantIndex < this.draggedIndex && this.lastNonEmptyVariantIndex >= adjustedTargetIndex) { + this.lastNonEmptyVariantIndex++; + } + + // Re-render with the new order + this.renderVariantInputs(this.activeVariantIndex); + + // Update the editor with the new order + this.updateVariantsInEditor(); + }); } - this.updateVariantsInEditor(); - } - }, 300); // 300ms debounce - slightly faster for better responsiveness - - variantInput.dataset.updateTimeout = timeoutId.toString(); - }); - - // Don't allow deleting the original variant or the empty "Add a variant" row - const isEmptyAddVariantRow = index === this.variants.length - 1 && variant.trim() === ''; - if (index > 0 && !isEmptyAddVariantRow) { - // Delete button - const deleteButton = variantRow.createEl('button', { - cls: 'variant-editor-delete-button clickable-icon', - attr: { - 'aria-label': 'Delete variant' - } - }); - deleteButton.innerHTML = '×'; - - // Use Obsidian's setTooltip API to position tooltip above the button - setTooltip(deleteButton, 'Delete variant', { - placement: 'top' - }); - - deleteButton.addEventListener('click', () => { - // If deleting the active variant, select the previous one - if (this.activeVariantIndex === index) { - this.activeVariantIndex = Math.max(0, index - 1); - } - // If deleting a variant before the active one, adjust the active index - else if (this.activeVariantIndex > index) { - this.activeVariantIndex--; - } - - // If we're deleting the last non-empty variant, update it - if (this.lastNonEmptyVariantIndex === index) { - // Find the new last non-empty variant - for (let i = this.variants.length - 1; i >= 0; i--) { - if (i !== index && (this.variants[i].trim().length > 0 || i === 0)) { - this.lastNonEmptyVariantIndex = i; - break; - } + + // Input for the variant - placeholder text varies by index + const placeholder = index === 0 ? 'Original text' : + index === this.variants.length - 1 && variant.trim() === '' ? 'Add a variant' : + `Variant ${index}`; + + // Create contenteditable div instead of input + const variantInput = variantRow.createDiv({ + cls: 'variant-editor-input', + attr: { + contenteditable: 'true', + 'data-placeholder': placeholder, + 'role': 'textbox', + 'aria-multiline': 'false' + } + }); + + // Set the text content + variantInput.textContent = variant; + + // Add keyboard navigation with arrow keys + variantInput.addEventListener('keydown', (e) => { + // Handle arrow key navigation + if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { + e.preventDefault(); // Prevent default scrolling behavior + + // Calculate the target index based on arrow key direction + const targetIndex = e.key === 'ArrowUp' + ? Math.max(0, index - 1) // Move up (minimum index is 0) + : Math.min(this.variants.length - 1, index + 1); // Move down (maximum is last variant) + + // Only proceed if we're actually moving to a different row + if (targetIndex !== index) { + // Set the new active variant index + this.activeVariantIndex = targetIndex; + + // If the target variant has content, update the last non-empty variant index + if (this.variants[targetIndex].trim().length > 0 || targetIndex === 0) { + this.lastNonEmptyVariantIndex = targetIndex; + } + + // Focus the target input + const targetRow = this.variantContainer.querySelectorAll('.variant-editor-row')[targetIndex]; + if (targetRow) { + const targetInput = targetRow.querySelector('.variant-editor-input'); + if (targetInput) { + // Update visual active state + const currentActive = this.variantContainer.querySelector('.variant-editor-row-active'); + if (currentActive) { + currentActive.removeClass('variant-editor-row-active'); + } + targetRow.addClass('variant-editor-row-active'); + + // Focus the target input + (targetInput as HTMLElement).focus(); + + // Place cursor at the end of the text + const range = document.createRange(); + const sel = window.getSelection(); + range.selectNodeContents(targetInput as Node); + range.collapse(false); // Collapse to end + sel?.removeAllRanges(); + sel?.addRange(range); + + // Only update the editor if the selected variant has content + if (this.variants[targetIndex].trim().length > 0 || targetIndex === 0) { + this.updateVariantsInEditor(); + } + } + } + } + } + }); + + // Add click handler specifically for the contenteditable div + variantInput.addEventListener('click', (e) => { + // Only handle the activation if this isn't already the active variant + // This allows text selection to work when clicking within the already active variant + if (this.activeVariantIndex !== index) { + // Set this as the active variant + this.activeVariantIndex = index; + + // If this variant has content, update the last non-empty variant index + if (variant.trim().length > 0 || index === 0) { + this.lastNonEmptyVariantIndex = index; + } + + // Only update the rows visually if we're changing the active variant + const currentActive = this.variantContainer.querySelector('.variant-editor-row-active'); + if (currentActive) { + currentActive.removeClass('variant-editor-row-active'); + } + variantRow.addClass('variant-editor-row-active'); + + // Only update the editor if the selected variant has content + if (variant.trim().length > 0 || index === 0) { + // Update the editor immediately when a non-empty variant is selected + this.updateVariantsInEditor(); + } + } + + // Always focus the input when clicked directly + (variantInput as HTMLElement).focus(); + + // Prevent the event from bubbling to avoid double-handling + e.stopPropagation(); + }); + + // Update the variant when the input changes + variantInput.addEventListener('input', (e) => { + this.variants[index] = variantInput.textContent || ''; + + // If this is the last row and user starts typing, add a new empty row + if (index === this.variants.length - 1 && variantInput.textContent && variantInput.textContent.trim() !== '') { + // Debounce adding a new row to avoid adding multiple rows rapidly + if (variantInput.dataset.addRowTimeout) { + clearTimeout(parseInt(variantInput.dataset.addRowTimeout)); + } + + const addRowTimeoutId = setTimeout(() => { + // Store the current active index (the one being typed in) + const currentActiveIndex = this.activeVariantIndex; + + // Add a new empty variant row + this.variants.push(''); + + // Re-render but keep focus on the current row + this.renderVariantInputs(currentActiveIndex); + }, 300); + + variantInput.dataset.addRowTimeout = addRowTimeoutId.toString(); + } + + // Debounce the update to avoid too many updates while typing + if (variantInput.dataset.updateTimeout) { + clearTimeout(parseInt(variantInput.dataset.updateTimeout)); + } + + const timeoutId = setTimeout(() => { + // Only update if we have at least two non-empty variants + const nonEmptyVariants = this.variants.filter(v => v.trim().length > 0); + if (nonEmptyVariants.length >= 2) { + // Make sure the active variant index is set to the current input's index + // This ensures the variant we're editing is the one that gets shown + if (this.variants[index].trim().length > 0) { + this.activeVariantIndex = index; + // Update the last non-empty variant index when a variant gets content + this.lastNonEmptyVariantIndex = index; + } + this.updateVariantsInEditor(); + } + }, 300); // 300ms debounce - slightly faster for better responsiveness + + variantInput.dataset.updateTimeout = timeoutId.toString(); + }); + + // Don't allow deleting the original variant or the empty "Add a variant" row + const isEmptyAddVariantRow = index === this.variants.length - 1 && variant.trim() === ''; + if (index > 0 && !isEmptyAddVariantRow) { + // Delete button + const deleteButton = variantRow.createEl('button', { + cls: 'variant-editor-delete-button clickable-icon', + attr: { + 'aria-label': 'Delete variant' + } + }); + deleteButton.innerHTML = '×'; + + // Use Obsidian's setTooltip API to position tooltip above the button + setTooltip(deleteButton, 'Delete variant', { + placement: 'top' + }); + + deleteButton.addEventListener('click', () => { + // If deleting the active variant, select the previous one + if (this.activeVariantIndex === index) { + this.activeVariantIndex = Math.max(0, index - 1); + } + // If deleting a variant before the active one, adjust the active index + else if (this.activeVariantIndex > index) { + this.activeVariantIndex--; + } + + // If we're deleting the last non-empty variant, update it + if (this.lastNonEmptyVariantIndex === index) { + // Find the new last non-empty variant + for (let i = this.variants.length - 1; i >= 0; i--) { + if (i !== index && (this.variants[i].trim().length > 0 || i === 0)) { + this.lastNonEmptyVariantIndex = i; + break; + } + } + } + + // Remove the variant from the array + this.variants.splice(index, 1); + + // If we deleted all variants except one, make sure it's not empty + if (this.variants.length === 1 && this.variants[0].trim() === '') { + this.variants[0] = ' '; // Use a space as default content + } + + // Re-render the variant inputs with focus on the new active index + this.renderVariantInputs(this.activeVariantIndex); + + // Update the editor content to reflect the deletion + this.updateVariantsInEditor(); + }); + } + + // Focus logic - prioritize the specified focusIndex if provided + const shouldFocus = + // If a specific index was provided to focus, focus that one + (focusIndex !== undefined && index === focusIndex) || + // Otherwise, focus the last empty row only if no specific focus index was provided + (focusIndex === undefined && variant === '' && index === this.variants.length - 1); + + if (shouldFocus) { + setTimeout(() => { + variantInput.focus(); + // Place cursor at the end + const range = document.createRange(); + const sel = window.getSelection(); + range.selectNodeContents(variantInput); + range.collapse(false); // false means collapse to end + sel?.removeAllRanges(); + sel?.addRange(range); + }, 10); } - } - - // Remove the variant from the array - this.variants.splice(index, 1); - - // If we deleted all variants except one, make sure it's not empty - if (this.variants.length === 1 && this.variants[0].trim() === '') { - this.variants[0] = ' '; // Use a space as default content - } - - // Re-render the variant inputs with focus on the new active index - this.renderVariantInputs(this.activeVariantIndex); - - // Update the editor content to reflect the deletion - this.updateVariantsInEditor(); }); - } - - // Focus logic - prioritize the specified focusIndex if provided - const shouldFocus = - // If a specific index was provided to focus, focus that one - (focusIndex !== undefined && index === focusIndex) || - // Otherwise, focus the last empty row only if no specific focus index was provided - (focusIndex === undefined && variant === '' && index === this.variants.length - 1); - - if (shouldFocus) { - setTimeout(() => { - variantInput.focus(); - // Place cursor at the end - const range = document.createRange(); - const sel = window.getSelection(); - range.selectNodeContents(variantInput); - range.collapse(false); // false means collapse to end - sel?.removeAllRanges(); - sel?.addRange(range); - }, 10); - } - }); - } + } - onClose() { - const {contentEl} = this; - contentEl.empty(); - - // Notify the parent that the modal was closed without committing - // We pass an explicit modalClosed=true flag to indicate this was triggered by modal closing - this.onSubmit(this.variants[this.activeVariantIndex], this.activeVariantIndex, false, this.currentFrom, this.currentTo, true); - } + onClose() { + const { contentEl } = this; + contentEl.empty(); + + // Notify the parent that the modal was closed without committing + // We pass an explicit modalClosed=true flag to indicate this was triggered by modal closing + this.onSubmit(this.variants[this.activeVariantIndex], this.activeVariantIndex, false, this.currentFrom, this.currentTo, true); + } } diff --git a/styles.css b/styles.css index 6ef171c..0541afd 100644 --- a/styles.css +++ b/styles.css @@ -36,10 +36,11 @@ } /* Make highlighted text stand out even when selected */ -.cm-line .cm-selectionBackground ~ .fh-highlight, -.cm-line .cm-selectionBackground + .fh-highlight, +.cm-line .cm-selectionBackground~.fh-highlight, +.cm-line .cm-selectionBackground+.fh-highlight, .cm-line .fh-highlight.cm-selectionBackground { - background-color: rgba(255, 207, 64, 0.6) !important; /* Lighter yellow that shows through selection */ + background-color: rgba(255, 207, 64, 0.6) !important; + /* Lighter yellow that shows through selection */ mix-blend-mode: normal; opacity: 1; } @@ -160,7 +161,8 @@ inset: -2px; padding: 2px; background: linear-gradient(135deg, #8b5cf6, #ec4899, #f97316); - border-radius: 10px; /* Match the row border-radius + 2px for the inset */ + border-radius: 10px; + /* Match the row border-radius + 2px for the inset */ mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); mask-composite: exclude; -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); @@ -305,7 +307,8 @@ /* Special handling for active rows with drag indicators */ .variant-editor-row-active.drag-over-top::after, .variant-editor-row-active.drag-over-bottom::after { - z-index: 20; /* Higher z-index to appear above the rainbow border */ + z-index: 20; + /* Higher z-index to appear above the rainbow border */ } @@ -353,12 +356,16 @@ .variant-editor-button { min-width: 144px; max-width: 216px; - padding: 10px 24px !important; /* Reduced vertical padding to make buttons thinner */ - border-radius: 8px !important; /* Match variant row border-radius */ + padding: 10px 24px !important; + /* Reduced vertical padding to make buttons thinner */ + border-radius: 8px !important; + /* Match variant row border-radius */ font-size: 16px !important; font-weight: 600 !important; - transition: all 0.25s cubic-bezier(0.25, 0.1, 0.25, 1) !important; /* Improved animation curve */ - border: 1px solid #333 !important; /* Match variant row border */ + transition: all 0.25s cubic-bezier(0.25, 0.1, 0.25, 1) !important; + /* Improved animation curve */ + border: 1px solid #333 !important; + /* Match variant row border */ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) !important; text-align: center !important; color: white !important; @@ -367,13 +374,15 @@ align-items: center !important; justify-content: center !important; gap: 10px !important; - height: 42px !important; /* Reduced height by 10px */ + height: 42px !important; + /* Reduced height by 10px */ } .variant-editor-commit-button { - background-color: #7E6BDC !important; /* Obsidian purple */ + background-color: #7E6BDC !important; + /* Obsidian purple */ color: white !important; } @@ -404,7 +413,8 @@ /* Style the variant editor modal */ .variant-editor-modal { max-width: 90vw !important; - width: 460px !important; /* Reduced from 500px to 460px (40px less) */ + width: 460px !important; + /* Reduced from 500px to 460px (40px less) */ max-height: 80vh !important; padding: 0; overflow-y: auto !important; @@ -461,7 +471,7 @@ } /* Remove background dimming for variant editor modal */ -.variant-editor-no-dim ~ .modal-bg { +.variant-editor-no-dim~.modal-bg { background-color: transparent !important; backdrop-filter: none !important; } @@ -470,4 +480,4 @@ outline: none; border-color: var(--interactive-accent); box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2); -} +} \ No newline at end of file