返回

MudBlazor MudText 组件回车键处理:两种方案详解

javascript

MudText 组件回车键处理方案

在开发自定义文本编辑器时,常常需要对回车键进行特殊处理。当使用 MudBlazor 的 MudText 组件时,如果想在用户按下回车键后根据当前行内容执行不同操作,例如:内容不为空时创建新行,为空时弹出菜单,就需要自定义事件逻辑。下面我们探讨一些具体的实现方案。

方案一: 基于 KeyDown 事件的逻辑控制

MudTextField 组件提供了 OnKeyDown 事件,可以捕获用户的键盘输入。通过判断按键和当前行内容,我们可以实现差异化的处理。

实现思路

  1. 捕获回车键:OnKeyDown 事件中,检查 e.Key 是否为 “Enter”。
  2. 获取当前行内容: 需要通过某种方式获取光标所在行的内容,这可能涉及文本区域的内容和光标位置计算,MudText 本身没有提供直接的接口,可能需要用javascript来辅助获取当前行。
  3. 根据内容执行操作:
    • 如果当前行有文本,允许组件的默认行为,即插入换行符。
    • 如果当前行为空,则设置 _preventDefaulttrue 以阻止插入换行,同时弹出菜单。

代码示例 (简化)

@using Microsoft.AspNetCore.Components.Web;
@inject IJSRuntime JSRuntime

<MudTextField T="string" Label="Multiline"  Variant="Variant.Text" Text="@sampleText" Lines="50" AutoGrow="true" Immediate="true" OnKeyDown="@HandleKeyDown" InputId="editor"/>

<div id="menuContainer" style="display: @(_showMenu ? "block" : "none"); position:absolute;">
        <MudMenu>
          <MudMenuItem>Option 1</MudMenuItem>
          <MudMenuItem>Option 2</MudMenuItem>
      </MudMenu>
</div>

@code {
    string sampleText = "wwswswLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
    bool _preventDefault;
    bool _showMenu = false;
    double _menuTop,_menuLeft;

     protected async override Task OnAfterRenderAsync(bool firstRender)
     {
          if (firstRender)
          {
             await JSRuntime.InvokeVoidAsync("addClickToHide", "menuContainer",DotNetObjectReference.Create(this));
          }
    }

     [JSInvokable]
     public void CloseMenu(){
            _showMenu = false;
             StateHasChanged();
     }


    protected async Task HandleKeyDown(KeyboardEventArgs e)
    {
        if (e.Key == "Enter")
        {
          var pos =  await JSRuntime.InvokeAsync<dynamic>("getCurrentCursorPosition", "editor");
          var textLines=sampleText.Split('\n');

           var rowCount = pos.row;
           if(rowCount>=textLines.Length){
                 rowCount=textLines.Length-1;
           }

         if (textLines[rowCount].Trim().Length>0)
           {
                _preventDefault = false;
            }
            else
            {
                 _preventDefault = true;
                _menuTop=pos.y;
                _menuLeft = pos.x;
                _showMenu=true;
                StateHasChanged();
                 await JSRuntime.InvokeVoidAsync("setPosition", "menuContainer",_menuTop,_menuLeft);
           }


        }else {
              _preventDefault=false;
        }
    }
}

javascript 函数:

function getCurrentCursorPosition(inputId) {
    var textarea = document.getElementById(inputId);
     var rowCount = 0, start = 0 , y=0,x=0;
        if(textarea){
           start= textarea.selectionStart;
             let text=textarea.value;
            var subText = text.substring(0,start)
              y =   textarea.getBoundingClientRect().y +textarea.offsetHeight;
            x = textarea.getBoundingClientRect().x ;
             rowCount = subText.split('\n').length-1
        }

  return { row:rowCount,x:x,y:y }
}


function setPosition(containerId, top, left){
        const menu =document.getElementById(containerId)

      menu.style.top =`${top}px`;
     menu.style.left=`${left}px`;
 }


  function addClickToHide(containerId, instance){
       document.addEventListener('click', (event)=>{
                const menu= document.getElementById(containerId)
               if(menu){
                     if(!menu.contains(event.target)){
                         instance.invokeMethodAsync('CloseMenu')
                 }
               }


            })
    }


