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

@kvndy/undo-manager

v4.0.13

Published

Undo and redo management for Preact Signals

Downloads

448

Readme

UndoManager

This package provides undo and redo management using Preact Signals. Its innovation is in having effectively two separate data models. Changes to one class, the Undoable, automatically register with an undo stack. Changes to a second class, the Preservable, do not themselves result in a new addition to the undo stack, but as their name would suggest are preserved when a change to an Undoable is made.

Undoables are meant to provide navigation of user data through history in a document-based architecture for web-based apps. Preservables are meant to restore presentational state and visual appearance. In other words, they take the user to where they were when they made the change. The canonical example is preserving expanded/collapsed state of a tree view with disclosure triangles.

An aspect of Preservables which may not be immediately intuitive is their value as restored by navigating the undo stack depends on the direction traveled. The value of a Preservable when reaching a certain state of an Undoable via undo may be different when reaching the very same state of an Undoable via redo. This is accomplished by capturing the value of all Preservables both before and after a change to an Undoable.

When undoing, the user typically wants appearances to be as they were right before they made a change. When redoing, the user typically wants appearances to be as they were right after a change. Without this behavior it would be impossible to see the location of any changes made because navigation would take them somewhere else every time.

If two consecutive undos drastically change the layout of an app making a change difficult to spot, when immediately followed by a redo appearances would align with user expectation. Toggling undo state back and forth to attract the eye to the location of change naturally occurs to the user, and is an inherent feature which does not need to be documented.

This project uses a technique first developed for Objective-C located at https://github.com/kevindoughty/cletustheslackjawedoutlineview.

Installation

npm install @kvndy/undo-manager

API

import { UndoManager } from "@kvndy/undo-manager"; // Javascript
//import { UndoManager, Undoable, Preservable, Localizer } from "@kvndy/undo-manager"; // Typescript
//const { UndoManager } = require("@kvndy/undo-manager"); // Node

The single export is the UndoManager object constructor which exposes primitives to be used for managing the undo stack.

new UndoManager(undoLocalizer, redoLocalizer, maxCount)

Creates a new UndoManager object for managing an undo stack. Its first two parameters are both optional functions meant to generate strings to be used as tooltips or menu items describing what specific change an undo or redo would produce. Each function is in turn passed a single argument of the developer’s choosing.

undoLocalizer is a function that generates the undoDescription for a given change to an Undoable. It has one parameter, the description from an Undoable or group, and should return a string or null.

redoLocalizer is a function that generates the redoDescription for a given change to an Undoable. It has one parameter, the description from an Undoable or group, and should return a string or null.

maxCount is a positive integer that determines the size of the undo stack. The default is Infinity.

const undoLocalizer = (description) => {
	return "Undo " + description;
}
const redoLocalizer = (description) => {
	return "Redo " + description;
}
const { undoable, preservable, group, undo, redo, canUndo, canRedo, undoDescription, redoDescription } = new UndoManager(undoLocalizer, redoLocalizer);

undoable(value, description, coalescing)

Creates an Undoable object which is meant to be used in place of a Signal. It privately maintains a Signal to hold its value and provide timely UI updates. It exposes a similar API as a Signal, with value getter and setter accessors.

value is passed along to its Signal upon creation.

description is an optional object which is passed to the undoLocalizer and redoLocalizer functions to generate an undoDescription and redoDescription. Pass null or undefined to bypass for no description. There is an alternative method for more dynamic descriptions using group.

coalescing is an optional object with a default value of false but is not limited to booleans. When true, multiple successive changes to an Undoable only register as a single change. When an object, referential equality determines if changes can also coalesce with a group using the same object.

const setting = undoable(0, "change setting", true);
setting.value = 1; // registers for undo

preservable(value, interrupting)

Creates a Preservable object which is also meant to be used in place of a Signal, privately maintains one of its own, and exposes a similar API as a Signal through value getter and setter accessors.

value is passed along to its Signal upon creation.

interrupting is an optional boolean that specifies if changes inhibit coalescing when not called from within an enclosing group.

const appearance = preservable(0, true);
appearance.value = 2; // does not register for undo
setting.value = 3; // previous appearance value of 2 is captured as both its before state and after state
appearance.value = 4; // not captured as the after state of the previous change to setting

group(callback, description, coalescing)

Makes use of the Signals batch function which permits multiple signal writes into one update. A change to a Preservable is considered made after any change to an Undoable regardless of call order. The description and coalescing key from the outer group are used.

