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:
@@ -10,7 +10,6 @@
|
|||||||
* - Hierarchical display with proper indentation
|
* - Hierarchical display with proper indentation
|
||||||
* - Clickable navigation links with smooth scrolling
|
* - Clickable navigation links with smooth scrolling
|
||||||
* - Real-time updates when document structure changes
|
* - Real-time updates when document structure changes
|
||||||
* - Collapsible sections for better organization
|
|
||||||
* - Search functionality within the table of contents
|
* - Search functionality within the table of contents
|
||||||
*
|
*
|
||||||
* Dependencies:
|
* Dependencies:
|
||||||
@@ -20,9 +19,9 @@
|
|||||||
/**
|
/**
|
||||||
* ContentsControl - Interactive table of contents control
|
* ContentsControl - Interactive table of contents control
|
||||||
*
|
*
|
||||||
* This control scans the document for headings (h1-h6) and presents them
|
* Built on the base class architecture for consistency with other panels.
|
||||||
* in a navigable tree structure. Users can click on any heading to jump
|
* Only implements content-specific functionality while inheriting all
|
||||||
* directly to that section with smooth scrolling.
|
* common panel behavior from ControlBase.
|
||||||
*/
|
*/
|
||||||
class ContentsControl extends ControlBase {
|
class ContentsControl extends ControlBase {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -45,6 +44,80 @@ class ContentsControl extends ControlBase {
|
|||||||
this.searchQuery = '';
|
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
|
* Extract all headings from the document
|
||||||
* Creates a hierarchical structure of the document's heading elements
|
* 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
|
* Navigate to a specific heading with smooth scrolling
|
||||||
*/
|
*/
|
||||||
@@ -212,20 +217,7 @@ class ContentsControl extends ControlBase {
|
|||||||
*/
|
*/
|
||||||
handleSearch(query) {
|
handleSearch(query) {
|
||||||
this.searchQuery = query;
|
this.searchQuery = query;
|
||||||
const filteredHeadings = this.filterHeadings(this.headings, query);
|
this.buildContent(); // Rebuild content with new filter
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -234,12 +226,11 @@ class ContentsControl extends ControlBase {
|
|||||||
refreshContents() {
|
refreshContents() {
|
||||||
return this.safeOperation(() => {
|
return this.safeOperation(() => {
|
||||||
this.extractHeadings();
|
this.extractHeadings();
|
||||||
const filteredHeadings = this.filterHeadings(this.headings, this.searchQuery);
|
this.buildContent(); // Rebuild content with updated headings
|
||||||
this.updateContentsDisplay(filteredHeadings);
|
|
||||||
|
|
||||||
// Show success feedback
|
// Show success feedback
|
||||||
const refreshBtn = this.element?.querySelector('button');
|
const refreshBtn = this.element?.querySelector('button');
|
||||||
if (refreshBtn) {
|
if (refreshBtn && refreshBtn.textContent.includes('Refresh')) {
|
||||||
const originalText = refreshBtn.innerHTML;
|
const originalText = refreshBtn.innerHTML;
|
||||||
refreshBtn.innerHTML = '✅ Updated';
|
refreshBtn.innerHTML = '✅ Updated';
|
||||||
refreshBtn.style.background = '#28a745';
|
refreshBtn.style.background = '#28a745';
|
||||||
@@ -252,23 +243,6 @@ class ContentsControl extends ControlBase {
|
|||||||
}, null, 'refreshContents');
|
}, 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
|
* Override buildContent to add control reference and auto-refresh
|
||||||
*/
|
*/
|
||||||
@@ -293,37 +267,6 @@ class ContentsControl extends ControlBase {
|
|||||||
}, 5000); // Check every 5 seconds
|
}, 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
|
* Clean up resources when control is destroyed
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -541,7 +541,7 @@ class ControlBase {
|
|||||||
resizeHandle.style.cssText = `
|
resizeHandle.style.cssText = `
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
right: -4px;
|
right: 1px;
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
cursor: se-resize;
|
cursor: se-resize;
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
* - Hierarchical display with proper indentation
|
* - Hierarchical display with proper indentation
|
||||||
* - Clickable navigation links with smooth scrolling
|
* - Clickable navigation links with smooth scrolling
|
||||||
* - Real-time updates when document structure changes
|
* - Real-time updates when document structure changes
|
||||||
* - Collapsible sections for better organization
|
|
||||||
* - Search functionality within the table of contents
|
* - Search functionality within the table of contents
|
||||||
*
|
*
|
||||||
* Dependencies:
|
* Dependencies:
|
||||||
@@ -20,9 +19,9 @@
|
|||||||
/**
|
/**
|
||||||
* ContentsControl - Interactive table of contents control
|
* ContentsControl - Interactive table of contents control
|
||||||
*
|
*
|
||||||
* This control scans the document for headings (h1-h6) and presents them
|
* Built on the base class architecture for consistency with other panels.
|
||||||
* in a navigable tree structure. Users can click on any heading to jump
|
* Only implements content-specific functionality while inheriting all
|
||||||
* directly to that section with smooth scrolling.
|
* common panel behavior from ControlBase.
|
||||||
*/
|
*/
|
||||||
class ContentsControl extends ControlBase {
|
class ContentsControl extends ControlBase {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -45,6 +44,80 @@ class ContentsControl extends ControlBase {
|
|||||||
this.searchQuery = '';
|
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
|
* Extract all headings from the document
|
||||||
* Creates a hierarchical structure of the document's heading elements
|
* 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
|
* Navigate to a specific heading with smooth scrolling
|
||||||
*/
|
*/
|
||||||
@@ -212,20 +217,7 @@ class ContentsControl extends ControlBase {
|
|||||||
*/
|
*/
|
||||||
handleSearch(query) {
|
handleSearch(query) {
|
||||||
this.searchQuery = query;
|
this.searchQuery = query;
|
||||||
const filteredHeadings = this.filterHeadings(this.headings, query);
|
this.buildContent(); // Rebuild content with new filter
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -234,12 +226,11 @@ class ContentsControl extends ControlBase {
|
|||||||
refreshContents() {
|
refreshContents() {
|
||||||
return this.safeOperation(() => {
|
return this.safeOperation(() => {
|
||||||
this.extractHeadings();
|
this.extractHeadings();
|
||||||
const filteredHeadings = this.filterHeadings(this.headings, this.searchQuery);
|
this.buildContent(); // Rebuild content with updated headings
|
||||||
this.updateContentsDisplay(filteredHeadings);
|
|
||||||
|
|
||||||
// Show success feedback
|
// Show success feedback
|
||||||
const refreshBtn = this.element?.querySelector('button');
|
const refreshBtn = this.element?.querySelector('button');
|
||||||
if (refreshBtn) {
|
if (refreshBtn && refreshBtn.textContent.includes('Refresh')) {
|
||||||
const originalText = refreshBtn.innerHTML;
|
const originalText = refreshBtn.innerHTML;
|
||||||
refreshBtn.innerHTML = '✅ Updated';
|
refreshBtn.innerHTML = '✅ Updated';
|
||||||
refreshBtn.style.background = '#28a745';
|
refreshBtn.style.background = '#28a745';
|
||||||
@@ -252,23 +243,6 @@ class ContentsControl extends ControlBase {
|
|||||||
}, null, 'refreshContents');
|
}, 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
|
* Override buildContent to add control reference and auto-refresh
|
||||||
*/
|
*/
|
||||||
@@ -293,37 +267,6 @@ class ContentsControl extends ControlBase {
|
|||||||
}, 5000); // Check every 5 seconds
|
}, 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
|
* Clean up resources when control is destroyed
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -541,7 +541,7 @@ class ControlBase {
|
|||||||
resizeHandle.style.cssText = `
|
resizeHandle.style.cssText = `
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
right: -4px;
|
right: 2px;
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
cursor: se-resize;
|
cursor: se-resize;
|
||||||
|
|||||||
Reference in New Issue
Block a user