fix: refactor contents control architecture and resolve resize handle positioning

- Streamlined ContentsControl to use base class generateContent pattern
- Removed duplicate methods and unified content generation approach
- Added overflow: visible to fix content visibility issues
- Fixed resize handle positioning (moved from -4px to 1px/2px from right edge)
- Improved search functionality to properly rebuild content
- Enhanced refresh button detection to prevent conflicts
- Removed unused getDocumentStats method and duplicate code blocks

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-14 23:31:13 +01:00
parent 4e3f112987
commit d98c3ae05a
4 changed files with 162 additions and 276 deletions

View File

@@ -10,7 +10,6 @@
* - Hierarchical display with proper indentation
* - Clickable navigation links with smooth scrolling
* - Real-time updates when document structure changes
* - Collapsible sections for better organization
* - Search functionality within the table of contents
*
* Dependencies:
@@ -20,9 +19,9 @@
/**
* ContentsControl - Interactive table of contents control
*
* This control scans the document for headings (h1-h6) and presents them
* in a navigable tree structure. Users can click on any heading to jump
* directly to that section with smooth scrolling.
* Built on the base class architecture for consistency with other panels.
* Only implements content-specific functionality while inheriting all
* common panel behavior from ControlBase.
*/
class ContentsControl extends ControlBase {
constructor() {
@@ -45,6 +44,80 @@ class ContentsControl extends ControlBase {
this.searchQuery = '';
}
/**
* Generate contents control content (called by base class buildContent)
*/
generateContent() {
// Extract headings first
this.extractHeadings();
return this.safeOperation(() => {
if (this.headings.length === 0) {
return `
<div style="text-align: center; color: #666; padding: 2rem 0;">
<p>No headings found in document</p>
<button onclick="this.closest('.contents-control').contentsControl.refreshContents()"
style="padding: 0.5rem 1rem; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; margin-top: 0.5rem;">
🔄 Refresh
</button>
</div>
`;
}
const searchHTML = `
<div style="margin-bottom: 0.5rem; border-bottom: 1px solid #eee; padding-bottom: 0.5rem;">
<input type="text"
placeholder="Search headings..."
style="width: 100%; padding: 0.25rem; border: 1px solid #ddd; border-radius: 3px; font-size: 0.8rem; box-sizing: border-box; overflow: visible;"
onkeyup="this.closest('.contents-control').contentsControl.handleSearch(this.value)">
</div>
`;
const filteredHeadings = this.filterHeadings(this.headings, this.searchQuery);
const contentsHTML = filteredHeadings.map(heading => {
const indentLevel = Math.max(0, heading.level - 1);
const indentPx = indentLevel * 15;
return `
<div class="contents-item"
style="margin-bottom: 0.3rem; padding-left: ${indentPx}px; overflow: visible;">
<a href="#${heading.id}"
onclick="event.preventDefault(); this.closest('.contents-control').contentsControl.navigateToHeading('${heading.id}')"
style="display: block; padding: 0.2rem 0; color: #007bff; text-decoration: none; font-size: 0.8rem; line-height: 1.2; overflow: visible;"
onmouseover="this.style.backgroundColor='#f8f9fa'"
onmouseout="this.style.backgroundColor='transparent'">
<span class="heading-level" style="color: #666; margin-right: 0.3rem;">H${heading.level}</span>
<span class="heading-text">${heading.text}</span>
</a>
</div>
`;
}).join('');
const statusHTML = `
<div style="margin-bottom: 0.5rem; font-size: 0.7rem; color: #666; text-align: center;">
Found ${filteredHeadings.length} heading${filteredHeadings.length !== 1 ? 's' : ''}
</div>
`;
const refreshButtonHTML = `
<div style="border-top: 1px solid #eee; padding-top: 0.5rem; text-align: center; margin-top: 0.5rem;">
<button onclick="this.closest('.contents-control').contentsControl.refreshContents()"
style="padding: 0.3rem 0.6rem; font-size: 0.7rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">
🔄 Refresh Contents
</button>
</div>
`;
return `
${searchHTML}
${statusHTML}
${contentsHTML}
${refreshButtonHTML}
`;
}, 'Error generating contents', 'generateContent');
}
/**
* Extract all headings from the document
* Creates a hierarchical structure of the document's heading elements
@@ -109,74 +182,6 @@ class ContentsControl extends ControlBase {
);
}
/**
* Generate HTML for the table of contents
*/
generateContentsHTML(headings = null) {
return this.safeOperation(() => {
const displayHeadings = headings || this.headings;
if (displayHeadings.length === 0) {
return `
<div style="text-align: center; color: #666; padding: 2rem 0;">
<p>No headings found in document</p>
<button onclick="this.closest('.contents-control').contentsControl.refreshContents()"
style="padding: 0.5rem 1rem; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; margin-top: 0.5rem;">
🔄 Refresh
</button>
</div>
`;
}
const searchHTML = `
<div style="margin-bottom: 0.5rem; border-bottom: 1px solid #eee; padding-bottom: 0.5rem;">
<input type="text"
placeholder="Search headings..."
style="width: 100%; padding: 0.25rem; border: 1px solid #ddd; border-radius: 3px; font-size: 0.8rem; box-sizing: border-box;"
onkeyup="this.closest('.contents-control').contentsControl.handleSearch(this.value)">
</div>
`;
const contentsHTML = displayHeadings.map(heading => {
const indentLevel = Math.max(0, heading.level - 1);
const indentPx = indentLevel * 15;
return `
<div class="contents-item"
style="margin-bottom: 0.3rem; padding-left: ${indentPx}px;">
<a href="#${heading.id}"
onclick="event.preventDefault(); this.closest('.contents-control').contentsControl.navigateToHeading('${heading.id}')"
style="display: block; padding: 0.2rem 0; color: #007bff; text-decoration: none; font-size: 0.8rem; line-height: 1.2;"
onmouseover="this.style.backgroundColor='#f8f9fa'"
onmouseout="this.style.backgroundColor='transparent'">
<span class="heading-level" style="color: #666; margin-right: 0.3rem;">H${heading.level}</span>
<span class="heading-text">${heading.text}</span>
</a>
</div>
`;
}).join('');
return `
<div>
${searchHTML}
<div style="margin-bottom: 0.5rem;">
<div style="margin-bottom: 0.5rem; font-size: 0.7rem; color: #666; text-align: center;">
Found ${displayHeadings.length} heading${displayHeadings.length !== 1 ? 's' : ''}
</div>
${contentsHTML}
</div>
<div style="border-top: 1px solid #eee; padding-top: 0.5rem; text-align: center;">
<button onclick="this.closest('.contents-control').contentsControl.refreshContents()"
style="padding: 0.3rem 0.6rem; font-size: 0.7rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">
🔄 Refresh Contents
</button>
</div>
</div>
`;
}, '<p>Error generating contents</p>', 'generateContentsHTML');
}
/**
* Navigate to a specific heading with smooth scrolling
*/
@@ -212,20 +217,7 @@ class ContentsControl extends ControlBase {
*/
handleSearch(query) {
this.searchQuery = query;
const filteredHeadings = this.filterHeadings(this.headings, query);
this.updateContentsDisplay(filteredHeadings);
}
/**
* Update the contents display with new headings
*/
updateContentsDisplay(headings) {
return this.safeOperation(() => {
const content = this.element?.querySelector('.control-content');
if (content) {
content.innerHTML = this.generateContentsHTML(headings);
}
}, null, 'updateContentsDisplay');
this.buildContent(); // Rebuild content with new filter
}
/**
@@ -234,12 +226,11 @@ class ContentsControl extends ControlBase {
refreshContents() {
return this.safeOperation(() => {
this.extractHeadings();
const filteredHeadings = this.filterHeadings(this.headings, this.searchQuery);
this.updateContentsDisplay(filteredHeadings);
this.buildContent(); // Rebuild content with updated headings
// Show success feedback
const refreshBtn = this.element?.querySelector('button');
if (refreshBtn) {
if (refreshBtn && refreshBtn.textContent.includes('Refresh')) {
const originalText = refreshBtn.innerHTML;
refreshBtn.innerHTML = '✅ Updated';
refreshBtn.style.background = '#28a745';
@@ -252,23 +243,6 @@ class ContentsControl extends ControlBase {
}, null, 'refreshContents');
}
/**
* Build the control content
* Override of base class method to provide contents-specific functionality
*/
/**
* Generate contents control content (called by base class buildContent)
*/
generateContent() {
// Extract headings first
this.extractHeadings();
return this.safeOperation(() => {
return this.generateContentsHTML();
}, 'Error generating contents', 'generateContent');
}
/**
* Override buildContent to add control reference and auto-refresh
*/
@@ -293,37 +267,6 @@ class ContentsControl extends ControlBase {
}, 5000); // Check every 5 seconds
}
/**
* Get statistics about the document structure
*/
getDocumentStats() {
return this.safeOperation(() => {
const stats = {
totalHeadings: this.headings.length,
byLevel: {},
deepestLevel: 1,
structure: 'flat'
};
// Count headings by level
this.headings.forEach(heading => {
stats.byLevel[heading.level] = (stats.byLevel[heading.level] || 0) + 1;
stats.deepestLevel = Math.max(stats.deepestLevel, heading.level);
});
// Determine structure type
const levels = Object.keys(stats.byLevel).map(Number).sort();
if (levels.length > 1) {
const hasSequentialLevels = levels.every((level, index) =>
index === 0 || level <= levels[index - 1] + 1
);
stats.structure = hasSequentialLevels ? 'hierarchical' : 'mixed';
}
return stats;
}, {}, 'getDocumentStats');
}
/**
* Clean up resources when control is destroyed
*/

View File

@@ -541,7 +541,7 @@ class ControlBase {
resizeHandle.style.cssText = `
position: absolute;
bottom: 0px;
right: -4px;
right: 1px;
width: 12px;
height: 12px;
cursor: se-resize;

View File

@@ -10,7 +10,6 @@
* - Hierarchical display with proper indentation
* - Clickable navigation links with smooth scrolling
* - Real-time updates when document structure changes
* - Collapsible sections for better organization
* - Search functionality within the table of contents
*
* Dependencies:
@@ -20,9 +19,9 @@
/**
* ContentsControl - Interactive table of contents control
*
* This control scans the document for headings (h1-h6) and presents them
* in a navigable tree structure. Users can click on any heading to jump
* directly to that section with smooth scrolling.
* Built on the base class architecture for consistency with other panels.
* Only implements content-specific functionality while inheriting all
* common panel behavior from ControlBase.
*/
class ContentsControl extends ControlBase {
constructor() {
@@ -45,6 +44,80 @@ class ContentsControl extends ControlBase {
this.searchQuery = '';
}
/**
* Generate contents control content (called by base class buildContent)
*/
generateContent() {
// Extract headings first
this.extractHeadings();
return this.safeOperation(() => {
if (this.headings.length === 0) {
return `
<div style="text-align: center; color: #666; padding: 2rem 0;">
<p>No headings found in document</p>
<button onclick="this.closest('.contents-control').contentsControl.refreshContents()"
style="padding: 0.5rem 1rem; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; margin-top: 0.5rem;">
🔄 Refresh
</button>
</div>
`;
}
const searchHTML = `
<div style="margin-bottom: 0.5rem; border-bottom: 1px solid #eee; padding-bottom: 0.5rem;">
<input type="text"
placeholder="Search headings..."
style="width: 100%; padding: 0.25rem; border: 1px solid #ddd; border-radius: 3px; font-size: 0.8rem; box-sizing: border-box; overflow: visible;"
onkeyup="this.closest('.contents-control').contentsControl.handleSearch(this.value)">
</div>
`;
const filteredHeadings = this.filterHeadings(this.headings, this.searchQuery);
const contentsHTML = filteredHeadings.map(heading => {
const indentLevel = Math.max(0, heading.level - 1);
const indentPx = indentLevel * 15;
return `
<div class="contents-item"
style="margin-bottom: 0.3rem; padding-left: ${indentPx}px; overflow: visible;">
<a href="#${heading.id}"
onclick="event.preventDefault(); this.closest('.contents-control').contentsControl.navigateToHeading('${heading.id}')"
style="display: block; padding: 0.2rem 0; color: #007bff; text-decoration: none; font-size: 0.8rem; line-height: 1.2; overflow: visible;"
onmouseover="this.style.backgroundColor='#f8f9fa'"
onmouseout="this.style.backgroundColor='transparent'">
<span class="heading-level" style="color: #666; margin-right: 0.3rem;">H${heading.level}</span>
<span class="heading-text">${heading.text}</span>
</a>
</div>
`;
}).join('');
const statusHTML = `
<div style="margin-bottom: 0.5rem; font-size: 0.7rem; color: #666; text-align: center;">
Found ${filteredHeadings.length} heading${filteredHeadings.length !== 1 ? 's' : ''}
</div>
`;
const refreshButtonHTML = `
<div style="border-top: 1px solid #eee; padding-top: 0.5rem; text-align: center; margin-top: 0.5rem;">
<button onclick="this.closest('.contents-control').contentsControl.refreshContents()"
style="padding: 0.3rem 0.6rem; font-size: 0.7rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">
🔄 Refresh Contents
</button>
</div>
`;
return `
${searchHTML}
${statusHTML}
${contentsHTML}
${refreshButtonHTML}
`;
}, 'Error generating contents', 'generateContent');
}
/**
* Extract all headings from the document
* Creates a hierarchical structure of the document's heading elements
@@ -109,74 +182,6 @@ class ContentsControl extends ControlBase {
);
}
/**
* Generate HTML for the table of contents
*/
generateContentsHTML(headings = null) {
return this.safeOperation(() => {
const displayHeadings = headings || this.headings;
if (displayHeadings.length === 0) {
return `
<div style="text-align: center; color: #666; padding: 2rem 0;">
<p>No headings found in document</p>
<button onclick="this.closest('.contents-control').contentsControl.refreshContents()"
style="padding: 0.5rem 1rem; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer; margin-top: 0.5rem;">
🔄 Refresh
</button>
</div>
`;
}
const searchHTML = `
<div style="margin-bottom: 0.5rem; border-bottom: 1px solid #eee; padding-bottom: 0.5rem;">
<input type="text"
placeholder="Search headings..."
style="width: 100%; padding: 0.25rem; border: 1px solid #ddd; border-radius: 3px; font-size: 0.8rem; box-sizing: border-box;"
onkeyup="this.closest('.contents-control').contentsControl.handleSearch(this.value)">
</div>
`;
const contentsHTML = displayHeadings.map(heading => {
const indentLevel = Math.max(0, heading.level - 1);
const indentPx = indentLevel * 15;
return `
<div class="contents-item"
style="margin-bottom: 0.3rem; padding-left: ${indentPx}px;">
<a href="#${heading.id}"
onclick="event.preventDefault(); this.closest('.contents-control').contentsControl.navigateToHeading('${heading.id}')"
style="display: block; padding: 0.2rem 0; color: #007bff; text-decoration: none; font-size: 0.8rem; line-height: 1.2;"
onmouseover="this.style.backgroundColor='#f8f9fa'"
onmouseout="this.style.backgroundColor='transparent'">
<span class="heading-level" style="color: #666; margin-right: 0.3rem;">H${heading.level}</span>
<span class="heading-text">${heading.text}</span>
</a>
</div>
`;
}).join('');
return `
<div>
${searchHTML}
<div style="margin-bottom: 0.5rem;">
<div style="margin-bottom: 0.5rem; font-size: 0.7rem; color: #666; text-align: center;">
Found ${displayHeadings.length} heading${displayHeadings.length !== 1 ? 's' : ''}
</div>
${contentsHTML}
</div>
<div style="border-top: 1px solid #eee; padding-top: 0.5rem; text-align: center;">
<button onclick="this.closest('.contents-control').contentsControl.refreshContents()"
style="padding: 0.3rem 0.6rem; font-size: 0.7rem; background: #28a745; color: white; border: none; border-radius: 3px; cursor: pointer;">
🔄 Refresh Contents
</button>
</div>
</div>
`;
}, '<p>Error generating contents</p>', 'generateContentsHTML');
}
/**
* Navigate to a specific heading with smooth scrolling
*/
@@ -212,20 +217,7 @@ class ContentsControl extends ControlBase {
*/
handleSearch(query) {
this.searchQuery = query;
const filteredHeadings = this.filterHeadings(this.headings, query);
this.updateContentsDisplay(filteredHeadings);
}
/**
* Update the contents display with new headings
*/
updateContentsDisplay(headings) {
return this.safeOperation(() => {
const content = this.element?.querySelector('.control-content');
if (content) {
content.innerHTML = this.generateContentsHTML(headings);
}
}, null, 'updateContentsDisplay');
this.buildContent(); // Rebuild content with new filter
}
/**
@@ -234,12 +226,11 @@ class ContentsControl extends ControlBase {
refreshContents() {
return this.safeOperation(() => {
this.extractHeadings();
const filteredHeadings = this.filterHeadings(this.headings, this.searchQuery);
this.updateContentsDisplay(filteredHeadings);
this.buildContent(); // Rebuild content with updated headings
// Show success feedback
const refreshBtn = this.element?.querySelector('button');
if (refreshBtn) {
if (refreshBtn && refreshBtn.textContent.includes('Refresh')) {
const originalText = refreshBtn.innerHTML;
refreshBtn.innerHTML = '✅ Updated';
refreshBtn.style.background = '#28a745';
@@ -252,23 +243,6 @@ class ContentsControl extends ControlBase {
}, null, 'refreshContents');
}
/**
* Build the control content
* Override of base class method to provide contents-specific functionality
*/
/**
* Generate contents control content (called by base class buildContent)
*/
generateContent() {
// Extract headings first
this.extractHeadings();
return this.safeOperation(() => {
return this.generateContentsHTML();
}, 'Error generating contents', 'generateContent');
}
/**
* Override buildContent to add control reference and auto-refresh
*/
@@ -293,37 +267,6 @@ class ContentsControl extends ControlBase {
}, 5000); // Check every 5 seconds
}
/**
* Get statistics about the document structure
*/
getDocumentStats() {
return this.safeOperation(() => {
const stats = {
totalHeadings: this.headings.length,
byLevel: {},
deepestLevel: 1,
structure: 'flat'
};
// Count headings by level
this.headings.forEach(heading => {
stats.byLevel[heading.level] = (stats.byLevel[heading.level] || 0) + 1;
stats.deepestLevel = Math.max(stats.deepestLevel, heading.level);
});
// Determine structure type
const levels = Object.keys(stats.byLevel).map(Number).sort();
if (levels.length > 1) {
const hasSequentialLevels = levels.every((level, index) =>
index === 0 || level <= levels[index - 1] + 1
);
stats.structure = hasSequentialLevels ? 'hierarchical' : 'mixed';
}
return stats;
}, {}, 'getDocumentStats');
}
/**
* Clean up resources when control is destroyed
*/

View File

@@ -541,7 +541,7 @@ class ControlBase {
resizeHandle.style.cssText = `
position: absolute;
bottom: 0px;
right: -4px;
right: 2px;
width: 12px;
height: 12px;
cursor: se-resize;