feat: enhance DocumentNavigator with dragging and compact header design
Some checks failed
Test Suite / security-scan (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Some checks failed
Test Suite / security-scan (push) Has been cancelled
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
- Add draggable functionality when expanded - click and drag the ☰ icon to reposition - Implement automatic position reset to original location when collapsed - Create compact header design with 40px height matching collapsed icon state - Remove duplicate icons and filter out navigation-related headings from content - Add visual feedback with cursor changes (grab/grabbing) during drag operations - Include viewport boundary constraints to prevent dragging outside browser window - Optimize header spacing and typography for clean, professional appearance - Maintain consistent UX across both viewing and edit modes 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1182,6 +1182,9 @@ class CleanDocumentManager:
|
||||
const documentNavigator = {{
|
||||
navElement: null,
|
||||
isExpanded: false,
|
||||
isDragging: false,
|
||||
dragOffset: {{ x: 0, y: 0 }},
|
||||
originalPosition: {{ top: '80px', left: '20px' }},
|
||||
|
||||
createControl: function() {{
|
||||
console.log("📋 Creating DocumentNavigator control for view mode...");
|
||||
@@ -1192,6 +1195,7 @@ class CleanDocumentManager:
|
||||
<button class="navigator-toggle" aria-label="Document Navigation">☰</button>
|
||||
<div class="navigator-panel" style="display: none;">
|
||||
<div class="navigator-header">
|
||||
<span class="navigator-icon">☰</span>
|
||||
<h3>Contents</h3>
|
||||
<button class="navigator-close">✕</button>
|
||||
</div>
|
||||
@@ -1230,8 +1234,12 @@ class CleanDocumentManager:
|
||||
|
||||
// Handle click to build navigation on-demand
|
||||
toggleBtn.addEventListener('click', () => {{
|
||||
console.log("📋 Navigator toggle clicked - building navigation...");
|
||||
this.buildNavigation();
|
||||
if (this.isExpanded) {{
|
||||
this.collapse();
|
||||
}} else {{
|
||||
console.log("📋 Navigator toggle clicked - building navigation...");
|
||||
this.buildNavigation();
|
||||
}}
|
||||
}});
|
||||
|
||||
// Close button handler
|
||||
@@ -1262,9 +1270,69 @@ class CleanDocumentManager:
|
||||
buildNavigation: function() {{
|
||||
const panel = this.navElement.querySelector('.navigator-panel');
|
||||
const content = this.navElement.querySelector('.navigator-content');
|
||||
const header = this.navElement.querySelector('.navigator-header');
|
||||
|
||||
// Style the header to show icon, title, and close button in one line
|
||||
// Match the height of the collapsed icon state (40px)
|
||||
header.style.cssText = `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 40px;
|
||||
padding: 0 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 0;
|
||||
`;
|
||||
|
||||
const icon = header.querySelector('.navigator-icon');
|
||||
if (icon) {{
|
||||
icon.style.cssText = `
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-right: 0.5rem;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
// Make icon draggable
|
||||
this.setupDragHandlers(icon);
|
||||
}}
|
||||
|
||||
const title = header.querySelector('h3');
|
||||
if (title) {{
|
||||
title.style.cssText = `
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
flex-grow: 1;
|
||||
line-height: 1;
|
||||
`;
|
||||
}}
|
||||
|
||||
const closeBtn = header.querySelector('.navigator-close');
|
||||
if (closeBtn) {{
|
||||
closeBtn.style.cssText = `
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
}}
|
||||
|
||||
// Build navigation content from current DOM
|
||||
const headings = document.querySelectorAll('h1, h2, h3');
|
||||
const allHeadings = document.querySelectorAll('h1, h2, h3');
|
||||
// Filter out headings that contain "Contents" or similar navigation-related text
|
||||
const headings = Array.from(allHeadings).filter(heading => {{
|
||||
const text = heading.textContent.trim().toLowerCase();
|
||||
return !text.includes('contents') && !text.includes('table of contents') && !text.includes('navigation');
|
||||
}});
|
||||
console.log("📋 Found headings for navigation:", headings.length);
|
||||
|
||||
if (headings.length === 0) {{
|
||||
@@ -1292,22 +1360,74 @@ class CleanDocumentManager:
|
||||
content.innerHTML = navHtml;
|
||||
}}
|
||||
|
||||
// Style the content area to work with compact header
|
||||
content.style.cssText = `
|
||||
padding: 0.5rem;
|
||||
max-height: calc(80vh - 40px);
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
// Show panel
|
||||
this.expand();
|
||||
}},
|
||||
}},
|
||||
|
||||
expand: function() {{
|
||||
this.isExpanded = true;
|
||||
const panel = this.navElement.querySelector('.navigator-panel');
|
||||
const toggleBtn = this.navElement.querySelector('.navigator-toggle');
|
||||
this.navElement.style.width = '280px';
|
||||
panel.style.display = 'block';
|
||||
toggleBtn.style.display = 'none';
|
||||
}},
|
||||
|
||||
collapse: function() {{
|
||||
this.isExpanded = false;
|
||||
const panel = this.navElement.querySelector('.navigator-panel');
|
||||
const toggleBtn = this.navElement.querySelector('.navigator-toggle');
|
||||
panel.style.display = 'none';
|
||||
this.navElement.style.width = '40px';
|
||||
toggleBtn.style.display = 'block';
|
||||
|
||||
// Reset position to original location
|
||||
this.navElement.style.top = this.originalPosition.top;
|
||||
this.navElement.style.left = this.originalPosition.left;
|
||||
}},
|
||||
|
||||
setupDragHandlers: function(dragElement) {{
|
||||
dragElement.addEventListener('mousedown', (e) => {{
|
||||
this.isDragging = true;
|
||||
const rect = this.navElement.getBoundingClientRect();
|
||||
this.dragOffset.x = e.clientX - rect.left;
|
||||
this.dragOffset.y = e.clientY - rect.top;
|
||||
|
||||
dragElement.style.cursor = 'grabbing';
|
||||
|
||||
e.preventDefault();
|
||||
}});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {{
|
||||
if (!this.isDragging || !this.isExpanded) return;
|
||||
|
||||
const newX = e.clientX - this.dragOffset.x;
|
||||
const newY = e.clientY - this.dragOffset.y;
|
||||
|
||||
// Keep within viewport bounds
|
||||
const maxX = window.innerWidth - this.navElement.offsetWidth;
|
||||
const maxY = window.innerHeight - this.navElement.offsetHeight;
|
||||
|
||||
const boundedX = Math.max(0, Math.min(newX, maxX));
|
||||
const boundedY = Math.max(0, Math.min(newY, maxY));
|
||||
|
||||
this.navElement.style.left = boundedX + 'px';
|
||||
this.navElement.style.top = boundedY + 'px';
|
||||
}});
|
||||
|
||||
document.addEventListener('mouseup', () => {{
|
||||
if (this.isDragging) {{
|
||||
this.isDragging = false;
|
||||
dragElement.style.cursor = 'grab';
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
}};
|
||||
|
||||
@@ -1384,6 +1504,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const documentNavigator = {
|
||||
navElement: null,
|
||||
isExpanded: false,
|
||||
isDragging: false,
|
||||
dragOffset: { x: 0, y: 0 },
|
||||
originalPosition: { top: '80px', left: '20px' },
|
||||
|
||||
createControl: function() {
|
||||
console.log("📋 Creating DocumentNavigator control for edit mode...");
|
||||
@@ -1394,6 +1517,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<button class="navigator-toggle" aria-label="Document Navigation">☰</button>
|
||||
<div class="navigator-panel" style="display: none;">
|
||||
<div class="navigator-header">
|
||||
<span class="navigator-icon">☰</span>
|
||||
<h3>Contents</h3>
|
||||
<button class="navigator-close">✕</button>
|
||||
</div>
|
||||
@@ -1432,8 +1556,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
// Handle click to build navigation on-demand
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
console.log("📋 Navigator toggle clicked - building navigation...");
|
||||
this.buildNavigation();
|
||||
if (this.isExpanded) {
|
||||
this.collapse();
|
||||
} else {
|
||||
console.log("📋 Navigator toggle clicked - building navigation...");
|
||||
this.buildNavigation();
|
||||
}
|
||||
});
|
||||
|
||||
// Close button handler
|
||||
@@ -1449,9 +1577,69 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
buildNavigation: function() {
|
||||
const panel = this.navElement.querySelector('.navigator-panel');
|
||||
const content = this.navElement.querySelector('.navigator-content');
|
||||
const header = this.navElement.querySelector('.navigator-header');
|
||||
|
||||
// Style the header to show icon, title, and close button in one line
|
||||
// Match the height of the collapsed icon state (40px)
|
||||
header.style.cssText = `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 40px;
|
||||
padding: 0 1rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 0;
|
||||
`;
|
||||
|
||||
const icon = header.querySelector('.navigator-icon');
|
||||
if (icon) {
|
||||
icon.style.cssText = `
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-right: 0.5rem;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
// Make icon draggable
|
||||
this.setupDragHandlers(icon);
|
||||
}
|
||||
|
||||
const title = header.querySelector('h3');
|
||||
if (title) {
|
||||
title.style.cssText = `
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
flex-grow: 1;
|
||||
line-height: 1;
|
||||
`;
|
||||
}
|
||||
|
||||
const closeBtn = header.querySelector('.navigator-close');
|
||||
if (closeBtn) {
|
||||
closeBtn.style.cssText = `
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
}
|
||||
|
||||
// Build navigation content from current DOM
|
||||
const headings = document.querySelectorAll('h1, h2, h3');
|
||||
const allHeadings = document.querySelectorAll('h1, h2, h3');
|
||||
// Filter out headings that contain "Contents" or similar navigation-related text
|
||||
const headings = Array.from(allHeadings).filter(heading => {
|
||||
const text = heading.textContent.trim().toLowerCase();
|
||||
return !text.includes('contents') && !text.includes('table of contents') && !text.includes('navigation');
|
||||
});
|
||||
console.log("📋 Found headings for navigation:", headings.length);
|
||||
|
||||
if (headings.length === 0) {
|
||||
@@ -1479,6 +1667,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
content.innerHTML = navHtml;
|
||||
}
|
||||
|
||||
// Style the content area to work with compact header
|
||||
content.style.cssText = `
|
||||
padding: 0.5rem;
|
||||
max-height: calc(80vh - 40px);
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
// Show panel
|
||||
this.expand();
|
||||
},
|
||||
@@ -1486,15 +1681,60 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
expand: function() {
|
||||
this.isExpanded = true;
|
||||
const panel = this.navElement.querySelector('.navigator-panel');
|
||||
const toggleBtn = this.navElement.querySelector('.navigator-toggle');
|
||||
this.navElement.style.width = '300px';
|
||||
panel.style.display = 'block';
|
||||
toggleBtn.style.display = 'none';
|
||||
},
|
||||
|
||||
collapse: function() {
|
||||
this.isExpanded = false;
|
||||
const panel = this.navElement.querySelector('.navigator-panel');
|
||||
const toggleBtn = this.navElement.querySelector('.navigator-toggle');
|
||||
panel.style.display = 'none';
|
||||
this.navElement.style.width = '40px';
|
||||
toggleBtn.style.display = 'block';
|
||||
|
||||
// Reset position to original location
|
||||
this.navElement.style.top = this.originalPosition.top;
|
||||
this.navElement.style.left = this.originalPosition.left;
|
||||
},
|
||||
|
||||
setupDragHandlers: function(dragElement) {
|
||||
dragElement.addEventListener('mousedown', (e) => {
|
||||
this.isDragging = true;
|
||||
const rect = this.navElement.getBoundingClientRect();
|
||||
this.dragOffset.x = e.clientX - rect.left;
|
||||
this.dragOffset.y = e.clientY - rect.top;
|
||||
|
||||
dragElement.style.cursor = 'grabbing';
|
||||
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!this.isDragging || !this.isExpanded) return;
|
||||
|
||||
const newX = e.clientX - this.dragOffset.x;
|
||||
const newY = e.clientY - this.dragOffset.y;
|
||||
|
||||
// Keep within viewport bounds
|
||||
const maxX = window.innerWidth - this.navElement.offsetWidth;
|
||||
const maxY = window.innerHeight - this.navElement.offsetHeight;
|
||||
|
||||
const boundedX = Math.max(0, Math.min(newX, maxX));
|
||||
const boundedY = Math.max(0, Math.min(newY, maxY));
|
||||
|
||||
this.navElement.style.left = boundedX + 'px';
|
||||
this.navElement.style.top = boundedY + 'px';
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
if (this.isDragging) {
|
||||
this.isDragging = false;
|
||||
dragElement.style.cursor = 'grab';
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user