feat: implement dynamic absolute positioning for the AI selection toolbar via JS to handle scroll and boundary collisions

This commit is contained in:
2026-06-02 20:25:50 +02:00
parent 5daab6ebfd
commit 2ef8dd4066
3 changed files with 129 additions and 36 deletions
@@ -4,10 +4,11 @@
@inject KnowledgeCoordinator Coordinator
@inject IReaderInteractionService InteractionService
@inject IQuizStateService QuizService
@inject IJSRuntime JS
@if (IsVisible)
{
<div class="selection-ai-panel @(PositionBelow ? "below" : "")" style="@PanelStyle">
<div class="selection-ai-panel @(_positionBelow ? "below" : "")" style="@_style">
<button class="toolbar-btn primary @(IsLoadingSummary ? "loading" : "") @(IsAnyLoading ? "disabled" : "")"
disabled="@IsAnyLoading"
@onclick="RequestSummaryAsync">
@@ -50,22 +51,53 @@
private bool IsLoadingSummary = false;
private bool IsLoadingQuiz = false;
private bool IsAnyLoading => IsLoadingSummary || IsLoadingQuiz;
private bool PositionBelow => Coordinates != null && Coordinates.Top < 250;
private string _style = "visibility: hidden; opacity: 0; pointer-events: none;";
private bool _positionBelow = false;
private SelectionCoordinates? _lastCoordinates;
protected override void OnParametersSet()
{
Console.WriteLine($"[SelectionAiPanel] Parameters set. SelectedText: {SelectedText.Length} chars, Coordinates: {Coordinates?.Top}, PositionBelow: {PositionBelow}");
Console.WriteLine($"[SelectionAiPanel] Parameters set. SelectedText: {SelectedText.Length} chars, Coordinates: {Coordinates?.Top}");
if (Coordinates != _lastCoordinates)
{
_lastCoordinates = Coordinates;
_style = "visibility: hidden; opacity: 0; pointer-events: none;";
_positionBelow = false;
}
// Reset loading states when parameters change
IsLoadingSummary = false;
IsLoadingQuiz = false;
}
private string PanelStyle => Coordinates != null
? string.Create(System.Globalization.CultureInfo.InvariantCulture,
$"top: {(PositionBelow ? Coordinates.Bottom + 16 : Coordinates.Top - 16):F1}px !important; " +
$"left: {Math.Max(140.0, Math.Min(Coordinates.ViewportWidth - 140.0, Coordinates.Left + Coordinates.Width / 2.0)):F1}px !important; " +
$"transform: translate(-50%, {(PositionBelow ? "0" : "-100%")}) !important;")
: "";
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (IsVisible && _style.Contains("visibility: hidden"))
{
try
{
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js");
var result = await module.InvokeAsync<PositionResult>("positionToolbar");
if (result != null)
{
_style = string.Create(System.Globalization.CultureInfo.InvariantCulture,
$"left: {result.Left:F1}px !important; " +
$"top: {result.Top:F1}px !important; " +
$"visibility: visible !important; " +
$"opacity: 1 !important; " +
$"pointer-events: auto !important;");
_positionBelow = result.Below;
StateHasChanged();
}
}
catch (Exception ex)
{
Console.WriteLine($"[SelectionAiPanel] Error positioning toolbar: {ex.Message}");
}
}
}
private async Task RequestSummaryAsync()
{
@@ -131,5 +163,13 @@
{
await InteractionService.NotifyTextSelected(string.Empty, string.Empty, null!);
}
private class PositionResult
{
public double Left { get; set; }
public double Top { get; set; }
public bool Below { get; set; }
}
}