---
title: "ページビルダー"
description: "ドラッグ＆ドロップで組み立てるランディングページ。ページの上で本物の姿に変わるパレットのセクション、列の間を移る要素、同じデータを使うレイヤーツリー、その場での編集、デバイス幅、元に戻す操作。"
canonical_url: "https://zizigy.github.io/vue-dnd-kit/ja/docs/examples/page-builder"
---

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

<example height="45rem" hint="セクションや要素をページにドラッグしてみましょう。テキストをダブルクリックすると編集できます" name="page-builder" title="ページビルダー">



</example>

フレームを全画面に展開してみましょう：パレットが左に、レイヤーが右に固定され、ページはその間を占めます。幅が狭いと、両者はトップバーから開くドロワーを共有します。

## 注目ポイント

- **Canvas.vue**：ページは `placeholder: 'indicator'` を使ったセクションの並べ替え可能なリストです。セクションが運ばれている間もページは動かず、2 つのセクションの間のラインが行き先を示します：枠ごとに 500px のセクションをスライドさせて道を空けると、ページ全体が揺れてしまうからです。`dropzone` はドロップをキャンバス全体に任せるので、ページの横や下で離したセクションも着地します。
- **ColumnList.vue**：すべてのセクションのすべての列が、グループ `blocks` のリストです。要素は列やセクションの間を移動し、その場で並べ替わります：ドラッグ中、ページには結果が表示されます。各列はそれぞれ独立したコンポーネントで、列は別のリストのアイテムの中にあるリストです。
- **Palette.vue**：セクションのテンプレートは、`pull: 'clone'`、`put: false`、`sort: false` を指定したページのグループのリストです：コピーを送り出し、何も受け取りません。その下の要素（**ElementChip.vue**）は通常のドラッグ可能な要素で、列が `createItem` でブロックを作ります。
- **Layers.vue**：同じデータを使った、並べ替え可能なツリーとしての同じページ。そこで動かした行はページ上でも動き、その逆も同じです。
- **builder.ts**：ページ、選択、デバイス、履歴。ドロップはリストを通じてページに書き込み、ツールバーのボタンとショートカットはここでページを変更します。各変更は元に戻す操作の 1 ステップです。

## 手に持ったセクションは本物のセクション

**SectionGhost.vue** は、ページのセクションとパレットのテンプレートの両方のオーバーレイです。ページの上では（`to.list` がページのリスト）、セクションそのものをページの幅で描画し、ポインターの下で縮小して表示します。ドロップが決まると（`status: 'dropping'`）、ボックスがセクションの位置へ飛ぶ間に倍率が 1 に戻り、ぴったりその位置に着地します。ドロップはそのトランジションを待つので、最後に何かが飛ぶことはありません。

```vue [SectionGhost.vue]
<script setup lang="ts">
const props = defineProps<{ items: DragItem[]; status: DnDStatus; to: DropDestination | null }>();
const section = computed(() => props.items[0]!.item as Section);
const fromPalette = computed(() => !find(section.value.id));
const onPage = computed(() => props.to?.list === PAGE);

const look = computed(() => {
  if (fromPalette.value && !onPage.value) return 'card'; // still the palette's card
  return props.status === 'dropping' ? 'landing' : 'ghost';
});
</script>

<template>
  <DragPreview v-if="look === 'card'" :id="items[0]!.id" :style="{ width: '100%', height: '100%' }" />
  <div v-else class="replica" :class="look" :style="{ width: `${pageWidth}px`, '--shrink': shrink }">
    <SectionView :section="section" />
  </div>
</template>

<style scoped>
.replica { transform-origin: 0 0; scale: var(--shrink); transition: scale 0.25s ease; }
.replica.landing { scale: 1; }
</style>
```

ページから離れている間、テンプレートはパレットにあったときの姿のままです：`DragPreview` がそのカードのライブラリによるコピーを表示します。ページ上のセクションの位置が枠です：ドラッグ中は薄く表示されてそこで待ち、ドロップすると新しい位置はオーバーレイが着地するまで空のままです。

```css [PageSection.vue]
.page-section[data-dnd-placeholder] { opacity: 0; }
html[data-dnd-dragging] .page-section[data-dnd-placeholder] { opacity: 0.35; }
```

各セクションは自分の `indicator` からラインを描き、ドロップで何が起きるかを伝えます：*Move Features here*、テンプレートなら *Add Pricing here*。

## 要素：入るセクションの色で

ブロックは本体全体でドラッグします。セクションは、本体がクリックやドラッグをするコンテンツなので、タグのグリップでドラッグします。ブロックとチップのオーバーレイである **BlockGhost.vue** は、入る先の列を `to` から読み取り、そのセクションの色を取り込みます：暗いフッターの上のボタンは、離す前に明るくなります。ボックスはそこでのブロックの枠のサイズを持ち（`overlayFit: 'slot'`、デフォルト）、ブロックはドロップ後と同じようにその中でレイアウトされます。

```ts [BlockGhost.vue]
// The column it would go into (`to.list` is the column's id): its section's tone.
const look = computed(() => {
  const at = props.to && find(String(props.to.list));
  return at ? { ...TONES[SECTIONS[at.section.kind].tone], ...typeScale(pageWidth.value) } : null;
});
```

