Adicionar uma galeria de imagens a um editor de texto rico no Blazor

A caixa de diálogo Insert Gallery do exemplo em Blazor listando as pastas blog, products e team e quatro fotos enviadas, uma delas selecionada

O RichTextEditor é um editor JavaScript; por isso, em um Blazor Web App ele é criado e removido por JS interop, e a galeria conversa com endpoints HTTP comuns ao lado dos seus componentes. Esta página mostra as partes do Blazor: onde ficam os scripts e as opções do editor, quando o componente pode criar o editor, como removê-lo depois e como as rotas da galeria ficam em Program.cs. Ela usa a renderização Interactive Server do .NET 8.

Scripts e opções no App.razor

Os arquivos do editor, o rte-files.js compartilhado e três pequenas funções de interop ficam no <head> de App.razor, que é renderizado uma única vez e não por um componente interativo:

Razor · Components/App.razor
<link rel="stylesheet" href="richtexteditor/rte_theme_default.css" />
<script src="richtexteditor/rte.js"></script>
<script src="richtexteditor/plugins/all_plugins.js"></script>
<script src="rte-files.js"></script>
<script>
    // Called from Home.razor. The editor options stay in JavaScript, where they can also hold
    // functions (custom buttons, event handlers) that cannot be sent from .NET as JSON.
    window.rteBlazor = {
        create: function (host) {
            host.rte = new RichTextEditor(host, {
                toolbar: "full",
                galleryEndpoint: "/rte/gallery",
                maxUploadFileSize: 4000000 // same limit as the server, checked before the upload starts
            });
        },
        // Returned as bytes: .NET reads them as a stream, so documents over Blazor's 32 KB
        // SignalR message limit still arrive.
        getHtml: function (host) {
            return new TextEncoder().encode(host.rte.getHTMLCode());
        },
        destroy: function (host) {
            if (!host || !host.rte) return;
            // editor.destroy() exists only in newer editor builds
            if (typeof host.rte.destroy === "function") host.rte.destroy();
            host.rte = null;
            host.innerHTML = "";
        }
    };
</script>

As opções do editor, com galleryEndpoint, são escritas em JavaScript em vez de enviadas pelo C#. As opções podem conter funções, como botões personalizados e handlers de eventos, e elas não podem vir do .NET como JSON. getHtml é explicado em Ler o HTML do editor no Blazor Server.

Criar o editor

Razor · Components/Pages/Home.razor
protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (!firstRender) return;
    await JS.InvokeVoidAsync("rteBlazor.create", host);
    created = true;
}

A página tem @rendermode InteractiveServer, e o Blazor a pré-renderiza: a primeira renderização acontece no servidor, sem nenhum navegador conectado. Nesse momento, o JS interop não é possível. OnAfterRenderAsync só é executado quando o circuito está conectado, e firstRender garante que o editor seja criado uma vez, e não a cada nova renderização. O elemento @ref="host" na marcação é uma <div> vazia; o Blazor nunca escreve dentro dela, então não disputa o DOM com o editor.

Remover o editor

Razor · Components/Pages/Home.razor
public async ValueTask DisposeAsync()
{
    if (!created) return;
    try { await JS.InvokeVoidAsync("rteBlazor.destroy", host); }
    catch (JSDisconnectedException) { } // the browser tab is already gone
}
  • A flag created. Sem ela, descartar a cópia pré-renderizada do componente lançava InvalidOperationException: JavaScript interop calls cannot be issued at this time.
  • JSDisconnectedException ocorre quando o usuário fecha a aba: o componente é descartado depois que o navegador já se foi.
  • destroy() pode não existir. O build do editor publicado que testamos não tem editor.destroy(); por isso, rteBlazor.destroy só o chama se ele existir e, em seguida, esvazia o elemento host.

As rotas da galeria

C# · Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
var app = builder.Build();

app.UseStaticFiles();
app.UseRteStaticFiles();   // /uploads, /richtexteditor, /rte-files.js
app.UseAntiforgery();      // Blazor's own forms; the /rte endpoints opt out (RteEndpoints.cs)

app.MapRteFiles();         // GET+POST /rte/gallery, POST /rte/upload
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();

app.Run();

MapRteFiles() adiciona GET e POST /rte/gallery e POST /rte/upload como endpoints de Minimal API. A listagem da galeria é o mesmo JSON do guia de galeria do ASP.NET Core. As rotas são mapeadas antes de MapRazorComponents, e as rotas POST chamam .DisableAntiforgery(), porque, caso contrário, o app.UseAntiforgery() do template recusaria as requisições do editor. Enviar imagens de um editor de texto rico no Blazor explica por que os arquivos não passam pelo circuito.

O que a caixa de diálogo faz hoje

A caixa de diálogo publicada lista "Accepted formats: JPG, JPEG, JFIF, PNG, GIF, WEBP, BMP, AVIF". O servidor aceita cinco deles; assim, um .bmp é enviado, recusado com um 400, e a caixa de diálogo mostra a própria mensagem "Upload failed for pic.bmp." em vez da mensagem do servidor.

Teste

Execute o projeto de exemplo deste guia com dotnet run (SDK do .NET 8), abra http://localhost:5304, aguarde a barra de ferramentas e clique em Insert Gallery. Você verá as pastas e as imagens da pasta uploads/ do app, como na imagem no topo da página. A listagem também responde a partir de um shell:

Shell
curl "http://localhost:5304/rte/gallery?folder=team"
{"currentFolder":"team","currentFolderDisplay":"/team","parentFolder":"","folders":[],"images":[]}

O mesmo guia para outras plataformas

JavaScript · Next.js · PHP · ASP.NET Core · Razor Pages · MVC 5 · Web Forms

Leia a seguir