@svgedit/svgcanvas 7.2.7 → 7.4.1

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/core/layer.js CHANGED
@@ -40,19 +40,16 @@ class Layer {
40
40
  const layerTitle = svgdoc.createElementNS(NS.SVG, 'title')
41
41
  layerTitle.textContent = name
42
42
  this.group_.append(layerTitle)
43
- if (group) {
44
- group.insertAdjacentElement('afterend', this.group_)
45
- } else {
46
- svgElem.append(this.group_)
47
- }
43
+
44
+ group ? group.insertAdjacentElement('afterend', this.group_) : svgElem.append(this.group_)
48
45
  }
49
46
 
50
47
  addLayerClass(this.group_)
51
48
  walkTree(this.group_, function (e) {
52
- e.setAttribute('style', 'pointer-events:inherit')
49
+ e.style.pointerEvents = 'inherit'
53
50
  })
54
51
 
55
- this.group_.setAttribute('style', svgElem ? 'pointer-events:all' : 'pointer-events:none')
52
+ this.group_.style.pointerEvents = svgElem ? 'all' : 'none'
56
53
  }
57
54
 
58
55
  /**
@@ -76,7 +73,7 @@ class Layer {
76
73
  * @returns {void}
77
74
  */
78
75
  activate () {
79
- this.group_.setAttribute('style', 'pointer-events:all')
76
+ this.group_.style.pointerEvents = 'all'
80
77
  }
81
78
 
82
79
  /**
@@ -84,7 +81,7 @@ class Layer {
84
81
  * @returns {void}
85
82
  */
86
83
  deactivate () {
87
- this.group_.setAttribute('style', 'pointer-events:none')
84
+ this.group_.style.pointerEvents = 'none'
88
85
  }
89
86
 
90
87
  /**
@@ -93,7 +90,7 @@ class Layer {
93
90
  * @returns {void}
94
91
  */
