using Lantean.QBTMud.Interop; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Microsoft.JSInterop; using MudBlazor; using MudBlazor.Utilities; namespace Lantean.QBTMud.Components.UI { // This is a very hacky approach but works for now. // This needs to inherit from MudMenu because MudMenuItem needs a MudMenu passed to it to control the close of the menu when an item is clicked. // MudPopover isn't ideal for this because that is designed to be used relative to an activator which in these cases it isn't. // Ideally this should be changed to use something like the way the DialogService works. // Or - rework this to have a hidden MudMenu and hook into the OpenChanged event to monitor when the MudMenuItem closes it. public partial class ContextMenu : MudComponentBase { private bool _open; private bool _showChildren; private string? _popoverStyle; private string? _id; private double _x; private double _y; private bool _isResized = false; private const double _diff = 64; private string Id { get { _id ??= Guid.NewGuid().ToString(); return _id; } } [Inject] public IJSRuntime JSRuntime { get; set; } = default!; [Inject] public IPopoverService PopoverService { get; set; } = default!; /// /// If true, compact vertical padding will be applied to all menu items. /// [Parameter] [Category(CategoryTypes.Menu.PopupAppearance)] public bool Dense { get; set; } /// /// Set to true if you want to prevent page from scrolling when the menu is open /// [Parameter] [Category(CategoryTypes.Menu.PopupAppearance)] public bool LockScroll { get; set; } /// /// If true, the list menu will be same width as the parent. /// [Parameter] [Category(CategoryTypes.Menu.PopupAppearance)] public bool FullWidth { get; set; } /// /// Sets the max height the menu can have when open. /// [Parameter] [Category(CategoryTypes.Menu.PopupAppearance)] public int? MaxHeight { get; set; } /// /// Set the anchor origin point to determine where the popover will open from. /// [Parameter] [Category(CategoryTypes.Menu.PopupAppearance)] public Origin AnchorOrigin { get; set; } = Origin.TopLeft; /// /// Sets the transform origin point for the popover. /// [Parameter] [Category(CategoryTypes.Menu.PopupAppearance)] public Origin TransformOrigin { get; set; } = Origin.TopLeft; /// /// If true, menu will be disabled. /// [Parameter] [Category(CategoryTypes.Menu.Behavior)] public bool Disabled { get; set; } /// /// Gets or sets whether to show a ripple effect when the user clicks the button. Default is true. /// [Parameter] [Category(CategoryTypes.Menu.Appearance)] public bool Ripple { get; set; } = true; /// /// Determines whether the component has a drop-shadow. Default is true /// [Parameter] [Category(CategoryTypes.Menu.Appearance)] public bool DropShadow { get; set; } = true; /// /// Add menu items here /// [Parameter] [Category(CategoryTypes.Menu.PopupBehavior)] public RenderFragment? ChildContent { get; set; } /// /// Fired when the menu property changes. /// [Parameter] [Category(CategoryTypes.Menu.PopupBehavior)] public EventCallback OpenChanged { get; set; } [Parameter] public int AdjustmentX { get; set; } [Parameter] public int AdjustmentY { get; set; } protected MudMenu? FakeMenu { get; set; } protected void FakeOpenChanged(bool value) { if (!value) { _open = false; } StateHasChanged(); } /// /// Opens the menu. /// /// /// The arguments of the calling mouse/pointer event. /// public async Task OpenMenuAsync(EventArgs args) { if (Disabled) { return; } // long press on iOS triggers selection, so clear it await JSRuntime.ClearSelection(); if (args is not LongPressEventArgs) { _showChildren = true; } _open = true; _isResized = false; StateHasChanged(); var (x, y) = GetPositionFromArgs(args); _x = x; _y = y; SetPopoverStyle(x, y); StateHasChanged(); await OpenChanged.InvokeAsync(_open); // long press on iOS triggers selection, so clear it await JSRuntime.ClearSelection(); if (args is LongPressEventArgs) { await Task.Delay(1000); _showChildren = true; } } /// /// Closes the menu. /// public Task CloseMenuAsync() { _open = false; _popoverStyle = null; StateHasChanged(); return OpenChanged.InvokeAsync(_open); } private void SetPopoverStyle(double x, double y) { _popoverStyle = $"margin-top: {y.ToPx()}; margin-left: {x.ToPx()};"; } /// /// Toggle the visibility of the menu. /// public async Task ToggleMenuAsync(EventArgs args) { if (Disabled) { return; } if (_open) { await CloseMenuAsync(); } else { await OpenMenuAsync(args); } } protected override async Task OnAfterRenderAsync(bool firstRender) { if (!_isResized) { await DeterminePosition(); } } private async Task DeterminePosition() { var mainContentSize = await JSRuntime.GetInnerDimensions(".mud-main-content"); double? contextMenuHeight = null; double? contextMenuWidth = null; var popoverHolder = PopoverService.ActivePopovers.FirstOrDefault(p => p.UserAttributes.ContainsKey("tracker") && (string?)p.UserAttributes["tracker"] == Id); var popoverSize = await JSRuntime.GetBoundingClientRect($"#popovercontent-{popoverHolder?.Id}"); if (popoverSize.Height > 0) { contextMenuHeight = popoverSize.Height; contextMenuWidth = popoverSize.Width; } else { return; } // the bottom position of the popover will be rendered off screen if (_y - _diff + contextMenuHeight.Value >= mainContentSize.Height) { // adjust the top of the context menu var overshoot = Math.Abs(mainContentSize.Height - (_y - _diff + contextMenuHeight.Value)); _y -= overshoot; if (_y - _diff + contextMenuHeight >= mainContentSize.Height) { MaxHeight = (int)(mainContentSize.Height - _y + _diff); } } if (_x + contextMenuWidth.Value > mainContentSize.Width) { var overshoot = Math.Abs(mainContentSize.Width - (_x + contextMenuWidth.Value)); _x -= overshoot; } SetPopoverStyle(_x, _y); _isResized = true; await InvokeAsync(StateHasChanged); } private (double x, double y) GetPositionFromArgs(EventArgs eventArgs) { double x, y; if (eventArgs is MouseEventArgs mouseEventArgs) { x = mouseEventArgs.ClientX; y = mouseEventArgs.ClientY; } else if (eventArgs is LongPressEventArgs longPressEventArgs) { x = longPressEventArgs.ClientX; y = longPressEventArgs.ClientY; } else { throw new NotSupportedException("Invalid eventArgs type."); } return (x + AdjustmentX, y + AdjustmentY); } } }