Skip to main content
You are viewing Pre-Alpha documentation.
Vuetify0 Logo
Theme
Mode
Accessibility
Vuetify

Sign in

Sign in with your preferred provider to access your account.

createTimeline

A bounded undo/redo system that manages a fixed-size timeline of registered items with automatic overflow handling and history management.


Advanced98.8% coverageFeb 4, 2026

Usage

The createTimeline composable extends createRegistry to provide undo/redo functionality with a bounded history. When the timeline reaches its size limit, older items are moved to an overflow buffer, allowing you to undo back to them while maintaining a fixed active timeline size.

ts
import { createTimeline } from '@vuetify/v0'

const timeline = createTimeline({ size: 10 })

// Register actions
timeline.register({ id: 'action-1', value: 'Created document' })
timeline.register({ id: 'action-2', value: 'Added title' })
timeline.register({ id: 'action-3', value: 'Added paragraph' })

console.log(timeline.size) // 3

// Undo the last action
timeline.undo()
console.log(timeline.size) // 2

// Redo the undone action
timeline.redo()
console.log(timeline.size) // 3

Examples

0 strokes
<script setup lang="ts">
  import { createTimeline, useProxyRegistry } from '@vuetify/v0'
  import { onMounted, shallowRef, toRef, useTemplateRef, watchEffect } from 'vue'

  type Point = { x: number, y: number }
  type Stroke = Point[]

  const timeline = createTimeline<{ id: string, value: Stroke }>({ size: 20, events: true })
  const proxy = useProxyRegistry(timeline)

  const canvasRef = useTemplateRef<HTMLCanvasElement>('canvas')
  const colorRef = useTemplateRef<HTMLDivElement>('color')
  const isDrawing = shallowRef(false)
  const currentStroke = shallowRef<Stroke>([])
  const strokeColor = shallowRef('#6366f1')

  const redoStackSize = shallowRef(0)
  const canUndo = toRef(() => proxy.size > 0)
  const canRedo = toRef(() => redoStackSize.value > 0)

  onMounted(() => {
    if (colorRef.value) {
      strokeColor.value = getComputedStyle(colorRef.value).backgroundColor
    }
  })

  watchEffect(() => {
    // Track proxy.values for reactive re-rendering
    const _ = proxy.values
    render()
  })

  function getPos (e: MouseEvent | TouchEvent): Point {
    const el = canvasRef.value!
    const rect = el.getBoundingClientRect()
    const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX
    const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY
    const scaleX = el.width / rect.width
    const scaleY = el.height / rect.height
    return {
      x: (clientX - rect.left) * scaleX,
      y: (clientY - rect.top) * scaleY,
    }
  }

  function startDraw (e: MouseEvent | TouchEvent) {
    isDrawing.value = true
    currentStroke.value = [getPos(e)]
  }

  function draw (e: MouseEvent | TouchEvent) {
    if (!isDrawing.value) return
    currentStroke.value.push(getPos(e))
    render()
  }

  function endDraw () {
    if (!isDrawing.value || currentStroke.value.length < 2) {
      isDrawing.value = false
      return
    }
    timeline.register({ value: [...currentStroke.value] })
    redoStackSize.value = 0
    currentStroke.value = []
    isDrawing.value = false
  }

  function render () {
    const ctx = canvasRef.value?.getContext('2d')
    if (!ctx) return
    ctx.clearRect(0, 0, 600, 400)
    ctx.strokeStyle = strokeColor.value
    ctx.lineWidth = 4
    ctx.lineCap = 'round'
    ctx.lineJoin = 'round'

    for (const { value: stroke } of proxy.values) {
      if (stroke.length < 2) continue
      ctx.beginPath()
      ctx.moveTo(stroke[0].x, stroke[0].y)
      for (let i = 1; i < stroke.length; i++) {
        ctx.lineTo(stroke[i].x, stroke[i].y)
      }
      ctx.stroke()
    }

    if (currentStroke.value.length > 1) {
      ctx.beginPath()
      ctx.moveTo(currentStroke.value[0].x, currentStroke.value[0].y)
      for (let i = 1; i < currentStroke.value.length; i++) {
        ctx.lineTo(currentStroke.value[i].x, currentStroke.value[i].y)
      }
      ctx.stroke()
    }
  }

  function undo () {
    if (proxy.size === 0) return
    timeline.undo()
    redoStackSize.value++
  }

  function redo () {
    if (redoStackSize.value === 0) return
    timeline.redo()
    redoStackSize.value--
  }

  function clear () {
    timeline.clear()
    redoStackSize.value = 0
  }
</script>

