VoxSum / EDIT_BUTTON_BUG_FIX.md
Luigi's picture
fix: prevent click-to-seek when editing utterance text
4d2f95d

πŸ› Bug Fix: Edit Button Accidentally Triggers Seek

Problem Description

Original Issue

When clicking the edit button (✏️) or clicking inside the textarea while editing an utterance, the click event would bubble up and trigger the seekToTime() function, causing unwanted audio player behavior:

  1. Click on Edit button β†’ Sometimes triggers seek if click slightly off-target
  2. Click on textarea β†’ Triggers seek, interrupting editing
  3. Click on Save/Cancel buttons β†’ Triggers seek

User Impact

  • 😟 Disorienting: Audio jumps unexpectedly while trying to edit
  • 😀 Frustrating: Can't select text in textarea without triggering seek
  • πŸ› Poor UX: Edit workflow is interrupted

Root Cause Analysis

Event Bubbling Problem

HTML Structure:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ .utterance-item (click listener)    β”‚ ← Event listener here
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ .utterance-header              β”‚ β”‚
β”‚  β”‚  [timestamp] [speaker] [✏️]    β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ .utterance-text               β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ .edit-area (hidden)            β”‚ β”‚
β”‚  β”‚  <textarea>...</textarea>      β”‚ β”‚
β”‚  β”‚  [Save] [Cancel]               β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Event Flow (Before Fix)

User clicks on textarea
      ↓
Click event on <textarea>
      ↓
Bubbles to .edit-area
      ↓
Bubbles to .utterance-item  ← Listener catches it here
      ↓
Checks: editButton? No
Checks: saveButton? No
Checks: cancelButton? No
      ↓
❌ Falls through to seekToTime(start)
      ↓
🎡 Audio jumps to utterance start time

Solution Implemented

Two-Pronged Approach

1. Event Propagation Control

Added event.stopPropagation() on all edit-related buttons to prevent event bubbling:

if (editButton) {
  event.stopPropagation();  // βœ… Stop bubbling
  toggleEdit(item, true);
  return;
}

if (saveButton) {
  event.stopPropagation();  // βœ… Stop bubbling
  // ... save logic ...
  return;
}

if (cancelButton) {
  event.stopPropagation();  // βœ… Stop bubbling
  toggleEdit(item, false);
  return;
}

2. Edit Area Detection

Added explicit checks for clicks within the edit area or textarea:

const editArea = event.target.closest('.edit-area');
const textarea = event.target.closest('textarea');

// Prevent seek when clicking on edit area or textarea
if (editArea || textarea) {
  return;  // βœ… Do nothing, allow text selection/editing
}

Event Flow (After Fix)

Scenario 1: Click Edit Button

User clicks edit button ✏️
      ↓
Click event on .edit-btn
      ↓
Bubbles to .utterance-item
      ↓
Checks: editButton? Yes
      ↓
event.stopPropagation()  βœ… Stop here!
      ↓
toggleEdit(item, true)
      ↓
βœ… Edit mode activated, no seek

Scenario 2: Click on Textarea

User clicks inside textarea
      ↓
Click event on <textarea>
      ↓
Bubbles to .edit-area
      ↓
Bubbles to .utterance-item
      ↓
Checks: editArea? Yes
      ↓
return;  βœ… Stop here!
      ↓
βœ… Text selection works, no seek

Scenario 3: Click on Save Button

User clicks Save button
      ↓
Click event on .save-edit
      ↓
Bubbles to .utterance-item
      ↓
Checks: saveButton? Yes
      ↓
event.stopPropagation()  βœ… Stop here!
      ↓
Save text + toggleEdit(item, false)
      ↓
βœ… Edit saved, no seek

Scenario 4: Click on Utterance Text (Normal Seek)

User clicks on .utterance-text
      ↓
Click event on .utterance-text
      ↓
Bubbles to .utterance-item
      ↓
Checks: editButton? No
Checks: saveButton? No
Checks: cancelButton? No
Checks: editArea? No
Checks: textarea? No
      ↓
Falls through to seekToTime(start)
      ↓
βœ… Audio seeks as expected

Code Changes

File: frontend/app.js

