Skip to content

Commit 7eee423

Browse files
feat: new composable useTimeline (#40)
* feat: initial implementation * feat: added redo/undo * feat: added test * refactor: changed to use ref * feat: refactor to not use ref * chore: minor cleanup * fix * chore: un-needed values * chore: cleanup * fix: overwrite register, and fix typing * fix: dont use options, use onboard * refactor: changed useHistory to useTimeline * fix: pr comments * test: formatting * lint: unused variable * fix: pass options to registry * chore(useTimeline): code style tweaks and add docs page --------- Co-authored-by: John Leider <john@vuetifyjs.com>
1 parent 41982ad commit 7eee423

File tree

7 files changed

+170
-1
lines changed

7 files changed

+170
-1
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
meta:
3+
title: useTimeline
4+
description: Composable for managing a timeline of registered items with undo/redo capabilities.
5+
keywords: timeline, undo, redo, registry, composable
6+
features:
7+
category: Registration
8+
label: 'E: useTimeline'
9+
github: /composables/useTimeline/
10+
---
11+
12+
# useTimeline
13+
14+
The `useTimeline` composable provides a way to manage a timeline of registered items, allowing you to undo and redo changes. It builds upon the `useRegistry` composable to maintain a history of actions.
15+
16+
## Usage
17+
18+
Documentation coming soon.

apps/docs/src/stores/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const useAppStore = defineStore('app', {
5050
name: 'Registration',
5151
children: [
5252
{ name: 'useRegistry', to: '/composables/registration/use-registry' },
53+
{ name: 'useTimeline', to: '/composables/registration/use-timeline' },
5354
{ name: 'useTokens', to: '/composables/registration/use-tokens' },
5455
],
5556
},

apps/docs/src/typed-router.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ declare module 'vue-router/auto-routes' {
4343
'/composables/plugins/use-storage': RouteRecordInfo<'/composables/plugins/use-storage', '/composables/plugins/use-storage', Record<never, never>, Record<never, never>>,
4444
'/composables/plugins/use-theme': RouteRecordInfo<'/composables/plugins/use-theme', '/composables/plugins/use-theme', Record<never, never>, Record<never, never>>,
4545
'/composables/registration/use-registry': RouteRecordInfo<'/composables/registration/use-registry', '/composables/registration/use-registry', Record<never, never>, Record<never, never>>,
46+
'/composables/registration/use-timeline': RouteRecordInfo<'/composables/registration/use-timeline', '/composables/registration/use-timeline', Record<never, never>, Record<never, never>>,
4647
'/composables/registration/use-tokens': RouteRecordInfo<'/composables/registration/use-tokens', '/composables/registration/use-tokens', Record<never, never>, Record<never, never>>,
4748
'/composables/selection/use-filter': RouteRecordInfo<'/composables/selection/use-filter', '/composables/selection/use-filter', Record<never, never>, Record<never, never>>,
4849
'/composables/selection/use-group': RouteRecordInfo<'/composables/selection/use-group', '/composables/selection/use-group', Record<never, never>, Record<never, never>>,
@@ -180,6 +181,10 @@ declare module 'vue-router/auto-routes' {
180181
routes: '/composables/registration/use-registry'
181182
views: never
182183
}
184+
'src/pages/composables/registration/use-timeline.md': {
185+
routes: '/composables/registration/use-timeline'
186+
views: never
187+
}
183188
'src/pages/composables/registration/use-tokens.md': {
184189
routes: '/composables/registration/use-tokens'
185190
views: never

packages/0/src/composables/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ export * from './useSingle'
2020
export * from './useStep'
2121
export * from './useStorage'
2222
export * from './useTheme'
23+
export * from './useTimeline'
2324
export * from './useTokens'
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { useTimeline } from './index'
3+
4+
describe('useTimeline', () => {
5+
it('should register to the timeline buffer', () => {
6+
const timeline = useTimeline({ size: 15 })
7+
for (let i = 0; i <= 20; i++) {
8+
timeline.register({
9+
id: `item${i}`,
10+
value: i,
11+
})
12+
}
13+
14+
expect(timeline.values()).toHaveLength(15)
15+
expect(timeline.values()[0]!.value).toEqual(6)
16+
expect(timeline.values()[14]!.value).toEqual(20)
17+
})
18+
19+
it('should undo the last action', () => {
20+
const timeline = useTimeline({ size: 5 })
21+
for (let i = 0; i < 7; i++) {
22+
timeline.register({
23+
id: `item${i}`,
24+
value: i,
25+
})
26+
}
27+
28+
expect(timeline.values()[4]!.value).toEqual(6)
29+
timeline.undo()
30+
expect(timeline.values()[0]!.value).toEqual(1)
31+
timeline.undo()
32+
expect(timeline.values()[0]!.value).toEqual(0)
33+
})
34+
35+
it('should redo the last action', () => {
36+
const timeline = useTimeline({ size: 5 })
37+
for (let i = 0; i < 5; i++) {
38+
timeline.register({
39+
id: `item${i}`,
40+
value: i,
41+
})
42+
}
43+
44+
timeline.undo()
45+
expect(timeline.values()[3]!.value).toEqual(3)
46+
timeline.redo()
47+
expect(timeline.values()[4]!.value).toEqual(4)
48+
})
49+
})
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Composables
2+
import { useRegistry } from '#v0/composables/useRegistry'
3+
4+
// Types
5+
import type { RegistryContext, RegistryOptions, RegistryTicket } from '#v0/composables/useRegistry'
6+
7+
export interface TimelineContext<Z extends TimelineTicket> extends RegistryContext<Z> {
8+
/* Unapplies the last registered ticket */
9+
undo: () => void
10+
/* Reapplies the last undone ticket */
11+
redo: () => void
12+
}
13+
14+
export interface TimelineTicket extends RegistryTicket {}
15+
16+
export interface TimelineOptions extends RegistryOptions {
17+
size?: number
18+
}
19+
20+
/**
21+
* Creates a registry with timeline capabilities (undo/redo)
22+
*
23+
* @param _options Optional configuration for timeline
24+
* @template Z The type of ticket to be stored in the timeline
25+
* @template E The type of the timeline context
26+
* @returns The timeline context object
27+
*
28+
* @see https://0.vuetifyjs.com/composables/registration/use-timeline
29+
*/
30+
export function useTimeline<
31+
Z extends TimelineTicket = TimelineTicket,
32+
E extends TimelineContext<Z> = TimelineContext<Z>,
33+
> (_options: TimelineOptions = {}) {
34+
const { size = 10, ...options } = _options
35+
const registry = useRegistry<Z, E>(options)
36+
37+
const undoTimeline: Z[] = []
38+
const redoTimeline: Z[] = []
39+
40+
function register (item: Partial<Z>) {
41+
if (registry.size < size) return registry.register({ ...item })
42+
43+
const id = registry.lookup(0)!
44+
const removing = registry.get(id)!
45+
46+
if (redoTimeline.length === size) redoTimeline.shift()
47+
redoTimeline.push(removing)
48+
49+
registry.unregister(id)
50+
51+
const ticket = registry.register({ ...item })
52+
registry.reindex()
53+
54+
return ticket
55+
}
56+
57+
function redo () {
58+
if (undoTimeline.length === 0) return
59+
60+
registry.register(undoTimeline.pop())
61+
registry.reindex()
62+
}
63+
64+
function undo () {
65+
const id = registry.lookup(registry.size - 1)
66+
if (!id) return
67+
68+
undoTimeline.push(registry.get(id)!)
69+
70+
registry.unregister(id!)
71+
72+
restore()
73+
}
74+
75+
function restore () {
76+
const value = redoTimeline.pop()
77+
const restored = value ? [value, ...registry.values()] : [...registry.values()]
78+
79+
registry.clear()
80+
registry.onboard(restored)
81+
registry.reindex()
82+
}
83+
84+
return {
85+
...registry,
86+
register,
87+
undo,
88+
redo,
89+
} as E
90+
}

playground/src/composables.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ declare global {
130130
const useFilter: typeof import('../../packages/0/src/composables/useFilter/index')['useFilter']
131131
const useForm: typeof import('../../packages/0/src/composables/useForm/index')['useForm']
132132
const useGroup: typeof import('../../packages/0/src/composables/useGroup/index')['useGroup']
133+
const useHistory: typeof import('../../packages/0/src/composables/useTimeline/index')['useHistory']
133134
const useHydration: typeof import('../../packages/0/src/composables/useHydration/index')['useHydration']
134135
const useHydrationContext: typeof import('../../packages/0/src/composables/useHydration/index')['useHydrationContext']
135136
const useId: typeof import('vue')['useId']
@@ -206,6 +207,9 @@ declare global {
206207
export type { GroupTicket, GroupContext, GroupOptions } from '../../packages/0/src/composables/useGroup/index'
207208
import('../../packages/0/src/composables/useGroup/index')
208209
// @ts-ignore
210+
export type { HistoryOptions, HistoryContext, HistoryTicket } from '../../packages/0/src/composables/useTimeline/index'
211+
import('../../packages/0/src/composables/useTimeline/index')
212+
// @ts-ignore
209213
export type { HydrationContext } from '../../packages/0/src/composables/useHydration/index'
210214
import('../../packages/0/src/composables/useHydration/index')
211215
// @ts-ignore
@@ -387,6 +391,7 @@ declare module 'vue' {
387391
readonly useFilter: UnwrapRef<typeof import('../../packages/0/src/composables/useFilter/index')['useFilter']>
388392
readonly useForm: UnwrapRef<typeof import('../../packages/0/src/composables/useForm/index')['useForm']>
389393
readonly useGroup: UnwrapRef<typeof import('../../packages/0/src/composables/useGroup/index')['useGroup']>
394+
readonly useHistory: UnwrapRef<typeof import('../../packages/0/src/composables/useTimeline/index')['useHistory']>
390395
readonly useHydration: UnwrapRef<typeof import('../../packages/0/src/composables/useHydration/index')['useHydration']>
391396
readonly useHydrationContext: UnwrapRef<typeof import('../../packages/0/src/composables/useHydration/index')['useHydrationContext']>
392397
readonly useId: UnwrapRef<typeof import('vue')['useId']>
@@ -417,4 +422,4 @@ declare module 'vue' {
417422
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
418423
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
419424
}
420-
}
425+
}

0 commit comments

Comments
 (0)