Skip to content

Instantly share code, notes, and snippets.

@simonrelet
Created November 18, 2019 15:56
Show Gist options
  • Save simonrelet/ffe4d77e43ece0ac8fd97744accc3724 to your computer and use it in GitHub Desktop.
Save simonrelet/ffe4d77e43ece0ac8fd97744accc3724 to your computer and use it in GitHub Desktop.
import throttle from 'lodash.throttle'
const DEFAULT_HISTORY_CAPACITY = 50
const DEFAULT_PUSH_THROTTLE_TIMEOUT = 500
/**
* The object defining the internal state of a History returned by
* `History.getState`.
*
* @template T
* @typedef {object} HistoryState
* @property {number} index The index of the current value.
* @property {T[]} values The values pushed so far.
*/
/**
* The History object itself.
*
* @template T
* @typedef {object} History
*
* @property {function(): HistoryState<T>} getState Get the current state of
* the History.
*
* @property {function(T): void} push Push a value in the History.
* This new value will also become the current one.
* All values that comes after the current index will be lost.
* If the capacity is exceeded the oldest value will be lost.
*
* @property {function(): T} previous Move one value backward and return it.
* If the current value is the first one, this function is a nop.
*
* @property {function(): T} next Move one value forward and return it.
* If the current value is the last one, this function is a nop.
*/
/**
* Create a new History object.
*
* @template T
* @param {T} initialValue The value to use to initialize the History.
* @param {object} options
*
* @param {number} [options.capacity=DEFAULT_HISTORY_CAPACITY] The capacity of
* the History (i.e. the maximum number of values it can keep).
*
* @param {number} [options.pushThrottle=DEFAULT_PUSH_THROTTLE_TIMEOUT] The
* minimum amount of time between to values are pushed.
* The pushed values are throttled to avoid spamming the History.
*
* @returns {History<T>} The History object.
*/
export function createHistory(
initialValue,
{
capacity = DEFAULT_HISTORY_CAPACITY,
pushThrottle = DEFAULT_PUSH_THROTTLE_TIMEOUT,
} = {},
) {
let values = [initialValue]
let index = 0
const push = throttle(value => {
// The previously known futur is now lost.
// We don't keep track of parallel universes.
// GREAT SCOTT!
if (index < values.length - 1) {
values = values.slice(0, index + 1)
}
values = values.concat([value])
index = index + 1
if (values.length > capacity) {
values = values.slice(1)
index = index - 1
}
}, pushThrottle)
function previous() {
// Make sure any previous `push` are ran before.
push.flush()
if (index > 0) {
index = index - 1
}
return values[index]
}
function next() {
// Make sure any previous `push` are ran before.
push.flush()
if (index < values.length - 1) {
index = index + 1
}
return values[index]
}
function getState() {
return { index, values }
}
return { push, previous, next, getState }
}
const MAC =
typeof navigator !== `undefined`
? navigator.appVersion.indexOf('Mac') !== -1
: true
const SHORTCUT_KEY = MAC ? 'metaKey' : 'ctrlKey'
/**
* Test whether a keyboard event is an undo event.
*
* @param {KeyboardEvent} event
* @returns {boolean}
*/
export function isUndoEvent(event) {
// cmd-z / ctrl-z
return (
event[SHORTCUT_KEY] && event.key.toLowerCase() === 'z' && !event.shiftKey
)
}
/**
* Test whether a keyboard event is a redo event.
*
* @param {KeyboardEvent} event
* @returns {boolean}
*/
export function isRedoEvent(event) {
const key = event.key.toLowerCase()
return (
// cmd-shift-z / ctrl-shift-z
(event[SHORTCUT_KEY] && event.shiftKey && key === 'z') ||
// cmd-y / ctrl-y
(event[SHORTCUT_KEY] && key === 'y' && !event.shiftKey)
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment