npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

state-rewind

v0.0.7

Published

A simple way to manage state with the ability to undo & redo actions & squash down history where needed

Downloads

7

Readme

JS StateRewind Build Status

Simple state management with the ability to undo, redo & squash history.

Want to skip to a full working example? https://jsfiddle.net/p4rebqw2/15/


Install

npm install state-rewind

Usage

Init a state object

const state = new StateRewind;

init with console logs for debug

const state = new StateRewind({ log: true });

Set the state

state.set(5);

or exec to run/execute a function at the same time that can later be undone or redone

state.exec(5, function () { console.log('foward'); }, function () { console.log('backward'); });

State can be anything, strings, numbers, arrays, objects, anything, e.g. (works with set & exec)

state.set({
    x: "test",
    y: [1, 2, 3]
});

Get the current state

state.get();

Undo/rewind state change

state.undo();

& then redo/fast-forward current state

state.redo();

You can also check if undo or redo are available with:

state.canUndo(); // boolean
state.canRedo(); // boolean

Get all recorded state changes

state.getAll();

Callbacks

Optionally listen in for changes to the state:

state.onChange(function () { ... });

Each set/exec can pass a forward and backward callback e.g. to handle the changes visually etc.

You can also set a default function that's given a direction of either forward or backward and the change where you can instruct how it should handle this. e.g.

state.setDefaultForwardBackwardCallback(function (direction, change) {
    if (direction == 'forward') {
        // do/redo it (e.g. on exec() or redo() or initial load() calls)
    } else if (direction == 'backward') {
        // undo it (e.g. on undo() calls)
    }
});

Editing history

Squash history, e.g. to remove duplicates or squash down similar objects such as changes to text, if the same elements text changes multiple times you might want to squash that down to just the latest change

state.squash(function (prev, next) {
    return prev == next;
});
// or for an object
state.squash(function (prev, next) {
    return prev.selector == next.selector && prev.type == next.type;
});

or just run squash against the last set value

state.squashLast(function (prev, next) {
    // same compare function as above
    return prev == next;
});

The squash functions can both also take a 2nd callback to modify the data as it squashes down, e.g. if these are text changes to the same thing, you'd probably want the original "from text", but the latest "to text".

state.squashLast(function (prev, next) {
    return prev.selector == next.selector && prev.type == next.type;
}, function (prev, next) {
    next.change.from = prev.change.from; // keep the initial "from text" as we squash down to the latest "to text"
    return next;
});

Remove specific entries from the history.

state.removeIndex(2); // index => starting at 0

Clear/reset all history.

state.clear();

Starting from stored data

You can load in initial data with the load() command:

state.load(data);

This can be used in combination with setDefaultForwardBackwardCallback to have it auto run the callbacks such as visually changing the page to adapt to the loaded history.

state.setDefaultForwardBackwardCallback(function (direction, change) { ... });

state.load([{ text: "x" }, { text: "y" }], { exec: true });

Initial locked states

You can also start the state with an initial value. By default it starts as undefined but you can set initialState as an option when creating the StateRewind instance. This lets you have a default state available via get() that cannot be undone and will always show at the start of the getAll() array.

See the test.js file for a unit test showing this in use & many other example uses.


Tips

Keyboard shortcuts

You may want to setup keyboard shortcuts for undo & redo.

This could be done like so, for ctrl+z (undo) & ctrl+y (redo)

document.addEventListener('keydown', function (e) {
    if (e.ctrlKey || e.metaKey) { // support either ctrl (win & linux) or cmd (mac)
        if (e.key == 'y' || (e.key == 'Z' && e.shiftKey)) {
            state.redo();
        } else if (e.key == 'z') {
            state.undo();
        }
    }
});

Debounce

Depending on how you're saving data, if it's user based such as on input, you may want to use this with a debounce function to not save constantly, find out more here or here's a package for debounce on npm.

Chaining

Almost all functions are chainable (except get, getAll, canUndo and canRedo).

E.g.

state.set(3).set(5).undo().get()

Change events

You may want to hook into the change event of the state, we expose a onChange() function for this.

This is especially useful for setting up undo and redo buttons, e.g.

<button id="undo-btn" disabled>Undo</button>
<button id="redo-btn" disabled>Redo</button>
let $undoBtn = document.querySelector('#undo-btn');
let $redoBtn = document.querySelector('#redo-btn');
state.onChange(function () {
    $undoBtn.disabled = ! state.canUndo();
    $redoBtn.disabled = ! state.canRedo();
});
$undoBtn.addEventListener('click', function (e) {
    e.preventDefault();
    state.undo();
});
$redoBtn.addEventListener('click', function (e) {
    e.preventDefault();
    state.redo();
});

Example workflow

E.g. here's an example where you could track changes to elements on a page with timestamps:

const state = new StateRewind;

// first change
state.exec({
    timestamp: (new Date).toISOString(),
    selector: '#el span',
    type: 'text',
    change: {
        from: 'ABC',
        to: 'XYZ'
    }
}, function () { console.log('A'); }, function () { console.log('B'); });

// another change
state.exec({
    timestamp: (new Date).toISOString(),
    selector: '.another strong',
    type: 'text',
    change: {
        from: 'Hello world',
        to: 'one thing'
    }
}, function () { console.log('C'); }, function () { console.log('D'); });

state.undo();

state.redo();

state.exec({
    timestamp: (new Date).toISOString(),
    selector: '.another strong',
    type: 'text',
    change: {
        from: 'one thing',
        to: 'Something else!'
    }
}, function () { console.log('E'); }, function () { console.log('F'); });

state.squashLast(function (prev, next) {
    return prev.selector == next.selector && prev.type == next.type;
});

state.getAll(); // should only show first and last due to the `squashLast` replacing the last 2

Local development

Run tests

npm test

Publish new version

Update the package.json's version, commit, push and then:

npm publish

Contributing

Please run the tests locally and add new tests for new features/options added. Thank you.