95
92
  setVisible (visible) {
96
- const expected = visible === undefined || visible ? 'inline' : 'none'
93
+ const expected = (visible === undefined || visible) ? 'inline' : 'none'
97
94
  const oldDisplay = this.group_.getAttribute('display')
98
95
  if (oldDisplay !== expected) {
99
96
  this.group_.setAttribute('display', expected)
@@ -114,10 +111,7 @@ class Layer {
114
111
  */
115
112
  getOpacity () {
116
113
  const opacity = this.group_.getAttribute('opacity')
117
- if (!opacity) {
118
- return 1
119
- }
120
- return Number.parseFloat(opacity)
114
+ return opacity ? Number.parseFloat(opacity) : 1
121
115
  }
122
116
 
123
117
  /**
@@ -208,7 +202,7 @@ Layer.CLASS_NAME = 'layer'
208
202
  /**
209
203
  * @property {RegExp} CLASS_REGEX - Used to test presence of class Layer.CLASS_NAME
210
204
  */
211
- Layer.CLASS_REGEX = new RegExp('(\\s|^)' + Layer.CLASS_NAME + '(\\s|$)')
205
+ Layer.CLASS_REGEX = new RegExp(`(\\s|^)${Layer.CLASS_NAME}(\\s|$)`)
212
206
 
213
207
  /**
214
208
  * Add class `Layer.CLASS_NAME` to the element (usually `class='layer'`).
@@ -216,12 +210,12 @@ Layer.CLASS_REGEX = new RegExp('(\\s|^)' + Layer.CLASS_NAME + '(\\s|$)')
216
210
  * @param {SVGGElement} elem - The SVG element to update
217
211
  * @returns {void}
218
212
  */
219
- function addLayerClass (elem) {
213
+ const addLayerClass = (elem) => {
220
214
  const classes = elem.getAttribute('class')
221
215
  if (!classes || !classes.length) {
222
216
  elem.setAttribute('class', Layer.CLASS_NAME)
223
217
  } else if (!Layer.CLASS_REGEX.test(classes)) {
224
- elem.setAttribute('class', classes + ' ' + Layer.CLASS_NAME)
218
+ elem.setAttribute('class', `${classes} ${Layer.CLASS_NAME}`)
225
219
  }
226
220
  }
227
221
 
package/core/math.js CHANGED
@@ -20,6 +20,7 @@
20
20
  */
21
21
 
22
22
  import { NS } from './namespaces.js'
23
+ import { warn } from '../common/logger.js'
23
24
 
24
25
  // Constants
25
26
  const NEAR_ZERO = 1e-10
@@ -27,6 +28,38 @@ const NEAR_ZERO = 1e-10
27
28
  // Create a throwaway SVG element for matrix operations
28
29
  const svg = document.createElementNS(NS.SVG, 'svg')
29
30
 
31
+ const createTransformFromMatrix = (m) => {
32
+ const createFallback = (matrix) => {
33
+ const fallback = svg.createSVGMatrix()
34
+ Object.assign(fallback, {
35
+ a: matrix.a,
36
+ b: matrix.b,
37
+ c: matrix.c,
38
+ d: matrix.d,
39
+ e: matrix.e,
40
+ f: matrix.f
41
+ })
42
+ return fallback
43
+ }
44
+
45
+ try {
46
+ return svg.createSVGTransformFromMatrix(m)
47
+ } catch (e) {
48
+ const t = svg.createSVGTransform()
49
+ try {
50
+ t.setMatrix(m)
51
+ return t
52
+ } catch (err) {
53
+ try {
54
+ return svg.createSVGTransformFromMatrix(createFallback(m))
55
+ } catch (e2) {
56
+ t.setMatrix(createFallback(m))
57
+ return t
58
+ }
59
+ }
60
+ }
61
+ }
62
+
30
63
  /**
31
64
  * Transforms a point by a given matrix without DOM calls.
32
65
  * @function transformPoint
@@ -56,7 +89,7 @@ export const getTransformList = elem => {
56
89
  if (elem.patternTransform?.baseVal) {
57
90
  return elem.patternTransform.baseVal
58
91
  }
59
- console.warn('No transform list found. Check browser compatibility.', elem)
92
+ warn('No transform list found. Check browser compatibility.', elem, 'math')
60
93
  }
61
94
 
62
95
  /**
@@ -66,7 +99,12 @@ export const getTransformList = elem => {
66
99
  * @returns {boolean} True if it's an identity matrix (1,0,0,1,0,0)
67
100
  */
68
101
  export const isIdentity = m =>
69
- m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && m.e === 0 && m.f === 0
102
+ Math.abs(m.a - 1) < NEAR_ZERO &&
103
+ Math.abs(m.b) < NEAR_ZERO &&
104
+ Math.abs(m.c) < NEAR_ZERO &&
105
+ Math.abs(m.d - 1) < NEAR_ZERO &&
106
+ Math.abs(m.e) < NEAR_ZERO &&
107
+ Math.abs(m.f) < NEAR_ZERO
70
108
 
71
109
  /**
72
110
  * Multiplies multiple matrices together (m1 * m2 * ...).
@@ -76,22 +114,54 @@ export const isIdentity = m =>
76
114
  * @returns {SVGMatrix} The resulting matrix
77
115
  */
78
116
  export const matrixMultiply = (...args) => {
79
- // If no matrices are given, return an identity matrix
80
117
  if (args.length === 0) {
81
118
  return svg.createSVGMatrix()
82
119
  }
83
120
 
84
- const m = args.reduceRight((prev, curr) => curr.multiply(prev))
121
+ const normalizeNearZero = (matrix) => {
122
+ const props = ['a', 'b', 'c', 'd', 'e', 'f']
123
+ for (const prop of props) {
124
+ if (Math.abs(matrix[prop]) < NEAR_ZERO) {
125
+ matrix[prop] = 0
126
+ }
127
+ }
128
+ return matrix
129
+ }
85
130
 
86
- // Round near-zero values to zero
87
- if (Math.abs(m.a) < NEAR_ZERO) m.a = 0
88
- if (Math.abs(m.b) < NEAR_ZERO) m.b = 0
89
- if (Math.abs(m.c) < NEAR_ZERO) m.c = 0
90
- if (Math.abs(m.d) < NEAR_ZERO) m.d = 0
91
- if (Math.abs(m.e) < NEAR_ZERO) m.e = 0
92
- if (Math.abs(m.f) < NEAR_ZERO) m.f = 0
131
+ if (typeof DOMMatrix === 'function' && typeof DOMMatrix.fromMatrix === 'function') {
132
+ const result = args.reduce(
133
+ (acc, curr) => acc.multiply(DOMMatrix.fromMatrix(curr)),
134
+ new DOMMatrix()
135
+ )
93
136
 
94
- return m
137
+ const out = svg.createSVGMatrix()
138
+ Object.assign(out, {
139
+ a: result.a,
140
+ b: result.b,
141
+ c: result.c,
142
+ d: result.d,
143
+ e: result.e,
144
+ f: result.f
145
+ })
146
+
147
+ return normalizeNearZero(out)
148
+ }
149
+
150
+ let m = svg.createSVGMatrix()
151
+ for (const curr of args) {
152
+ const next = svg.createSVGMatrix()
153
+ Object.assign(next, {
154
+ a: m.a * curr.a + m.c * curr.b,
155
+ b: m.b * curr.a + m.d * curr.b,
156
+ c: m.a * curr.c + m.c * curr.d,
157
+ d: m.b * curr.c + m.d * curr.d,
158
+ e: m.a * curr.e + m.c * curr.f + m.e,
159
+ f: m.b * curr.e + m.d * curr.f + m.f
160
+ })
161
+ m = next
162
+ }
163
+
164
+ return normalizeNearZero(m)
95
165
  }
96
166
 
97
167
  /**
@@ -172,25 +242,34 @@ export const transformBox = (l, t, w, h, m) => {
172
242
  */
173
243
  export const transformListToTransform = (tlist, min = 0, max = null) => {
174
244
  if (!tlist) {
175
- return svg.createSVGTransformFromMatrix(svg.createSVGMatrix())
245
+ return createTransformFromMatrix(svg.createSVGMatrix())
176
246
  }
177
247
 
178
248
  const start = Number.parseInt(min, 10)
179
249
  const end = Number.parseInt(max ?? tlist.numberOfItems - 1, 10)
180
- const low = Math.min(start, end)
181
- const high = Math.max(start, end)
250
+ const [low, high] = [Math.min(start, end), Math.max(start, end)]
182
251
 
183
- let combinedMatrix = svg.createSVGMatrix()
252
+ const matrices = []
184
253
  for (let i = low; i <= high; i++) {
185
- // If out of range, use identity
186
- const currentMatrix =
187
- i >= 0 && i < tlist.numberOfItems
188
- ? tlist.getItem(i).matrix
189
- : svg.createSVGMatrix()
190
- combinedMatrix = matrixMultiply(combinedMatrix, currentMatrix)
254
+ const matrix = (i >= 0 && i < tlist.numberOfItems)
255
+ ? tlist.getItem(i).matrix
256
+ : svg.createSVGMatrix()
257
+ matrices.push(matrix)
191
258
  }
192
259
 
193
- return svg.createSVGTransformFromMatrix(combinedMatrix)
260
+ const combinedMatrix = matrixMultiply(...matrices)
261
+
262
+ const out = svg.createSVGMatrix()
263
+ Object.assign(out, {
264
+ a: combinedMatrix.a,
265
+ b: combinedMatrix.b,
266
+ c: combinedMatrix.c,
267
+ d: combinedMatrix.d,
268
+ e: combinedMatrix.e,
269
+ f: combinedMatrix.f
270
+ })
271
+
272
+ return createTransformFromMatrix(out)
194
273
  }
195
274
 
196
275
  /**
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  /**
8
- * Common namepaces constants in alpha order.
8
+ * Common namespaces constants in alpha order.
9
9
  * @enum {string}
10
10
  * @type {PlainObject}
11
11
  * @memberof module:namespaces
@@ -29,12 +29,12 @@ export const NS = {
29
29
 
30
30
  /**
31
31
  * @function module:namespaces.getReverseNS
32
- * @returns {string} The NS with key values switched and lowercase
32
+ * @returns {PlainObject<string, string>} The namespace URI map with values swapped to their lowercase keys
33
33
  */
34
- export const getReverseNS = function () {
34
+ export const getReverseNS = () => {
35
35
  const reverseNS = {}
36
- Object.entries(NS).forEach(([name, URI]) => {
36
+ for (const [name, URI] of Object.entries(NS)) {
37
37
  reverseNS[URI] = name.toLowerCase()
38
- })
38
+ }
39
39
  return reverseNS
40
40
  }
package/core/paint.js CHANGED
@@ -2,12 +2,95 @@
2
2
  *
3
3
  */
4
4
  export default class Paint {
5
+ static #normalizeAlpha (alpha) {
6
+ const numeric = Number(alpha)
7
+ if (!Number.isFinite(numeric)) return 100
8
+ return Math.min(100, Math.max(0, numeric))
9
+ }
10
+
11
+ static #normalizeSolidColor (color) {
12
+ if (color === null || color === undefined) return null
13
+ const str = String(color).trim()
14
+ if (!str) return null
15
+ if (str === 'none') return 'none'
16
+ return str.startsWith('#') ? str.slice(1) : str
17
+ }
18
+
19
+ static #extractHrefId (hrefAttr) {
20
+ if (!hrefAttr) return null
21
+ const href = String(hrefAttr).trim()
22
+ if (!href) return null
23
+ if (href.startsWith('#')) return href.slice(1)
24
+ const urlMatch = href.match(/url\(\s*['"]?#([^'")\s]+)['"]?\s*\)/)
25
+ if (urlMatch?.[1]) return urlMatch[1]
26
+ const hashIndex = href.lastIndexOf('#')
27
+ if (hashIndex >= 0 && hashIndex < href.length - 1) {
28
+ return href.slice(hashIndex + 1)
29
+ }
30
+ return null
31
+ }
32
+
33
+ static #resolveGradient (gradient) {
34
+ if (!gradient?.cloneNode) return null
35
+ const doc = gradient.ownerDocument || document
36
+ const visited = new Set()
37
+ const clone = gradient.cloneNode(true)
38
+
39
+ let refId = Paint.#extractHrefId(
40
+ clone.getAttribute('href') || clone.getAttribute('xlink:href')
41
+ )
42
+
43
+ while (refId && !visited.has(refId)) {
44
+ visited.add(refId)
45
+
46
+ const referenced = doc.getElementById(refId)
47
+ if (!referenced?.getAttribute) break
48
+
49
+ const cloneTag = String(clone.tagName || '').toLowerCase()
50
+ const referencedTag = String(referenced.tagName || '').toLowerCase()
51
+ if (
52
+ !['lineargradient', 'radialgradient'].includes(referencedTag) ||
53
+ referencedTag !== cloneTag
54
+ ) {
55
+ break
56
+ }
57
+
58
+ // Copy missing attributes from referenced gradient (matches SVG href inheritance).
59
+ for (const attr of referenced.attributes || []) {
60
+ const name = attr.name
61
+ if (name === 'id' || name === 'href' || name === 'xlink:href') continue
62
+ const current = clone.getAttribute(name)
63
+ if (current === null || current === '') {
64
+ clone.setAttribute(name, attr.value)
65
+ }
66
+ }
67
+
68
+ // If the referencing gradient has no stops, inherit stops from the referenced gradient.
69
+ if (clone.querySelectorAll('stop').length === 0) {
70
+ for (const stop of referenced.querySelectorAll?.('stop') || []) {
71
+ clone.append(stop.cloneNode(true))
72
+ }
73
+ }
74
+
75
+ // Prepare to continue resolving deeper links if present.
76
+ refId = Paint.#extractHrefId(
77
+ referenced.getAttribute('href') || referenced.getAttribute('xlink:href')
78
+ )
79
+ }
80
+
81
+ // The clone is now self-contained; remove any href.
82
+ clone.removeAttribute('href')
83
+ clone.removeAttribute('xlink:href')
84
+
85
+ return clone
86
+ }
87
+
5
88
  /**
6
89
  * @param {module:jGraduate.jGraduatePaintOptions} [opt]
7
90
  */
