---
title: "Editor de blocos"
description: "Uma página no estilo Notion — blocos arrastados pelos handles e aninhados arrastando para o lado, um retângulo de seleção que move vários de uma vez, novos blocos arrastados de um menu \"/\" ou de uma barra lateral, cópias com Alt, e Undo para tudo isso."
canonical_url: "https://zizigy.github.io/vue-dnd-kit/pt/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="Arraste ⋮⋮ para mover um bloco, para o lado para aninhá-lo; digite / para novos blocos" name="block-editor" title="Editor de blocos">



</example>

Todo bloco tem um handle (⋮⋮) e um "+" na margem. Digite em qualquer bloco: Enter começa o
próximo, Tab o aninha, "/" abre o menu de novos blocos, e Markdown funciona ("## ", "- ", "<span>



</span>

 ").
Desenhe um retângulo na margem para selecionar vários blocos, depois arraste-os juntos. Expanda o
quadro: em uma tela larga, os blocos a adicionar e o sumário da página ganham barras laterais
próprias.

## O que observar

- **Page.vue**: a página é uma única árvore ordenável, com `placeholder: 'indicator'`: a página
fica parada enquanto você arrasta, e uma linha mostra onde os blocos pousam e em qual nível. A
folha em volta dos blocos é o `dropzone` dela, então um drop abaixo do último bloco pousa no fim.
- **BlockRow.vue**: um bloco. Só o handle arrasta (`handle`), então o texto continua editável e
selecionável. O handle é um botão: um clique seleciona o bloco e abre o menu dele, Space pega o
bloco para o teclado.
- **BlockText.vue** e **Editable.vue**: texto `contenteditable` simples com as teclas de um
editor. O que se digita vai do DOM para o model; o model volta para o DOM só quando outra coisa o
mudou (Undo, uma fusão), então o cursor nunca pula.
- **editor.ts**: o documento e toda edição nele, sem nenhum arrastar e soltar: dividir, fundir,
indentar, duplicar, transformar em outro tipo, e um histórico que desfaz drops e digitação do
mesmo jeito.

## Aninhar arrastando para o 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'` tira o nível só do deslocamento lateral do arraste: passar sobre o meio de um
parágrafo não solta dentro dele, como em um editor de documentos. `canNest` diz quais blocos
aceitam blocos aninhados (parágrafos, listas, to-dos, toggles, citações, callouts; não títulos,
código ou imagens), e quando o bloco de cima recusa, a linha fica no nível mais profundo que passa.
Um toggle fechado (`collapsed`) continua aceitando blocos: uma linha um nível para dentro embaixo
dele os coloca dentro, em primeiro.

Cada linha desenha a linha de inserção a partir do seu `indicator`, do nível em que o drop pousa.
Listas numeradas contam a partir de `items`, então "2." e "a." acompanham cada drop e cada Tab.
`animateChanges: true` faz as mudanças que nenhum arraste faz também deslizarem e esmaecerem: Enter, Tab,
Delete, Undo, um toggle abrindo.

```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` };
});
```

## Selecionar vários

A página é uma área de seleção: um retângulo desenhado a partir das margens seleciona os blocos que
toca. `strategy: 'select'` faz um clique na margem vazia limpar a seleção, e `accept` restringe o
retângulo aos blocos. Um clique em um handle seleciona o bloco dele, Shift-clique uma sequência de
blocos, Ctrl/Cmd-clique mais um; Escape em um texto seleciona o bloco dele. Então:

```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
});
```

Arraste um bloco selecionado e os outros vêm junto: a árvore os move como uma única sequência, cada
um com o que está aninhado nele, na ordem de leitura. Delete os remove, Ctrl/Cmd+D os duplica, e o
menu do handle os transforma em outro tipo. As setas vão de handle em handle, Shift adiciona à
seleção.

## Novos blocos a partir de um menu

O "+" na margem e o "/" em um bloco abrem o mesmo menu. Um clique adiciona o bloco embaixo; cada
opção também é um draggable simples, que o `createItem` da árvore transforma em um novo bloco onde
ele é solto. A barra lateral das telas largas lista as mesmas opções, e ali Space pega uma para o
teclado: uma seta em direção à página a traz para dentro.

```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
});
```

Enquanto uma opção viaja, o menu esmaece para que a página mostre para onde ela vai, e ele fecha
quando o novo bloco pousa:

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

## O que o drop vai fazer

**BlockGhost.vue** é o `overOverlay` da árvore: enquanto um arraste está sobre a página, ele
envolve o overlay do próprio arraste (`<slot />`, o clone dos blocos ou da opção) com uma contagem
e uma nota. `to.parentKey` nomeia o bloco em que o drop entraria, e `mode` diz quando ele 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

A árvore escreve cada drop em `editor.blocks`, um ref gravável. O `onDrop` dela roda logo antes
dessa escrita, então ele registra a página como era, e Undo desfaz o drop. Toda outra edição se
registra do mesmo jeito; as teclas digitadas em um bloco formam um único passo.

```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;
};
```

O histórico guarda cópias dos blocos com os seus ids, então um bloco mantém a sua linha e a sua
seleção através de Undo e Redo. Depois de um drop, um novo bloco recebe o cursor, e um bloco movido
dá o foco ao seu handle: a linha dele se moveu na página, o que tira o foco, e os atalhos continuam
funcionando a partir do handle.

## Teclado e leitores de tela

Escape em um texto seleciona o bloco e foca o handle dele. Dali, Space o pega, as setas movem a
linha (esquerda e direita: um nível para fora ou para dentro), Space o solta; Enter abre o menu
dele, Delete o remove. Os `announcements` do provider falam dos blocos pelo tipo e pelo texto
("Picked up to-do list “Freeze the API”"), e a linha de status da barra superior fala o que os
botões e os atalhos fazem.

Leia mais: [Árvores](https://zizigy.github.io/docs/guides/trees) ·
[Multi-drag e seleção](https://zizigy.github.io/docs/guides/multi-drag#rubber-band-selection) ·
[Draggables simples em uma lista](https://zizigy.github.io/docs/guides/drag-and-drop#plain-draggables-into-a-list) ·
[Alvos que mudam o overlay](https://zizigy.github.io/docs/guides/overlay#targets-that-change-the-overlay) ·
[Quando uma pressão vira arraste](https://zizigy.github.io/docs/guides/provider#when-a-press-becomes-a-drag) ·
[Acessibilidade](https://zizigy.github.io/docs/guides/accessibility)

## Sitemap

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