Add draggable splitter for resizable editor/preview panes

Implemented a vertical splitter between the editor and preview panes, allowing users to resize them interactively. The split ratio is persisted in localStorage and clamped for usability. Added JavaScript interop for drag handling, updated styles for the splitter, and improved the preview toggle button with an eye icon. Layout remains responsive for mobile devices.
This commit is contained in:
Michal Vrba
2026-01-30 17:58:06 +01:00
parent fe8fa73692
commit 2ce5cf359d
3 changed files with 169 additions and 4 deletions
+83 -4
View File
@@ -15,7 +15,7 @@
<div class="top-bar-left">
<button class="btn-toggle @(showPreview ? "active" : "")"
@onclick="TogglePreview">
@(showPreview ? "Hide Preview" : "Show Preview")
👁️ @(showPreview ? "Hide Preview" : "Show Preview")
</button>
</div>
@@ -64,9 +64,9 @@
</div>
<!-- Editor Layout -->
<div class="editor-layout @(showPreview ? "split-view" : "full-view")">
<div class="editor-layout @(showPreview ? "split-view" : "full-view")" @ref="editorLayoutRef">
<!-- Editor Pane -->
<div class="editor-pane">
<div class="editor-pane" style="@GetEditorPaneStyle()">
<textarea class="markdown-editor"
@bind="markdownContent"
@oninput="OnContentChanged"
@@ -76,10 +76,18 @@
</textarea>
</div>
<!-- Simple Splitter -->
@if (showPreview)
{
<div class="splitter" @onmousedown="OnSplitterMouseDown" @onmousedown:preventDefault="true">
<div class="splitter-handle"></div>
</div>
}
<!-- Preview Pane -->
@if (showPreview)
{
<div class="preview-pane">
<div class="preview-pane" style="@GetPreviewPaneStyle()">
<div class="preview-content">
@((MarkupString)renderedHtml)
</div>
@@ -298,4 +306,75 @@
{
autoSaveTimer?.Dispose();
}
// Simple splitter functionality
private ElementReference editorLayoutRef;
private double splitRatio = 0.5; // 50/50 by default
private bool isDragging = false;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
try
{
// Load saved split ratio from localStorage
var savedRatio = await JSRuntime.InvokeAsync<string?>("localStorage.getItem", "notemark-split-ratio");
if (!string.IsNullOrEmpty(savedRatio) && double.TryParse(savedRatio, out var ratio))
{
splitRatio = Math.Max(0.2, Math.Min(0.8, ratio));
StateHasChanged();
}
}
catch
{
// Ignore localStorage errors
}
}
}
private string GetEditorPaneStyle()
{
if (!showPreview) return "";
return $"flex: 0 0 {splitRatio * 100:F1}%;";
}
private string GetPreviewPaneStyle()
{
if (!showPreview) return "";
return $"flex: 0 0 {(1 - splitRatio) * 100:F1}%;";
}
private async Task OnSplitterMouseDown(MouseEventArgs e)
{
isDragging = true;
await JSRuntime.InvokeVoidAsync("startSplitterDrag", DotNetObjectReference.Create(this));
}
[JSInvokable]
public async Task UpdateSplitRatio(double clientX, double containerWidth)
{
if (!isDragging || containerWidth <= 0) return;
var newRatio = clientX / containerWidth;
splitRatio = Math.Max(0.2, Math.Min(0.8, newRatio)); // Clamp between 20% and 80%
// Save to localStorage
try
{
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "notemark-split-ratio", splitRatio.ToString());
}
catch
{
// Ignore localStorage errors
}
StateHasChanged();
}
[JSInvokable]
public void StopDragging()
{
isDragging = false;
}
}
@@ -416,6 +416,35 @@
to { transform: rotate(360deg); }
}
/* Splitter Styles */
.splitter {
width: 8px;
background-color: var(--border);
cursor: col-resize;
display: flex;
align-items: center;
justify-content: center;
position: relative;
flex-shrink: 0;
transition: background-color 0.2s ease;
}
.splitter:hover {
background-color: var(--accent);
}
.splitter-handle {
width: 2px;
height: 40px;
background-color: var(--secondary-text);
border-radius: 1px;
transition: background-color 0.2s ease;
}
.splitter:hover .splitter-handle {
background-color: white;
}
/* Mobile Responsive */
@media (max-width: 768px) {
.editor-layout.split-view {
+57
View File
@@ -32,3 +32,60 @@ window.notemarkTheme = {
return localStorage.getItem('notemark-theme') || 'auto';
}
};
// Simple splitter drag functionality
let splitterState = {
isDragging: false,
blazorInstance: null,
container: null
};
window.startSplitterDrag = function(blazorInstance) {
splitterState.blazorInstance = blazorInstance;
splitterState.isDragging = true;
// Find the editor layout container
splitterState.container = document.querySelector('.editor-layout');
// Add global mouse events
document.addEventListener('mousemove', handleSplitterMove);
document.addEventListener('mouseup', handleSplitterUp);
// Prevent text selection
document.body.style.userSelect = 'none';
document.body.style.cursor = 'col-resize';
};
function handleSplitterMove(e) {
if (!splitterState.isDragging || !splitterState.container || !splitterState.blazorInstance) return;
const rect = splitterState.container.getBoundingClientRect();
const clientX = e.clientX - rect.left;
const containerWidth = rect.width;
// Call Blazor method to update split ratio
splitterState.blazorInstance.invokeMethodAsync('UpdateSplitRatio', clientX, containerWidth);
}
function handleSplitterUp(e) {
if (!splitterState.isDragging) return;
splitterState.isDragging = false;
// Remove global mouse events
document.removeEventListener('mousemove', handleSplitterMove);
document.removeEventListener('mouseup', handleSplitterUp);
// Reset cursor and selection
document.body.style.cursor = '';
document.body.style.userSelect = '';
// Notify Blazor that dragging stopped
if (splitterState.blazorInstance) {
splitterState.blazorInstance.invokeMethodAsync('StopDragging');
}
// Clean up
splitterState.blazorInstance = null;
splitterState.container = null;
}