Skip to content

Commit dfb62ad

Browse files
committed
Added annotations , annotations registery service, and annotations page.
1 parent 32be5ae commit dfb62ad

350 files changed

Lines changed: 532 additions & 66485 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
@using OsayamiBlog.Services
2+
@inject AnnotationRegistry Registry
3+
4+
<div @onmouseup="CaptureSelection">
5+
<ParagraphRenderer Paragraph="Paragraph" />
6+
</div>
7+
8+
@if (_selection is not null)
9+
{
10+
@if (_selection is not null)
11+
{
12+
<button @onclick="OpenDialog">Annotate</button>
13+
}
14+
15+
@if (_showDialog)
16+
{
17+
<CreateAnnotationDialog
18+
OnConfirm="CreateAnnotation"
19+
OnCancel="CloseDialog" />
20+
}
21+
22+
}
23+
24+
@code {
25+
private bool _showDialog;
26+
private AnnotationDraft? _pendingDraft;
27+
28+
[Parameter, EditorRequired]
29+
public string CurrentPostSlug { get; set; } = "";
30+
31+
[Inject] IJSRuntime JS { get; set; } = default!;
32+
33+
[Parameter, EditorRequired]
34+
public Paragraph Paragraph { get; set; } = default!;
35+
36+
private TextSelection? _selection;
37+
38+
private async Task CaptureSelection()
39+
{
40+
_selection = await JS.InvokeAsync<TextSelection?>("getSelectionText");
41+
}
42+
43+
private void CreateAnnotation(AnnotationDraft draft)
44+
{
45+
if (_selection is null) return;
46+
47+
var annotation = new Annotation
48+
{
49+
Label = draft.Label,
50+
Body = draft.Body
51+
};
52+
53+
var location = new AnnotationLocation
54+
{
55+
PostSlug = CurrentPostSlug,
56+
ParagraphId = Paragraph.Id,
57+
Start = _selection.StartOffset,
58+
Length = _selection.EndOffset - _selection.StartOffset
59+
};
60+
61+
Registry.Register(annotation, location);
62+
63+
var id = Registry.All.Last().Id;
64+
65+
Paragraph.Annotations.Add(new InlineAnnotation
66+
{
67+
AnnotationId = id,
68+
Start = location.Start,
69+
Length = location.Length
70+
});
71+
72+
_showDialog = false;
73+
_selection = null;
74+
}
75+
76+
77+
private void OpenDialog()
78+
{
79+
_showDialog = true;
80+
}
81+
82+
private void CloseDialog()
83+
{
84+
_showDialog = false;
85+
_selection = null;
86+
}
87+
88+
}

