Skip to content

Instantly share code, notes, and snippets.

@brunocalou
Created August 17, 2022 14:59
Show Gist options
  • Save brunocalou/b4999f9bbd942317257b689f835b3b83 to your computer and use it in GitHub Desktop.
Save brunocalou/b4999f9bbd942317257b689f835b3b83 to your computer and use it in GitHub Desktop.
Undo and Redo features in Typescript
type Timeline<State> = {
past: State[];
present: State;
future: State[];
};
export class Undoable<State> {
timeline: Timeline<State>;
constructor(initialValue: State) {
this.timeline = {
past: [],
present: initialValue,
future: []
};
}
get state() {
return this.timeline.present;
}
add(state: State) {
const { past, present } = this.timeline;
this.timeline = {
past: [...past, present],
present: state,
future: []
};
}
undo() {
if (!this.canUndo()) {
return;
}
const { past, present, future } = this.timeline;
this.timeline = {
past: past.slice(0, past.length - 1),
present: past[past.length - 1],
future: [present, ...future]
};
}
redo() {
if (!this.canRedo()) {
return;
}
const { past, present, future } = this.timeline;
this.timeline = {
past: [...past, present],
present: future[0],
future: future.slice(1)
};
}
replace(state: State) {
const { past, future } = this.timeline;
this.timeline = {
past,
present: state,
future
};
}
reset() {
const { past, present } = this.timeline;
this.timeline = {
past: [],
present: past[0] ?? present,
future: []
};
}
canUndo() {
return this.timeline.past.length > 0;
}
canRedo() {
return this.timeline.future.length > 0;
}
}
export const isIOS = (): boolean => {
return /(iPhone|iPod|iPad)/i.test(navigator.platform);
};
export const isMacLike = (): boolean => {
return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
};
type Callback = () => void;
type ShortcutsTags = 'INPUT' | 'SELECT' | 'TEXTAREA';
const elementTagsToIgnoreEvents: ShortcutsTags[] = ['INPUT', 'TEXTAREA', 'SELECT'];
export class UndoableKeyboardListener {
onUndo: Callback;
onRedo: Callback;
constructor(onUndo: Callback, onRedo: Callback) {
this.onUndo = onUndo;
this.onRedo = onRedo;
}
listen() {
if (document) {
document?.addEventListener('keydown', this.listener);
}
}
dispose() {
if (document) {
document?.removeEventListener('keydown', this.listener);
}
}
private listener = (event: KeyboardEvent) => {
if (
elementTagsToIgnoreEvents.includes((event.target as HTMLElement).tagName as ShortcutsTags)
) {
return;
}
const key = event.key.toLocaleLowerCase();
if (isMacLike() || isIOS()) {
if (event.metaKey && key === 'z') {
if (event.shiftKey) {
this.onRedo();
} else {
this.onUndo();
}
}
} else {
if (event.ctrlKey && key === 'z') {
if (event.shiftKey) {
this.onRedo();
} else {
this.onUndo();
}
}
}
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment