@svgedit/svgcanvas 7.2.6 → 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/event.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  convertAttrs
13
13
  } from './units.js'
14
14
  import {
15
- transformPoint, hasMatrixTransform, getMatrix, snapToAngle, getTransformList
15
+ transformPoint, hasMatrixTransform, getMatrix, snapToAngle, getTransformList, transformListToTransform
16
16
  } from './math.js'
17
17
  import * as draw from './draw.js'
18
18
  import * as pathModule from './path.js'
@@ -20,7 +20,9 @@ import * as hstry from './history.js'
20
20
  import { findPos } from '../../svgcanvas/common/util.js'
21
21
 
22
22
  const {
23
- InsertElementCommand
23
+ InsertElementCommand,
24
+ BatchCommand,
25
+ ChangeElementCommand
24
26
  } = hstry
25
27
 
26
28
  let svgCanvas = null
@@ -84,6 +86,7 @@ const updateTransformList = (svgRoot, element, dx, dy) => {
84
86
  const xform = svgRoot.createSVGTransform()
85
87
  xform.setTranslate(dx, dy)
86
88
  const tlist = getTransformList(element)
89
+ if (!tlist) { return }
87
90
  if (tlist.numberOfItems) {
88
91
  const firstItem = tlist.getItem(0)
89
92
  if (firstItem.type === 2) { // SVG_TRANSFORM_TRANSLATE = 2
@@ -145,6 +148,25 @@ const mouseMoveEvent = (evt) => {
145
148
  let tlist
146
149
  switch (svgCanvas.getCurrentMode()) {
147
150
  case 'select': {
151
+ // Insert dummy transform on first mouse move (drag start), not on click.
152
+ // This avoids creating multiple transforms that trigger unwanted flattening.
153
+ if (!svgCanvas.hasDragStartTransform && selectedElements.length > 0) {
154
+ // Store original transforms BEFORE adding the drag transform (for undo)
155
+ svgCanvas.dragStartTransforms = new Map()
156
+ for (const selectedElement of selectedElements) {
157
+ if (!selectedElement) { continue }
158
+ // Capture the transform attribute before we modify it
159
+ svgCanvas.dragStartTransforms.set(selectedElement, selectedElement.getAttribute('transform') || '')
160
+ const slist = getTransformList(selectedElement)
161
+ if (!slist) { continue }
162
+ if (slist.numberOfItems) {
163
+ slist.insertItemBefore(svgRoot.createSVGTransform(), 0)
164
+ } else {
165
+ slist.appendItem(svgRoot.createSVGTransform())
166
+ }
167
+ }
168
+ svgCanvas.hasDragStartTransform = true
169
+ }
148
170
  // we temporarily use a translate on the element(s) being dragged
149
171
  // this transform is removed upon mousing up and the element is
150
172
  // relocated to the new location
@@ -222,6 +244,7 @@ const mouseMoveEvent = (evt) => {
222
244
  // while the mouse is down, when mouse goes up, we use this to recalculate
223
245
  // the shape's coordinates
224
246
  tlist = getTransformList(selected)
247
+ if (!tlist) { break }
225
248
  const hasMatrix = hasMatrixTransform(tlist)
226
249
  box = hasMatrix ? svgCanvas.getInitBbox() : getBBox(selected)
227
250
  let left = box.x
@@ -548,10 +571,21 @@ const mouseMoveEvent = (evt) => {
548
571
  *
549
572
  * @returns {void}
550
573
  */
551
- const mouseOutEvent = () => {
574
+ const mouseOutEvent = (evt) => {
552
575
  const { $id } = svgCanvas
553
576
  if (svgCanvas.getCurrentMode() !== 'select' && svgCanvas.getStarted()) {
554
- const event = new Event('mouseup')
577
+ const event = new MouseEvent('mouseup', {
578
+ bubbles: true,
579
+ cancelable: true,
580
+ clientX: evt?.clientX ?? 0,
581
+ clientY: evt?.clientY ?? 0,
582
+ button: evt?.button ?? 0,
583
+ buttons: evt?.buttons ?? 0,
584
+ altKey: evt?.altKey ?? false,
585
+ ctrlKey: evt?.ctrlKey ?? false,
586
+ metaKey: evt?.metaKey ?? false,
587
+ shiftKey: evt?.shiftKey ?? false
588
+ })
555
589
  $id('svgcanvas').dispatchEvent(event)
556
590
  }
557
591
  }
@@ -637,10 +671,71 @@ const mouseUpEvent = (evt) => {
637
671
  }
638
672
  svgCanvas.selectorManager.requestSelector(selected).showGrips(true)
639
673
  }
640
- // always recalculate dimensions to strip off stray identity transforms
641
- svgCanvas.recalculateAllSelectedDimensions()
642
674
  // if it was being dragged/resized
643
675
  if (realX !== svgCanvas.getRStartX() || realY !== svgCanvas.getRStartY()) {
676
+ // Only recalculate dimensions after actual dragging/resizing to avoid
677
+ // unwanted transform flattening on simple clicks
678
+
679
+ // Create a single batch command for all moved elements
680
+ const batchCmd = new BatchCommand('position')
681
+
682
+ selectedElements.forEach((elem) => {
683
+ if (!elem) return
684
+
685
+ const tlist = getTransformList(elem)
686
+ if (!tlist || tlist.numberOfItems === 0) return
687
+
688
+ // Get the transform from BEFORE the drag started
689
+ const oldTransform = svgCanvas.dragStartTransforms?.get(elem) || ''
690
+
691
+ // Check if the first transform is a translate (the drag transform we added)
692
+ const firstTransform = tlist.getItem(0)
693
+ const hasDragTranslate = firstTransform.type === 2 // SVG_TRANSFORM_TRANSLATE
694
+
695
+ // For groups, we always consolidate the transforms (recalculateDimensions returns null for groups)
696
+ const isGroup = elem.tagName === 'g' || elem.tagName === 'a'
697
+
698
+ // If element has 2+ transforms, or is a group with a drag translate, consolidate
699
+ if ((tlist.numberOfItems > 1 && hasDragTranslate) || (isGroup && hasDragTranslate)) {
700
+ const consolidatedMatrix = transformListToTransform(tlist).matrix
701
+
702
+ // Clear the transform list
703
+ while (tlist.numberOfItems > 0) {
704
+ tlist.removeItem(0)
705
+ }
706
+
707
+ // Add the consolidated matrix
708
+ const newTransform = svgCanvas.getSvgRoot().createSVGTransform()
709
+ newTransform.setMatrix(consolidatedMatrix)
710
+ tlist.appendItem(newTransform)
711
+
712
+ // Record the transform change for undo
713
+ batchCmd.addSubCommand(new ChangeElementCommand(elem, { transform: oldTransform }))
714
+ return
715
+ }
716
+
717
+ // For non-group elements with simple transforms, try recalculateDimensions
718
+ const cmd = svgCanvas.recalculateDimensions(elem)
719
+ if (cmd) {
720
+ batchCmd.addSubCommand(cmd)
721
+ } else {
722
+ // recalculateDimensions returned null
723
+ // Check if the transform actually changed and record it manually
724
+ const newTransform = elem.getAttribute('transform') || ''
725
+ if (newTransform !== oldTransform) {
726
+ batchCmd.addSubCommand(new ChangeElementCommand(elem, { transform: oldTransform }))
727
+ }
728
+ }
729
+ })
730
+
731
+ if (!batchCmd.isEmpty()) {
732
+ svgCanvas.addCommandToHistory(batchCmd)
733
+ }
734
+
735
+ // Clear the stored transforms AND reset the flag together
736
+ svgCanvas.dragStartTransforms = null
737
+ svgCanvas.hasDragStartTransform = false
738
+
644
739
  const len = selectedElements.length
645
740
  for (let i = 0; i < len; ++i) {
646
741
  if (!selectedElements[i]) { break }
@@ -799,6 +894,8 @@ const mouseUpEvent = (evt) => {
799
894
  svgCanvas.textActions.mouseUp(evt, mouseX, mouseY)
800
895
  break
801
896
  case 'rotate': {
897
+ svgCanvas.hasDragStartTransform = false
898
+ svgCanvas.dragStartTransforms = null
802
899
  keep = true
803
900
  element = null
804
901
  svgCanvas.setCurrentMode('select')
@@ -812,8 +909,13 @@ const mouseUpEvent = (evt) => {
812
909
  break
813
910
  } default:
814
911
  // This could occur in an extension
912
+ svgCanvas.hasDragStartTransform = false
913
+ svgCanvas.dragStartTransforms = null
815
914
  break
816
915
  }
916
+ // Reset drag flag after any mouseUp
917
+ svgCanvas.hasDragStartTransform = false
918
+ svgCanvas.dragStartTransforms = null
817
919
 
818
920
  /**
819
921
  * The main (left) mouse button is released (anywhere).
@@ -979,7 +1081,13 @@ const mouseDownEvent = (evt) => {
979
1081
  svgCanvas.cloneSelectedElements(0, 0)
980
1082
  }
981
1083
 
982
- svgCanvas.setRootSctm($id('svgcontent').querySelector('g').getScreenCTM().inverse())
1084
+ // Get screenCTM from the first child group of svgcontent
1085
+ // Note: svgcontent itself has x/y offset attributes, so we use its first child
1086
+ const svgContent = $id('svgcontent')
1087
+ const rootGroup = svgContent?.querySelector('g')
1088
+ const screenCTM = rootGroup?.getScreenCTM?.()
1089
+ if (!screenCTM) { return }
1090
+ svgCanvas.setRootSctm(screenCTM.inverse())
983
1091
 
984
1092
  const pt = transformPoint(evt.clientX, evt.clientY, svgCanvas.getrootSctm())
985
1093
  const mouseX = pt.x * zoom
@@ -1039,12 +1147,22 @@ const mouseDownEvent = (evt) => {
1039
1147
  svgCanvas.setStartTransform(mouseTarget.getAttribute('transform'))
1040
1148
 
1041
1149
  const tlist = getTransformList(mouseTarget)
1042
- // consolidate transforms using standard SVG but keep the transformation used for the move/scale
1043
- if (tlist.numberOfItems > 1) {
1044
- const firstTransform = tlist.getItem(0)
1045
- tlist.removeItem(0)
1046
- tlist.consolidate()
1047
- tlist.insertItemBefore(firstTransform, 0)
1150
+
1151
+ // Consolidate transforms for non-group elements to simplify dragging
1152
+ // For elements with multiple transforms (e.g., after ungrouping), consolidate them
1153
+ // into a single matrix so the dummy translate can be properly applied during drag
1154
+ if (tlist?.numberOfItems > 1 && mouseTarget.tagName !== 'g' && mouseTarget.tagName !== 'a') {
1155
+ // Compute the consolidated matrix from all transforms
1156
+ const consolidatedMatrix = transformListToTransform(tlist).matrix
1157
+
1158
+ // Clear the transform list and add a single matrix transform
1159
+ while (tlist.numberOfItems > 0) {
1160
+ tlist.removeItem(0)
1161
+ }
1162
+
1163
+ const newTransform = svgCanvas.getSvgRoot().createSVGTransform()
1164
+ newTransform.setMatrix(consolidatedMatrix)
1165
+ tlist.appendItem(newTransform)
1048
1166
  }
1049
1167
  switch (svgCanvas.getCurrentMode()) {
1050
1168
  case 'select':
@@ -1067,19 +1185,9 @@ const mouseDownEvent = (evt) => {
1067
1185
  }
1068
1186
  // else if it's a path, go into pathedit mode in mouseup
1069
1187
 
1070
- if (!rightClick) {
1071
- // insert a dummy transform so if the element(s) are moved it will have
1072
- // a transform to use for its translate
1073
- for (const selectedElement of selectedElements) {
1074
- if (!selectedElement) { continue }
1075
- const slist = getTransformList(selectedElement)
1076
- if (slist.numberOfItems) {
1077
- slist.insertItemBefore(svgRoot.createSVGTransform(), 0)
1078
- } else {
1079
- slist.appendItem(svgRoot.createSVGTransform())
1080
- }
1081
- }
1082
- }
1188
+ // Note: Dummy transform insertion moved to mouseMove to avoid triggering
1189
+ // recalculateDimensions on simple clicks. The dummy transform is only needed
1190
+ // when actually starting a drag operation.
1083
1191
  } else if (!rightClick) {
1084
1192
  svgCanvas.clearSelection()
1085
1193
  svgCanvas.setCurrentMode('multiselect')
@@ -1105,13 +1213,14 @@ const mouseDownEvent = (evt) => {
1105
1213
  }
1106
1214
  assignAttributes(svgCanvas.getRubberBox(), {
1107
1215
  x: realX * zoom,
1108
- y: realX * zoom,
1216
+ y: realY * zoom,
1109
1217
  width: 0,
1110
1218
  height: 0,
1111
1219
  display: 'inline'
1112
1220
  }, 100)
1113
1221
  break
1114
1222
  case 'resize': {
1223
+ if (!tlist) { break }
1115
1224
  svgCanvas.setStarted(true)
1116
1225
  svgCanvas.setStartX(x)
1117
1226
  svgCanvas.setStartY(y)
@@ -1339,7 +1448,13 @@ const DOMMouseScrollEvent = (e) => {
1339
1448
 
1340
1449
  e.preventDefault()
1341
1450
 
1342
- svgCanvas.setRootSctm($id('svgcontent').querySelector('g').getScreenCTM().inverse())
1451
+ // Get screenCTM from the first child group of svgcontent
1452
+ // Note: svgcontent itself has x/y offset attributes, so we use its first child
1453
+ const svgContent = $id('svgcontent')
1454
+ const rootGroup = svgContent?.querySelector('g')
1455
+ const screenCTM = rootGroup?.getScreenCTM?.()
1456
+ if (!screenCTM) { return }
1457
+ svgCanvas.setRootSctm(screenCTM.inverse())
1343
1458
 
1344
1459
  const workarea = document.getElementById('workarea')
1345
1460
  const scrbar = 15
package/core/history.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * @copyright 2010 Jeff Schiller
7
7
  */
8
8
 
9
+ import { NS } from './namespaces.js'
9
10
  import { getHref, setHref, getRotationAngle, getBBox } from './utilities.js'
10
11
 
11
12
  /**
@@ -140,7 +141,7 @@ export class MoveElementCommand extends Command {
140
141
  constructor (elem, oldNextSibling, oldParent, text) {
141
142
  super()
142
143
  this.elem = elem
143
- this.text = text ? ('Move ' + elem.tagName + ' to ' + text) : ('Move ' + elem.tagName)
144
+ this.text = text ? `Move ${elem.tagName} to ${text}` : `Move ${elem.tagName}`
144
145
  this.oldNextSibling = oldNextSibling
145
146
  this.oldParent = oldParent
146
147
  this.newNextSibling = elem.nextSibling
@@ -155,7 +156,11 @@ export class MoveElementCommand extends Command {
155
156
  */
156
157
  apply (handler) {
157
158
  super.apply(handler, () => {
158
- this.elem = this.newParent.insertBefore(this.elem, this.newNextSibling)
159
+ const reference =
160
+ this.newNextSibling && this.newNextSibling.parentNode === this.newParent
161
+ ? this.newNextSibling
162
+ : null
163
+ this.elem = this.newParent.insertBefore(this.elem, reference)
159
164
  })
160
165
  }
161
166
 
@@ -167,7 +172,11 @@ export class MoveElementCommand extends Command {
167
172
  */
168
173
  unapply (handler) {
169
174
  super.unapply(handler, () => {
170
- this.elem = this.oldParent.insertBefore(this.elem, this.oldNextSibling)
175
+ const reference =
176
+ this.oldNextSibling && this.oldNextSibling.parentNode === this.oldParent
177
+ ? this.oldNextSibling
178
+ : null
179
+ this.elem = this.oldParent.insertBefore(this.elem, reference)
171
180
  })
172
181
  }
173
182
  }
@@ -184,7 +193,7 @@ export class InsertElementCommand extends Command {
184
193
  constructor (elem, text) {
185
194
  super()
186
195
  this.elem = elem
187
- this.text = text || ('Create ' + elem.tagName)
196
+ this.text = text || `Create ${elem.tagName}`
188
197
  this.parent = elem.parentNode
189
198
  this.nextSibling = this.elem.nextSibling
190
199
  }
@@ -197,7 +206,11 @@ export class InsertElementCommand extends Command {
197
206
  */
198
207
  apply (handler) {
199
208
  super.apply(handler, () => {
200
- this.elem = this.parent.insertBefore(this.elem, this.nextSibling)
209
+ const reference =
210
+ this.nextSibling && this.nextSibling.parentNode === this.parent
211
+ ? this.nextSibling
212
+ : null
213
+ this.elem = this.parent.insertBefore(this.elem, reference)
201
214
  })
202
215
  }
203
216
 
@@ -229,7 +242,7 @@ export class RemoveElementCommand extends Command {
229
242
  constructor (elem, oldNextSibling, oldParent, text) {
230
243
  super()
231
244
  this.elem = elem
232
- this.text = text || ('Delete ' + elem.tagName)
245
+ this.text = text || `Delete ${elem.tagName}`
233
246
  this.nextSibling = oldNextSibling
234
247
  this.parent = oldParent
235
248
  }
@@ -255,10 +268,11 @@ export class RemoveElementCommand extends Command {
255
268
  */
256
269
  unapply (handler) {
257
270
  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`
271
+ const reference =
272
+ this.nextSibling && this.nextSibling.parentNode === this.parent
273
+ ? this.nextSibling
274
+ : null
275
+ this.parent.insertBefore(this.elem, reference) // Don't use `before` or `prepend` as `reference` may be `null`
262
276
  })
263
277
  }
264
278
  }
@@ -284,7 +298,7 @@ export class ChangeElementCommand extends Command {
284
298
  constructor (elem, attrs, text) {
285
299
  super()
286
300
  this.elem = elem
287
- this.text = text ? ('Change ' + elem.tagName + ' ' + text) : ('Change ' + elem.tagName)
301
+ this.text = text ? `Change ${elem.tagName} ${text}` : `Change ${elem.tagName}`
288
302
  this.newValues = {}
289
303
  this.oldValues = attrs
290
304
  for (const attr in attrs) {
@@ -308,19 +322,21 @@ export class ChangeElementCommand extends Command {
308
322
  super.apply(handler, () => {
309
323
  let bChangedTransform = false
310
324
  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)
325
+ const isNullishOrEmpty = value === null || value === undefined || value === ''
326
+ if (attr === '#text') {
327
+ this.elem.textContent = value === null || value === undefined ? '' : String(value)
328
+ } else if (attr === '#href') {
329
+ if (isNullishOrEmpty) {
330
+ this.elem.removeAttribute('href')
331
+ this.elem.removeAttributeNS(NS.XLINK, 'href')
316
332
  } else {
317
- this.elem.setAttribute(attr, value)
333
+ setHref(this.elem, String(value))
318
334
  }
319
- } else if (attr === '#text') {
320
- this.elem.textContent = ''
321
- } else {
335
+ } else if (isNullishOrEmpty) {
322
336
  this.elem.setAttribute(attr, '')
323
337
  this.elem.removeAttribute(attr)
338
+ } else {
339
+ this.elem.setAttribute(attr, value)
324
340
  }
325
341
 
326
342
  if (attr === 'transform') { bChangedTransform = true }
@@ -331,6 +347,7 @@ export class ChangeElementCommand extends Command {
331
347
  const angle = getRotationAngle(this.elem)
332
348
  if (angle) {
333
349
  const bbox = getBBox(this.elem)
350
+ if (!bbox) return
334
351
  const cx = bbox.x + bbox.width / 2
335
352
  const cy = bbox.y + bbox.height / 2
336
353
  const rotate = ['rotate(', angle, ' ', cx, ',', cy, ')'].join('')
@@ -352,18 +369,20 @@ export class ChangeElementCommand extends Command {
352
369
  super.unapply(handler, () => {
353
370
  let bChangedTransform = false
354
371
  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)
372
+ const isNullishOrEmpty = value === null || value === undefined || value === ''
373
+ if (attr === '#text') {
374
+ this.elem.textContent = value === null || value === undefined ? '' : String(value)
375
+ } else if (attr === '#href') {
376
+ if (isNullishOrEmpty) {
377
+ this.elem.removeAttribute('href')
378
+ this.elem.removeAttributeNS(NS.XLINK, 'href')
360
379
  } else {
361
- this.elem.setAttribute(attr, value)
380
+ setHref(this.elem, String(value))
362
381
  }
363
- } else if (attr === '#text') {
364
- this.elem.textContent = ''
365
- } else {
382
+ } else if (isNullishOrEmpty) {
366
383
  this.elem.removeAttribute(attr)
384
+ } else {
385
+ this.elem.setAttribute(attr, value)
367
386
  }
368
387
  if (attr === 'transform') { bChangedTransform = true }
369
388
  })
@@ -372,6 +391,7 @@ export class ChangeElementCommand extends Command {
372
391
  const angle = getRotationAngle(this.elem)
373
392
  if (angle) {
374
393
  const bbox = getBBox(this.elem)
394
+ if (!bbox) return
375
395
  const cx = bbox.x + bbox.width / 2
376
396
  const cy = bbox.y + bbox.height / 2
377
397
  const rotate = ['rotate(', angle, ' ', cx, ',', cy, ')'].join('')
@@ -425,7 +445,7 @@ export class BatchCommand extends Command {
425
445
  */
426
446
  unapply (handler) {
427
447
  super.unapply(handler, () => {
428
- this.stack.reverse().forEach((stackItem) => {
448
+ [...this.stack].reverse().forEach((stackItem) => {
429
449
  console.assert(stackItem, 'stack item should not be null')
430
450
  stackItem && stackItem.unapply(handler)
431
451
  })
@@ -602,7 +622,7 @@ export class UndoManager {
602
622
  const p = this.undoChangeStackPointer--
603
623
  const changeset = this.undoableChangeStack[p]
604
624
  const { attrName } = changeset
605
- const batchCmd = new BatchCommand('Change ' + attrName)
625
+ const batchCmd = new BatchCommand(`Change ${attrName}`)
606
626
  let i = changeset.elements.length
607
627
  while (i--) {
608
628
  const elem = changeset.elements[i]
@@ -79,7 +79,9 @@ class HistoryRecordingService {
79
79
  this.batchCommandStack_.pop()
80
80
  const { length: len } = this.batchCommandStack_
81
81
  this.currentBatchCommand_ = len ? this.batchCommandStack_[len - 1] : null
82
- this.addCommand_(batchCommand)
82
+ if (!batchCommand.isEmpty()) {
83
+ this.addCommand_(batchCommand)
84
+ }
83
85
  }
84
86
  return this
85
87
  }
@@ -157,5 +159,5 @@ class HistoryRecordingService {
157
159
  * @memberof module:history.HistoryRecordingService
158
160
  * @property {module:history.HistoryRecordingService} NO_HISTORY - Singleton that can be passed to functions that record history, but the caller requires that no history be recorded.
159
161
  */
160
- HistoryRecordingService.NO_HISTORY = new HistoryRecordingService()
162
+ HistoryRecordingService.NO_HISTORY = new HistoryRecordingService(null)
161
163
  export default HistoryRecordingService
package/core/json.js CHANGED
@@ -27,7 +27,7 @@ let svgdoc_ = null
27
27
  */
28
28
  export const init = (canvas) => {
29
29
  svgCanvas = canvas
30
- svgdoc_ = canvas.getDOMDocument()
30
+ svgdoc_ = canvas.getDOMDocument?.() || (typeof document !== 'undefined' ? document : null)
31
31
  }
32
32
  /**
33
33
  * @function module:json.getJsonFromSvgElements Iterate element and return json format
@@ -35,8 +35,12 @@ export const init = (canvas) => {
35
35
  * @returns {svgRootElement}
36
36
  */
37
37
  export const getJsonFromSvgElements = (data) => {
38
+ if (!data) return null
39
+
38
40
  // Text node
39
- if (data.nodeType === 3) return data.nodeValue
41
+ if (data.nodeType === 3 || data.nodeType === 4) return data.nodeValue
42
+ // Ignore non-element nodes (e.g., comments)
43
+ if (data.nodeType !== 1) return null
40
44
 
41
45
  const retval = {
42
46
  element: data.tagName,
@@ -46,13 +50,25 @@ export const getJsonFromSvgElements = (data) => {
46
50
  }
47
51
 
48
52
  // Iterate attributes
49
- for (let i = 0, attr; (attr = data.attributes[i]); i++) {
50
- retval.attr[attr.name] = attr.value
53
+ const attributes = data.attributes
54
+ if (attributes) {
55
+ for (let i = 0; i < attributes.length; i++) {
56
+ const attr = attributes[i]
57
+ if (!attr) continue
58
+ retval.attr[attr.name] = attr.value
59
+ }
51
60
  }
52
61
 
53
62
  // Iterate children
54
- for (let i = 0, node; (node = data.childNodes[i]); i++) {
55
- retval.children[i] = getJsonFromSvgElements(node)
63
+ const childNodes = data.childNodes
64
+ if (childNodes) {
65
+ for (let i = 0; i < childNodes.length; i++) {
66
+ const node = childNodes[i]
67
+ const child = getJsonFromSvgElements(node)
68
+ if (child !== null && child !== undefined) {
69
+ retval.children.push(child)
70
+ }
71
+ }
56
72
  }
57
73
 
58
74
  return retval
@@ -65,11 +81,29 @@ export const getJsonFromSvgElements = (data) => {
65
81
  */
66
82
 
67
83
  export const addSVGElementsFromJson = (data) => {
84
+ if (!svgdoc_) { return null }
85
+ if (data === null || data === undefined) return svgdoc_.createTextNode('')
68
86
  if (typeof data === 'string') return svgdoc_.createTextNode(data)
69
87
 
70
- let shape = getElement(data.attr.id)
88
+ const attrs = data.attr || {}
89
+ const id = attrs.id
90
+ let shape = null
91
+ if (typeof id === 'string' && id) {
92
+ try {
93
+ shape = getElement(id)
94
+ } catch (e) {
95
+ // Ignore (CSS selector may be invalid); fallback to getElementById below
96
+ }
97
+ if (!shape) {
98
+ const byId = svgdoc_.getElementById?.(id)
99
+ const svgRoot = svgCanvas?.getSvgRoot?.()
100
+ if (byId && (!svgRoot || svgRoot.contains(byId))) {
101
+ shape = byId
102
+ }
103
+ }
104
+ }
71
105
  // if shape is a path but we need to create a rect/ellipse, then remove the path
72
- const currentLayer = svgCanvas.getDrawing().getCurrentLayer()
106
+ const currentLayer = svgCanvas?.getDrawing?.()?.getCurrentLayer?.()
73
107
  if (shape && data.element !== shape.tagName) {
74
108
  shape.remove()
75
109
  shape = null
@@ -81,8 +115,10 @@ export const addSVGElementsFromJson = (data) => {
81
115
  (svgCanvas.getCurrentGroup() || currentLayer).append(shape)
82
116
  }
83
117
  }
84
- const curShape = svgCanvas.getCurShape()
118
+ const curShape = svgCanvas.getCurShape?.() || {}
85
119
  if (data.curStyles) {
120
+ const curOpacity = Number(curShape.opacity)
121
+ const opacity = Number.isFinite(curOpacity) ? (curOpacity / 2) : 0.5
86
122
  assignAttributes(shape, {
87
123
  fill: curShape.fill,
88
124
  stroke: curShape.stroke,
@@ -92,17 +128,23 @@ export const addSVGElementsFromJson = (data) => {
92
128
  'stroke-linecap': curShape.stroke_linecap,
93
129
  'stroke-opacity': curShape.stroke_opacity,
94
130
  'fill-opacity': curShape.fill_opacity,
95
- opacity: curShape.opacity / 2,
131
+ opacity,
96
132
  style: 'pointer-events:inherit'
97
133
  }, 100)
98
134
  }
99
- assignAttributes(shape, data.attr, 100)
135
+ assignAttributes(shape, attrs, 100)
100
136
  cleanupElement(shape)
101
137
 
102
138
  // Children
103
139
  if (data.children) {
140
+ while (shape.firstChild) {
141
+ shape.firstChild.remove()
142
+ }
104
143
  data.children.forEach((child) => {
105
- shape.append(addSVGElementsFromJson(child))
144
+ const childNode = addSVGElementsFromJson(child)
145
+ if (childNode) {
146
+ shape.append(childNode)
147
+ }
106
148
  })
107
149
  }
108
150