K
KoreUI

Editor

Form

Texto enriquecido sin dependencias: negrita, títulos, listas, citas, código, alineación, enlaces e imágenes. 6,5 kB gzip sobre contenteditable, con la salida pasada por una lista blanca.

Requiere Livewire. El editor sincroniza su contenido con $wire.$set desde un plugin Alpine (KoreEditor), así que la vista necesita un componente Livewire con una propiedad pública recibiendo el wire:model.

En vivo

Los dos modos, uno debajo del otro. Escribe, pega texto con formato de otra página, y mira lo que llega al servidor en el panel gris: HTML arriba, texto plano abajo.

HTML — el modo por defecto

Prueba a pegar texto con formato desde otra página.

<p>Un párrafo con <strong>negrita</strong>, <em>cursiva</em> y un <a href="https://laravel.com">enlace</a>.</p><ul><li>Una lista</li><li>Con dos puntos</li></ul>

Y así se publica, con <x-kore::prose>

Un párrafo con negrita, cursiva y un enlace.

  • Una lista
  • Con dos puntos

Markdown — lo que se guarda es texto plano

## Markdown

Escribe con la barra o con **atajos**, y lo que se guarda es texto plano.

- Sin marcado del usuario que sanear
- Y legible en cualquier parte
{{-- HTML, el modo por defecto --}}
<x-kore::editor wire:model="cuerpo" label="Cuerpo del artículo" />

{{-- Markdown, con una barra más corta --}}
<x-kore::editor
    wire:model="notas"
    label="Notas"
    markdown
    :toolbar="['bold', 'italic', '|', 'h2', 'h3', '|', 'ul', 'ol', 'quote', '|', 'link']"
/>
{{-- HTML, el modo por defecto --}}
<x-kore::editor wire:model="cuerpo" label="Cuerpo del artículo" />

{{-- Markdown, con una barra más corta --}}
<x-kore::editor
    wire:model="notas"
    label="Notas"
    markdown
    :toolbar="['bold', 'italic', '|', 'h2', 'h3', '|', 'ul', 'ol', 'quote', '|', 'link']"
/>

Lo primero: sanear en el servidor

El editor limpia lo que se escribe y lo que se pega, pero eso pasa en el navegador.

El navegador no es una frontera de seguridad. El valor viaja por wire:model, y cualquiera con las herramientas de desarrollo abiertas puede mandar por ese hilo lo que quiera. Como el texto enriquecido solo se ve enriquecido si se pinta con {!! !!}, guardar sin filtrar es un XSS almacenado.

Por eso el paquete trae la contrapartida en PHP. Admite las mismas etiquetas que el editor, y se le puede dar una lista más corta:

app/Livewire/Articulo.php
use KoreUi\Editor\HtmlSanitizer;

class Articulo extends Component
{
    public string $cuerpo = '';

    public function updatedCuerpo(string $valor): void
    {
        $this->cuerpo = HtmlSanitizer::limpiar($valor);
    }
}

// O con una lista más corta:
HtmlSanitizer::limpiar($html, ['p', 'strong', 'em']);
use KoreUi\Editor\HtmlSanitizer;

class Articulo extends Component
{
    public string $cuerpo = '';

    public function updatedCuerpo(string $valor): void
    {
        $this->cuerpo = HtmlSanitizer::limpiar($valor);
    }
}

// O con una lista más corta:
HtmlSanitizer::limpiar($html, ['p', 'strong', 'em']);

Llamarlo a mano funciona hasta el día en que alguien guarda desde otro sitio —un comando, un import, una API— y se le olvida. Para eso está el cast, que lo hace en el modelo:

app/Models/Articulo.php
use KoreUi\Editor\Casts\SanitizedHtml;

protected function casts(): array
{
    return ['cuerpo' => SanitizedHtml::class];
}
use KoreUi\Editor\Casts\SanitizedHtml;

protected function casts(): array
{
    return ['cuerpo' => SanitizedHtml::class];
}

Para HTML de cualquier otro origen —no el que produce este editor— la respuesta sigue siendo HTMLPurifier.

Markdown

La forma de no tener que acordarse de nada.