操作步骤:

  1. 添加一个 MudTextField 组件并设置相关的属性。
  2. 实现 HandleKeyDown 事件。
  3. 通过js 获取鼠标当前行及位置信息,判断是否显示菜单
  4. 如果需要则调用 StateHasChanged 更新UI。

注意事项:

  • 需要仔细计算光标所在行的内容。文本区域的多行输入与单行输入有所区别,并且文本内容本身在持续变化,对性能要求较高时可能要考虑防抖节流。
  • js 相关方法依赖 DOM ,确保js在浏览器端已经初始化

方案二: @bind-Value 的数据驱动处理

可以使用双向绑定的 @bind-Value ,监测输入数据的变化,然后在 oninput 或者失去焦点时进行检测。该方案相较于方案一,更容易理解,在复杂场景下控制力更强,并且不依赖Javascript,性能更好,并且可以支持@ref 来做其他处理,可以用来扩展更多的文本框控制操作。

实现思路

  1. 使用 @bind-Value 将文本内容与一个变量绑定。
  2. 实现 oninput 事件, 获取事件 e.Value 以及 上一个绑定的变量,通过分析两个字符串来判断是否回车
  3. 根据文本内容判断执行相应逻辑

代码示例

<MudTextField T="string"  Label="Multiline"  Variant="Variant.Text" @bind-Value="_textContent" Lines="50" AutoGrow="true" Immediate="true" oninput="@HandleInput" @ref="@_mudTextRef" />
    <div id="menuContainer" style="display: @(_showMenu ? "block" : "none"); position:absolute; background:wheat">
           <MudMenu>
              <MudMenuItem>Option 1</MudMenuItem>
               <MudMenuItem>Option 2</MudMenuItem>
          </MudMenu>
</div>

@code{

    string _textContent = "wwswswLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
     string? _previousText = null;
    bool _showMenu=false;
      double _menuTop,_menuLeft;

       MudTextField<string> _mudTextRef;



      protected override void OnInitialized()
      {

        _previousText = _textContent;
    }


     protected  void HandleInput(ChangeEventArgs e){

         var text =(string)e.Value;

            if ( text != null &&_previousText != null )
              {

               var index=  text.IndexOf( "\n", StringComparison.Ordinal);

                 if ( index!=-1&& text.Length==_previousText.Length+1){


                       var lines=_previousText.Split("\n");

                            var rowIndex = text.Substring(0,index).Split("\n").Length;


                        if(rowIndex>lines.Length){
                            rowIndex = lines.Length;
                         }
                        var content=  lines[rowIndex-1];

                       if(string.IsNullOrWhiteSpace(content)){
                           //  避免重新渲染时,将内容添加到input
                           var newValue = _previousText.Substring(0,_previousText.Length-1);
                         _mudTextRef.Text = newValue;
                            _textContent=newValue;
                           _previousText=_textContent;

                           _showMenu=true;
                             StateHasChanged();
                          var textRect=  _mudTextRef.Element.BoundingClientRect;
                              _menuLeft = textRect.X ;
                              _menuTop=textRect.Y+textRect.Height;

                           //TODO JS Set positon
                           } else{
                            _textContent=text;
                            _previousText =text;

                       }

                     return;


               }

                    _textContent=text;
                     _previousText =text;


           }


      _showMenu=false;
        StateHasChanged();


     }

}

操作步骤:

  1. MudTextField@bind-Value 与字符串变量绑定。
  2. 实现 oninput 方法来判断是否回车。
  3. 设置MudTextField@ref,并根据位置判断是否弹出菜单。

注意事项:

  • 该方案性能更好,完全由 c# 代码实现,没有 JSRuntime 的额外调用,更可靠。

总结

上面介绍了处理 MudText 组件回车键的两种方法。实际应用中可根据需要选用合适的方案。第一种方案更贴近初始的需求,在细节方面更精确,可以控制preventDefault 的动作,第二种方案理解更容易,更推荐使用第二种。

注意考虑组件的重绘及性能损耗,在需求复杂的时候尽量使用状态驱动。另外当需求增加例如支持自定义字符,自动补全,关键词,错误标记等需求时候第二种方式的可维护性更高,方便进行扩展,并且配合js 来达到某些特定效果。