Fix markdown draft preview sync in PostIt blog editor

This commit is contained in:
Paul Schneider 2026-10-04 20:03:01 +01:00
commit 7e9e7613ba
Signed by: notazof
GPG key ID: AD4B9B545FBD7D12
4 changed files with 209 additions and 90 deletions

View file

@ -0,0 +1,51 @@
<UserControl
x:Class="PostIt.Controls.MarkdownEditorControl"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:AvaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
xmlns:md="using:MarkdownViewer.Core.Controls"
x:Name="Root"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Border
BorderBrush="LightGray"
BorderThickness="1"
CornerRadius="5"
IsVisible="{Binding !IsPreviewVisible, ElementName=Root}"
Padding="0">
<AvaloniaEdit:TextEditor
x:Name="SourceEditor"
Document="{Binding Document, ElementName=Root, Mode=TwoWay}"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace"
ShowLineNumbers="True"
WordWrap="True"
BorderThickness="0"
Padding="5,5,5,15"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
TextInputOptions.Multiline="True"
TextInputOptions.ReturnKeyType="Default" />
</Border>
<Border
BorderBrush="LightGray"
BorderThickness="1"
CornerRadius="5"
IsVisible="{Binding IsPreviewVisible, ElementName=Root}"
Padding="8">
<ScrollViewer
BringIntoViewOnFocusChange="False"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<md:MarkdownViewer
x:Name="PreviewViewer"
MarkdownText="{Binding MarkdownText, ElementName=Root}"
ClipToBounds="False" />
</ScrollViewer>
</Border>
</Grid>
</UserControl>

View file

@ -0,0 +1,63 @@
using System;
using Avalonia;
using Avalonia.Controls;
using AvaloniaEdit.Document;
namespace PostIt.Controls;
public partial class MarkdownEditorControl : UserControl
{
public static readonly StyledProperty<string?> MarkdownTextProperty =
AvaloniaProperty.Register<MarkdownEditorControl, string?>(nameof(MarkdownText));
public static readonly StyledProperty<TextDocument?> DocumentProperty =
AvaloniaProperty.Register<MarkdownEditorControl, TextDocument?>(nameof(Document));
public static readonly StyledProperty<bool> IsPreviewVisibleProperty =
AvaloniaProperty.Register<MarkdownEditorControl, bool>(nameof(IsPreviewVisible), false);
public string? MarkdownText
{
get => GetValue(MarkdownTextProperty);
set => SetValue(MarkdownTextProperty, value);
}
public TextDocument? Document
{
get => GetValue(DocumentProperty);
set => SetValue(DocumentProperty, value);
}
public bool IsPreviewVisible
{
get => GetValue(IsPreviewVisibleProperty);
set => SetValue(IsPreviewVisibleProperty, value);
}
public MarkdownEditorControl()
{
InitializeComponent();
PropertyChanged += OnPropertyChanged;
}
private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == MarkdownTextProperty)
{
var text = e.NewValue as string ?? string.Empty;
if (Document is null || !string.Equals(Document.Text, text, StringComparison.Ordinal))
{
Document = new TextDocument(text);
}
}
else if (e.Property == DocumentProperty)
{
var newDocument = e.NewValue as TextDocument;
var text = newDocument?.Text ?? string.Empty;
if (!string.Equals(MarkdownText, text, StringComparison.Ordinal))
{
MarkdownText = text;
}
}
}
}

View file

@ -39,18 +39,56 @@ public partial class BlogsViewModel : ViewModelBase, IActionStatusViewModel
/// <summary>Editor buffer for the post body. Same pattern as /// <summary>Editor buffer for the post body. Same pattern as
/// <see cref="DraftTitle"/>.</summary> /// <see cref="DraftTitle"/>.</summary>
public string DraftArticle public string? DraftArticle
{ {
get => DraftArticleDocument.Text; get => DraftArticleDocument?.Text ?? null;
set { set
{
var currentText = DraftArticleDocument?.Text;
if (string.Equals(currentText, value, StringComparison.Ordinal))
{
OnPropertyChanged(nameof(DraftArticle));
return;
}
OnPropertyChanging(nameof(DraftArticle)); OnPropertyChanging(nameof(DraftArticle));
DraftArticleDocument = new TextDocument(value); DraftArticleDocument = value is not null ? new TextDocument(value) : null;
this.OnPropertyChanged(nameof(DraftArticle)); OnPropertyChanged(nameof(DraftArticle));
} }
} }
[ObservableProperty] private TextDocument? _draftArticleDocument;
public partial TextDocument DraftArticleDocument {get; set;}
public TextDocument? DraftArticleDocument
{
get => _draftArticleDocument;
set
{
if (ReferenceEquals(_draftArticleDocument, value))
{
return;
}
if (_draftArticleDocument is not null)
{
_draftArticleDocument.TextChanged -= DraftArticleDocument_TextChanged;
}
_draftArticleDocument = value;
if (_draftArticleDocument is not null)
{
_draftArticleDocument.TextChanged += DraftArticleDocument_TextChanged;
}
OnPropertyChanged(nameof(DraftArticleDocument));
}
}
private void DraftArticleDocument_TextChanged(object? sender, EventArgs e)
{
OnPropertyChanged(nameof(DraftArticle));
}
/// <summary>Editor buffer for the post's publication state. /// <summary>Editor buffer for the post's publication state.
/// Reflects the server-side <c>IsPublished</c> flag (the /// Reflects the server-side <c>IsPublished</c> flag (the