Components/AnnotatedSpan.razor

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<span class="annotated"
2+
@onmouseenter="() => _show = true"
3+
@onmouseleave="() => _show = false">
4+
5+
@ChildContent
6+
7+
@if (_show)
8+
{
9+
<span class="annotation-popup">
10+
<strong>@Annotation.Label</strong>
11+
<div>@Annotation.Body</div>
12+
</span>
13+
}
14+
</span>
15+
16+
@code {
17+
[Parameter, EditorRequired]
18+
public Annotation Annotation { get; set; } = default!;
19+
20+
[Parameter, EditorRequired]
21+
public RenderFragment ChildContent { get; set; } = default!;
22+
23+
private bool _show;
24+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<div class="annotation-dialog-backdrop">
2+
<div class="annotation-dialog">
3+
<h3>Create annotation</h3>
4+
5+
<div class="field">
6+
<label>Label</label>
7+
<input @bind="_draft.Label" />
8+
</div>
9+
10+
<div class="field">
11+
<label>Body</label>
12+
<textarea @bind="_draft.Body"></textarea>
13+
</div>
14+
15+
<div class="actions">
16+
<button @onclick="Confirm">Save</button>
17+
<button @onclick="Cancel">Cancel</button>
18+
</div>
19+
</div>
20+
</div>
21+
22+
@code {
23+
[Parameter, EditorRequired]
24+
public EventCallback<AnnotationDraft> OnConfirm { get; set; }
25+
26+
[Parameter, EditorRequired]
27+
public EventCallback OnCancel { get; set; }
28+
29+
private readonly AnnotationDraft _draft = new();
30+
31+
private async Task Confirm()
32+
{
33+
if (string.IsNullOrWhiteSpace(_draft.Body))
34+
return;
35+
36+
await OnConfirm.InvokeAsync(_draft);
37+
}
38+
39+
private async Task Cancel()
40+
{
41+
await OnCancel.InvokeAsync();
42+
}
43+
}

Components/ParagraphRenderer.razor

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
@using OsayamiBlog.Services
2+
@inject AnnotationRegistry Registry
3+
4+
<p>
5+
@foreach (var segment in Segments)
6+
{
7+
if (segment.Annotation is null)
8+
{
9+
@segment.Text
10+
}
11+
else
12+
{
13+
<AnnotatedSpan Annotation="segment.Annotation">
14+
@segment.Text
15+
</AnnotatedSpan>
16+
}
17+
}
18+
</p>
19+
20+
@code {
21+
[Parameter, EditorRequired]
22+
public Paragraph Paragraph { get; set; } = default!;
23+
24+
private List<Segment> Segments = [];
25+
26+
protected override void OnParametersSet()
27+
{
28+
Segments = BuildSegments();
29+
}
30+
31+
private List<Segment> BuildSegments()
32+
{
33+
var segments = new List<Segment>();
34+
35+
if (Paragraph.Annotations.Count == 0)
36+
{
37+
segments.Add(new Segment { Text = Paragraph.Text });
38+
return segments;
39+
}
40+
41+
var index = 0;
42+
43+
foreach (var ia in Paragraph.Annotations.OrderBy(a => a.Start))
44+
{
45+
var reg = Registry.Get(ia.AnnotationId);
46+
if (reg is null) continue;
47+
48+
if (ia.Start > index)
49+
{
50+
segments.Add(new Segment
51+
{
52+
Text = Paragraph.Text[index..ia.Start]
53+
});
54+
}
55+
56+
segments.Add(new Segment
57+
{
58+
Text = Paragraph.Text.Substring(ia.Start, ia.Length),
59+
Annotation = reg.Annotation
60+
});
61+
62+
index = ia.Start + ia.Length;
63+
}
64+
65+
if (index < Paragraph.Text.Length)
66+
{
67+
segments.Add(new Segment
68+
{
69+
Text = Paragraph.Text[index..]
70+
});
71+
}
72+
73+
return segments;
74+
}
75+
76+
private sealed class Segment
77+
{
78+
public string Text { get; init; } = "";
79+
public Annotation? Annotation { get; init; }
80+
}
81+
}

Data/BlogPosts.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Microsoft.AspNetCore.Components;
2+
using OsayamiBlog.Models;
3+
using OsayamiBlog.Components;
4+
5+
namespace OsayamiBlog.Data;
6+
7+
public static class BlogPosts
8+
{
9+
public static readonly IReadOnlyList<BlogPost> All =
10+
[
11+
new BlogPost
12+
{
13+
Slug = "user-annotations",
14+
Title = "User Annotations",
15+
Published = DateTime.Today,
16+
Paragraphs =
17+
[
18+
new Paragraph
19+
{
20+
Text = "This paragraph can be annotated by selecting text."
21+
}
22+
]
23+
}
24+
25+
];
26+
27+
public static BlogPost? Get(string slug)
28+
=> All.FirstOrDefault(p => p.Slug == slug);
29+
}

Layout/NavMenu.razor

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,18 @@
1818
<NavLink class="nav-link" href="blog">
1919
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Blog
2020
</NavLink>
21+
</div>
22+
<div class="nav-item px-3">
23+
<NavLink class="nav-link" href="annotations">
24+
<span class="bi bi-journal-text-nav-menu" aria-hidden="true"></span> Annotations
25+
</NavLink>
2126
</div>
2227
<div class="nav-item px-3">
2328
<NavLink class="nav-link" href="About">
2429
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> About me
2530
</NavLink>
2631
</div>
32+
2733
</nav>
2834
</div>
2935

Models/Annotation.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace OsayamiBlog.Models;
2+
public sealed class Annotation
3+
{
4+
public string Id { get; init; } = Guid.NewGuid().ToString();
5+
public string Label { get; init; } = "";
6+
public string Body { get; init; } = "";
7+
}

Models/AnnotationDraft.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace OsayamiBlog.Models;
2+
3+
public sealed class AnnotationDraft
4+
{
5+
public string Label { get; set; } = "";
6+
public string Body { get; set; } = "";
7+
}

Models/AnnotationLocation.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace OsayamiBlog.Models;
2+
3+
public sealed class AnnotationLocation
4+
{
5+
public string PostSlug { get; init; } = "";
6+
public string ParagraphId { get; init; } = "";
7+
public int Start { get; init; }
8+
public int Length { get; init; }
9+
}

Models/BlogPost.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace OsayamiBlog.Models;
5+
6+
public sealed class BlogPost
7+
{
8+
public string Slug { get; init; } = "";
9+
public string Title { get; init; } = "";
10+
public DateTime Published { get; init; }
11+
12+
/// <summary>
13+
/// Ordered paragraphs that make up the blog post body.
14+
/// Each paragraph owns its own text and annotations.
15+
/// </summary>
16+
public List<Paragraph> Paragraphs { get; init; } = [];
17+
}

0 commit comments

Comments
 (0)