π 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:
- Click on Edit button β Sometimes triggers seek if click slightly off-target
- Click on textarea β Triggers seek, interrupting editing
- 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.targetdirectly
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)
Visual feedback on buttons
- Add hover states to make clickable areas more obvious
- Increase button size for better touch targets
Keyboard shortcuts
Enterto save (with Ctrl/Cmd modifier)Escapeto cancel- Already implemented for speaker name editing
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:
- β Stopping event propagation on all edit-related buttons
- β Detecting clicks within the edit area
- β Preserving normal seek behavior for non-edit interactions
- β Maintaining clean, maintainable code
The edit workflow is now smooth and intuitive! π