View file

@ -17,10 +17,7 @@
<Design.DataContext> <Design.DataContext>
<vm:BlogsViewModel /> <vm:BlogsViewModel />
</Design.DataContext> </Design.DataContext>
<ScrollViewer HorizontalScrollBarVisibility="Disabled" <StackPanel Margin="5,5,10,5" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
Margin="5,5,10,5" HorizontalAlignment="Stretch" BringIntoViewOnFocusChange="False"
VerticalAlignment="Stretch">
<StackPanel>
<WrapPanel x:Name="BlogsHeaderPanel" <WrapPanel x:Name="BlogsHeaderPanel"
Margin="5" Margin="5"
@ -87,7 +84,7 @@
<PathIcon Data="{StaticResource PublishedIcon}" /> <PathIcon Data="{StaticResource PublishedIcon}" />
</CommandBarToggleButton.Icon> </CommandBarToggleButton.Icon>
</CommandBarToggleButton> </CommandBarToggleButton>
<CommandBarToggleButton IsChecked="{Binding IsMarkdownViewerVisible, Mode=TwoWay}" Label="Markdown Viewer"> <CommandBarToggleButton IsChecked="{Binding IsMarkdownViewerVisible, Mode=TwoWay}" Label="Aperçu">
<CommandBarToggleButton.Icon> <CommandBarToggleButton.Icon>
<PathIcon Data="{StaticResource MarkdownIcon}" /> <PathIcon Data="{StaticResource MarkdownIcon}" />
</CommandBarToggleButton.Icon> </CommandBarToggleButton.Icon>
@ -137,91 +134,61 @@
</ListBox> </ListBox>
</Border> </Border>
<Border <Grid
Grid.Row="2" Grid.Row="2"
Padding="8" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
RowSpacing="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Row="0"
FontWeight="SemiBold"
Text="Post detail" />
<TextBox
x:Name="DraftTitleTextBox"
Grid.Row="1"
PlaceholderText="Title"
Text="{Binding DraftTitle, Mode=TwoWay}" />
<postitControls:MarkdownEditorControl
Grid.Row="2"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
BorderBrush="Gray" IsPreviewVisible="{Binding IsMarkdownViewerVisible, Mode=TwoWay}"
BorderThickness="1" MarkdownText="{Binding DraftArticle, Mode=TwoWay}"
IsVisible="{Binding IsMarkdownViewerVisible}"> Document="{Binding DraftArticleDocument, Mode=TwoWay}" />
<md:MarkdownViewer Name="MarkdownViewer" MarkdownText="{Binding DraftArticle}" />
</Border>
<Border <Border x:Name="AttachmentsBorder"
Grid.Row="2" Grid.Row="3"
Padding="8" Padding="8"
BorderBrush="Gray" BorderBrush="LightGray"
BorderThickness="1" BorderThickness="1"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch">
IsVisible="{Binding !IsMarkdownViewerVisible}"> <StackPanel Spacing="4">
<Grid <TextBlock FontWeight="SemiBold" Text="Pièces jointes" />
HorizontalAlignment="Stretch" <ItemsControl ItemsSource="{Binding DraftAttachments}">
VerticalAlignment="Stretch" <ItemsControl.ItemTemplate>
RowSpacing="10"> <DataTemplate>
<Grid.RowDefinitions> <TextBlock Text="{Binding FileName}" />
<RowDefinition Height="Auto" /> </DataTemplate>
<RowDefinition Height="Auto" /> </ItemsControl.ItemTemplate>
<RowDefinition Height="*" /> </ItemsControl>
<RowDefinition Height="Auto" /> </StackPanel>
</Grid.RowDefinitions> </Border>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock <postitControls:StatusBar Grid.Row="4" DataContext="{Binding ActionStatus}" x:Name="StatusBar" />
Grid.Row="0"
FontWeight="SemiBold"
Text="Post detail" />
<TextBox
x:Name="DraftTitleTextBox"
Grid.Row="1"
PlaceholderText="Title"
Text="{Binding DraftTitle, Mode=TwoWay}" />
</Grid>
<AvaloniaEdit:TextEditor
x:Name="DraftArticleTextEditor"
Grid.Row="2"
VerticalAlignment="Stretch"
Document="{Binding DraftArticleDocument, Mode=TwoWay}"
FontFamily="Cascadia Code,Consolas,Menlo,Monospace"
ShowLineNumbers="True"
WordWrap="True"
BorderThickness="1"
BorderBrush="LightGray"
CornerRadius="5"
Padding="5,5,5,15"
HorizontalScrollBarVisibility="Disabled"
TextInputOptions.Multiline="True"
TextInputOptions.ReturnKeyType="Default"
/>
<Border x:Name="AttachmentsBorder"
Grid.Row="3"
Padding="8"
BorderBrush="LightGray"
BorderThickness="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<StackPanel Spacing="4">
<TextBlock FontWeight="SemiBold" Text="Pièces jointes" />
<ItemsControl ItemsSource="{Binding DraftAttachments}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding FileName}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
<postitControls:StatusBar Grid.Row="4" DataContext="{Binding ActionStatus}" x:Name="StatusBar" />
</Grid>
</Border>
</Grid> </Grid>
</StackPanel> </StackPanel>
</ScrollViewer>
</ContentPage> </ContentPage>