History Tracker
The Jspreadsheet History Tracker automatically records all changes to the spreadsheet and provides undo/redo functionality through Ctrl+Z and Ctrl+Y keyboard shortcuts. Developers can monitor interactions through historical events and manage the history stack programmatically, including options to ignore specific operations or cascade multiple actions into a single history entry.
What's new with Version 13
- Collaboration-aware undo: history records follow rows, columns and worksheets by identity. Undo lands on the right cells after inserts, deletes or moves made locally or by other connected users, relocates targets deleted and later restored, and skips targets that no longer exist instead of writing to the wrong cell
- One entry per operation: a paste with its cut and merges, a nested header move or a multi column sort apply and revert as a unit
- Personal view state: zoom, group open and close, search terms and the current page are never recorded in the history
- Performance: recording is constant-time per edit, so long sessions no longer degrade
Documentation
Methods
The following methods provide programmatic control over the history tracker. The undo() and redo() methods are automatically bound to Ctrl+Z and Ctrl+Y keyboard shortcuts:
| Method | Description |
|---|---|
history.undo |
Undo the last spreadsheet changes.jspreadsheet.history.undo() : void |
history.redo |
Redo the most recent spreadsheet changes.jspreadsheet.history.redo() : void |
history.reset |
Clear all history entries.jspreadsheet.history.reset() : void |
Advanced usage
| Method | Description |
|---|---|
history(object) |
Add a new entry to the history tracker.jspreadsheet.history(changes: Object) : void |
Events
The following events are triggered during history operations:
| Event | Description |
|---|---|
onredo |
onredo(worksheet: Object, info: Object) : nullTriggered after a redo operation is performed. |
onundo |
onundo(worksheet: Object, info: Object) : nullTriggered after an undo operation is performed. |
Interface
The history tracker interface provides the following properties and methods:
| Property | Description |
|---|---|
index: number |
Current position in the history stack |
actions: [] |
Array of history items |
cascade: boolean |
When true, the next action is cascaded into the same existing history element |
ignore: boolean |
When true, no history will be added to the tracker |
progress: string |
Indicates if a history process is ongoing |
undo: () => void |
Undo the last action |
redo: () => void |
Redo the most recent action |
reset: () => void |
Clear all history entries |
Undo in collaborative sessions
History records reference worksheets by id and rows and columns by identity, not by position. When another connected user inserts, deletes or moves rows or columns, a local undo still applies to the cells it was recorded for, at their current position. A target deleted by another user and later restored is relocated through its identity; a target that no longer exists is skipped, and the rest of the same record still applies. This covers values, formulas, styles, comments, merges, visibility, widths, heights, headers, groups and filters. Remote operations replayed through the Jspreadsheet Server never enter the local history.
Examples
Programmatic History Control
This example demonstrates how to control undo/redo operations programmatically using the global jspreadsheet.history object.
<html>
<script src="https://jspreadsheet.com/v13/jspreadsheet.js"></script>
<script src="https://jsuites.net/v6/jsuites.js"></script>
<link rel="stylesheet" href="https://jspreadsheet.com/v13/jspreadsheet.css" type="text/css" />
<link rel="stylesheet" href="https://jsuites.net/v6/jsuites.css" type="text/css" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons" />
<div id='spreadsheet'></div>
<p><input type="button" value="Undo" id="btn1" />
<input type="button" value="Redo" id="btn2" />
<input type="button" value="Reset" id="btn3" /></p>
<script>
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');
// Create the spreadsheet
let spreadsheet = jspreadsheet(document.getElementById('spreadsheet'), {
worksheets: [{
minDimensions: [6, 6],
data: [
['Sample', 'Data', 'Here'],
['Row 2', 'Value', '123'],
['Test', 'More', 'Data']
]
}],
onundo: function(worksheet, info) {
console.log('Undo action:', info);
},
onredo: function(worksheet, info) {
console.log('Redo action:', info);
}
});
document.getElementById("btn1").onclick = () => {
console.log('History index before undo:', jspreadsheet.history.index);
jspreadsheet.history.undo();
}
document.getElementById("btn2").onclick = () => {
console.log('History index before redo:', jspreadsheet.history.index);
jspreadsheet.history.redo();
}
document.getElementById("btn3").onclick = () => {
console.log('Clearing', jspreadsheet.history.actions.length, 'history items');
jspreadsheet.history.reset();
}
</script>
</html>
import React, {useRef} from "react";
import { Spreadsheet, Worksheet, jspreadsheet } from "@jspreadsheet/react";
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');
export default function App() {
// Spreadsheet array of worksheets
const spreadsheet = useRef();
const handleUndo = () => {
console.log('History before undo:', jspreadsheet.history.index);
jspreadsheet.history.undo();
};
const handleRedo = () => {
console.log('History before redo:', jspreadsheet.history.index);
jspreadsheet.history.redo();
};
const handleReset = () => {
console.log('Clearing', jspreadsheet.history.actions.length, 'history items');
jspreadsheet.history.reset();
};
const onundo = (worksheet, info) => {
console.log('Undo event:', info);
};
const onredo = (worksheet, info) => {
console.log('Redo event:', info);
};
// Render component
return (
<>
<Spreadsheet ref={spreadsheet} onundo={onundo} onredo={onredo}>
<Worksheet
minDimensions={[8, 8]}
data={[['Sample', 'Data', 'Here'], ['Row 2', 'Value', '123']]}
/>
</Spreadsheet>
<input type="button" value="Undo" onClick={handleUndo}/>
<input type="button" value="Redo" onClick={handleRedo}/>
<input type="button" value="Reset" onClick={handleReset}/>
</>
);
}
<template>
<Spreadsheet ref="spreadsheet" :onundo="onundo" :onredo="onredo">
<Worksheet :minDimensions="[8,8]" :data="data" />
</Spreadsheet>
<input type="button" value="Undo" @click="undo" />
<input type="button" value="Redo" @click="redo" />
<input type="button" value="Reset" @click="reset" />
</template>
<script>
import { Spreadsheet, Worksheet, jspreadsheet } from "@jspreadsheet/vue";
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');
export default {
components: {
Spreadsheet,
Worksheet,
},
methods: {
undo() {
console.log('History before undo:', jspreadsheet.history.index);
jspreadsheet.history.undo();
},
redo() {
console.log('History before redo:', jspreadsheet.history.index);
jspreadsheet.history.redo();
},
reset() {
console.log('Clearing', jspreadsheet.history.actions.length, 'history items');
jspreadsheet.history.reset();
},
onundo(worksheet, info) {
console.log('Undo event:', info);
},
onredo(worksheet, info) {
console.log('Redo event:', info);
}
},
data() {
return {
data: [['Sample', 'Data', 'Here'], ['Row 2', 'Value', '123']]
}
}
}
</script>
import { Component, ViewChild, ElementRef } from '@angular/core';
import jspreadsheet from 'jspreadsheet';
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');
// Create component
@Component({
standalone: true,
selector: 'app-root',
template: `<div #spreadsheet></div>
<input type="button" value="Undo" (click)="undo()" />
<input type="button" value="Redo" (click)="redo()" />
<input type="button" value="Reset" (click)="reset()" />`,
})
export class AppComponent {
@ViewChild('spreadsheet') spreadsheet: ElementRef;
// Worksheets
worksheets: jspreadsheet.worksheetInstance[];
// Create a new data grid
ngAfterViewInit() {
// Create spreadsheet
this.worksheets = jspreadsheet(this.spreadsheet.nativeElement, {
worksheets: [
{
minDimensions: [8, 8],
data: [
['Sample', 'Data', 'Here'],
['Row 2', 'Value', '123'],
['Test', 'More', 'Data']
]
},
],
onundo: (worksheet, info) => {
console.log('Undo event:', info);
},
onredo: (worksheet, info) => {
console.log('Redo event:', info);
}
});
}
undo(): void {
console.log('History before undo:', jspreadsheet.history.index);
jspreadsheet.history.undo();
}
redo(): void {
console.log('History before redo:', jspreadsheet.history.index);
jspreadsheet.history.redo();
}
reset(): void {
console.log('Clearing', jspreadsheet.history.actions.length, 'history items');
jspreadsheet.history.reset();
}
}
Ignoring History for Specific Operations
This example shows how to make changes without adding them to the history stack using the ignore flag.
<html>
<script src="https://jspreadsheet.com/v13/jspreadsheet.js"></script>
<script src="https://jsuites.net/v6/jsuites.js"></script>
<link rel="stylesheet" href="https://jspreadsheet.com/v13/jspreadsheet.css" type="text/css" />
<link rel="stylesheet" href="https://jsuites.net/v6/jsuites.css" type="text/css" />
<div id='spreadsheet'></div>
<p><input type="button" value="Set A1 (untracked)" id="btn1" />
<input type="button" value="Undo" id="btn2" /></p>
<script>
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');
// Create the spreadsheet
let spreadsheet = jspreadsheet(document.getElementById('spreadsheet'), {
worksheets: [{
minDimensions: [6, 6],
}],
});
document.getElementById("btn1").onclick = () => {
// Ignored operation - will not be tracked in history
jspreadsheet.history.ignore = true;
spreadsheet[0].setValue('A1', Math.random()*1000);
jspreadsheet.history.ignore = false;
}
document.getElementById("btn2").onclick = () => {
jspreadsheet.history.undo();
}
</script>
</html>
import React, {useRef} from "react";
import { Spreadsheet, Worksheet, jspreadsheet } from "@jspreadsheet/react";
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');
export default function App() {
const spreadsheet = useRef();
const setUntrackedValue = () => {
if (spreadsheet.current) {
jspreadsheet.history.ignore = true;
spreadsheet.current[0].setValue('A1', Math.random() * 1000);
jspreadsheet.history.ignore = false;
}
};
const handleUndo = () => {
jspreadsheet.history.undo();
};
return (
<>
<Spreadsheet ref={spreadsheet}>
<Worksheet minDimensions={[6, 6]} />
</Spreadsheet>
<input type="button" value="Set A1 (untracked)" onClick={setUntrackedValue}/>
<input type="button" value="Undo" onClick={handleUndo}/>
</>
);
}
<template>
<Spreadsheet ref="spreadsheet">
<Worksheet :minDimensions="[6,6]" />
</Spreadsheet>
<input type="button" value="Set A1 (untracked)" @click="setUntrackedValue" />
<input type="button" value="Undo" @click="undo" />
</template>
<script>
import { Spreadsheet, Worksheet, jspreadsheet } from "@jspreadsheet/vue";
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');
export default {
components: {
Spreadsheet,
Worksheet,
},
methods: {
setUntrackedValue() {
if (this.$refs.spreadsheet.current[0]) {
jspreadsheet.history.ignore = true;
this.$refs.spreadsheet.current[0].setValue('A1', Math.random() * 1000);
jspreadsheet.history.ignore = false;
}
},
undo() {
jspreadsheet.history.undo();
}
}
}
</script>
import { Component, ViewChild, ElementRef } from '@angular/core';
import jspreadsheet from 'jspreadsheet';
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');
@Component({
standalone: true,
selector: 'app-root',
template: `<div #spreadsheet></div>
<input type="button" value="Set A1 (untracked)" (click)="setUntrackedValue()" />
<input type="button" value="Undo" (click)="undo()" />`,
})
export class AppComponent {
@ViewChild('spreadsheet') spreadsheet: ElementRef;
worksheets: jspreadsheet.worksheetInstance[];
ngAfterViewInit() {
this.worksheets = jspreadsheet(this.spreadsheet.nativeElement, {
worksheets: [
{
minDimensions: [6, 6],
},
],
});
}
setUntrackedValue(): void {
jspreadsheet.history.ignore = true;
this.worksheets[0].setValue('A1', Math.random() * 1000);
jspreadsheet.history.ignore = false;
}
undo(): void {
jspreadsheet.history.undo();
}
}
Cascading History Actions
This example shows how to group multiple changes into a single undo action using the cascade flag.
<html>
<script src="https://jspreadsheet.com/v13/jspreadsheet.js"></script>
<script src="https://jsuites.net/v6/jsuites.js"></script>
<link rel="stylesheet" href="https://jspreadsheet.com/v13/jspreadsheet.css" type="text/css" />
<link rel="stylesheet" href="https://jsuites.net/v6/jsuites.css" type="text/css" />
<div id='spreadsheet'></div>
<p><input type="button" value="Set Value & Style (cascaded)" id="btn1" class="jss_object" />
<input type="button" value="Undo" id="btn2" class="jss_object" /></p>
<script>
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');
// Create the spreadsheet
let spreadsheet = jspreadsheet(document.getElementById('spreadsheet'), {
worksheets: [{
minDimensions: [6, 6],
}],
});
document.getElementById("btn1").onclick = () => {
// Generate random cell coordinates
let col = Math.floor(Math.random() * 6);
let row = Math.floor(Math.random() * 6);
let cellName = jspreadsheet.helpers.getCellNameFromCoords(col, row);
// Enable cascade to group multiple actions
spreadsheet[0].setValue(cellName, 'test');
jspreadsheet.history.cascade = true;
spreadsheet[0].setStyle(cellName, 'background-color: red');
jspreadsheet.history.cascade = false;
}
document.getElementById("btn2").onclick = () => {
jspreadsheet.history.undo();
}
</script>
</html>
import React, {useRef} from "react";
import { Spreadsheet, Worksheet, jspreadsheet } from "@jspreadsheet/react";
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');
export default function App() {
const spreadsheet = useRef();
const setCascadedValueAndStyle = () => {
if (spreadsheet.current) {
// Generate random cell coordinates
let col = Math.floor(Math.random() * 6);
let row = Math.floor(Math.random() * 6);
let cellName = jspreadsheet.helpers.getCellNameFromCoords(col, row);
// Enable cascade to group multiple actions
spreadsheet.current[0].setValue(cellName, 'test');
jspreadsheet.history.cascade = true;
spreadsheet.current[0].setStyle(cellName, 'background-color: red');
jspreadsheet.history.cascade = false;
}
};
const handleUndo = () => {
jspreadsheet.history.undo();
};
return (
<>
<Spreadsheet ref={spreadsheet}>
<Worksheet minDimensions={[6, 6]} />
</Spreadsheet>
<input type="button" value="Set Value & Style (cascaded)" onClick={setCascadedValueAndStyle} className="jss_object"/>
<input type="button" value="Undo" onClick={handleUndo} className="jss_object"/>
</>
);
}
<template>
<Spreadsheet ref="spreadsheet">
<Worksheet :minDimensions="[6,6]" />
</Spreadsheet>
<input type="button" value="Set Value & Style (cascaded)" @click="setCascadedValueAndStyle" class="jss_object" />
<input type="button" value="Undo" @click="undo" class="jss_object" />
</template>
<script>
import { Spreadsheet, Worksheet, jspreadsheet } from "@jspreadsheet/vue";
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');
export default {
components: {
Spreadsheet,
Worksheet,
},
methods: {
setCascadedValueAndStyle() {
if (this.$refs.spreadsheet.current[0]) {
// Generate random cell coordinates
let col = Math.floor(Math.random() * 6);
let row = Math.floor(Math.random() * 6);
let cellName = jspreadsheet.helpers.getCellNameFromCoords(col, row);
// Enable cascade to group multiple actions
this.$refs.spreadsheet.current[0].setValue(cellName, 'test');
jspreadsheet.history.cascade = true;
this.$refs.spreadsheet.current[0].setStyle(cellName, 'background-color: red');
jspreadsheet.history.cascade = false;
}
},
undo() {
jspreadsheet.history.undo();
}
}
}
</script>
import { Component, ViewChild, ElementRef } from '@angular/core';
import jspreadsheet from 'jspreadsheet';
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('MGQ2MmIzN2VlMGM2ODRiMDY1OTc3MGRiMzY4OTJhMzk5ZjY4MmQyMzUxZmE2MzRhMzVlZjE5ZDM1NzE5NTZhNzIwNzU5MjgzMjg5NWU3YmNjZTNiNzMwM2I1OWEyZDVmODkwYjUwNjQ4MmVkYzM4MDM0NmRhMGUzZDU3OTI5MWYsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpnNU16ZzJPRFF5TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');
@Component({
standalone: true,
selector: 'app-root',
template: `<div #spreadsheet></div>
<input type="button" value="Set Value & Style (cascaded)" (click)="setCascadedValueAndStyle()" class="jss_object" />
<input type="button" value="Undo" (click)="undo()" class="jss_object" />`,
})
export class AppComponent {
@ViewChild('spreadsheet') spreadsheet: ElementRef;
worksheets: jspreadsheet.worksheetInstance[];
ngAfterViewInit() {
this.worksheets = jspreadsheet(this.spreadsheet.nativeElement, {
worksheets: [
{
minDimensions: [6, 6],
},
],
});
}
setCascadedValueAndStyle(): void {
// Generate random cell coordinates
let col = Math.floor(Math.random() * 6);
let row = Math.floor(Math.random() * 6);
let cellName = jspreadsheet.helpers.getCellNameFromCoords(col, row);
// Enable cascade to group multiple actions
this.worksheets[0].setValue(cellName, 'test');
jspreadsheet.history.cascade = true;
this.worksheets[0].setStyle(cellName, 'background-color: red');
jspreadsheet.history.cascade = false;
}
undo(): void {
jspreadsheet.history.undo();
}
}