8
91
  constructor (opt) {
9
92
  const options = opt || {}
10
- this.alpha = isNaN(options.alpha) ? 100 : options.alpha
93
+ this.alpha = Paint.#normalizeAlpha(options.alpha)
11
94
  // copy paint object
12
95
  if (options.copy) {
13
96
  /**
@@ -20,7 +103,7 @@ export default class Paint {
20
103
  * @name module:jGraduate~Paint#alpha
21
104
  * @type {Float}
22
105
  */
23
- this.alpha = options.copy.alpha
106
+ this.alpha = Paint.#normalizeAlpha(options.copy.alpha)
24
107
  /**
25
108
  * Represents #RRGGBB hex of color.
26
109
  * @name module:jGraduate~Paint#solidColor
@@ -42,13 +125,17 @@ export default class Paint {
42
125
  case 'none':
43
126
  break
44
127
  case 'solidColor':
45
- this.solidColor = options.copy.solidColor
128
+ this.solidColor = Paint.#normalizeSolidColor(options.copy.solidColor)
46
129
  break
47
130
  case 'linearGradient':
48
- this.linearGradient = options.copy.linearGradient.cloneNode(true)
131
+ this.linearGradient = options.copy.linearGradient?.cloneNode
132
+ ? options.copy.linearGradient.cloneNode(true)
133
+ : null
49
134
  break
50
135
  case 'radialGradient':
51
- this.radialGradient = options.copy.radialGradient.cloneNode(true)
136
+ this.radialGradient = options.copy.radialGradient?.cloneNode
137
+ ? options.copy.radialGradient.cloneNode(true)
138
+ : null
52
139
  break
53
140
  }
54
141
  // create linear gradient paint
@@ -56,33 +143,17 @@ export default class Paint {
56
143
  this.type = 'linearGradient'
57
144
  this.solidColor = null
58
145
  this.radialGradient = null
59
- const hrefAttr =
60
- options.linearGradient.getAttribute('href') ||
61
- options.linearGradient.getAttribute('xlink:href')
62
- if (hrefAttr) {
63
- const xhref = document.getElementById(hrefAttr.replace(/^#/, ''))
64
- this.linearGradient = xhref.cloneNode(true)
65
- } else {
66
- this.linearGradient = options.linearGradient.cloneNode(true)
67
- }
146
+ this.linearGradient = Paint.#resolveGradient(options.linearGradient)
68
147
  // create linear gradient paint
69
148
  } else if (options.radialGradient) {
70
149
  this.type = 'radialGradient'
71
150
  this.solidColor = null
72
151
  this.linearGradient = null
73
- const hrefAttr =
74
- options.radialGradient.getAttribute('href') ||
75
- options.radialGradient.getAttribute('xlink:href')
76
- if (hrefAttr) {
77
- const xhref = document.getElementById(hrefAttr.replace(/^#/, ''))
78
- this.radialGradient = xhref.cloneNode(true)
79
- } else {
80
- this.radialGradient = options.radialGradient.cloneNode(true)
81
- }
152
+ this.radialGradient = Paint.#resolveGradient(options.radialGradient)
82
153
  // create solid color paint
83
154
  } else if (options.solidColor) {
84
155
  this.type = 'solidColor'
85
- this.solidColor = options.solidColor
156
+ this.solidColor = Paint.#normalizeSolidColor(options.solidColor)
86
157
  // create empty paint
87
158
  } else {
88
159
  this.type = 'none'
@@ -1,5 +1,6 @@
1
1
  import {
2
- getStrokedBBoxDefaultVisible
2
+ getStrokedBBoxDefaultVisible,
3
+ getUrlFromAttr
3
4
  } from './utilities.js'
4
5
  import * as hstry from './history.js'
5
6
 
@@ -27,11 +28,15 @@ export const init = (canvas) => {
27
28
  * @fires module:svgcanvas.SvgCanvas#event:ext_IDsUpdated
28
29
  * @returns {void}
29
30
  */
30
- export const pasteElementsMethod = function (type, x, y) {
31
- let clipb = JSON.parse(sessionStorage.getItem(svgCanvas.getClipboardID()))
32
- if (!clipb) return
33
- let len = clipb.length
34
- if (!len) return
31
+ export const pasteElementsMethod = (type, x, y) => {
32
+ const rawClipboard = sessionStorage.getItem(svgCanvas.getClipboardID())
33
+ let clipb
34
+ try {
35
+ clipb = JSON.parse(rawClipboard)
36
+ } catch {
37
+ return
38
+ }
39
+ if (!Array.isArray(clipb) || !clipb.length) return
35
40
 
36
41
  const pasted = []
37
42
  const batchCmd = new BatchCommand('Paste elements')
@@ -50,7 +55,7 @@ export const pasteElementsMethod = function (type, x, y) {
50
55
  * @param {module:svgcanvas.SVGAsJSON} elem
51
56
  * @returns {void}
52
57
  */
53
- function checkIDs (elem) {
58
+ const checkIDs = (elem) => {
54
59
  if (elem.attr?.id) {
55
60
  changedIDs[elem.attr.id] = svgCanvas.getNextId()
56
61
  elem.attr.id = changedIDs[elem.attr.id]
@@ -59,6 +64,35 @@ export const pasteElementsMethod = function (type, x, y) {
59
64
  }
60
65
  clipb.forEach((elem) => checkIDs(elem))
61
66
 
67
+ // Update any internal references in the clipboard to match the new IDs.
68
+ /**
69
+ * @param {module:svgcanvas.SVGAsJSON} elem
70
+ * @returns {void}
71
+ */
72
+ const remapReferences = (elem) => {
73
+ const attrs = elem?.attr
74
+ if (attrs) {
75
+ for (const [attrName, attrVal] of Object.entries(attrs)) {
76
+ if (typeof attrVal !== 'string' || !attrVal) continue
77
+ if ((attrName === 'href' || attrName === 'xlink:href') && attrVal.startsWith('#')) {
78
+ const refId = attrVal.slice(1)
79
+ if (refId in changedIDs) {
80
+ attrs[attrName] = `#${changedIDs[refId]}`
81
+ }
82
+ }
83
+ const url = getUrlFromAttr(attrVal)
84
+ if (url) {
85
+ const refId = url.slice(1)
86
+ if (refId in changedIDs) {
87
+ attrs[attrName] = attrVal.replace(url, `#${changedIDs[refId]}`)
88
+ }
89
+ }
90
+ }
91
+ }
92
+ if (elem.children) elem.children.forEach((child) => remapReferences(child))
93
+ }
94
+ clipb.forEach((elem) => remapReferences(elem))
95
+
62
96
  // Give extensions like the connector extension a chance to reflect new IDs and remove invalid elements
63
97
  /**
64
98
  * Triggered when `pasteElements` is called from a paste action (context menu or key).
@@ -77,12 +111,14 @@ export const pasteElementsMethod = function (type, x, y) {
77
111
 
78
112
  extChanges.remove.forEach(function (removeID) {
79
113
  clipb = clipb.filter(function (clipBoardItem) {
80
- return clipBoardItem.attr.id !== removeID
114
+ return clipBoardItem?.attr?.id !== removeID
81
115
  })
82
116
  })
83
117
  })
84
118
 
85
119
  // Move elements to lastClickPoint
120
+ let len = clipb.length
121
+ if (!len) return
86
122
  while (len--) {
87
123
  const elem = clipb[len]
88
124
  if (!elem) { continue }
@@ -94,6 +130,7 @@ export const pasteElementsMethod = function (type, x, y) {
94
130
  svgCanvas.restoreRefElements(copy)
95
131
  }
96
132
 
133
+ if (!pasted.length) return
97
134
  svgCanvas.selectOnly(pasted)
98
135
 
99
136
  if (type !== 'in_place') {
@@ -108,18 +145,20 @@ export const pasteElementsMethod = function (type, x, y) {
108
145
  }
109
146
 
110
147
  const bbox = getStrokedBBoxDefaultVisible(pasted)
111
- const cx = ctrX - (bbox.x + bbox.width / 2)
112
- const cy = ctrY - (bbox.y + bbox.height / 2)
113
- const dx = []
114
- const dy = []
115
-
116
- pasted.forEach(function (_item) {
117
- dx.push(cx)
118
- dy.push(cy)
119
- })
148
+ if (bbox && Number.isFinite(ctrX) && Number.isFinite(ctrY)) {
149
+ const cx = ctrX - (bbox.x + bbox.width / 2)
150
+ const cy = ctrY - (bbox.y + bbox.height / 2)
151
+ const dx = []
152
+ const dy = []
153
+
154
+ pasted.forEach(function (_item) {
155
+ dx.push(cx)
156
+ dy.push(cy)
157
+ })
120
158
 
121
- const cmd = svgCanvas.moveSelectedElements(dx, dy, false)
122
- if (cmd) batchCmd.addSubCommand(cmd)
159
+ const cmd = svgCanvas.moveSelectedElements(dx, dy, false)
160
+ if (cmd) batchCmd.addSubCommand(cmd)
161
+ }
123
162
  }
124
163
 
125
164
  svgCanvas.addCommandToHistory(batchCmd)