@wordpress/undo-manager
Version:
A small package to manage undo/redo.
175 lines (165 loc) • 4.42 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.createUndoManager = createUndoManager;
var _isShallowEqual = _interopRequireDefault(require("@wordpress/is-shallow-equal"));
/**
* WordPress dependencies
*/
/**
* Internal dependencies
*/
/**
* Represents a single change in history.
*/
/**
* Represents changes for a single item.
*/
/**
* Represents a record of history changes.
*/
/**
* The undo manager interface.
*/
/**
* Merge changes for a single item into a record of changes.
*
* @param changes1 Previous changes
* @param changes2 Next changes
*
* @return Merged changes
*/
function mergeHistoryChanges(changes1, changes2) {
const newChanges = {
...changes1
};
Object.entries(changes2).forEach(([key, value]) => {
if (newChanges[key]) {
newChanges[key] = {
...newChanges[key],
to: value.to
};
} else {
newChanges[key] = value;
}
});
return newChanges;
}
/**
* Adds history changes for a single item into a record of changes.
*
* @param record The record to merge into.
* @param changes The changes to merge.
*/
const addHistoryChangesIntoRecord = (record, changes) => {
const existingChangesIndex = record?.findIndex(({
id: recordIdentifier
}) => {
return typeof recordIdentifier === 'string' ? recordIdentifier === changes.id : (0, _isShallowEqual.default)(recordIdentifier, changes.id);
});
const nextRecord = [...record];
if (existingChangesIndex !== -1) {
// If the edit is already in the stack leave the initial "from" value.
nextRecord[existingChangesIndex] = {
id: changes.id,
changes: mergeHistoryChanges(nextRecord[existingChangesIndex].changes, changes.changes)
};
} else {
nextRecord.push(changes);
}
return nextRecord;
};
/**
* Creates an undo manager.
*
* @return Undo manager.
*/
function createUndoManager() {
let history = [];
let stagedRecord = [];
let offset = 0;
const dropPendingRedos = () => {
history = history.slice(0, offset || undefined);
offset = 0;
};
const appendStagedRecordToLatestHistoryRecord = () => {
var _history$index;
const index = history.length === 0 ? 0 : history.length - 1;
let latestRecord = (_history$index = history[index]) !== null && _history$index !== void 0 ? _history$index : [];
stagedRecord.forEach(changes => {
latestRecord = addHistoryChangesIntoRecord(latestRecord, changes);
});
stagedRecord = [];
history[index] = latestRecord;
};
/**
* Checks whether a record is empty.
* A record is considered empty if it the changes keep the same values.
* Also updates to function values are ignored.
*
* @param record The record to check.
* @return Whether the record is empty.
*/
const isRecordEmpty = record => {
const filteredRecord = record.filter(({
changes
}) => {
return Object.values(changes).some(({
from,
to
}) => typeof from !== 'function' && typeof to !== 'function' && !(0, _isShallowEqual.default)(from, to));
});
return !filteredRecord.length;
};
return {
addRecord(record, isStaged = false) {
const isEmpty = !record || isRecordEmpty(record);
if (isStaged) {
if (isEmpty) {
return;
}
record.forEach(changes => {
stagedRecord = addHistoryChangesIntoRecord(stagedRecord, changes);
});
} else {
dropPendingRedos();
if (stagedRecord.length) {
appendStagedRecordToLatestHistoryRecord();
}
if (isEmpty) {
return;
}
history.push(record);
}
},
undo() {
if (stagedRecord.length) {
dropPendingRedos();
appendStagedRecordToLatestHistoryRecord();
}
const undoRecord = history[history.length - 1 + offset];
if (!undoRecord) {
return;
}
offset -= 1;
return undoRecord;
},
redo() {
const redoRecord = history[history.length + offset];
if (!redoRecord) {
return;
}
offset += 1;
return redoRecord;
},
hasUndo() {
return !!history[history.length - 1 + offset];
},
hasRedo() {
return !!history[history.length + offset];
}
};
}
//# sourceMappingURL=index.js.map