@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/undo.js ADDED
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Tools for undo.
3
+ * @module undo
4
+ * @license MIT
5
+ * @copyright 2011 Jeff Schiller
6
+ */
7
+ import * as draw from './draw.js'
8
+ import * as hstry from './history.js'
9
+ import {
10
+ getRotationAngle, getBBox as utilsGetBBox, setHref, getStrokedBBoxDefaultVisible
11
+ } from './utilities.js'
12
+ import {
13
+ isGecko
14
+ } from '../../src/common/browser.js'
15
+ import {
16
+ transformPoint, transformListToTransform
17
+ } from './math.js'
18
+
19
+ const {
20
+ UndoManager, HistoryEventTypes
21
+ } = hstry
22
+
23
+ let svgCanvas = null
24
+
25
+ /**
26
+ * @function module:undo.init
27
+ * @param {module:undo.undoContext} undoContext
28
+ * @returns {void}
29
+ */
30
+ export const init = (canvas) => {
31
+ svgCanvas = canvas
32
+ canvas.undoMgr = getUndoManager()
33
+ }
34
+
35
+ export const getUndoManager = () => {
36
+ return new UndoManager({
37
+ /**
38
+ * @param {string} eventType One of the HistoryEvent types
39
+ * @param {module:history.HistoryCommand} cmd Fulfills the HistoryCommand interface
40
+ * @fires module:undo.SvgCanvas#event:changed
41
+ * @returns {void}
42
+ */
43
+ handleHistoryEvent (eventType, cmd) {
44
+ const EventTypes = HistoryEventTypes
45
+ // TODO: handle setBlurOffsets.
46
+ if (eventType === EventTypes.BEFORE_UNAPPLY || eventType === EventTypes.BEFORE_APPLY) {
47
+ svgCanvas.clearSelection()
48
+ } else if (eventType === EventTypes.AFTER_APPLY || eventType === EventTypes.AFTER_UNAPPLY) {
49
+ const elems = cmd.elements()
50
+ svgCanvas.pathActions.clear()
51
+ svgCanvas.call('changed', elems)
52
+ const cmdType = cmd.type()
53
+ const isApply = (eventType === EventTypes.AFTER_APPLY)
54
+ if (cmdType === 'MoveElementCommand') {
55
+ const parent = isApply ? cmd.newParent : cmd.oldParent
56
+ if (parent === svgCanvas.getSvgContent()) {
57
+ draw.identifyLayers()
58
+ }
59
+ } else if (cmdType === 'InsertElementCommand' || cmdType === 'RemoveElementCommand') {
60
+ if (cmd.parent === svgCanvas.getSvgContent()) {
61
+ draw.identifyLayers()
62
+ }
63
+ if (cmdType === 'InsertElementCommand') {
64
+ if (isApply) {
65
+ svgCanvas.restoreRefElements(cmd.elem)
66
+ }
67
+ } else if (!isApply) {
68
+ svgCanvas.restoreRefElements(cmd.elem)
69
+ }
70
+ if (cmd.elem?.tagName === 'use') {
71
+ svgCanvas.setUseData(cmd.elem)
72
+ }
73
+ } else if (cmdType === 'ChangeElementCommand') {
74
+ // if we are changing layer names, re-identify all layers
75
+ if (cmd.elem.tagName === 'title' &&
76
+ cmd.elem.parentNode.parentNode === svgCanvas.getSvgContent()
77
+ ) {
78
+ draw.identifyLayers()
79
+ }
80
+ const values = isApply ? cmd.newValues : cmd.oldValues
81
+ // If stdDeviation was changed, update the blur.
82
+ if (values.stdDeviation) {
83
+ svgCanvas.setBlurOffsets(cmd.elem.parentNode, values.stdDeviation)
84
+ }
85
+ if (cmd.elem.tagName === 'text') {
86
+ const [dx, dy] = [cmd.newValues.x - cmd.oldValues.x,
87
+ cmd.newValues.y - cmd.oldValues.y]
88
+
89
+ const tspans = cmd.elem.children
90
+
91
+ for (let i = 0; i < tspans.length; i++) {
92
+ let x = Number(tspans[i].getAttribute('x'))
93
+ let y = Number(tspans[i].getAttribute('y'))
94
+
95
+ const unapply = (eventType === EventTypes.AFTER_UNAPPLY)
96
+ x = unapply ? x - dx : x + dx
97
+ y = unapply ? y - dy : y + dy
98
+
99
+ tspans[i].setAttribute('x', x)
100
+ tspans[i].setAttribute('y', y)
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }
106
+ })
107
+ }
108
+
109
+ /**
110
+ * Hack for Firefox bugs where text element features aren't updated or get
111
+ * messed up. See issue 136 and issue 137.
112
+ * This function clones the element and re-selects it.
113
+ * @function module:svgcanvas~ffClone
114
+ * @todo Test for this bug on load and add it to "support" object instead of
115
+ * browser sniffing
116
+ * @param {Element} elem - The (text) DOM element to clone
117
+ * @returns {Element} Cloned element
118
+ */
119
+ export const ffClone = function (elem) {
120
+ if (!isGecko()) { return elem }
121
+ const clone = elem.cloneNode(true)
122
+ elem.before(clone)
123
+ elem.remove()
124
+ svgCanvas.selectorManager.releaseSelector(elem)
125
+ svgCanvas.setSelectedElements(0, clone)
126
+ svgCanvas.selectorManager.requestSelector(clone).showGrips(true)
127
+ return clone
128
+ }
129
+
130
+ /**
131
+ * This function makes the changes to the elements. It does not add the change
132
+ * to the history stack.
133
+ * @param {string} attr - Attribute name
134
+ * @param {string|Float} newValue - String or number with the new attribute value
135
+ * @param {Element[]} elems - The DOM elements to apply the change to
136
+ * @returns {void}
137
+ */
138
+ export const changeSelectedAttributeNoUndoMethod = (attr, newValue, elems) => {
139
+ if (attr === 'id') {
140
+ // if the user is changing the id, then de-select the element first
141
+ // change the ID, then re-select it with the new ID
142
+ // as this change can impact other extensions, a 'renamedElement' event is thrown
143
+ const elem = elems[0]
144
+ const oldId = elem.id
145
+ if (oldId !== newValue) {
146
+ svgCanvas.clearSelection()
147
+ elem.id = newValue
148
+ svgCanvas.addToSelection([elem], true)
149
+ svgCanvas.call('elementRenamed', { elem, oldId, newId: newValue })
150
+ }
151
+ return
152
+ }
153
+ const selectedElements = svgCanvas.getSelectedElements()
154
+ const zoom = svgCanvas.getZoom()
155
+ if (svgCanvas.getCurrentMode() === 'pathedit') {
156
+ // Editing node
157
+ svgCanvas.pathActions.moveNode(attr, newValue)
158
+ }
159
+ elems = elems ?? selectedElements
160
+ let i = elems.length
161
+ const noXYElems = ['g', 'polyline', 'path']
162
+
163
+ while (i--) {
164
+ let elem = elems[i]
165
+ if (!elem) { continue }
166
+
167
+ // Set x,y vals on elements that don't have them
168
+ if ((attr === 'x' || attr === 'y') && noXYElems.includes(elem.tagName)) {
169
+ const bbox = getStrokedBBoxDefaultVisible([elem])
170
+ const diffX = attr === 'x' ? newValue - bbox.x : 0
171
+ const diffY = attr === 'y' ? newValue - bbox.y : 0
172
+ svgCanvas.moveSelectedElements(diffX * zoom, diffY * zoom, true)
173
+ continue
174
+ }
175
+
176
+ let oldval = attr === '#text' ? elem.textContent : elem.getAttribute(attr)
177
+ if (!oldval) { oldval = '' }
178
+ if (oldval !== String(newValue)) {
179
+ if (attr === '#text') {
180
+ // const oldW = utilsGetBBox(elem).width;
181
+ elem.textContent = newValue
182
+
183
+ // FF bug occurs on on rotated elements
184
+ if ((/rotate/).test(elem.getAttribute('transform'))) {
185
+ elem = ffClone(elem)
186
+ }
187
+ // Hoped to solve the issue of moving text with text-anchor="start",
188
+ // but this doesn't actually fix it. Hopefully on the right track, though. -Fyrd
189
+ } else if (attr === '#href') {
190
+ setHref(elem, newValue)
191
+ } else if (newValue) {
192
+ elem.setAttribute(attr, newValue)
193
+ } else if (typeof newValue === 'number') {
194
+ elem.setAttribute(attr, newValue)
195
+ } else {
196
+ elem.removeAttribute(attr)
197
+ }
198
+
199
+ // Go into "select" mode for text changes
200
+ // NOTE: Important that this happens AFTER elem.setAttribute() or else attributes like
201
+ // font-size can get reset to their old value, ultimately by svgEditor.updateContextPanel(),
202
+ // after calling textActions.toSelectMode() below
203
+ if (svgCanvas.getCurrentMode() === 'textedit' && attr !== '#text' && elem.textContent.length) {
204
+ svgCanvas.textActions.toSelectMode(elem)
205
+ }
206
+
207
+ // Use the Firefox ffClone hack for text elements with gradients or
208
+ // where other text attributes are changed.
209
+ if (isGecko() &&
210
+ elem.nodeName === 'text' &&
211
+ (/rotate/).test(elem.getAttribute('transform')) &&
212
+ (String(newValue).startsWith('url') || (['font-size', 'font-family', 'x', 'y'].includes(attr) && elem.textContent))) {
213
+ elem = ffClone(elem)
214
+ }
215
+ // Timeout needed for Opera & Firefox
216
+ // codedread: it is now possible for this function to be called with elements
217
+ // that are not in the selectedElements array, we need to only request a
218
+ // selector if the element is in that array
219
+ if (selectedElements.includes(elem)) {
220
+ setTimeout(function () {
221
+ // Due to element replacement, this element may no longer
222
+ // be part of the DOM
223
+ if (!elem.parentNode) { return }
224
+ svgCanvas.selectorManager.requestSelector(elem).resize()
225
+ }, 0)
226
+ }
227
+ // if this element was rotated, and we changed the position of this element
228
+ // we need to update the rotational transform attribute
229
+ const angle = getRotationAngle(elem)
230
+ if (angle !== 0 && attr !== 'transform') {
231
+ const tlist = elem.transform?.baseVal
232
+ let n = tlist.numberOfItems
233
+ while (n--) {
234
+ const xform = tlist.getItem(n)
235
+ if (xform.type === 4) {
236
+ // remove old rotate
237
+ tlist.removeItem(n)
238
+
239
+ const box = utilsGetBBox(elem)
240
+ const center = transformPoint(
241
+ box.x + box.width / 2, box.y + box.height / 2, transformListToTransform(tlist).matrix
242
+ )
243
+ const cx = center.x
244
+ const cy = center.y
245
+ const newrot = svgCanvas.getSvgRoot().createSVGTransform()
246
+ newrot.setRotate(angle, cx, cy)
247
+ tlist.insertItemBefore(newrot, n)
248
+ break
249
+ }
250
+ }
251
+ }
252
+ } // if oldValue != newValue
253
+ } // for each elem
254
+ }
255
+
256
+ /**
257
+ * Change the given/selected element and add the original value to the history stack.
258
+ * If you want to change all `selectedElements`, ignore the `elems` argument.
259
+ * If you want to change only a subset of `selectedElements`, then send the
260
+ * subset to this function in the `elems` argument.
261
+ * @function module:svgcanvas.SvgCanvas#changeSelectedAttribute
262
+ * @param {string} attr - String with the attribute name
263
+ * @param {string|Float} val - String or number with the new attribute value
264
+ * @param {Element[]} elems - The DOM elements to apply the change to
265
+ * @returns {void}
266
+ */
267
+ export const changeSelectedAttributeMethod = function (attr, val, elems) {
268
+ const selectedElements = svgCanvas.getSelectedElements()
269
+ elems = elems || selectedElements
270
+ svgCanvas.undoMgr.beginUndoableChange(attr, elems)
271
+
272
+ changeSelectedAttributeNoUndoMethod(attr, val, elems)
273
+
274
+ const batchCmd = svgCanvas.undoMgr.finishUndoableChange()
275
+ if (!batchCmd.isEmpty()) {
276
+ // svgCanvas.addCommandToHistory(batchCmd);
277
+ svgCanvas.undoMgr.addCommandToHistory(batchCmd)
278
+ }
279
+ }