elements.transcriptList.addEventListener('click', (event) => {
  const item = event.target.closest('.utterance-item');
  if (!item) return;
  
  // ✨ NEW: Additional selectors for edit-related elements
  const editButton = event.target.closest('.edit-btn');
  const saveButton = event.target.closest('.save-edit');
  const cancelButton = event.target.closest('.cancel-edit');
  const speakerTag = event.target.closest('.editable-speaker');
  const editArea = event.target.closest('.edit-area');      // NEW
  const textarea = event.target.closest('textarea');        // NEW

  const index = Number(item.dataset.index);

  // Handle speaker tag editing
  if (speakerTag && !speakerTag.querySelector('input')) {
    startSpeakerEdit(speakerTag);
    return;
  }

  // ✨ MODIFIED: Stop propagation on edit button
  if (editButton) {
    event.stopPropagation();  // NEW
    toggleEdit(item, true);
    return;
  }

  // ✨ MODIFIED: Stop propagation on save button
  if (saveButton) {
    event.stopPropagation();  // NEW
    const textarea = item.querySelector('textarea');
    const newText = textarea.value.trim();
    if (newText.length === 0) return;
    state.utterances[index].text = newText;
    item.querySelector('.utterance-text').textContent = newText;
    toggleEdit(item, false);
    return;
  }

  // ✨ MODIFIED: Stop propagation on cancel button
  if (cancelButton) {
    event.stopPropagation();  // NEW
    toggleEdit(item, false);
    return;
  }

  // ✨ NEW: Prevent seek when clicking on edit area or textarea
  if (editArea || textarea) {
    return;  // Do nothing, allow text selection/editing
  }

  // Default behavior: seek to utterance start time
  const start = Number(item.dataset.start);
  seekToTime(start);
});

Testing Scenarios

βœ… Test 1: Edit Button Click

Steps:
1. Click the edit button (✏️) on any utterance
2. Observe audio player

Expected: 
- Edit mode activates
- Textarea appears
- Audio player does NOT seek

Result: βœ… Pass

βœ… Test 2: Click Inside Textarea

Steps:
1. Enter edit mode
2. Click inside the textarea to position cursor
3. Try to select text
4. Observe audio player

Expected:
- Cursor positioning works
- Text selection works
- Audio player does NOT seek

Result: βœ… Pass

βœ… Test 3: Save Button Click

Steps:
1. Enter edit mode
2. Modify text
3. Click Save button
4. Observe audio player

Expected:
- Changes saved
- Edit mode exits
- Audio player does NOT seek

Result: βœ… Pass

βœ… Test 4: Cancel Button Click

Steps:
1. Enter edit mode
2. Modify text (don't save)
3. Click Cancel button
4. Observe audio player

Expected:
- Changes discarded
- Edit mode exits
- Audio player does NOT seek

Result: βœ… Pass

βœ… Test 5: Normal Seek (Not in Edit Mode)

Steps:
1. Ensure no utterance is in edit mode
2. Click on utterance text or timestamp
3. Observe audio player

Expected:
- Audio player seeks to utterance start time
- Audio starts playing

Result: βœ… Pass

βœ… Test 6: Edge Case - Click on Edit Area Border

Steps:
1. Enter edit mode
2. Click on the border/padding of edit area (not textarea)
3. Observe audio player

Expected:
- No seek triggered
- Edit mode remains active

Result: βœ… Pass

Impact Summary

Aspect Before After Improvement
Edit button usability Unreliable Reliable βœ… Fixed
Textarea interaction Triggers seek Works normally βœ… Fixed
Save/Cancel buttons May trigger seek No seek βœ… Fixed
Normal seek behavior Works Still works βœ… Preserved
Event handling Single check Multi-layered πŸ›‘οΈ More robust
Code clarity Implicit Explicit πŸ“– Better

Technical Details

Why event.stopPropagation()?

stopPropagation() prevents the event from bubbling up the DOM tree, which is crucial when:

  • Child elements need different behavior than parent
  • Multiple nested click handlers exist
  • You want fine-grained control over event handling

Why closest() Selector?

const editArea = event.target.closest('.edit-area');

closest() traverses up the DOM tree to find the first matching ancestor, which:

  • βœ… Catches clicks on any descendant of .edit-area
  • βœ… Works even if clicking on nested elements (textarea, buttons)
  • βœ… More reliable than checking event.target directly

Alternative Approaches (Not Used)

❌ Approach 1: Separate listeners on each button

  • Pro: More explicit
  • Con: Harder to maintain, more memory

❌ Approach 2: Check if edit mode is active

  • Pro: Simple boolean check
  • Con: Doesn't handle clicks on buttons themselves

βœ… Approach 3: Multi-layered detection (Chosen)

  • Pro: Handles all cases elegantly
  • Pro: Maintains single event listener
  • Pro: Easy to understand and debug

Browser Compatibility

Feature Support
event.stopPropagation() βœ… All browsers
element.closest() βœ… All modern browsers (IE9+)
Event bubbling βœ… All browsers

Future Improvements (Optional)

  1. Visual feedback on buttons

    • Add hover states to make clickable areas more obvious
    • Increase button size for better touch targets
  2. Keyboard shortcuts

    • Enter to save (with Ctrl/Cmd modifier)
    • Escape to cancel
    • Already implemented for speaker name editing
  3. Double-click to edit

    • Alternative to clicking edit button
    • Single click = seek, double click = edit

Conclusion

This fix provides a robust solution to prevent accidental seeks during editing by:

  1. βœ… Stopping event propagation on all edit-related buttons
  2. βœ… Detecting clicks within the edit area
  3. βœ… Preserving normal seek behavior for non-edit interactions
  4. βœ… Maintaining clean, maintainable code

The edit workflow is now smooth and intuitive! πŸŽ‰