Con markdown, lo que se guarda es texto plano: no hay marcado del usuario que sanear, y el problema de arriba desaparece de raíz. Se escribe igual —con la barra o con los atajos— y el editor traduce en los dos sentidos.

&lt;x-kore::editor wire:model="notas" label="Notas" markdown /&gt;
&lt;x-kore::editor wire:model="notas" label="Notas" markdown /&gt;

El botón de subrayado se cae solo en este modo, y no es un olvido: markdown no tiene sintaxis para subrayar. Dejarlo sería prometer un formato que se pierde en cuanto el texto va y vuelve del servidor.

Cómo se publica

No hace falta acordarse ni de sanear ni de los estilos.

&lt;x-kore::prose :html="$articulo-&gt;cuerpo" /&gt;


&lt;x-kore::prose :markdown="$articulo-&gt;cuerpo" /&gt;
&lt;x-kore::prose :html="$articulo-&gt;cuerpo" /&gt;


&lt;x-kore::prose :markdown="$articulo-&gt;cuerpo" /&gt;

prose sanea antes de pintar y aplica los estilos del texto enriquecido, que si no hay que conocer de memoria: los títulos, las listas y los bloques de código solo tenían estilo dentro del propio editor.

Ojo con el slot: por :html y :markdown, prose sanea; por slot pinta tal cual, porque ahí puede haber componentes y sanearlo se llevaría por delante lo que no es suyo. Quien lo use por slot, sanea él.

La barra

Qué botones y en qué orden. Las barras verticales separan grupos.

&lt;x-kore::editor
    wire:model="cuerpo"
    :toolbar="['bold', 'italic', '|', 'h2', 'h3', '|', 'ul', 'ol', '|', 'link']"
/&gt;
&lt;x-kore::editor
    wire:model="cuerpo"
    :toolbar="['bold', 'italic', '|', 'h2', 'h3', '|', 'ul', 'ol', '|', 'link']"
/&gt;

El orden por defecto sale de kore-ui.form.editor.toolbar, así que se puede fijar para toda la aplicación sin repetirlo en cada campo.

Imágenes

Sin uploadProperty no hay botón.

Y es a propósito: el editor no sabe dónde guardar nada, y esa decisión es de la aplicación. Hacen falta las dos props —la propiedad que recibe el archivo y el método que lo guarda y devuelve la URL—.

&lt;x-kore::editor
    wire:model="cuerpo"
    upload-property="imagen"
    upload-method="guardarImagen"
/&gt;
&lt;x-kore::editor
    wire:model="cuerpo"
    upload-property="imagen"
    upload-method="guardarImagen"
/&gt;
En el componente Livewire
use Livewire\WithFileUploads;

public $imagen;

public function guardarImagen(): string
{
    return $this->imagen->store('articulos', 'public');
}
use Livewire\WithFileUploads;

public $imagen;

public function guardarImagen(): string
{
    return $this->imagen->store('articulos', 'public');
}

El tamaño máximo se comprueba en el navegador antes de gastar el viaje, y los tipos admitidos y el límite salen de la configuración.

Props

Prop Tipo Default Descripción
label string|null null Etiqueta del campo
hint string|null null Texto de ayuda
name string|null null Nombre; si falta se deduce del wire:model
error string|null null Error manual
placeholder string|null config Texto mientras está vacío
toolbar array|null config Qué botones y en qué orden; | es un separador
minHeight string 12rem Alto mínimo del área de escritura
maxHeight string|null null Alto máximo; a partir de ahí, scroll
maxlength int|null null Límite de caracteres (cuenta texto, no marcado)
counter bool false Enseña el contador. Con maxlength se activa solo
markdown bool false Guarda markdown en vez de HTML
uploadProperty string|null null Propiedad Livewire que recibe la imagen. Sin ella no hay botón
uploadMethod string|null null Método que guarda la imagen y devuelve la URL
uploadMimes array|null config Tipos de imagen admitidos
uploadMaxSize int|null 2048 Tamaño máximo en KB
debounce int 400 Milisegundos desde la última tecla hasta mandar
disabled bool false Atenuado y sin barra
readonly bool false Se lee y se envía, pero no se edita
required bool false Marca el campo y añade aria-required
showError bool true Pinta los errores de validación. A false calla todos: ni el bag \$errors ni un error escrito a mano
Cargando