refactor: Still trying to reorganize edit mode to be more robust
Some checks failed
Test Suite / code-quality (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 / security-scan (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
Test Suite / test-summary (push) Has been cancelled
Some checks failed
Test Suite / code-quality (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 / security-scan (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
Test Suite / test-summary (push) Has been cancelled
This commit is contained in:
@@ -1234,7 +1234,37 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
documentControls.setEventHandlers({
|
||||
'save-document': () => {
|
||||
console.log('Save document clicked');
|
||||
// TODO: Implement save functionality
|
||||
try {
|
||||
// Get current markdown content from section manager
|
||||
const currentMarkdown = sectionManager.getDocumentMarkdown();
|
||||
|
||||
// Create filename with timestamp suffix following the established convention
|
||||
const now = new Date();
|
||||
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-').replace('T', '-');
|
||||
|
||||
// Extract original filename from config or use default
|
||||
const originalFilename = window.editorConfig?.originalFilename || 'document';
|
||||
const editedFilename = `${originalFilename}-edited-${timestamp}.md`;
|
||||
|
||||
// Create and download the file
|
||||
const blob = new Blob([currentMarkdown], { type: 'text/markdown' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = editedFilename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
// Log success to debug panel
|
||||
debugPanel.addMessage(`Document saved as: ${editedFilename}`, 'SUCCESS');
|
||||
console.log(`Document successfully saved as: ${editedFilename}`);
|
||||
|
||||
} catch (error) {
|
||||
debugPanel.addMessage(`Save failed: ${error.message}`, 'ERROR');
|
||||
console.error('Save error:', error);
|
||||
}
|
||||
},
|
||||
'reset-all': () => {
|
||||
console.log('Reset all clicked');
|
||||
|
||||
@@ -1978,10 +1978,18 @@ def md_list_command(ctx, output_format, names_only):
|
||||
help='Copy referenced assets to output directory')
|
||||
@click.option('--no-ship-assets', is_flag=True,
|
||||
help='Don\'t copy referenced assets to output directory')
|
||||
@click.option('--verbose', '-v', is_flag=True,
|
||||
help='Show detailed output including asset operations')
|
||||
@click.option('--silent', '-s', is_flag=True,
|
||||
help='Suppress non-essential output')
|
||||
@click.option('--image-max-width', type=str, default=None,
|
||||
help='Maximum width for images (default: 12cm, supports px, em, %, cm, in, etc.)')
|
||||
@click.option('--image-max-height', type=str, default=None,
|
||||
help='Maximum height for images (default: 20cm, supports px, em, %, cm, in, etc.)')
|
||||
@click.pass_context
|
||||
def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_theme,
|
||||
keyboard_shortcuts, use_publication_dir, dont_use_publication_dir, nodogtag,
|
||||
ship_assets, no_ship_assets):
|
||||
ship_assets, no_ship_assets, verbose, silent, image_max_width, image_max_height):
|
||||
"""
|
||||
Render a markdown file to HTML with basic templates and live preview capabilities.
|
||||
|
||||
@@ -2013,10 +2021,35 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_
|
||||
if edit and insert:
|
||||
raise click.BadParameter("Cannot use both --edit and --insert flags simultaneously. Choose one mode.")
|
||||
|
||||
# Check environment variables for edit/insert modes (if not set via CLI flags)
|
||||
import os
|
||||
if not edit and not insert:
|
||||
if os.environ.get('MARKITECT_EDIT_MODE', '').lower() in ('true', '1', 'yes'):
|
||||
edit = True
|
||||
elif os.environ.get('MARKITECT_INSERT_MODE', '').lower() in ('true', '1', 'yes'):
|
||||
insert = True
|
||||
|
||||
# Validate asset shipping flags
|
||||
if ship_assets and no_ship_assets:
|
||||
raise click.BadParameter("Cannot use both --ship-assets and --no-ship-assets flags simultaneously.")
|
||||
|
||||
# Validate verbosity flags
|
||||
if verbose and silent:
|
||||
raise click.BadParameter("Cannot use both --verbose and --silent flags simultaneously.")
|
||||
|
||||
# Handle image size configuration with environment variable support
|
||||
import os
|
||||
|
||||
# Get image max width (CLI > ENV > default)
|
||||
final_image_max_width = image_max_width
|
||||
if final_image_max_width is None:
|
||||
final_image_max_width = os.environ.get('MARKITECT_IMAGE_MAX_WIDTH', '12cm')
|
||||
|
||||
# Get image max height (CLI > ENV > default)
|
||||
final_image_max_height = image_max_height
|
||||
if final_image_max_height is None:
|
||||
final_image_max_height = os.environ.get('MARKITECT_IMAGE_MAX_HEIGHT', '20cm')
|
||||
|
||||
# Determine output path with environment variable support
|
||||
if output:
|
||||
output_path = Path(output)
|
||||
@@ -2066,7 +2099,7 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_
|
||||
if should_ship_assets:
|
||||
if output_is_directory:
|
||||
# For directory output, ship to the same directory as the HTML file
|
||||
_ship_assets(input_path, output_path.parent, config.get('verbose', False))
|
||||
_ship_assets(input_path, output_path.parent, verbose, silent)
|
||||
# For file output, we don't ship assets (shouldn't reach here anyway)
|
||||
|
||||
# Initialize clean document manager
|
||||
@@ -2081,11 +2114,14 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_
|
||||
edit_mode=True,
|
||||
editor_theme=editor_theme,
|
||||
keyboard_shortcuts=keyboard_shortcuts,
|
||||
nodogtag=nodogtag)
|
||||
nodogtag=nodogtag,
|
||||
image_max_width=final_image_max_width,
|
||||
image_max_height=final_image_max_height)
|
||||
|
||||
click.echo(f"✓ Rendered with interactive editing capabilities to: {output_path}")
|
||||
if not silent:
|
||||
click.echo(f"✓ Rendered with interactive editing capabilities to: {output_path}")
|
||||
|
||||
if config.get('verbose', False):
|
||||
if verbose:
|
||||
click.echo(f"Editor theme: {editor_theme}")
|
||||
click.echo(f"Keyboard shortcuts: {'enabled' if keyboard_shortcuts else 'disabled'}")
|
||||
click.echo(f"Theme: {theme or 'default'}")
|
||||
@@ -2097,11 +2133,14 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_
|
||||
insert_mode=True,
|
||||
editor_theme=editor_theme,
|
||||
keyboard_shortcuts=keyboard_shortcuts,
|
||||
nodogtag=nodogtag)
|
||||
nodogtag=nodogtag,
|
||||
image_max_width=final_image_max_width,
|
||||
image_max_height=final_image_max_height)
|
||||
|
||||
click.echo(f"✓ Rendered with interactive insert capabilities to: {output_path}")
|
||||
if not silent:
|
||||
click.echo(f"✓ Rendered with interactive insert capabilities to: {output_path}")
|
||||
|
||||
if config.get('verbose', False):
|
||||
if verbose:
|
||||
click.echo(f"Editor theme: {editor_theme}")
|
||||
click.echo(f"Keyboard shortcuts: {'enabled' if keyboard_shortcuts else 'disabled'}")
|
||||
click.echo(f"Heading protection: levels 1-3 read-only")
|
||||
@@ -2113,10 +2152,13 @@ def md_render_command(ctx, input_file, output, theme, css, edit, insert, editor_
|
||||
template=theme, css=css,
|
||||
edit_mode=False,
|
||||
insert_mode=False,
|
||||
nodogtag=nodogtag)
|
||||
click.echo(f"✓ Rendered to: {output_path}")
|
||||
nodogtag=nodogtag,
|
||||
image_max_width=final_image_max_width,
|
||||
image_max_height=final_image_max_height)
|
||||
if not silent:
|
||||
click.echo(f"✓ Rendered to: {output_path}")
|
||||
|
||||
if config.get('verbose', False):
|
||||
if verbose:
|
||||
click.echo(f"Theme: {theme or 'default'}")
|
||||
click.echo(f"CSS: {css or 'default'}")
|
||||
|
||||
@@ -3482,18 +3524,28 @@ class FilenameDecoder:
|
||||
return [self.decode(filename) for filename in filenames]
|
||||
|
||||
|
||||
def _ship_assets(input_path: Path, output_dir: Path, verbose: bool = False):
|
||||
def _ship_assets(input_path: Path, output_dir: Path, verbose: bool = False, silent: bool = False):
|
||||
"""
|
||||
Ship (copy) assets referenced in markdown file to output directory.
|
||||
|
||||
Args:
|
||||
input_path: Path to the markdown file
|
||||
output_dir: Directory where assets should be copied
|
||||
verbose: Whether to print verbose output
|
||||
verbose: Whether to print detailed output
|
||||
silent: Whether to suppress non-essential output
|
||||
"""
|
||||
import shutil
|
||||
import hashlib
|
||||
from markitect.assets.discovery import discover_assets_from_markdown
|
||||
|
||||
def get_file_hash(file_path):
|
||||
"""Get SHA-256 hash of file content for content comparison."""
|
||||
hash_sha256 = hashlib.sha256()
|
||||
with open(file_path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(4096), b""):
|
||||
hash_sha256.update(chunk)
|
||||
return hash_sha256.hexdigest()
|
||||
|
||||
try:
|
||||
# Read the markdown content
|
||||
markdown_content = input_path.read_text(encoding='utf-8')
|
||||
@@ -3524,14 +3576,49 @@ def _ship_assets(input_path: Path, output_dir: Path, verbose: bool = False):
|
||||
# Create destination directory
|
||||
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Check if we need to copy (timestamp-based)
|
||||
# Check if we need to copy (smart comparison for cross-filesystem compatibility)
|
||||
should_copy = True
|
||||
if dest_path.exists():
|
||||
source_mtime = asset_ref.resolved_path.stat().st_mtime
|
||||
dest_mtime = dest_path.stat().st_mtime
|
||||
if source_mtime <= dest_mtime:
|
||||
should_copy = False
|
||||
skipped_count += 1
|
||||
source_stat = asset_ref.resolved_path.stat()
|
||||
dest_stat = dest_path.stat()
|
||||
|
||||
# Detect if we're in a cross-filesystem scenario where timestamps might be unreliable
|
||||
# Heuristics: different filesystems, or timestamps that don't make sense
|
||||
is_cross_fs = (
|
||||
# Different device IDs suggests different filesystems
|
||||
source_stat.st_dev != dest_stat.st_dev or
|
||||
# Destination path starts with /mnt/ (common WSL Windows mount)
|
||||
str(dest_path).startswith('/mnt/') or
|
||||
# Very large timestamp differences (>1 hour) for same content suggest sync issues
|
||||
abs(source_stat.st_mtime - dest_stat.st_mtime) > 3600
|
||||
)
|
||||
|
||||
if is_cross_fs:
|
||||
# Use content-based comparison for cross-filesystem scenarios
|
||||
if source_stat.st_size == dest_stat.st_size:
|
||||
try:
|
||||
source_hash = get_file_hash(asset_ref.resolved_path)
|
||||
dest_hash = get_file_hash(dest_path)
|
||||
|
||||
if source_hash == dest_hash:
|
||||
should_copy = False
|
||||
skipped_count += 1
|
||||
if verbose:
|
||||
click.echo(f" → Content verified (cross-fs): {asset_ref.asset_path}")
|
||||
# If hashes differ, should_copy remains True
|
||||
except (OSError, IOError):
|
||||
if verbose:
|
||||
click.echo(f" ⚠ Could not verify content, will copy: {asset_ref.asset_path}")
|
||||
pass
|
||||
# If sizes differ, should_copy remains True
|
||||
else:
|
||||
# Use fast timestamp comparison for same-filesystem scenarios
|
||||
if source_stat.st_mtime <= dest_stat.st_mtime and source_stat.st_size == dest_stat.st_size:
|
||||
should_copy = False
|
||||
skipped_count += 1
|
||||
if verbose:
|
||||
click.echo(f" → Timestamp verified: {asset_ref.asset_path}")
|
||||
# If timestamp suggests newer source or different size, should_copy remains True
|
||||
|
||||
if should_copy:
|
||||
shutil.copy2(asset_ref.resolved_path, dest_path)
|
||||
@@ -3541,12 +3628,21 @@ def _ship_assets(input_path: Path, output_dir: Path, verbose: bool = False):
|
||||
elif verbose:
|
||||
click.echo(f" → Skipped (up-to-date): {asset_ref.asset_path}")
|
||||
|
||||
# Summary
|
||||
if verbose or shipped_count > 0:
|
||||
# Summary - provide feedback based on verbosity settings
|
||||
total_assets = shipped_count + skipped_count + missing_count
|
||||
|
||||
if total_assets > 0 and not silent:
|
||||
if shipped_count > 0:
|
||||
click.echo(f"✓ Shipped {shipped_count} assets")
|
||||
if skipped_count > 0:
|
||||
click.echo(f" → Skipped {skipped_count} up-to-date assets")
|
||||
elif skipped_count > 0:
|
||||
click.echo(f"✓ All {skipped_count} assets up-to-date")
|
||||
|
||||
# Additional details for verbose or when there are mixed results
|
||||
if verbose or (shipped_count > 0 and skipped_count > 0):
|
||||
if skipped_count > 0 and shipped_count > 0:
|
||||
click.echo(f" → {skipped_count} already up-to-date")
|
||||
|
||||
# Always show missing assets as it's important information
|
||||
if missing_count > 0:
|
||||
click.echo(f" ⚠ {missing_count} assets not found", err=True)
|
||||
|
||||
|
||||
@@ -32,6 +32,31 @@ class FloatingMenu {
|
||||
const targetElement = this.renderer.findSectionElement(this.sectionId);
|
||||
if (!targetElement) return null;
|
||||
|
||||
// Get content dimensions and position
|
||||
const rect = targetElement.getBoundingClientRect();
|
||||
const viewport = {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
};
|
||||
|
||||
// Calculate content width and responsive extension
|
||||
const contentWidth = rect.width;
|
||||
const buttonAreaWidth = 120; // Space needed for buttons
|
||||
const minMenuWidth = Math.max(300, contentWidth); // At least content width or 300px
|
||||
const preferredMenuWidth = contentWidth + buttonAreaWidth;
|
||||
|
||||
// Check if we have space to extend to the right
|
||||
const spaceOnRight = viewport.width - rect.right;
|
||||
const canExtendRight = spaceOnRight >= buttonAreaWidth + 20; // 20px margin
|
||||
|
||||
// Determine final menu width
|
||||
let menuWidth;
|
||||
if (canExtendRight && viewport.width >= 800) { // Only on wide screens
|
||||
menuWidth = Math.min(preferredMenuWidth, viewport.width - rect.left - 20);
|
||||
} else {
|
||||
menuWidth = Math.min(minMenuWidth, viewport.width - 40); // 20px margins
|
||||
}
|
||||
|
||||
// Create floating menu element
|
||||
this.element = document.createElement('div');
|
||||
this.element.className = 'ui-edit-floating-menu';
|
||||
@@ -42,65 +67,101 @@ class FloatingMenu {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
padding: 16px;
|
||||
min-width: 300px;
|
||||
padding: 0;
|
||||
width: ${menuWidth}px;
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
// Smart positioning with viewport boundary detection
|
||||
const rect = targetElement.getBoundingClientRect();
|
||||
const viewport = {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
};
|
||||
// Add headline
|
||||
const headline = document.createElement('div');
|
||||
headline.className = 'ui-edit-headline';
|
||||
headline.textContent = `Editing ${this.type === 'image' ? 'Image' : 'Section'}`;
|
||||
headline.style.cssText = `
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 8px 16px;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: #495057;
|
||||
border-radius: 8px 8px 0 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
`;
|
||||
|
||||
// Calculate initial position (below the section)
|
||||
// Create content wrapper with padding
|
||||
const contentWrapper = document.createElement('div');
|
||||
contentWrapper.style.cssText = `
|
||||
padding: 16px;
|
||||
`;
|
||||
|
||||
this.element.appendChild(headline);
|
||||
|
||||
// Position directly over content (overlay positioning)
|
||||
let left = rect.left;
|
||||
let top = rect.bottom + 10;
|
||||
let top = rect.top;
|
||||
|
||||
// Adjust horizontal position if menu would go off-screen
|
||||
const menuWidth = 350; // Estimated menu width
|
||||
// Ensure menu doesn't go off-screen horizontally
|
||||
if (left + menuWidth > viewport.width) {
|
||||
left = viewport.width - menuWidth - 20; // 20px margin from edge
|
||||
left = viewport.width - menuWidth - 20;
|
||||
}
|
||||
if (left < 10) {
|
||||
left = 10; // Minimum margin from left edge
|
||||
left = 10;
|
||||
}
|
||||
|
||||
// Adjust vertical position if menu would go off-screen
|
||||
const menuHeight = 300; // Estimated menu height
|
||||
if (top + menuHeight > viewport.height) {
|
||||
// Position above the section instead
|
||||
top = rect.top - menuHeight - 10;
|
||||
if (top < 10) {
|
||||
// If still off-screen, position at viewport top
|
||||
top = 10;
|
||||
}
|
||||
// For vertical positioning, prefer staying on top of content
|
||||
// Only move if absolutely necessary
|
||||
const menuHeight = this.type === 'image' ? 350 : 200; // Better height estimates
|
||||
const wouldGoOffBottom = top + menuHeight > viewport.height;
|
||||
const wouldGoOffTop = top < 10;
|
||||
|
||||
if (wouldGoOffBottom && !wouldGoOffTop) {
|
||||
// Try to fit by moving up, but keep some overlay if possible
|
||||
const maxTop = viewport.height - menuHeight - 10;
|
||||
top = Math.max(rect.top - 50, maxTop); // Prefer staying near original position
|
||||
} else if (wouldGoOffTop) {
|
||||
top = 10; // Minimum distance from top
|
||||
}
|
||||
// Otherwise, keep the original overlay position
|
||||
|
||||
this.element.style.left = `${left}px`;
|
||||
this.element.style.top = `${top}px`;
|
||||
|
||||
// Add content
|
||||
// Add content to wrapper
|
||||
if (contentElement) {
|
||||
this.element.appendChild(contentElement);
|
||||
contentWrapper.appendChild(contentElement);
|
||||
}
|
||||
if (controlsElement) {
|
||||
this.element.appendChild(controlsElement);
|
||||
contentWrapper.appendChild(controlsElement);
|
||||
}
|
||||
|
||||
// Add close button
|
||||
this.element.appendChild(contentWrapper);
|
||||
|
||||
// Add close button to headline
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.textContent = '×';
|
||||
closeButton.style.cssText = `
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
top: 4px;
|
||||
right: 8px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
`;
|
||||
closeButton.addEventListener('mouseover', () => {
|
||||
closeButton.style.backgroundColor = '#e9ecef';
|
||||
});
|
||||
closeButton.addEventListener('mouseout', () => {
|
||||
closeButton.style.backgroundColor = 'transparent';
|
||||
});
|
||||
closeButton.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
this.hide();
|
||||
@@ -360,13 +421,36 @@ class DOMRenderer {
|
||||
// Create content area for text editing
|
||||
const editorContent = document.createElement('div');
|
||||
editorContent.className = 'ui-edit-editor-content';
|
||||
editorContent.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
// Check if we have space for side-by-side layout
|
||||
const targetElement = this.findSectionElement(sectionId);
|
||||
const rect = targetElement ? targetElement.getBoundingClientRect() : null;
|
||||
const viewport = { width: window.innerWidth, height: window.innerHeight };
|
||||
const hasWideLayout = rect && viewport.width >= 800 && (viewport.width - rect.right) >= 120;
|
||||
|
||||
if (hasWideLayout) {
|
||||
// Side-by-side layout: textarea on left, controls on right
|
||||
editorContent.style.cssText = `
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
align-items: flex-start;
|
||||
`;
|
||||
} else {
|
||||
// Stacked layout: textarea above, controls below
|
||||
editorContent.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
`;
|
||||
}
|
||||
|
||||
// Create textarea container
|
||||
const textareaContainer = document.createElement('div');
|
||||
textareaContainer.style.cssText = hasWideLayout ? 'flex: 1; min-width: 0;' : 'width: 100%;';
|
||||
|
||||
// Create textarea
|
||||
const textarea = document.createElement('textarea');
|
||||
@@ -381,55 +465,81 @@ class DOMRenderer {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
// Create controls
|
||||
const controls = document.createElement('div');
|
||||
controls.style.cssText = `
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
if (hasWideLayout) {
|
||||
controls.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 100px;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
} else {
|
||||
controls.style.cssText = `
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
}
|
||||
|
||||
const acceptButton = document.createElement('button');
|
||||
acceptButton.textContent = 'Accept';
|
||||
acceptButton.textContent = hasWideLayout ? '✓' : 'Accept';
|
||||
acceptButton.style.cssText = `
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
padding: ${hasWideLayout ? '8px 12px' : '8px 16px'};
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
${hasWideLayout ? 'width: 100%;' : ''}
|
||||
font-size: ${hasWideLayout ? '14px' : '13px'};
|
||||
`;
|
||||
|
||||
const cancelButton = document.createElement('button');
|
||||
cancelButton.textContent = 'Cancel';
|
||||
cancelButton.textContent = hasWideLayout ? '✗' : 'Cancel';
|
||||
cancelButton.style.cssText = `
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
padding: ${hasWideLayout ? '8px 12px' : '8px 16px'};
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
${hasWideLayout ? 'width: 100%;' : ''}
|
||||
font-size: ${hasWideLayout ? '14px' : '13px'};
|
||||
`;
|
||||
|
||||
const resetButton = document.createElement('button');
|
||||
resetButton.textContent = '↺ Reset';
|
||||
resetButton.textContent = hasWideLayout ? '↺' : '↺ Reset';
|
||||
resetButton.style.cssText = `
|
||||
background: #fd7e14;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
padding: ${hasWideLayout ? '8px 12px' : '8px 16px'};
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
${hasWideLayout ? 'width: 100%;' : ''}
|
||||
font-size: ${hasWideLayout ? '14px' : '13px'};
|
||||
`;
|
||||
|
||||
controls.appendChild(acceptButton);
|
||||
controls.appendChild(cancelButton);
|
||||
controls.appendChild(resetButton);
|
||||
|
||||
editorContent.appendChild(textarea);
|
||||
editorContent.appendChild(controls);
|
||||
// Assemble the layout
|
||||
textareaContainer.appendChild(textarea);
|
||||
|
||||
if (hasWideLayout) {
|
||||
editorContent.appendChild(textareaContainer);
|
||||
editorContent.appendChild(controls);
|
||||
} else {
|
||||
editorContent.appendChild(textareaContainer);
|
||||
editorContent.appendChild(controls);
|
||||
}
|
||||
|
||||
// Create floating menu
|
||||
const floatingMenu = new FloatingMenu(sectionId, 'text', this);
|
||||
@@ -494,16 +604,52 @@ class DOMRenderer {
|
||||
stagingState.currentImageSrc = imageSrc;
|
||||
}
|
||||
|
||||
// Check if we have space for side-by-side layout
|
||||
const targetElement = this.findSectionElement(sectionId);
|
||||
const rect = targetElement ? targetElement.getBoundingClientRect() : null;
|
||||
const viewport = { width: window.innerWidth, height: window.innerHeight };
|
||||
const hasWideLayout = rect && viewport.width >= 800 && (viewport.width - rect.right) >= 120;
|
||||
|
||||
// Create image editor content area
|
||||
const editorContent = document.createElement('div');
|
||||
editorContent.className = 'ui-edit-image-content';
|
||||
editorContent.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
if (hasWideLayout) {
|
||||
// Side-by-side layout: content on left, controls on right
|
||||
editorContent.style.cssText = `
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
align-items: flex-start;
|
||||
`;
|
||||
} else {
|
||||
// Stacked layout: content above, controls below
|
||||
editorContent.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
`;
|
||||
}
|
||||
|
||||
// Create content container for image and alt text
|
||||
const contentContainer = document.createElement('div');
|
||||
contentContainer.style.cssText = hasWideLayout ? 'flex: 1; min-width: 0;' : 'width: 100%;';
|
||||
if (!hasWideLayout) {
|
||||
contentContainer.style.cssText += `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
`;
|
||||
} else {
|
||||
contentContainer.style.cssText += `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
`;
|
||||
}
|
||||
|
||||
// Image preview with drop zone
|
||||
const imagePreview = document.createElement('div');
|
||||
@@ -718,27 +864,37 @@ class DOMRenderer {
|
||||
}
|
||||
};
|
||||
|
||||
// Assemble content
|
||||
editorContent.appendChild(imagePreview);
|
||||
editorContent.appendChild(altTextContainer);
|
||||
editorContent.appendChild(changeIndicator);
|
||||
editorContent.appendChild(fileInput);
|
||||
// Assemble content container
|
||||
contentContainer.appendChild(imagePreview);
|
||||
contentContainer.appendChild(altTextContainer);
|
||||
contentContainer.appendChild(changeIndicator);
|
||||
contentContainer.appendChild(fileInput);
|
||||
|
||||
// Create controls
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'ui-edit-controls';
|
||||
controls.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
`;
|
||||
if (hasWideLayout) {
|
||||
controls.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 100px;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
} else {
|
||||
controls.style.cssText = `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
`;
|
||||
}
|
||||
|
||||
const acceptBtn = document.createElement('button');
|
||||
acceptBtn.textContent = '✓ Accept';
|
||||
acceptBtn.textContent = hasWideLayout ? '✓' : '✓ Accept';
|
||||
acceptBtn.style.cssText = `
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
padding: ${hasWideLayout ? '8px 12px' : '8px 12px'};
|
||||
font-size: ${hasWideLayout ? '14px' : '12px'};
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
color: white;
|
||||
@@ -751,10 +907,10 @@ class DOMRenderer {
|
||||
`;
|
||||
|
||||
const cancelBtn = document.createElement('button');
|
||||
cancelBtn.textContent = '✗ Cancel';
|
||||
cancelBtn.textContent = hasWideLayout ? '✗' : '✗ Cancel';
|
||||
cancelBtn.style.cssText = `
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
padding: ${hasWideLayout ? '8px 12px' : '8px 12px'};
|
||||
font-size: ${hasWideLayout ? '14px' : '12px'};
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
color: white;
|
||||
@@ -767,10 +923,10 @@ class DOMRenderer {
|
||||
`;
|
||||
|
||||
const resetBtn = document.createElement('button');
|
||||
resetBtn.textContent = '↺ Reset';
|
||||
resetBtn.textContent = hasWideLayout ? '↺' : '↺ Reset';
|
||||
resetBtn.style.cssText = `
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
padding: ${hasWideLayout ? '8px 12px' : '8px 12px'};
|
||||
font-size: ${hasWideLayout ? '14px' : '12px'};
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
color: white;
|
||||
@@ -871,12 +1027,21 @@ class DOMRenderer {
|
||||
}
|
||||
});
|
||||
|
||||
// Assemble the final layout
|
||||
if (hasWideLayout) {
|
||||
editorContent.appendChild(contentContainer);
|
||||
editorContent.appendChild(controls);
|
||||
} else {
|
||||
editorContent.appendChild(contentContainer);
|
||||
editorContent.appendChild(controls);
|
||||
}
|
||||
|
||||
// Create floating menu
|
||||
const floatingMenu = new FloatingMenu(sectionId, 'image', this);
|
||||
this.currentFloatingMenu = floatingMenu;
|
||||
this.editingSections.add(sectionId);
|
||||
|
||||
floatingMenu.show(editorContent, controls);
|
||||
floatingMenu.show(editorContent);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -340,39 +340,51 @@ class SectionManager {
|
||||
}
|
||||
|
||||
createSectionsFromMarkdown(markdownContent) {
|
||||
const lines = markdownContent.split('\n');
|
||||
// Split content into blocks separated by double newlines
|
||||
const blocks = markdownContent.split(/\n\s*\n/);
|
||||
const sections = [];
|
||||
let currentSection = '';
|
||||
let position = 0;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const isHeading = /^#{1,6}\s/.test(line);
|
||||
const isNewParagraph = line.trim() && i > 0 && !lines[i-1].trim();
|
||||
const isNewSection = isHeading || isNewParagraph;
|
||||
for (const block of blocks) {
|
||||
const trimmedBlock = block.trim();
|
||||
if (!trimmedBlock) continue;
|
||||
|
||||
if (isNewSection && currentSection.trim()) {
|
||||
// Check if this block should be split further
|
||||
const lines = trimmedBlock.split('\n');
|
||||
let currentSection = '';
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const isHeading = /^#{1,6}\s/.test(line.trim());
|
||||
const isImage = /^\s*!\[.*?\]\(.*?\)\s*$/.test(line);
|
||||
|
||||
// Each heading or image starts a new section
|
||||
if ((isHeading || isImage) && currentSection.trim()) {
|
||||
// Save the previous section
|
||||
const sectionId = Section.generateId(currentSection, position);
|
||||
const sectionType = Section.detectType(currentSection);
|
||||
const section = new Section(sectionId, currentSection.trim(), sectionType);
|
||||
sections.push(section);
|
||||
this.sections.set(sectionId, section);
|
||||
position++;
|
||||
currentSection = line;
|
||||
} else {
|
||||
if (currentSection) currentSection += '\n';
|
||||
currentSection += line;
|
||||
}
|
||||
}
|
||||
|
||||
// Save the final section from this block
|
||||
if (currentSection.trim()) {
|
||||
const sectionId = Section.generateId(currentSection, position);
|
||||
const sectionType = Section.detectType(currentSection);
|
||||
const section = new Section(sectionId, currentSection.trim(), sectionType);
|
||||
sections.push(section);
|
||||
this.sections.set(sectionId, section);
|
||||
position++;
|
||||
currentSection = line;
|
||||
} else {
|
||||
if (currentSection) currentSection += '\n';
|
||||
currentSection += line;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentSection.trim()) {
|
||||
const sectionId = Section.generateId(currentSection, position);
|
||||
const sectionType = Section.detectType(currentSection);
|
||||
const section = new Section(sectionId, currentSection.trim(), sectionType);
|
||||
sections.push(section);
|
||||
this.sections.set(sectionId, section);
|
||||
}
|
||||
|
||||
this.emit('sections-created', { sections, count: sections.length });
|
||||
return sections;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user