@svgedit/svgcanvas 7.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/blur-event.js +156 -0
- package/clear.js +43 -0
- package/coords.js +298 -0
- package/copy-elem.js +45 -0
- package/dataStorage.js +28 -0
- package/dist/svgcanvas.js +515 -0
- package/dist/svgcanvas.js.map +1 -0
- package/draw.js +1064 -0
- package/elem-get-set.js +1077 -0
- package/event.js +1388 -0
- package/history.js +619 -0
- package/historyrecording.js +161 -0
- package/json.js +110 -0
- package/layer.js +228 -0
- package/math.js +221 -0
- package/namespaces.js +40 -0
- package/package.json +54 -0
- package/paint.js +88 -0
- package/paste-elem.js +127 -0
- package/path-actions.js +1237 -0
- package/path-method.js +1012 -0
- package/path.js +781 -0
- package/recalculate.js +794 -0
- package/rollup.config.js +40 -0
- package/sanitize.js +252 -0
- package/select.js +543 -0
- package/selected-elem.js +1297 -0
- package/selection.js +482 -0
- package/svg-exec.js +1289 -0
- package/svgcanvas.js +1347 -0
- package/svgroot.js +36 -0
- package/text-actions.js +530 -0
- package/touch.js +51 -0
- package/undo.js +279 -0
- package/utilities.js +1214 -0
package/history.js
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
/**
|
|
3
|
+
* For command history tracking and undo functionality.
|
|
4
|
+
* @module history
|
|
5
|
+
* @license MIT
|
|
6
|
+
* @copyright 2010 Jeff Schiller
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { getHref, setHref, getRotationAngle, getBBox } from './utilities.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Group: Undo/Redo history management.
|
|
13
|
+
*/
|
|
14
|
+
export const HistoryEventTypes = {
|
|
15
|
+
BEFORE_APPLY: 'before_apply',
|
|
16
|
+
AFTER_APPLY: 'after_apply',
|
|
17
|
+
BEFORE_UNAPPLY: 'before_unapply',
|
|
18
|
+
AFTER_UNAPPLY: 'after_unapply'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Base class for commands.
|
|
23
|
+
*/
|
|
24
|
+
export class Command {
|
|
25
|
+
/**
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
getText () {
|
|
29
|
+
return this.text
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {module:history.HistoryEventHandler} handler
|
|
34
|
+
* @param {callback} applyFunction
|
|
35
|
+
* @returns {void}
|
|
36
|
+
*/
|
|
37
|
+
apply (handler, applyFunction) {
|
|
38
|
+
handler && handler.handleHistoryEvent(HistoryEventTypes.BEFORE_APPLY, this)
|
|
39
|
+
applyFunction(handler)
|
|
40
|
+
handler && handler.handleHistoryEvent(HistoryEventTypes.AFTER_APPLY, this)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {module:history.HistoryEventHandler} handler
|
|
45
|
+
* @param {callback} unapplyFunction
|
|
46
|
+
* @returns {void}
|
|
47
|
+
*/
|
|
48
|
+
unapply (handler, unapplyFunction) {
|
|
49
|
+
handler && handler.handleHistoryEvent(HistoryEventTypes.BEFORE_UNAPPLY, this)
|
|
50
|
+
unapplyFunction()
|
|
51
|
+
handler && handler.handleHistoryEvent(HistoryEventTypes.AFTER_UNAPPLY, this)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @returns {Element[]} Array with element associated with this command
|
|
56
|
+
* This function needs to be surcharged if multiple elements are returned.
|
|
57
|
+
*/
|
|
58
|
+
elements () {
|
|
59
|
+
return [this.elem]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @returns {string} String with element associated with this command
|
|
64
|
+
*/
|
|
65
|
+
type () {
|
|
66
|
+
return this.constructor.name
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Todo: Figure out why the interface members aren't showing
|
|
71
|
+
// up (with or without modules applied), despite our apparently following
|
|
72
|
+
// http://usejsdoc.org/tags-interface.html#virtual-comments
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* An interface that all command objects must implement.
|
|
76
|
+
* @interface module:history.HistoryCommand
|
|
77
|
+
*/
|
|
78
|
+
/**
|
|
79
|
+
* Applies.
|
|
80
|
+
*
|
|
81
|
+
* @function module:history.HistoryCommand#apply
|
|
82
|
+
* @param {module:history.HistoryEventHandler} handler
|
|
83
|
+
* @fires module:history~Command#event:history
|
|
84
|
+
* @returns {void|true}
|
|
85
|
+
*/
|
|
86
|
+
/**
|
|
87
|
+
*
|
|
88
|
+
* Unapplies.
|
|
89
|
+
* @function module:history.HistoryCommand#unapply
|
|
90
|
+
* @param {module:history.HistoryEventHandler} handler
|
|
91
|
+
* @fires module:history~Command#event:history
|
|
92
|
+
* @returns {void|true}
|
|
93
|
+
*/
|
|
94
|
+
/**
|
|
95
|
+
* Returns the elements.
|
|
96
|
+
* @function module:history.HistoryCommand#elements
|
|
97
|
+
* @returns {Element[]}
|
|
98
|
+
*/
|
|
99
|
+
/**
|
|
100
|
+
* Gets the text.
|
|
101
|
+
* @function module:history.HistoryCommand#getText
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
/**
|
|
105
|
+
* Gives the type.
|
|
106
|
+
* @function module:history.HistoryCommand.type
|
|
107
|
+
* @returns {string}
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @event module:history~Command#event:history
|
|
112
|
+
* @type {module:history.HistoryCommand}
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* An interface for objects that will handle history events.
|
|
117
|
+
* @interface module:history.HistoryEventHandler
|
|
118
|
+
*/
|
|
119
|
+
/**
|
|
120
|
+
*
|
|
121
|
+
* @function module:history.HistoryEventHandler#handleHistoryEvent
|
|
122
|
+
* @param {string} eventType One of the HistoryEvent types
|
|
123
|
+
* @param {module:history~Command#event:history} command
|
|
124
|
+
* @listens module:history~Command#event:history
|
|
125
|
+
* @returns {void}
|
|
126
|
+
*
|
|
127
|
+
*/
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* History command for an element that had its DOM position changed.
|
|
131
|
+
* @implements {module:history.HistoryCommand}
|
|
132
|
+
*/
|
|
133
|
+
export class MoveElementCommand extends Command {
|
|
134
|
+
/**
|
|
135
|
+
* @param {Element} elem - The DOM element that was moved
|
|
136
|
+
* @param {Element} oldNextSibling - The element's next sibling before it was moved
|
|
137
|
+
* @param {Element} oldParent - The element's parent before it was moved
|
|
138
|
+
* @param {string} [text] - An optional string visible to user related to this change
|
|
139
|
+
*/
|
|
140
|
+
constructor (elem, oldNextSibling, oldParent, text) {
|
|
141
|
+
super()
|
|
142
|
+
this.elem = elem
|
|
143
|
+
this.text = text ? ('Move ' + elem.tagName + ' to ' + text) : ('Move ' + elem.tagName)
|
|
144
|
+
this.oldNextSibling = oldNextSibling
|
|
145
|
+
this.oldParent = oldParent
|
|
146
|
+
this.newNextSibling = elem.nextSibling
|
|
147
|
+
this.newParent = elem.parentNode
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Re-positions the element.
|
|
152
|
+
* @param {module:history.HistoryEventHandler} handler
|
|
153
|
+
* @fires module:history~Command#event:history
|
|
154
|
+
* @returns {void}
|
|
155
|
+
*/
|
|
156
|
+
apply (handler) {
|
|
157
|
+
super.apply(handler, () => {
|
|
158
|
+
this.elem = this.newParent.insertBefore(this.elem, this.newNextSibling)
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Positions the element back to its original location.
|
|
164
|
+
* @param {module:history.HistoryEventHandler} handler
|
|
165
|
+
* @fires module:history~Command#event:history
|
|
166
|
+
* @returns {void}
|
|
167
|
+
*/
|
|
168
|
+
unapply (handler) {
|
|
169
|
+
super.unapply(handler, () => {
|
|
170
|
+
this.elem = this.oldParent.insertBefore(this.elem, this.oldNextSibling)
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* History command for an element that was added to the DOM.
|
|
177
|
+
* @implements {module:history.HistoryCommand}
|
|
178
|
+
*/
|
|
179
|
+
export class InsertElementCommand extends Command {
|
|
180
|
+
/**
|
|
181
|
+
* @param {Element} elem - The newly added DOM element
|
|
182
|
+
* @param {string} text - An optional string visible to user related to this change
|
|
183
|
+
*/
|
|
184
|
+
constructor (elem, text) {
|
|
185
|
+
super()
|
|
186
|
+
this.elem = elem
|
|
187
|
+
this.text = text || ('Create ' + elem.tagName)
|
|
188
|
+
this.parent = elem.parentNode
|
|
189
|
+
this.nextSibling = this.elem.nextSibling
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Re-inserts the new element.
|
|
194
|
+
* @param {module:history.HistoryEventHandler} handler
|
|
195
|
+
* @fires module:history~Command#event:history
|
|
196
|
+
* @returns {void}
|
|
197
|
+
*/
|
|
198
|
+
apply (handler) {
|
|
199
|
+
super.apply(handler, () => {
|
|
200
|
+
this.elem = this.parent.insertBefore(this.elem, this.nextSibling)
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Removes the element.
|
|
206
|
+
* @param {module:history.HistoryEventHandler} handler
|
|
207
|
+
* @fires module:history~Command#event:history
|
|
208
|
+
* @returns {void}
|
|
209
|
+
*/
|
|
210
|
+
unapply (handler) {
|
|
211
|
+
super.unapply(handler, () => {
|
|
212
|
+
this.parent = this.elem.parentNode
|
|
213
|
+
this.elem.remove()
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* History command for an element removed from the DOM.
|
|
220
|
+
* @implements {module:history.HistoryCommand}
|
|
221
|
+
*/
|
|
222
|
+
export class RemoveElementCommand extends Command {
|
|
223
|
+
/**
|
|
224
|
+
* @param {Element} elem - The removed DOM element
|
|
225
|
+
* @param {Node} oldNextSibling - The DOM element's nextSibling when it was in the DOM
|
|
226
|
+
* @param {Element} oldParent - The DOM element's parent
|
|
227
|
+
* @param {string} [text] - An optional string visible to user related to this change
|
|
228
|
+
*/
|
|
229
|
+
constructor (elem, oldNextSibling, oldParent, text) {
|
|
230
|
+
super()
|
|
231
|
+
this.elem = elem
|
|
232
|
+
this.text = text || ('Delete ' + elem.tagName)
|
|
233
|
+
this.nextSibling = oldNextSibling
|
|
234
|
+
this.parent = oldParent
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Re-removes the new element.
|
|
239
|
+
* @param {module:history.HistoryEventHandler} handler
|
|
240
|
+
* @fires module:history~Command#event:history
|
|
241
|
+
* @returns {void}
|
|
242
|
+
*/
|
|
243
|
+
apply (handler) {
|
|
244
|
+
super.apply(handler, () => {
|
|
245
|
+
this.parent = this.elem.parentNode
|
|
246
|
+
this.elem.remove()
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Re-adds the new element.
|
|
252
|
+
* @param {module:history.HistoryEventHandler} handler
|
|
253
|
+
* @fires module:history~Command#event:history
|
|
254
|
+
* @returns {void}
|
|
255
|
+
*/
|
|
256
|
+
unapply (handler) {
|
|
257
|
+
super.unapply(handler, () => {
|
|
258
|
+
if (!this.nextSibling) {
|
|
259
|
+
console.error('Reference element was lost')
|
|
260
|
+
}
|
|
261
|
+
this.parent.insertBefore(this.elem, this.nextSibling) // Don't use `before` or `prepend` as `this.nextSibling` may be `null`
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* @typedef {"#text"|"#href"|string} module:history.CommandAttributeName
|
|
268
|
+
*/
|
|
269
|
+
/**
|
|
270
|
+
* @typedef {PlainObject<module:history.CommandAttributeName, string>} module:history.CommandAttributes
|
|
271
|
+
*/
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* History command to make a change to an element.
|
|
275
|
+
* Usually an attribute change, but can also be textcontent.
|
|
276
|
+
* @implements {module:history.HistoryCommand}
|
|
277
|
+
*/
|
|
278
|
+
export class ChangeElementCommand extends Command {
|
|
279
|
+
/**
|
|
280
|
+
* @param {Element} elem - The DOM element that was changed
|
|
281
|
+
* @param {module:history.CommandAttributes} attrs - Attributes to be changed with the values they had *before* the change
|
|
282
|
+
* @param {string} text - An optional string visible to user related to this change
|
|
283
|
+
*/
|
|
284
|
+
constructor (elem, attrs, text) {
|
|
285
|
+
super()
|
|
286
|
+
this.elem = elem
|
|
287
|
+
this.text = text ? ('Change ' + elem.tagName + ' ' + text) : ('Change ' + elem.tagName)
|
|
288
|
+
this.newValues = {}
|
|
289
|
+
this.oldValues = attrs
|
|
290
|
+
for (const attr in attrs) {
|
|
291
|
+
if (attr === '#text') {
|
|
292
|
+
this.newValues[attr] = (elem) ? elem.textContent : ''
|
|
293
|
+
} else if (attr === '#href') {
|
|
294
|
+
this.newValues[attr] = getHref(elem)
|
|
295
|
+
} else {
|
|
296
|
+
this.newValues[attr] = elem.getAttribute(attr)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Performs the stored change action.
|
|
303
|
+
* @param {module:history.HistoryEventHandler} handler
|
|
304
|
+
* @fires module:history~Command#event:history
|
|
305
|
+
* @returns {void}
|
|
306
|
+
*/
|
|
307
|
+
apply (handler) {
|
|
308
|
+
super.apply(handler, () => {
|
|
309
|
+
let bChangedTransform = false
|
|
310
|
+
Object.entries(this.newValues).forEach(([attr, value]) => {
|
|
311
|
+
if (value) {
|
|
312
|
+
if (attr === '#text') {
|
|
313
|
+
this.elem.textContent = value
|
|
314
|
+
} else if (attr === '#href') {
|
|
315
|
+
setHref(this.elem, value)
|
|
316
|
+
} else {
|
|
317
|
+
this.elem.setAttribute(attr, value)
|
|
318
|
+
}
|
|
319
|
+
} else if (attr === '#text') {
|
|
320
|
+
this.elem.textContent = ''
|
|
321
|
+
} else {
|
|
322
|
+
this.elem.setAttribute(attr, '')
|
|
323
|
+
this.elem.removeAttribute(attr)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (attr === 'transform') { bChangedTransform = true }
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
// relocate rotational transform, if necessary
|
|
330
|
+
if (!bChangedTransform) {
|
|
331
|
+
const angle = getRotationAngle(this.elem)
|
|
332
|
+
if (angle) {
|
|
333
|
+
const bbox = getBBox(this.elem)
|
|
334
|
+
const cx = bbox.x + bbox.width / 2
|
|
335
|
+
const cy = bbox.y + bbox.height / 2
|
|
336
|
+
const rotate = ['rotate(', angle, ' ', cx, ',', cy, ')'].join('')
|
|
337
|
+
if (rotate !== this.elem.getAttribute('transform')) {
|
|
338
|
+
this.elem.setAttribute('transform', rotate)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Reverses the stored change action.
|
|
347
|
+
* @param {module:history.HistoryEventHandler} handler
|
|
348
|
+
* @fires module:history~Command#event:history
|
|
349
|
+
* @returns {void}
|
|
350
|
+
*/
|
|
351
|
+
unapply (handler) {
|
|
352
|
+
super.unapply(handler, () => {
|
|
353
|
+
let bChangedTransform = false
|
|
354
|
+
Object.entries(this.oldValues).forEach(([attr, value]) => {
|
|
355
|
+
if (value) {
|
|
356
|
+
if (attr === '#text') {
|
|
357
|
+
this.elem.textContent = value
|
|
358
|
+
} else if (attr === '#href') {
|
|
359
|
+
setHref(this.elem, value)
|
|
360
|
+
} else {
|
|
361
|
+
this.elem.setAttribute(attr, value)
|
|
362
|
+
}
|
|
363
|
+
} else if (attr === '#text') {
|
|
364
|
+
this.elem.textContent = ''
|
|
365
|
+
} else {
|
|
366
|
+
this.elem.removeAttribute(attr)
|
|
367
|
+
}
|
|
368
|
+
if (attr === 'transform') { bChangedTransform = true }
|
|
369
|
+
})
|
|
370
|
+
// relocate rotational transform, if necessary
|
|
371
|
+
if (!bChangedTransform) {
|
|
372
|
+
const angle = getRotationAngle(this.elem)
|
|
373
|
+
if (angle) {
|
|
374
|
+
const bbox = getBBox(this.elem)
|
|
375
|
+
const cx = bbox.x + bbox.width / 2
|
|
376
|
+
const cy = bbox.y + bbox.height / 2
|
|
377
|
+
const rotate = ['rotate(', angle, ' ', cx, ',', cy, ')'].join('')
|
|
378
|
+
if (rotate !== this.elem.getAttribute('transform')) {
|
|
379
|
+
this.elem.setAttribute('transform', rotate)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// TODO: create a 'typing' command object that tracks changes in text
|
|
388
|
+
// if a new Typing command is created and the top command on the stack is also a Typing
|
|
389
|
+
// and they both affect the same element, then collapse the two commands into one
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* History command that can contain/execute multiple other commands.
|
|
393
|
+
* @implements {module:history.HistoryCommand}
|
|
394
|
+
*/
|
|
395
|
+
export class BatchCommand extends Command {
|
|
396
|
+
/**
|
|
397
|
+
* @param {string} [text] - An optional string visible to user related to this change
|
|
398
|
+
*/
|
|
399
|
+
constructor (text) {
|
|
400
|
+
super()
|
|
401
|
+
this.text = text || 'Batch Command'
|
|
402
|
+
this.stack = []
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Runs "apply" on all subcommands.
|
|
407
|
+
* @param {module:history.HistoryEventHandler} handler
|
|
408
|
+
* @fires module:history~Command#event:history
|
|
409
|
+
* @returns {void}
|
|
410
|
+
*/
|
|
411
|
+
apply (handler) {
|
|
412
|
+
super.apply(handler, () => {
|
|
413
|
+
this.stack.forEach((stackItem) => {
|
|
414
|
+
console.assert(stackItem, 'stack item should not be null')
|
|
415
|
+
stackItem && stackItem.apply(handler)
|
|
416
|
+
})
|
|
417
|
+
})
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Runs "unapply" on all subcommands.
|
|
422
|
+
* @param {module:history.HistoryEventHandler} handler
|
|
423
|
+
* @fires module:history~Command#event:history
|
|
424
|
+
* @returns {void}
|
|
425
|
+
*/
|
|
426
|
+
unapply (handler) {
|
|
427
|
+
super.unapply(handler, () => {
|
|
428
|
+
this.stack.reverse().forEach((stackItem) => {
|
|
429
|
+
console.assert(stackItem, 'stack item should not be null')
|
|
430
|
+
stackItem && stackItem.unapply(handler)
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Iterate through all our subcommands.
|
|
437
|
+
* @returns {Element[]} All the elements we are changing
|
|
438
|
+
*/
|
|
439
|
+
elements () {
|
|
440
|
+
const elems = []
|
|
441
|
+
let cmd = this.stack.length
|
|
442
|
+
while (cmd--) {
|
|
443
|
+
if (!this.stack[cmd]) continue
|
|
444
|
+
const thisElems = this.stack[cmd].elements()
|
|
445
|
+
let elem = thisElems.length
|
|
446
|
+
while (elem--) {
|
|
447
|
+
if (!elems.includes(thisElems[elem])) { elems.push(thisElems[elem]) }
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return elems
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Adds a given command to the history stack.
|
|
455
|
+
* @param {Command} cmd - The undo command object to add
|
|
456
|
+
* @returns {void}
|
|
457
|
+
*/
|
|
458
|
+
addSubCommand (cmd) {
|
|
459
|
+
console.assert(cmd !== null, 'cmd should not be null')
|
|
460
|
+
this.stack.push(cmd)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* @returns {boolean} Indicates whether or not the batch command is empty
|
|
465
|
+
*/
|
|
466
|
+
isEmpty () {
|
|
467
|
+
return !this.stack.length
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
*
|
|
473
|
+
*/
|
|
474
|
+
export class UndoManager {
|
|
475
|
+
/**
|
|
476
|
+
* @param {module:history.HistoryEventHandler} historyEventHandler
|
|
477
|
+
*/
|
|
478
|
+
constructor (historyEventHandler) {
|
|
479
|
+
this.handler_ = historyEventHandler || null
|
|
480
|
+
this.undoStackPointer = 0
|
|
481
|
+
this.undoStack = []
|
|
482
|
+
|
|
483
|
+
// this is the stack that stores the original values, the elements and
|
|
484
|
+
// the attribute name for begin/finish
|
|
485
|
+
this.undoChangeStackPointer = -1
|
|
486
|
+
this.undoableChangeStack = []
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Resets the undo stack, effectively clearing the undo/redo history.
|
|
491
|
+
* @returns {void}
|
|
492
|
+
*/
|
|
493
|
+
resetUndoStack () {
|
|
494
|
+
this.undoStack = []
|
|
495
|
+
this.undoStackPointer = 0
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* @returns {Integer} Current size of the undo history stack
|
|
500
|
+
*/
|
|
501
|
+
getUndoStackSize () {
|
|
502
|
+
return this.undoStackPointer
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* @returns {Integer} Current size of the redo history stack
|
|
507
|
+
*/
|
|
508
|
+
getRedoStackSize () {
|
|
509
|
+
return this.undoStack.length - this.undoStackPointer
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* @returns {string} String associated with the next undo command
|
|
514
|
+
*/
|
|
515
|
+
getNextUndoCommandText () {
|
|
516
|
+
return this.undoStackPointer > 0 ? this.undoStack[this.undoStackPointer - 1].getText() : ''
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* @returns {string} String associated with the next redo command
|
|
521
|
+
*/
|
|
522
|
+
getNextRedoCommandText () {
|
|
523
|
+
return this.undoStackPointer < this.undoStack.length ? this.undoStack[this.undoStackPointer].getText() : ''
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Performs an undo step.
|
|
528
|
+
* @returns {void}
|
|
529
|
+
*/
|
|
530
|
+
undo () {
|
|
531
|
+
if (this.undoStackPointer > 0) {
|
|
532
|
+
const cmd = this.undoStack[--this.undoStackPointer]
|
|
533
|
+
cmd.unapply(this.handler_)
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Performs a redo step.
|
|
539
|
+
* @returns {void}
|
|
540
|
+
*/
|
|
541
|
+
redo () {
|
|
542
|
+
if (this.undoStackPointer < this.undoStack.length && this.undoStack.length > 0) {
|
|
543
|
+
const cmd = this.undoStack[this.undoStackPointer++]
|
|
544
|
+
cmd.apply(this.handler_)
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Adds a command object to the undo history stack.
|
|
550
|
+
* @param {Command} cmd - The command object to add
|
|
551
|
+
* @returns {void}
|
|
552
|
+
*/
|
|
553
|
+
addCommandToHistory (cmd) {
|
|
554
|
+
// TODO: we MUST compress consecutive text changes to the same element
|
|
555
|
+
// (right now each keystroke is saved as a separate command that includes the
|
|
556
|
+
// entire text contents of the text element)
|
|
557
|
+
// TODO: consider limiting the history that we store here (need to do some slicing)
|
|
558
|
+
|
|
559
|
+
// if our stack pointer is not at the end, then we have to remove
|
|
560
|
+
// all commands after the pointer and insert the new command
|
|
561
|
+
if (this.undoStackPointer < this.undoStack.length && this.undoStack.length > 0) {
|
|
562
|
+
this.undoStack = this.undoStack.splice(0, this.undoStackPointer)
|
|
563
|
+
}
|
|
564
|
+
this.undoStack.push(cmd)
|
|
565
|
+
this.undoStackPointer = this.undoStack.length
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* This function tells the canvas to remember the old values of the
|
|
570
|
+
* `attrName` attribute for each element sent in. The elements and values
|
|
571
|
+
* are stored on a stack, so the next call to `finishUndoableChange()` will
|
|
572
|
+
* pop the elements and old values off the stack, gets the current values
|
|
573
|
+
* from the DOM and uses all of these to construct the undo-able command.
|
|
574
|
+
* @param {string} attrName - The name of the attribute being changed
|
|
575
|
+
* @param {Element[]} elems - Array of DOM elements being changed
|
|
576
|
+
* @returns {void}
|
|
577
|
+
*/
|
|
578
|
+
beginUndoableChange (attrName, elems) {
|
|
579
|
+
const p = ++this.undoChangeStackPointer
|
|
580
|
+
let i = elems.length
|
|
581
|
+
const oldValues = new Array(i); const elements = new Array(i)
|
|
582
|
+
while (i--) {
|
|
583
|
+
const elem = elems[i]
|
|
584
|
+
if (!elem) { continue }
|
|
585
|
+
elements[i] = elem
|
|
586
|
+
oldValues[i] = elem.getAttribute(attrName)
|
|
587
|
+
}
|
|
588
|
+
this.undoableChangeStack[p] = {
|
|
589
|
+
attrName,
|
|
590
|
+
oldValues,
|
|
591
|
+
elements
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* This function returns a `BatchCommand` object which summarizes the
|
|
597
|
+
* change since `beginUndoableChange` was called. The command can then
|
|
598
|
+
* be added to the command history.
|
|
599
|
+
* @returns {BatchCommand} Batch command object with resulting changes
|
|
600
|
+
*/
|
|
601
|
+
finishUndoableChange () {
|
|
602
|
+
const p = this.undoChangeStackPointer--
|
|
603
|
+
const changeset = this.undoableChangeStack[p]
|
|
604
|
+
const { attrName } = changeset
|
|
605
|
+
const batchCmd = new BatchCommand('Change ' + attrName)
|
|
606
|
+
let i = changeset.elements.length
|
|
607
|
+
while (i--) {
|
|
608
|
+
const elem = changeset.elements[i]
|
|
609
|
+
if (!elem) { continue }
|
|
610
|
+
const changes = {}
|
|
611
|
+
changes[attrName] = changeset.oldValues[i]
|
|
612
|
+
if (changes[attrName] !== elem.getAttribute(attrName)) {
|
|
613
|
+
batchCmd.addSubCommand(new ChangeElementCommand(elem, changes, attrName))
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
this.undoableChangeStack[p] = null
|
|
617
|
+
return batchCmd
|
|
618
|
+
}
|
|
619
|
+
}
|