---
title: "Editor de bloques"
description: "Una página al estilo de Notion — bloques que se arrastran por sus asas y se anidan de lado, un rectángulo de selección que mueve varios a la vez, bloques nuevos sacados de un menú «/» o de una barra lateral, copias con Alt, y Undo para todo."
canonical_url: "https://zizigy.github.io/vue-dnd-kit/es/docs/examples/block-editor"
---

> Index of every page on this site: [llms.txt](https://zizigy.github.io/vue-dnd-kit/llms.txt)

<example height="40rem" hint="Arrastra ⋮⋮ para mover un bloque, de lado para anidarlo; escribe / para bloques nuevos" name="block-editor" title="Editor de bloques">



</example>

Cada bloque tiene un asa (⋮⋮) y un «+» en el margen. Escribe en cualquier bloque: Enter empieza el
siguiente, Tab lo anida, «/» abre el menú de bloques nuevos, y el Markdown funciona ("## ", "- ",
"<span>



</span>

 "). Dibuja un rectángulo en el margen para seleccionar varios bloques y arrástralos juntos.
Amplía el marco: en una pantalla ancha, los bloques que añadir y el esquema de la página tienen
barras laterales propias.

## Qué observar

- **Page.vue**: la página es un solo árbol ordenable, con `placeholder: 'indicator'`: la página
se queda quieta mientras arrastras, y una línea muestra dónde aterrizan los bloques y a qué
nivel. La hoja alrededor de los bloques es su `dropzone`, así que un drop debajo del último
bloque aterriza al final.
- **BlockRow.vue**: un bloque. Solo su asa lo arrastra (`handle`), así que el texto sigue siendo
editable y seleccionable. El asa es un botón: un clic selecciona el bloque y abre su menú,
Espacio agarra el bloque para el teclado.
- **BlockText.vue** y **Editable.vue**: texto `contenteditable` simple con las teclas de un
editor. Lo que se escribe va del DOM al modelo; el modelo vuelve al DOM solo cuando otra cosa lo
ha cambiado (Undo, una fusión), así que el cursor nunca salta.
- **editor.ts**: el documento y cada edición, sin nada de arrastrar y soltar: dividir, fusionar,
sangrar, duplicar, convertir, y un historial que deshace tanto drops como escritura.

## Anidar arrastrando de lado

```ts [Page.vue]
const { items } = makeSortableTree(root, {
  id: 'page',
  items: editor.blocks,
  group: 'blocks',
  placeholder: 'indicator',
  nesting: 'offset',        // sideways: every 24px is a level
  indentation: INDENT,
  maxDepth: MAX_DEPTH,
  canNest: (_block, parent) => !!KINDS[parent.kind].nests,
  collapsed: editor.collapsed,
  clone: copyBlock,         // Alt-drag copies a block with what is nested in it
  createItem: (active) => createBlock((active.data as { kind: BlockKind }).kind),
  animateChanges: true,
  overOverlay: BlockGhost,
  dropzone: sheet,
  onDrop, // see Undo, below
});
```

`nesting: 'offset'` toma el nivel solo del desplazamiento lateral del arrastre: pasar por encima del
centro de un párrafo no suelta dentro de él, como en un editor de documentos. `canNest` dice qué
bloques aceptan bloques anidados (párrafos, listas, to-dos, toggles, citas, avisos; no títulos,
código ni imágenes), y cuando el bloque de arriba rechaza, la línea se queda en el nivel más
profundo que pasa. Un toggle cerrado (`collapsed`) sigue aceptando bloques: una línea un nivel más
adentro bajo él los mete dentro, los primeros.

Cada fila dibuja la línea a partir de su `indicator`, del nivel en el que aterriza el drop. Las
listas numeradas cuentan desde `items`, así que "2." y "a." siguen cada drop y cada Tab.
`animateChanges: true` hace que también se deslicen y se desvanezcan los cambios que no hace ningún
arrastre: Enter, Tab, Supr, Undo, un toggle que se abre.

```ts [BlockRow.vue]
const { indicator, selected } = makeSortableItem(el, {
  id: () => props.node.key,
  handle: '.handle',
  // A mouse press on a handle drags at once; after 4px instead, a click stays a click.
  activation: { mouse: { distance: 4 } },
  // Enter opens the block's menu; Space picks the block up.
  canDrag: (event) => !('key' in event && event.key === 'Enter'),
});

const line = computed(() => {
  const at = indicator.value;
  if (!at || at.side === 'inside') return null;
  return { side: at.side, left: `${((at.depth ?? 0) - props.node.depth) * props.indent}px` };
});
```

## Seleccionar varios

La página es un área de selección: un rectángulo dibujado desde sus márgenes selecciona los bloques
que toca. `strategy: 'select'` hace que un clic en el margen vacío borre la selección, y `accept`
limita el rectángulo a los bloques. Un clic en un asa selecciona su bloque, Mayús-clic un tramo de
bloques, Ctrl/Cmd-clic uno más; Escape en un texto selecciona su bloque. Luego:

```ts [Page.vue]
const { isSelecting, style } = makeSelectionArea(scroller, {
  strategy: 'select',
  accept: 'blocks',                     // the rows' type: their group's name
  disabled: () => menu.value !== null,  // a press closes the menu first
});
```

Arrastra un bloque seleccionado y los demás lo acompañan: el árbol los mueve como un solo tramo,
cada uno con lo que tiene anidado, en orden de lectura. Supr los borra, Ctrl/Cmd+D los duplica, y
el menú del asa los convierte en otro tipo. Las flechas van de asa en asa, Mayús añade a la
selección.

## Bloques nuevos desde un menú

«+» en el margen y «/» en un bloque abren el mismo menú. Un clic añade el bloque debajo; cada
opción también es un elemento arrastrable simple, que el `createItem` del árbol convierte en un
bloque nuevo donde se suelta. La barra lateral de las pantallas anchas lista las mismas opciones,
y ahí Espacio agarra una para el teclado: una flecha hacia la página la mete en ella.

```ts [InsertOption.vue]
makeDraggable(el, {
  id: () => `new-${props.kind}${props.option ? '-option' : ''}`,
  type: 'new-block',
  data: () => ({ kind: props.kind }),
  attributes: () => !props.option, // in the menu, an option of its listbox
});
```

Mientras una opción viaja, el menú se desvanece para que la página muestre adónde va, y se cierra
cuando el bloque nuevo ha aterrizado:

```ts [Page.vue]
useDnDMonitor({
  onDragStart: ({ active }) => (lifted.value = !!popover.value?.contains(active.element)),
  onDragSettle: () => {
    if (!lifted.value) return;
    lifted.value = false;
    menu.value = null;
  },
});
```

## Lo que hará el drop

**BlockGhost.vue** es el `overOverlay` del árbol: mientras un arrastre está sobre la página,
envuelve el overlay propio del arrastre (`<slot />`, el clon de los bloques o de la opción) con un
contador y una nota. `to.parentKey` nombra el bloque en el que entraría el drop, y `mode` dice
cuándo copia.

```vue [BlockGhost.vue]
<script setup lang="ts">
const props = defineProps<{ items: DragItem[]; mode: DragMode; to: DropDestination | null }>();

// A new block: a kind dragged from the menu or the sidebar, by its `type`.
const adding = computed(() => props.items[0]?.type.includes('new-block'));

const note = computed(() => {
  const verb = props.mode === 'copy' ? 'Copy' : adding.value ? 'Add' : null;
  const parent = props.to?.parentKey == null ? undefined : editor.get(String(props.to.parentKey));
  if (!parent) return verb; // a plain move at the page's top level needs no words
  return `${verb ? `${verb} into` : 'Into'} “${parent.text}”`; // "Copy into “Open questions”"
});
</script>

<template>
  <div class="ghost">
    <slot />
    <span v-if="items.length > 1" class="count">{{ items.length }}</span>
    <span v-if="note" class="note">{{ note }}</span>
  </div>
</template>
```

## Undo

El árbol escribe cada drop en `editor.blocks`, una ref modificable. Su `onDrop` se ejecuta justo
antes de esa escritura, así que guarda la página tal como estaba, y Undo deshace el drop. Todas
las demás ediciones se guardan de la misma forma; las pulsaciones escritas en un mismo bloque
forman un solo paso.

```ts [Page.vue]
onDrop: (event) => {
  editor.record(); // the library writes the drop after this: Undo takes it back
  const first = event.items[0]!.id;
  if (event.mode === 'insert') editor.focus(first, 'start');
  else editor.focusHandle(first);
},
```

```ts [editor.ts]
/** Called before every change: the state it changes goes on the undo stack. */
const record = () => {
  past.value = [...past.value.slice(-99), capture()];
  future.value = [];
  typing = null;
};
```

El historial guarda copias de los bloques con sus ids, así que un bloque conserva su fila y su
selección a través de Undo y Redo. Tras un drop, un bloque nuevo se queda con el cursor, y uno
movido le da el foco a su asa: su fila se movió en la página, lo que hace perder el foco, y los
atajos siguen funcionando desde el asa.

## Teclado y lectores de pantalla

Escape en un texto selecciona el bloque y pone el foco en su asa. Desde ahí Espacio lo agarra, las
flechas mueven la línea (izquierda y derecha: un nivel hacia fuera o hacia dentro), Espacio lo
suelta; Enter abre su menú, Supr lo borra. Los `announcements` del provider nombran los bloques
por su tipo y su texto ("Picked up to-do list “Freeze the API”"), y la línea de estado de la barra
superior dice lo que hacen los botones y los atajos.

Más información: [Árboles](https://zizigy.github.io/docs/guides/trees) ·
[Multi-drag y selección](https://zizigy.github.io/docs/guides/multi-drag#rubber-band-selection) ·
[Elementos arrastrables simples en una lista](https://zizigy.github.io/docs/guides/drag-and-drop#plain-draggables-into-a-list) ·
[Destinos que cambian el overlay](https://zizigy.github.io/docs/guides/overlay#targets-that-change-the-overlay) ·
[Cuándo una pulsación se convierte en arrastre](https://zizigy.github.io/docs/guides/provider#when-a-press-becomes-a-drag) ·
[Accesibilidad](https://zizigy.github.io/docs/guides/accessibility)

## Sitemap

See the full [sitemap](https://zizigy.github.io/vue-dnd-kit/sitemap.md) for all pages.
