Skip to content

Commit 94ffb93

Browse files
authored
Add undo/redo (#18)
* Add undo/redo * Remove commented crossframe code Co-authored-by: Nope <[email protected]>
1 parent 777de6f commit 94ffb93

File tree

9 files changed

+92
-2
lines changed

9 files changed

+92
-2
lines changed

src/annotator/components/toolbar.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ ToolbarButton.propTypes = {
7878
* Callback to toggle the visibility of the sidebar.
7979
* @prop {(object) => any} setDoodleOptions
8080
* Callback to set the options of the doodle canvas
81+
* @prop {() => any} undoDoodle
82+
* Callback to undo the last drawn line
83+
* @prop {() => any} redoDoodle
84+
* Callback to undo the last doodle undo
8185
* @prop {() => any} saveDoodle
8286
* Callback to set the options of the doodle canvas
8387
* @prop {import("preact").Ref<HTMLButtonElement>} [toggleSidebarRef] -
@@ -109,6 +113,8 @@ export default function Toolbar({
109113
toggleDoodles,
110114
toggleSidebar,
111115
setDoodleOptions,
116+
undoDoodle,
117+
redoDoodle,
112118
saveDoodle,
113119
toggleSidebarRef,
114120
useMinimalControls = false,
@@ -381,6 +387,8 @@ export default function Toolbar({
381387
</div>
382388
</span>
383389
</button>
390+
<ToolbarButton label="Undo" icon="undo" onClick={undoDoodle} />
391+
<ToolbarButton label="Redo" icon="redo" onClick={redoDoodle} />
384392
<ToolbarButton
385393
label="Save"
386394
icon="save"

src/annotator/guest.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { normalizeURI } from './util/url';
2222
* @typedef {import('../types/annotator').Anchor} Anchor
2323
* @typedef {import('../types/api').Target} Target
2424
* @typedef {import('./toolbar').ToolbarController} ToolbarController
25+
* @typedef {import('../doodle/doodleController').DoodleController} DoodleController
2526
*/
2627

2728
/**
@@ -124,7 +125,7 @@ export default class Guest extends Delegator {
124125
/** @type {ToolbarController|null} */
125126
this.toolbar = null;
126127

127-
/** TODO add this type back while still passing linter {DoodleController|null}* } */
128+
/** @type {DoodleController|null} */
128129
this.doodleCanvasController = null;
129130

130131
this.adderToolbar = document.createElement('hypothesis-adder');
@@ -806,6 +807,19 @@ export default class Guest extends Delegator {
806807
}
807808
}
808809

810+
/**
811+
* Undo a doodle line
812+
*/
813+
undoDoodle() {
814+
this.doodleCanvasController?.undo();
815+
}
816+
817+
/**
818+
* Re-add a line that was removed with "undo"
819+
*/
820+
redoDoodle() {
821+
this.doodleCanvasController?.redo();
822+
}
809823
/**
810824
* Save the doodle
811825
*/

src/annotator/icons.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export default {
1919
close: require('../images/icons/close.svg'),
2020
pen: require('../images/icons/pen.svg'),
2121
erase: require('../images/icons/erase.svg'),
22+
undo: require('../images/icons/undo.svg'),
23+
redo: require('../images/icons/redo.svg'),
2224
save: require('../images/icons/save.svg'),
2325
'circle-small': require('../images/icons/circleSmall.svg'),
2426
'circle-medium': require('../images/icons/circleMedium.svg'),

src/annotator/sidebar.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ export default class Sidebar extends Guest {
138138
setDoodlesVisible: show => this.setAllVisibleDoodles(show),
139139
setUserCanDoodle: show => this.setAllDoodleability(show),
140140
setDoodleOptions: options => this.setAllDoodleOptions(options),
141+
undoDoodle: () => this.undoDoodle(),
142+
redoDoodle: () => this.redoDoodle(),
141143
saveDoodle: () => this.saveDoodle(),
142144
});
143145
this.toolbar.useMinimalControls = config.theme === 'clean';

src/annotator/toolbar.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import Toolbar from './components/toolbar';
1010
* @prop {(visible: boolean) => any} setDoodlesVisible
1111
* @prop {(doodleable: boolean) => any} setUserCanDoodle
1212
* @prop {(doodleable: boolean) => any} setDoodleOptions
13+
* @prop {() => any} undoDoodle
14+
* @prop {() => any} redoDoodle
1315
* @prop {() => any} saveDoodle
1416
*/
1517

@@ -32,6 +34,8 @@ export class ToolbarController {
3234
setDoodlesVisible,
3335
setUserCanDoodle,
3436
setDoodleOptions,
37+
undoDoodle,
38+
redoDoodle,
3539
saveDoodle,
3640
} = options;
3741

@@ -68,6 +72,12 @@ export class ToolbarController {
6872
createAnnotation();
6973
setSidebarOpen(true);
7074
};
75+
this._undoDoodle = () => {
76+
undoDoodle();
77+
};
78+
this._redoDoodle = () => {
79+
redoDoodle();
80+
};
7181
this._saveDoodle = () => {
7282
saveDoodle();
7383
};
@@ -171,6 +181,8 @@ export class ToolbarController {
171181
setDoodleOptions={this._setDoodleOptions}
172182
drawingToolbarActivated={this._drawingToolbar}
173183
drawingToolbarToggle={this._toggleDoodleToolbar}
184+
undoDoodle={this._undoDoodle}
185+
redoDoodle={this._redoDoodle}
174186
saveDoodle={this._saveDoodle}
175187
/>,
176188
this._container

src/doodle/doodleCanvas.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import propTypes from 'prop-types';
1212
* @prop {HTMLElement} attachedElement - Which element the DoodleCanvas should cover.
1313
* @prop {Array<import('../types/api').DoodleLine>} lines - An array of lines that compose this doodle.
1414
* @prop {Function} setLines - A function to set the lines
15+
* @prop {Function} onUndo - a function called when undo key commands pressed
16+
* @prop {Function} onRedo - a function called when redo key commands pressed
1517
*/
1618

1719
/**
@@ -27,6 +29,8 @@ const DoodleCanvas = ({
2729
attachedElement,
2830
lines,
2931
setLines,
32+
onUndo,
33+
onRedo,
3034
}) => {
3135
const [isDrawing, setIsDrawing] = useState(false);
3236
const [everActive, setEverActive] = useState(false);
@@ -35,6 +39,27 @@ const DoodleCanvas = ({
3539
setEverActive(true);
3640
}
3741

42+
useEffect(() => {
43+
const KEY_UNDO = 90; // Code for "Z" key
44+
const KEY_REDO = 89; // Code for "Y" key
45+
if (!active) {
46+
return () => {};
47+
}
48+
const listener = e => {
49+
const key = e.charCode || e.keyCode;
50+
if (e.ctrlKey) {
51+
if (key === KEY_UNDO) {
52+
onUndo();
53+
} else if (key === KEY_REDO) {
54+
onRedo();
55+
}
56+
}
57+
};
58+
document.addEventListener('keydown', listener);
59+
return () => {
60+
document.removeEventListener('keydown', listener);
61+
};
62+
}, [active, onUndo, onRedo]);
3863
useEffect(() => {
3964
if (lines.length === 0) {
4065
return () => {};
@@ -50,7 +75,7 @@ const DoodleCanvas = ({
5075
return () => {
5176
window.removeEventListener('beforeunload', warn);
5277
};
53-
}, [lines]);
78+
}, [lines.length]);
5479

5580
const handleMouseDown = e => {
5681
setIsDrawing(true);
@@ -137,6 +162,8 @@ DoodleCanvas.propTypes = {
137162
lines: propTypes.array.isRequired,
138163
setLines: propTypes.func.isRequired,
139164
attachedElement: propTypes.any.isRequired,
165+
onUndo: propTypes.func.isRequired,
166+
onRedo: propTypes.func.isRequired,
140167
};
141168

142169
export { DoodleCanvas };

src/doodle/doodleController.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export class DoodleController {
1212
this._lines = [];
1313
this._savedDoodles = [];
1414
this._newLines = [];
15+
this._redoLines = [];
1516

1617
this._container = container === null ? document.body : container;
1718
this._tool = tool;
@@ -65,6 +66,7 @@ export class DoodleController {
6566

6667
set newLines(lines) {
6768
this._newLines = lines;
69+
this._redoLines = []; // Clear redo queue when doodle changes
6870
this.render();
6971
}
7072

@@ -103,6 +105,19 @@ export class DoodleController {
103105
return this._doodleable;
104106
}
105107

108+
undo() {
109+
if (this._newLines.length) {
110+
this._redoLines.push(this._newLines.shift());
111+
this.render();
112+
}
113+
}
114+
redo() {
115+
if (this._redoLines.length) {
116+
this._newLines = [this._redoLines.pop(), ...this._newLines];
117+
this.render();
118+
}
119+
}
120+
106121
render() {
107122
const setLines = lines => {
108123
this.newLines = lines;
@@ -117,6 +132,8 @@ export class DoodleController {
117132
lines={this.newLines}
118133
setLines={setLines}
119134
color={this._color}
135+
onUndo={this.undo.bind(this)}
136+
onRedo={this.redo.bind(this)}
120137
/>
121138
<DisplayCanvas
122139
handleDoodleClick={this._handleDoodleClick}

src/images/icons/redo.svg

Lines changed: 4 additions & 0 deletions
Loading

src/images/icons/undo.svg

Lines changed: 4 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)