パレットのチップは、自分の種類を運ぶ通常のドラッグ可能な要素です。各列はそこからブロックを作り、新しいブロックは、離すか別の場所へ移るまで、列自身のテンプレートで描画されてその列に収まります。

```ts [ColumnList.vue]
makeSortableList(list, {
  id: props.column.id,
  items: blocks,
  group: 'blocks',
  overlay: BlockGhost,
  createItem: (active) => {
    const kind = (active.data as ChipData | undefined)?.kind;
    return kind ? createBlock(BLOCKS[kind].spec) : undefined;
  },
});
```

`blocks` は `toRef` ではなく、`props.column.blocks` に対する書き込み可能な `computed` です：元に戻す操作はページに新しいオブジェクトを入れるので、列はそれに追従する必要があります。

## テンプレート：コピーを出し、何も受け取らない

```ts [Palette.vue]
makeSortableList(list, {
  id: 'templates',
  items: templates,
  group: { name: 'sections', pull: 'clone', put: false },
  sort: false,
  clone: (template) => createSection(template.kind), // new ids all the way down
  overlay: SectionGhost,
});
```

テンプレートは本物のセクションなので、カードはそれを（縮小して）表示し、オーバーレイもそれを描画できます。カードのクリック、または <kbd value="Enter">



</kbd>

 で、選択中のセクションの後にセクションを追加します：ドラッグをまったくしなくてもページを組み立てられます。

## レイヤー：ツリーとしての同じページ

```ts [Layers.vue]
makeSortableTree(tree, {
  id: LAYERS,
  items: sections as Ref<LayerNode[]>,
  // Keys of their own: the canvas's lists already drag these ids.
  getKey: (node) => `layer-${node.id}`,
  getChildren: (node) => (isSection(node) ? node.columns : isColumn(node) ? node.blocks : undefined),
  setChildren: (node, children) => { … },
  // Blocks go into columns, nothing into blocks, sections stay at the top.
  canNest: (node, parent) => levelOf(node) === levelOf(parent) + 1,
  maxDepth: 2,
  placeholder: 'indicator',
  // A tree takes any row at its top level: a block let go there flies back.
  onBeforeDrop: (event) => (event.to.parentKey !== null || event.items.every(isSection) ? 'accept' : 'reject'),
});
```

キーはプロバイダー全体で共有されるので、行は独自のキーを持ちます。列も行で、`disabled` です：ブロックを受け入れ、その場に留まります。ブロックが入れない場所（2 つのセクションの間）では、ラインが赤くなります。行にホバーするとページ上のそのノードが輪郭で示され、ページ上でノードを選択すると、ここでそのセクションが開いて行が見える位置までスクロールします。

## その場での編集とキーボード

ブロックはどこからでもドラッグできるので、テキストはダブルクリックするまで編集できません：ダブルクリックすると `contenteditable` になり、`canDrag` がドラッグを断るので、押下でキャレットが置かれます。**EditableText.vue** はテキストが編集されていない間だけモデルから DOM に書き込むので、キャレットが飛ぶことはなく、元に戻す操作の結果も表示されます。

プロバイダーは <kbd value="Space">



</kbd>

 をドラッグ用に取っておき、:kbd{value="Enter"} はエディターに任せます。

```vue [index.vue]
<DnDProvider
  :keyboard="{ keys: { start: ['Space'], drop: ['Space', 'Enter'], cancel: ['Escape'] } }"
  :announcements="announcements"
  :screen-reader-instructions="instructions"
>
```

Tab でブロックに移ります：:kbd{value="Space"} で持ち上げ、矢印キーで列の中を通って次の列へと動かします。:kbd{value="Enter"} で編集します。セクションも同じようにグリップで動かせるほか、ツールバーの矢印でも動かせます。:kbd{value="Ctrl"} <kbd value="Z">



</kbd>

 で元に戻し、:kbd{value="Delete"} で削除し、:kbd{value="Ctrl"} <kbd value="D">



</kbd>

 で複製します。スクリーンリーダーには、id ではなく「the Hero section, position 3 of 7 on the page」と聞こえます。

## 元に戻す操作と、ドラッグ以外の変更

どのドロップも履歴の 1 ステップです：リストがページを書き込んだ後で、モニターがページを保存します。

```ts [Builder.vue]
useDnDMonitor({ onDragEnd: () => builder.commit() });
```

元に戻す操作はスナップショットを戻します。リストは `animateChanges` がオンなので、スナップショットで動いたものはそこへスライドし、ツールバーで上下に動かしたセクションも同じです。追加されたセクションはフェードインし、削除されたセクションはフェードアウトします。

さらに読む：[並べ替え可能なリスト](https://zizigy.github.io/docs/guides/sortable-lists#an-insertion-line) · [アイテムを共有するリスト](https://zizigy.github.io/docs/guides/shared-lists) · [ツリー](https://zizigy.github.io/docs/guides/trees) · [オーバーレイ](https://zizigy.github.io/docs/guides/overlay) · [アニメーション](https://zizigy.github.io/docs/guides/animations) · [アクセシビリティ](https://zizigy.github.io/docs/guides/accessibility)

## Sitemap

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