callback is the function which gets passed to a Signals batch call.

description is an optional object similar to the second parameter of undoable and is not limited to strings. The undoLocalizer or redoLocalizer functions can be written to handle an array or other object for more precise and dynamic descriptions of a change. Pass null or undefined to bypass for no description.

coalescing is an optional object similar to the third parameter of undoable and is not limited to booleans. If true, the description is used as a unique key to determine if changes should be coalesced. Otherwise if the argument is not null, undefined, or false it is used as the unique key.

group( () => {
	setting.value = 5; // registers for undo
	appearance.value = 6; // properly registers as the after change value
}, "change setting and more", true); // does not coalesce with previous change
group( () => {
	setting.value = 7; // registers for undo
	appearance.value = 8; // properly registers as the after change value
}, "change setting and more", true); // does coalesce with previous change

It is a commonly held belief that parameters should not come after a function, but rather before for readability. Not adhering to this was a concious choice as the second and third parameter are optional.

undo()

Navigates to the previous state.

undo();
assert.equal(setting.value, 3); // both grouped changes were coalesced and now undone
assert.equal(appearance.value, 4); // this was the value before those changes were made
undo();
assert.equal(setting.value, 1);
assert.equal(appearance.value, 2);

redo()

Navigates to the next state.

redo();
assert.equal(setting.value, 3); // arriving at same value from a different direction
assert.equal(appearance.value, 2); // as commented above, value did not change

canUndo

A Signals computed whose value getter returns a boolean that provides if undo is possible.

assert.equal(canUndo.value, true);

canRedo

A Signals computed whose value getter returns a boolean that provides if redo is possible.

assert.equal(canRedo.value, true);

undoDescription

A Signals computed whose value getter returns the result of the undoLocalizer function passed to the UndoManager constructor.

assert.equal(undoDescription.value, "Undo change setting");

redoDescription

A Signals computed whose value getter returns the result of the redoLocalizer function passed to the UndoManager constructor.

assert.equal(redoDescription.value, "Redo change setting and more");

Conceptual

For anything other than the simplest of use cases, all changes should be wrapped in a group. Consistency avoids confusion that may arise due to an overly generous optional API.

It is possible to create undoables and preservables after changes are underway to the undo stack. This is not considered best practice. Their value in the undo stack prior to their creation will be represented as their initial value.

const dont = undoable("just"); // just don't
dont.value = "dont";
undo();
undo();// before it existed
assert(dont.value, "just");
redo();
redo();

coalescing

For the strictest use, the coalescing parameter of undoable and group should only ever be passed a Symbol. It can be thought of as a coalescing key. Passing a boolean or string is for developer convenience and perfectly fine however. Its intended use is for but not limited to changes made by continuous dragging events.

Coalescing does not overwrite any captured preservable before values. Changing a preservable inside a group set to coalesce will not register as a before value even if there is no change to an undoable. It will be captured as an after value, regardless of interrupting behavior or any coalesced changes to an undoable that may follow after its group.

Navigating the undo stack via undo and redo will interrupt and prevent coalescing. Changes to preservables without a change to an undoable are lost on undo.

group( () => {
	appearance.value = 9; // will be registered as the after change value 
}, "conceptual section changes", true);
group( () => {
	setting.value = 7; 
}, "conceptual section changes", true); // coalesces
undo();
assert.equal(appearance.value, 2); // coalescing group did not affect before value
redo();
assert.equal(appearance.value, 9);
group( () => {
	appearance.value = 10;
}, "conceptual section changes", true); // does not coalesce
assert.equal(appearance.value, 10);
undo();
redo();
assert.equal(appearance.value, 9); // changes are lost

interrupting

If preservable changes are always wrapped in a group, the interrupting parameter of preservable can be ignored and omitted. If no changes are ever coalesced it can also be ignored and omitted. Only if both of these two conditions are not met does the author need to decide if changing presentational state should affect coalescing. Pass true to prevent coalescing or false to permit coalescing.

Example

A non-virtual, non-animated tree view that preserves selection and expanded/collapsed state:

https://gitlab.com/kevindoughty/cute-tree

Live demo:

https://kevindoughty.gitlab.io/cute-tree/index.html

License

MIT

Issues and PRs

Welcome, especially for tooling, bundling, or Typescript/JSDoc definitions.