<template>
  <div class="flex flex-col gap-3">
    <!-- Hidden element to get primary color -->
    <div ref="color" class="hidden bg-primary" />

    <div class="flex gap-2 items-center">
      <button
        class="px-3 py-1 border border-divider rounded transition-opacity"
        :class="canUndo ? 'hover:bg-surface-tint' : 'opacity-40 cursor-not-allowed'"
        :disabled="!canUndo"
        @click="undo"
      >
        Undo
      </button>
      <button
        class="px-3 py-1 border border-divider rounded transition-opacity"
        :class="canRedo ? 'hover:bg-surface-tint' : 'opacity-40 cursor-not-allowed'"
        :disabled="!canRedo"
        @click="redo"
      >
        Redo
      </button>
      <button
        class="px-3 py-1 border border-divider rounded hover:bg-surface-tint"
        @click="clear"
      >
        Clear
      </button>
      <span class="ml-auto text-sm text-on-surface opacity-60">
        {{ proxy.size }} strokes
      </span>
    </div>

    <canvas
      ref="canvas"
      class="w-full aspect-[3/2] border border-divider rounded cursor-crosshair bg-surface touch-none"
      height="400"
      width="600"
      @mousedown="startDraw"
      @mouseleave="endDraw"
      @mousemove="draw"
      @mouseup="endDraw"
      @touchend="endDraw"
      @touchmove.prevent="draw"
      @touchstart.prevent="startDraw"
    />

    <div class="flex gap-1 h-2">
      <div
        v-for="i in 20"
        :key="i"
        class="flex-1 rounded-sm transition-all duration-200"
        :class="i <= proxy.size ? 'bg-primary' : 'bg-divider'"
      />
    </div>
  </div>
</template>

Reactivity

createTimeline uses minimal reactivity like its parent createRegistry. History state is managed internally without reactive primitives.

Tip

Need reactive history? Wrap with useProxyRegistry(timeline) for full template reactivity on the active timeline.

Architecture

createTimeline extends createRegistry with bounded history and overflow management:

Timeline Hierarchy

Use controls to zoom and pan. Click outside or press Escape to close.

Timeline Hierarchy

API Reference

The following API details are for the createTimeline composable.

Functions

createTimeline

(_options?: TimelineOptions) => E

Creates a new timeline instance.

createTimelineContext

(_options?: TimelineContextOptions) => ContextTrinity<E>

Creates a new timeline context.

useTimeline

(namespace?: string) => E

Returns the current timeline instance.

Options

events

boolean

Enable event emission for registry operations

Default: false

reactive

boolean

Enable reactive behavior for registry operations

Default: false

size

number

The maximum size of the timeline.

Default: 10

Properties

collection

ReadonlyMap<ID, Z>

The collection of tickets in the registry

size

number

The number of tickets in the registry

Methods

clear

() => void

Clear the entire registry

has

(id: ID) => boolean

Check if a ticket exists by ID

keys

() => readonly ID[]

Get all registered IDs

browse

(value: Z["value"]) => ID[] | undefined

Browse for an ID(s) by value

lookup

(index: number) => ID | undefined

lookup a ticket by index number

get

(id: ID) => Z | undefined

Get a ticket by ID

upsert

(id: ID, ticket?: Partial<Z>) => Z

Update or insert a ticket by ID

values

() => readonly Z[]

Get all values of registered tickets

entries

() => readonly [ID, Z][]

Get all entries of registered tickets

register

(ticket?: Partial<Z>) => Z

Register a new ticket

unregister

(id: ID) => void

Unregister a ticket by ID

reindex

() => void

Reset the index directory and update all tickets

seek

(direction?: "first" | "last", from?: number, predicate?: (ticket) => boolean) => Z | undefined

Seek for a ticket based on direction and optional predicate

on

<K extends Extensible<RegistryEventName>>(event: K, cb: EventHandler<Z, K>) => void

Listen for registry events

off

<K extends Extensible<RegistryEventName>>(event: K, cb: EventHandler<Z, K>) => void

Stop listening for registry events

emit

<K extends Extensible<RegistryEventName>>(event: K, data: EventPayload<Z, K>) => void

Emit an event with data

dispose

() => void

Clears the registry and removes all listeners

onboard

(registrations: Partial<Z>[]) => Z[]

Onboard multiple tickets at once

offboard

(ids: ID[]) => void

Offboard multiple tickets at once

batch

<R>(fn: () => R) => R

Execute operations in a batch, deferring cache invalidation and event emission until complete

undo

() => Z | undefined

Removes the last registered ticket and stores it for redo

redo

() => Z | undefined

Restores the last undone ticket

Was this page helpful?

© 2016-1970 Vuetify, LLC
Ctrl+/