fix: improve control positioning and drag behavior

- Update compass positioning to be top-aligned instead of center-aligned
- Fix drag offset calculation to maintain cursor position at icon
- Ensure expanded controls appear top-aligned with anchor position
- Apply fixes to both viewing and edit mode Control implementations
- Improve user experience with more intuitive positioning and dragging

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-10 23:10:33 +01:00
parent 2d9175ec05
commit 3839a6761e

View File

@@ -1177,50 +1177,133 @@ class CleanDocumentManager:
console.error("Scroll indicators failed to initialize:", error);
}}
// Step 4: Initialize DocumentNavigator (lazy loading for all modes)
try {{
const documentNavigator = {{
navElement: null,
// Step 4: Define abstract Control class for UI controls
const Control = {{
// Abstract control properties
element: 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...");
// Configuration properties (to be overridden by subclasses)
config: {{
icon: '?',
title: 'Control',
className: 'control',
defaultContent: 'Template only',
ariaLabel: 'Control',
position: 'w' // Default compass position: west (middle-left)
}},
this.navElement = document.createElement('nav');
this.navElement.className = 'document-navigator';
this.navElement.innerHTML = `
<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>
// Compass positioning system (top-aligned for proper expansion)
compassPositions: {{
// North positions (top)
'n': {{ top: '20px', left: '50%', transform: 'translateX(-50%)' }},
'nne': {{ top: '20px', left: '65%', transform: 'translateX(-50%)' }},
'ne': {{ top: '20px', right: '20px' }},
'ene': {{ top: '80px', right: '20px' }}, // Top-aligned instead of center
// East positions (right)
'e': {{ top: '50vh', right: '20px', transform: 'translateY(-20px)' }}, // Anchor at icon level
'ese': {{ top: 'calc(65vh - 20px)', right: '20px' }}, // Top-aligned
'se': {{ bottom: '20px', right: '20px' }},
'sse': {{ bottom: '20px', right: '35%', transform: 'translateX(50%)' }},
// South positions (bottom)
's': {{ bottom: '20px', left: '50%', transform: 'translateX(-50%)' }},
'ssw': {{ bottom: '20px', left: '35%', transform: 'translateX(-50%)' }},
'sw': {{ bottom: '20px', left: '20px' }},
'wsw': {{ bottom: '80px', left: '20px' }}, // Top-aligned instead of center
// West positions (left) - top-aligned for proper expansion
'w': {{ top: '50vh', left: '20px', transform: 'translateY(-20px)' }}, // Anchor at icon level
'wnw': {{ top: '80px', left: '20px' }}, // Top-aligned instead of center
'nw': {{ top: '20px', left: '20px' }},
'nnw': {{ top: '20px', left: '35%', transform: 'translateX(-50%)' }}
}},
// Get expansion direction based on compass position
getExpansionDirection: function() {{
const pos = this.config.position;
const rightBorderPositions = ['ne', 'ene', 'e', 'ese', 'se'];
const bottomBorderPositions = ['sw', 'ssw', 's', 'sse', 'se'];
return {{
header: rightBorderPositions.includes(pos) ? 'left' : 'right',
body: bottomBorderPositions.includes(pos) ? 'up' : 'down'
}};
}},
// Calculate position styles based on compass direction
getPositionStyles: function() {{
const compassPos = this.compassPositions[this.config.position] || this.compassPositions['w'];
return {{
position: 'fixed',
top: compassPos.top || 'auto',
right: compassPos.right || 'auto',
bottom: compassPos.bottom || 'auto',
left: compassPos.left || 'auto',
transform: compassPos.transform || 'none',
zIndex: 1000
}};
}},
// Abstract methods (to be implemented by subclasses)
buildContent: function() {{
const content = this.element.querySelector('.control-content');
content.innerHTML = `<p style="padding: 1rem; color: #666;">${{this.config.defaultContent}}</p>`;
}},
// Concrete methods (shared by all controls)
createControl: function() {{
console.log(`🎛️ Creating ${{this.config.title}} control...`);
this.element = document.createElement('div');
this.element.className = this.config.className;
this.element.innerHTML = `
<button class="control-toggle" aria-label="${{this.config.ariaLabel}}">${{this.config.icon}}</button>
<div class="control-panel" style="display: none;">
<div class="control-header">
<span class="control-icon">${{this.config.icon}}</span>
<h3>${{this.config.title}}</h3>
<button class="control-close">✕</button>
</div>
<div class="navigator-content">Loading...</div>
<div class="control-content">Loading...</div>
</div>
`;
// Position on left side following UI convention
this.navElement.style.cssText = `
position: fixed;
top: 80px;
left: 20px;
z-index: 1000;
// Position using compass direction
const positionStyles = this.getPositionStyles();
this.element.style.cssText = `
position: ${{positionStyles.position}};
top: ${{positionStyles.top}};
right: ${{positionStyles.right}};
bottom: ${{positionStyles.bottom}};
left: ${{positionStyles.left}};
transform: ${{positionStyles.transform}};
z-index: ${{positionStyles.zIndex}};
background: rgba(255, 255, 255, 0.95);
border: 1px solid #e1e5e9;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(8px);
width: 40px;
transition: width 0.3s ease;
transition: all 0.3s ease;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
`;
// Store original position for reset
this.originalPosition = {{
top: positionStyles.top,
right: positionStyles.right,
bottom: positionStyles.bottom,
left: positionStyles.left,
transform: positionStyles.transform
}};
// Style toggle button
const toggleBtn = this.navElement.querySelector('.navigator-toggle');
const toggleBtn = this.element.querySelector('.control-toggle');
toggleBtn.style.cssText = `
width: 100%;
height: 40px;
@@ -1232,18 +1315,18 @@ class CleanDocumentManager:
transition: color 0.2s ease;
`;
// Handle click to build navigation on-demand
// Handle click to build content on-demand
toggleBtn.addEventListener('click', () => {{
if (this.isExpanded) {{
this.collapse();
}} else {{
console.log("📋 Navigator toggle clicked - building navigation...");
this.buildNavigation();
console.log(`🎛️ ${{this.config.title}} toggle clicked - building content...`);
this.buildContent();
}}
}});
// Close button handler
const closeBtn = this.navElement.querySelector('.navigator-close');
const closeBtn = this.element.querySelector('.control-close');
closeBtn.addEventListener('click', () => {{
this.collapse();
}});
@@ -1251,26 +1334,24 @@ class CleanDocumentManager:
// Responsive behavior
window.addEventListener('resize', () => {{
if (window.innerWidth <= 768) {{
this.navElement.style.display = 'none';
this.element.style.display = 'none';
}} else {{
this.navElement.style.display = '';
this.element.style.display = '';
}}
}});
document.body.appendChild(this.navElement);
document.body.appendChild(this.element);
// Hide on mobile
if (window.innerWidth <= 768) {{
this.navElement.style.display = 'none';
this.element.style.display = 'none';
}}
console.log("📋 DocumentNavigator control created");
console.log(`🎛️ ${{this.config.title}} control created`);
}},
buildNavigation: function() {{
const panel = this.navElement.querySelector('.navigator-panel');
const content = this.navElement.querySelector('.navigator-content');
const header = this.navElement.querySelector('.navigator-header');
styleHeader: function() {{
const header = this.element.querySelector('.control-header');
// Style the header to show icon, title, and close button in one line
// Match the height of the collapsed icon state (40px)
@@ -1284,7 +1365,7 @@ class CleanDocumentManager:
margin-bottom: 0;
`;
const icon = header.querySelector('.navigator-icon');
const icon = header.querySelector('.control-icon');
if (icon) {{
icon.style.cssText = `
font-size: 16px;
@@ -1309,7 +1390,7 @@ class CleanDocumentManager:
`;
}}
const closeBtn = header.querySelector('.navigator-close');
const closeBtn = header.querySelector('.control-close');
if (closeBtn) {{
closeBtn.style.cssText = `
background: none;
@@ -1325,6 +1406,135 @@ class CleanDocumentManager:
justify-content: center;
`;
}}
}},
styleContent: function() {{
const content = this.element.querySelector('.control-content');
const expansion = this.getExpansionDirection();
// Style the content area based on expansion direction
let contentStyles = `
padding: 0.5rem;
overflow-y: auto;
`;
if (expansion.body === 'up') {{
// Body expands upward (for bottom border positions)
contentStyles += `
max-height: calc(80vh - 40px);
`;
content.parentElement.style.flexDirection = 'column-reverse';
}} else {{
// Body expands downward (default)
contentStyles += `
max-height: calc(80vh - 40px);
`;
content.parentElement.style.flexDirection = 'column';
}}
content.style.cssText = contentStyles;
}},
expand: function() {{
this.isExpanded = true;
const panel = this.element.querySelector('.control-panel');
const toggleBtn = this.element.querySelector('.control-toggle');
// Get expansion direction based on compass position
const expansion = this.getExpansionDirection();
// Apply expansion styling based on direction
if (expansion.header === 'left') {{
// Header expands to the left (for right border positions)
this.element.style.width = '280px';
this.element.style.transformOrigin = 'top right';
}} else {{
// Header expands to the right (default)
this.element.style.width = '280px';
this.element.style.transformOrigin = 'top left';
}}
panel.style.display = 'block';
toggleBtn.style.display = 'none';
this.styleHeader();
this.styleContent();
}},
collapse: function() {{
this.isExpanded = false;
const panel = this.element.querySelector('.control-panel');
const toggleBtn = this.element.querySelector('.control-toggle');
panel.style.display = 'none';
this.element.style.width = '40px';
toggleBtn.style.display = 'block';
// Reset position to original compass location
this.element.style.top = this.originalPosition.top;
this.element.style.right = this.originalPosition.right;
this.element.style.bottom = this.originalPosition.bottom;
this.element.style.left = this.originalPosition.left;
this.element.style.transform = this.originalPosition.transform;
}},
setupDragHandlers: function(dragElement) {{
dragElement.addEventListener('mousedown', (e) => {{
this.isDragging = true;
const rect = this.element.getBoundingClientRect();
const iconRect = dragElement.getBoundingClientRect();
// Calculate offset relative to the icon position, not the element
this.dragOffset.x = e.clientX - rect.left;
this.dragOffset.y = iconRect.top - rect.top + (iconRect.height / 2); // Keep mouse at icon center
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.element.offsetWidth;
const maxY = window.innerHeight - this.element.offsetHeight;
const boundedX = Math.max(0, Math.min(newX, maxX));
const boundedY = Math.max(0, Math.min(newY, maxY));
this.element.style.left = boundedX + 'px';
this.element.style.top = boundedY + 'px';
}});
document.addEventListener('mouseup', () => {{
if (this.isDragging) {{
this.isDragging = false;
dragElement.style.cursor = 'grab';
}}
}});
}}
}};
// Step 5: Initialize ContentsControl (new implementation based on Control class)
try {{
const contentsControl = Object.create(Control);
// Configure for contents navigation
contentsControl.config = {{
icon: '',
title: 'Contents',
className: 'contents-control',
defaultContent: 'No headings found',
ariaLabel: 'Document Navigation',
position: 'wnw' // West-north-west positioning
}};
// Override buildContent method for navigation functionality
contentsControl.buildContent = function() {{
const content = this.element.querySelector('.control-content');
// Build navigation content from current DOM
const allHeadings = document.querySelectorAll('h1, h2, h3');
@@ -1352,7 +1562,7 @@ class CleanDocumentManager:
border-radius: 4px; cursor: pointer;"
onmouseover="this.style.backgroundColor='#f5f5f5'"
onmouseout="this.style.backgroundColor=''"
onclick="event.preventDefault(); document.getElementById('${{heading.id}}').scrollIntoView({{behavior: 'smooth'}}); if (window.innerWidth <= 768) setTimeout(() => documentNavigator.collapse(), 500);">
onclick="event.preventDefault(); document.getElementById('${{heading.id}}').scrollIntoView({{behavior: 'smooth'}}); if (window.innerWidth <= 768) setTimeout(() => contentsControl.collapse(), 500);">
${{heading.textContent.trim()}}
</a>
`;
@@ -1360,85 +1570,19 @@ 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';
}}
}});
}}
}};
// Initialize the DocumentNavigator control
documentNavigator.createControl();
// Initialize the ContentsControl
contentsControl.createControl();
// Make globally available for mobile collapse
window.documentNavigator = documentNavigator;
window.contentsControl = contentsControl;
}} catch (error) {{
console.error("DocumentNavigator failed to initialize:", error);
console.error("ContentsControl failed to initialize:", error);
}}
}});
// Handle CDN loading errors
@@ -1500,49 +1644,133 @@ document.addEventListener('DOMContentLoaded', function() {
// Create document controls
documentControls.create();
// Create DocumentNavigator for edit mode (lazy loading)
const documentNavigator = {
navElement: null,
// Define abstract Control class for UI controls (same as viewing mode)
const Control = {
// Abstract control properties
element: 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...");
// Configuration properties (to be overridden by subclasses)
config: {
icon: '?',
title: 'Control',
className: 'control',
defaultContent: 'Template only',
ariaLabel: 'Control',
position: 'w' // Default compass position: west (middle-left)
},
this.navElement = document.createElement('nav');
this.navElement.className = 'document-navigator edit-mode';
this.navElement.innerHTML = `
<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>
// Compass positioning system (top-aligned for proper expansion)
compassPositions: {
// North positions (top)
'n': { top: '20px', left: '50%', transform: 'translateX(-50%)' },
'nne': { top: '20px', left: '65%', transform: 'translateX(-50%)' },
'ne': { top: '20px', right: '20px' },
'ene': { top: '80px', right: '20px' }, // Top-aligned instead of center
// East positions (right)
'e': { top: '50vh', right: '20px', transform: 'translateY(-20px)' }, // Anchor at icon level
'ese': { top: 'calc(65vh - 20px)', right: '20px' }, // Top-aligned
'se': { bottom: '20px', right: '20px' },
'sse': { bottom: '20px', right: '35%', transform: 'translateX(50%)' },
// South positions (bottom)
's': { bottom: '20px', left: '50%', transform: 'translateX(-50%)' },
'ssw': { bottom: '20px', left: '35%', transform: 'translateX(-50%)' },
'sw': { bottom: '20px', left: '20px' },
'wsw': { bottom: '80px', left: '20px' }, // Top-aligned instead of center
// West positions (left) - top-aligned for proper expansion
'w': { top: '50vh', left: '20px', transform: 'translateY(-20px)' }, // Anchor at icon level
'wnw': { top: '80px', left: '20px' }, // Top-aligned instead of center
'nw': { top: '20px', left: '20px' },
'nnw': { top: '20px', left: '35%', transform: 'translateX(-50%)' }
},
// Get expansion direction based on compass position
getExpansionDirection: function() {
const pos = this.config.position;
const rightBorderPositions = ['ne', 'ene', 'e', 'ese', 'se'];
const bottomBorderPositions = ['sw', 'ssw', 's', 'sse', 'se'];
return {
header: rightBorderPositions.includes(pos) ? 'left' : 'right',
body: bottomBorderPositions.includes(pos) ? 'up' : 'down'
};
},
// Calculate position styles based on compass direction
getPositionStyles: function() {
const compassPos = this.compassPositions[this.config.position] || this.compassPositions['w'];
return {
position: 'fixed',
top: compassPos.top || 'auto',
right: compassPos.right || 'auto',
bottom: compassPos.bottom || 'auto',
left: compassPos.left || 'auto',
transform: compassPos.transform || 'none',
zIndex: 1001
};
},
// Abstract methods (to be implemented by subclasses)
buildContent: function() {
const content = this.element.querySelector('.control-content');
content.innerHTML = `<p style="padding: 1rem; color: #666;">${this.config.defaultContent}</p>`;
},
// Concrete methods (shared by all controls)
createControl: function() {
console.log(`🎛️ Creating ${this.config.title} control...`);
this.element = document.createElement('div');
this.element.className = this.config.className;
this.element.innerHTML = `
<button class="control-toggle" aria-label="${this.config.ariaLabel}">${this.config.icon}</button>
<div class="control-panel" style="display: none;">
<div class="control-header">
<span class="control-icon">${this.config.icon}</span>
<h3>${this.config.title}</h3>
<button class="control-close">✕</button>
</div>
<div class="navigator-content">Loading...</div>
<div class="control-content">Loading...</div>
</div>
`;
// Position on left side following UI convention
this.navElement.style.cssText = `
position: fixed;
top: 80px;
left: 20px;
z-index: 1001;
// Position using compass direction
const positionStyles = this.getPositionStyles();
this.element.style.cssText = `
position: ${positionStyles.position};
top: ${positionStyles.top};
right: ${positionStyles.right};
bottom: ${positionStyles.bottom};
left: ${positionStyles.left};
transform: ${positionStyles.transform};
z-index: ${positionStyles.zIndex};
background: rgba(255, 255, 255, 0.95);
border: 1px solid #e1e5e9;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(8px);
width: 40px;
transition: width 0.3s ease;
transition: all 0.3s ease;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
`;
// Store original position for reset
this.originalPosition = {
top: positionStyles.top,
right: positionStyles.right,
bottom: positionStyles.bottom,
left: positionStyles.left,
transform: positionStyles.transform
};
// Style toggle button
const toggleBtn = this.navElement.querySelector('.navigator-toggle');
const toggleBtn = this.element.querySelector('.control-toggle');
toggleBtn.style.cssText = `
width: 100%;
height: 40px;
@@ -1554,30 +1782,28 @@ document.addEventListener('DOMContentLoaded', function() {
transition: color 0.2s ease;
`;
// Handle click to build navigation on-demand
// Handle click to build content on-demand
toggleBtn.addEventListener('click', () => {
if (this.isExpanded) {
this.collapse();
} else {
console.log("📋 Navigator toggle clicked - building navigation...");
this.buildNavigation();
console.log(`🎛️ ${this.config.title} toggle clicked - building content...`);
this.buildContent();
}
});
// Close button handler
const closeBtn = this.navElement.querySelector('.navigator-close');
const closeBtn = this.element.querySelector('.control-close');
closeBtn.addEventListener('click', () => {
this.collapse();
});
document.body.appendChild(this.navElement);
console.log("📋 DocumentNavigator control created");
document.body.appendChild(this.element);
console.log(`🎛️ ${this.config.title} control created`);
},
buildNavigation: function() {
const panel = this.navElement.querySelector('.navigator-panel');
const content = this.navElement.querySelector('.navigator-content');
const header = this.navElement.querySelector('.navigator-header');
styleHeader: function() {
const header = this.element.querySelector('.control-header');
// Style the header to show icon, title, and close button in one line
// Match the height of the collapsed icon state (40px)
@@ -1591,7 +1817,7 @@ document.addEventListener('DOMContentLoaded', function() {
margin-bottom: 0;
`;
const icon = header.querySelector('.navigator-icon');
const icon = header.querySelector('.control-icon');
if (icon) {
icon.style.cssText = `
font-size: 16px;
@@ -1616,7 +1842,7 @@ document.addEventListener('DOMContentLoaded', function() {
`;
}
const closeBtn = header.querySelector('.navigator-close');
const closeBtn = header.querySelector('.control-close');
if (closeBtn) {
closeBtn.style.cssText = `
background: none;
@@ -1632,6 +1858,135 @@ document.addEventListener('DOMContentLoaded', function() {
justify-content: center;
`;
}
},
styleContent: function() {
const content = this.element.querySelector('.control-content');
const expansion = this.getExpansionDirection();
// Style the content area based on expansion direction
let contentStyles = `
padding: 0.5rem;
overflow-y: auto;
`;
if (expansion.body === 'up') {
// Body expands upward (for bottom border positions)
contentStyles += `
max-height: calc(80vh - 40px);
`;
content.parentElement.style.flexDirection = 'column-reverse';
} else {
// Body expands downward (default)
contentStyles += `
max-height: calc(80vh - 40px);
`;
content.parentElement.style.flexDirection = 'column';
}
content.style.cssText = contentStyles;
},
expand: function() {
this.isExpanded = true;
const panel = this.element.querySelector('.control-panel');
const toggleBtn = this.element.querySelector('.control-toggle');
// Get expansion direction based on compass position
const expansion = this.getExpansionDirection();
// Apply expansion styling based on direction
if (expansion.header === 'left') {
// Header expands to the left (for right border positions)
this.element.style.width = '300px';
this.element.style.transformOrigin = 'top right';
} else {
// Header expands to the right (default)
this.element.style.width = '300px';
this.element.style.transformOrigin = 'top left';
}
panel.style.display = 'block';
toggleBtn.style.display = 'none';
this.styleHeader();
this.styleContent();
},
collapse: function() {
this.isExpanded = false;
const panel = this.element.querySelector('.control-panel');
const toggleBtn = this.element.querySelector('.control-toggle');
panel.style.display = 'none';
this.element.style.width = '40px';
toggleBtn.style.display = 'block';
// Reset position to original compass location
this.element.style.top = this.originalPosition.top;
this.element.style.right = this.originalPosition.right;
this.element.style.bottom = this.originalPosition.bottom;
this.element.style.left = this.originalPosition.left;
this.element.style.transform = this.originalPosition.transform;
},
setupDragHandlers: function(dragElement) {
dragElement.addEventListener('mousedown', (e) => {
this.isDragging = true;
const rect = this.element.getBoundingClientRect();
const iconRect = dragElement.getBoundingClientRect();
// Calculate offset relative to the icon position, not the element
this.dragOffset.x = e.clientX - rect.left;
this.dragOffset.y = iconRect.top - rect.top + (iconRect.height / 2); // Keep mouse at icon center
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.element.offsetWidth;
const maxY = window.innerHeight - this.element.offsetHeight;
const boundedX = Math.max(0, Math.min(newX, maxX));
const boundedY = Math.max(0, Math.min(newY, maxY));
this.element.style.left = boundedX + 'px';
this.element.style.top = boundedY + 'px';
});
document.addEventListener('mouseup', () => {
if (this.isDragging) {
this.isDragging = false;
dragElement.style.cursor = 'grab';
}
});
}
};
// Create ContentsControl for edit mode (new implementation based on Control class)
try {
const contentsControl = Object.create(Control);
// Configure for contents navigation in edit mode
contentsControl.config = {
icon: '',
title: 'Contents',
className: 'contents-control edit-mode',
defaultContent: 'No headings found',
ariaLabel: 'Document Navigation',
position: 'wnw' // West-north-west positioning
};
// Override buildContent method for navigation functionality
contentsControl.buildContent = function() {
const content = this.element.querySelector('.control-content');
// Build navigation content from current DOM
const allHeadings = document.querySelectorAll('h1, h2, h3');
@@ -1659,7 +2014,7 @@ document.addEventListener('DOMContentLoaded', function() {
border-radius: 4px; cursor: pointer;"
onmouseover="this.style.backgroundColor='#f5f5f5'"
onmouseout="this.style.backgroundColor=''"
onclick="event.preventDefault(); document.getElementById('${heading.id}').scrollIntoView({behavior: 'smooth'});">
onclick="event.preventDefault(); document.getElementById('${heading.id}').scrollIntoView({behavior: 'smooth'}); if (window.innerWidth <= 768) setTimeout(() => contentsControl.collapse(), 500);">
${heading.textContent.trim()}
</a>
`;
@@ -1667,79 +2022,19 @@ 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();
},
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';
}
});
}
};
// Initialize the DocumentNavigator control
documentNavigator.createControl();
// Initialize the ContentsControl
contentsControl.createControl();
// Make globally available for mobile collapse
window.contentsControl = contentsControl;
} catch (error) {
console.error("ContentsControl failed to initialize:", error);
}
// Wire up event handlers
documentControls.setEventHandlers({