Editor
FormTexto 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:
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:
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.
<x-kore::editor wire:model="notas" label="Notas" markdown /><x-kore::editor wire:model="notas" label="Notas" markdown />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.
<x-kore::prose :html="$articulo->cuerpo" />
<x-kore::prose :markdown="$articulo->cuerpo" /><x-kore::prose :html="$articulo->cuerpo" />
<x-kore::prose :markdown="$articulo->cuerpo" />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.
<x-kore::editor
wire:model="cuerpo"
:toolbar="['bold', 'italic', '|', 'h2', 'h3', '|', 'ul', 'ol', '|', 'link']"
/><x-kore::editor
wire:model="cuerpo"
:toolbar="['bold', 'italic', '|', 'h2', 'h3', '|', 'ul', 'ol', '|', 'link']"
/>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—.
<x-kore::editor
wire:model="cuerpo"
upload-property="imagen"
upload-method="guardarImagen"
/><x-kore::editor
wire:model="cuerpo"
upload-property="imagen"
upload-method="guardarImagen"
/>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 |