@svgedit/svgcanvas 7.2.7 → 7.4.2

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.
@@ -122,25 +122,36 @@ const setGroupTitleMethod = (val) => {
122
122
  const selectedElements = svgCanvas.getSelectedElements()
123
123
  const dataStorage = svgCanvas.getDataStorage()
124
124
  let elem = selectedElements[0]
125
+ if (!elem) { return }
125
126
  if (dataStorage.has(elem, 'gsvg')) {
126
127
  elem = dataStorage.get(elem, 'gsvg')
128
+ } else if (dataStorage.has(elem, 'symbol')) {
129
+ elem = dataStorage.get(elem, 'symbol')
127
130
  }
128
-
129
- const ts = elem.querySelectorAll('title')
131
+ if (!elem) { return }
130
132
 
131
133
  const batchCmd = new BatchCommand('Set Label')
132
134
 
133
- let title
135
+ let title = null
136
+ for (const child of elem.childNodes) {
137
+ if (child.nodeName === 'title') {
138
+ title = child
139
+ break
140
+ }
141
+ }
142
+
134
143
  if (val.length === 0) {
144
+ if (!title) { return }
135
145
  // Remove title element
136
- const tsNextSibling = ts.nextSibling
137
- batchCmd.addSubCommand(new RemoveElementCommand(ts[0], tsNextSibling, elem))
138
- ts.remove()
139
- } else if (ts.length) {
146
+ const { nextSibling } = title
147
+ title.remove()
148
+ batchCmd.addSubCommand(new RemoveElementCommand(title, nextSibling, elem))
149
+ } else if (title) {
140
150
  // Change title contents
141
- title = ts[0]
142
- batchCmd.addSubCommand(new ChangeElementCommand(title, { '#text': title.textContent }))
151
+ const oldText = title.textContent
152
+ if (oldText === val) { return }
143
153
  title.textContent = val
154
+ batchCmd.addSubCommand(new ChangeElementCommand(title, { '#text': oldText }))
144
155
  } else {
145
156
  // Add title element
146
157
  title = svgCanvas.getDOMDocument().createElementNS(NS.SVG, 'title')
@@ -149,7 +160,9 @@ const setGroupTitleMethod = (val) => {
149
160
  batchCmd.addSubCommand(new InsertElementCommand(title))
150
161
  }
151
162
 
152
- svgCanvas.addCommandToHistory(batchCmd)
163
+ if (!batchCmd.isEmpty()) {
164
+ svgCanvas.addCommandToHistory(batchCmd)
165
+ }
153
166
  }
154
167
 
155
168
  /**
@@ -160,33 +173,44 @@ const setGroupTitleMethod = (val) => {
160
173
  * @returns {void}
161
174
  */
162
175
  const setDocumentTitleMethod = (newTitle) => {
163
- const { ChangeElementCommand, BatchCommand } = svgCanvas.history
164
- const childs = svgCanvas.getSvgContent().childNodes
165
- let docTitle = false; let oldTitle = ''
176
+ const {
177
+ InsertElementCommand, RemoveElementCommand,
178
+ ChangeElementCommand, BatchCommand
179
+ } = svgCanvas.history
180
+ const svgContent = svgCanvas.getSvgContent()
166
181
 
167
182
  const batchCmd = new BatchCommand('Change Image Title')
168
183
 
169
- for (const child of childs) {
184
+ /** @type {Element|null} */
185
+ let docTitle = null
186
+ for (const child of svgContent.childNodes) {
170
187
  if (child.nodeName === 'title') {
171
188
  docTitle = child
172
- oldTitle = docTitle.textContent
173
189
  break
174
190
  }
175
191
  }
192
+
176
193
  if (!docTitle) {
194
+ if (!newTitle.length) { return }
177
195
  docTitle = svgCanvas.getDOMDocument().createElementNS(NS.SVG, 'title')
178
- svgCanvas.getSvgContent().insertBefore(docTitle, svgCanvas.getSvgContent().firstChild)
179
- // svgContent.firstChild.before(docTitle); // Ok to replace above with this?
180
- }
181
-
182
- if (newTitle.length) {
183
196
  docTitle.textContent = newTitle
197
+ svgContent.insertBefore(docTitle, svgContent.firstChild)
198
+ batchCmd.addSubCommand(new InsertElementCommand(docTitle))
199
+ } else if (newTitle.length) {
200
+ const oldTitle = docTitle.textContent
201
+ if (oldTitle === newTitle) { return }
202
+ docTitle.textContent = newTitle
203
+ batchCmd.addSubCommand(new ChangeElementCommand(docTitle, { '#text': oldTitle }))
184
204
  } else {
185
205
  // No title given, so element is not necessary
206
+ const { nextSibling } = docTitle
186
207
  docTitle.remove()
208
+ batchCmd.addSubCommand(new RemoveElementCommand(docTitle, nextSibling, svgContent))
209
+ }
210
+
211
+ if (!batchCmd.isEmpty()) {
212
+ svgCanvas.addCommandToHistory(batchCmd)
187
213
  }
188
- batchCmd.addSubCommand(new ChangeElementCommand(docTitle, { '#text': oldTitle }))
189
- svgCanvas.addCommandToHistory(batchCmd)
190
214
  }
191
215
 
192
216
  /**
@@ -201,7 +225,6 @@ const setDocumentTitleMethod = (newTitle) => {
201
225
  */
202
226
  const setResolutionMethod = (x, y) => {
203
227
  const { ChangeElementCommand, BatchCommand } = svgCanvas.history
204
- const zoom = svgCanvas.getZoom()
205
228
  const res = svgCanvas.getResolution()
206
229
  const { w, h } = res
207
230
  let batchCmd
@@ -220,8 +243,10 @@ const setResolutionMethod = (x, y) => {
220
243
  dy.push(bbox.y * -1)
221
244
  })
222
245
 
223
- const cmd = svgCanvas.moveSelectedElements(dx, dy, true)
224
- batchCmd.addSubCommand(cmd)
246
+ const cmd = svgCanvas.moveSelectedElements(dx, dy, false)
247
+ if (cmd) {
248
+ batchCmd.addSubCommand(cmd)
249
+ }
225
250
  svgCanvas.clearSelection()
226
251
 
227
252
  x = Math.round(bbox.width)
@@ -230,26 +255,25 @@ const setResolutionMethod = (x, y) => {
230
255
  return false
231
256
  }
232
257
  }
233
- if (x !== w || y !== h) {
258
+ const newW = convertToNum('width', x)
259
+ const newH = convertToNum('height', y)
260
+ if (newW !== w || newH !== h) {
234
261
  if (!batchCmd) {
235
262
  batchCmd = new BatchCommand('Change Image Dimensions')
236
263
  }
264
+ const svgContent = svgCanvas.getSvgContent()
265
+ const oldViewBox = svgContent.getAttribute('viewBox')
237
266
 
238
- x = convertToNum('width', x)
239
- y = convertToNum('height', y)
240
-
241
- svgCanvas.getSvgContent().setAttribute('width', x)
242
- svgCanvas.getSvgContent().setAttribute('height', y)
243
-
244
- svgCanvas.contentW = x
245
- svgCanvas.contentH = y
246
- batchCmd.addSubCommand(new ChangeElementCommand(svgCanvas.getSvgContent(), { width: w, height: h }))
267
+ svgContent.setAttribute('width', newW)
268
+ svgContent.setAttribute('height', newH)
247
269
 
248
- svgCanvas.getSvgContent().setAttribute('viewBox', [0, 0, x / zoom, y / zoom].join(' '))
249
- batchCmd.addSubCommand(new ChangeElementCommand(svgCanvas.getSvgContent(), { viewBox: ['0 0', w, h].join(' ') }))
270
+ svgCanvas.contentW = newW
271
+ svgCanvas.contentH = newH
272
+ svgContent.setAttribute('viewBox', [0, 0, newW, newH].join(' '))
273
+ batchCmd.addSubCommand(new ChangeElementCommand(svgContent, { width: w, height: h, viewBox: oldViewBox }))
250
274
 
251
275
  svgCanvas.addCommandToHistory(batchCmd)
252
- svgCanvas.call('changed', [svgCanvas.getSvgContent()])
276
+ svgCanvas.call('changed', [svgContent])
253
277
  }
254
278
  return true
255
279
  }
@@ -286,20 +310,36 @@ const setBBoxZoomMethod = (val, editorW, editorH) => {
286
310
  let spacer = 0.85
287
311
  let bb
288
312
  const calcZoom = (bb) => {
289
- if (!bb) { return false }
313
+ if (!bb) { return undefined }
314
+ if (!Number.isFinite(editorW) || !Number.isFinite(editorH) || editorW <= 0 || editorH <= 0) {
315
+ return undefined
316
+ }
317
+ if (!Number.isFinite(bb.width) || !Number.isFinite(bb.height) || bb.width <= 0 || bb.height <= 0) {
318
+ return undefined
319
+ }
290
320
  const wZoom = Math.round((editorW / bb.width) * 100 * spacer) / 100
291
321
  const hZoom = Math.round((editorH / bb.height) * 100 * spacer) / 100
292
322
  const zoom = Math.min(wZoom, hZoom)
323
+ if (!Number.isFinite(zoom) || zoom <= 0) {
324
+ return undefined
325
+ }
293
326
  svgCanvas.setZoom(zoom)
294
327
  return { zoom, bbox: bb }
295
328
  }
296
329
 
297
- if (typeof val === 'object') {
330
+ if (val && typeof val === 'object') {
298
331
  bb = val
299
332
  if (bb.width === 0 || bb.height === 0) {
300
- const newzoom = bb.zoom ? bb.zoom : zoom * bb.factor
301
- svgCanvas.setZoom(newzoom)
302
- return { zoom, bbox: bb }
333
+ let newzoom = zoom
334
+ if (Number.isFinite(bb.zoom) && bb.zoom > 0) {
335
+ newzoom = bb.zoom
336
+ } else if (Number.isFinite(bb.factor) && bb.factor > 0) {
337
+ newzoom = zoom * bb.factor
338
+ }
339
+ if (Number.isFinite(newzoom) && newzoom > 0) {
340
+ svgCanvas.setZoom(newzoom)
341
+ }
342
+ return { zoom: newzoom, bbox: bb }
303
343
  }
304
344
  return calcZoom(bb)
305
345
  }
@@ -307,12 +347,7 @@ const setBBoxZoomMethod = (val, editorW, editorH) => {
307
347
  switch (val) {
308
348
  case 'selection': {
309
349
  if (!selectedElements[0]) { return undefined }
310
- const selectedElems = selectedElements.map((n, _) => {
311
- if (n) {
312
- return n
313
- }
314
- return undefined
315
- })
350
+ const selectedElems = selectedElements.filter(Boolean)
316
351
  bb = getStrokedBBoxDefaultVisible(selectedElems)
317
352
  break
318
353
  } case 'canvas': {
@@ -340,13 +375,22 @@ const setBBoxZoomMethod = (val, editorW, editorH) => {
340
375
  * @returns {void}
341
376
  */
342
377
  const setZoomMethod = (zoomLevel) => {
378
+ if (!Number.isFinite(zoomLevel) || zoomLevel <= 0) {
379
+ return
380
+ }
343
381
  const selectedElements = svgCanvas.getSelectedElements()
344
382
  const res = svgCanvas.getResolution()
345
- svgCanvas.getSvgContent().setAttribute('viewBox', '0 0 ' + res.w / zoomLevel + ' ' + res.h / zoomLevel)
383
+ const w = res.w / zoomLevel
384
+ const h = res.h / zoomLevel
385
+ if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {
386
+ return
387
+ }
388
+ svgCanvas.getSvgContent().setAttribute('viewBox', `0 0 ${w} ${h}`)
346
389
  svgCanvas.setZoom(zoomLevel)
347
390
  selectedElements.forEach((elem) => {
348
391
  if (!elem) { return }
349
- svgCanvas.selectorManager.requestSelector(elem).resize()
392
+ const selector = svgCanvas.selectorManager.requestSelector(elem)
393
+ selector && selector.resize()
350
394
  })
351
395
  svgCanvas.pathActions.zoomChange()
352
396
  svgCanvas.runExtensions('zoomChanged', zoomLevel)
@@ -364,7 +408,7 @@ const setZoomMethod = (zoomLevel) => {
364
408
  const setColorMethod = (type, val, preventUndo) => {
365
409
  const selectedElements = svgCanvas.getSelectedElements()
366
410
  svgCanvas.setCurShape(type, val)
367
- svgCanvas.setCurProperties(type + '_paint', { type: 'solidColor' })
411
+ svgCanvas.setCurProperties(`${type}_paint`, { type: 'solidColor' })
368
412
  const elems = []
369
413
  /**
370
414
  *
@@ -408,10 +452,11 @@ const setColorMethod = (type, val, preventUndo) => {
408
452
  * @returns {void}
409
453
  */
410
454
  const setGradientMethod = (type) => {
411
- if (!svgCanvas.getCurProperties(type + '_paint') ||
412
- svgCanvas.getCurProperties(type + '_paint').type === 'solidColor') { return }
455
+ if (!svgCanvas.getCurProperties(`${type}_paint`) ||
456
+ svgCanvas.getCurProperties(`${type}_paint`).type === 'solidColor') { return }
413
457
  const canvas = svgCanvas
414
458
  let grad = canvas[type + 'Grad']
459
+ if (!grad) { return }
415
460
  // find out if there is a duplicate gradient already in the defs
416
461
  const duplicateGrad = findDuplicateGradient(grad)
417
462
  const defs = findDefs()
@@ -425,7 +470,7 @@ const setGradientMethod = (type) => {
425
470
  } else { // use existing gradient
426
471
  grad = duplicateGrad
427
472
  }
428
- svgCanvas.setColor(type, 'url(#' + grad.id + ')')
473
+ svgCanvas.setColor(type, `url(#${grad.id})`)
429
474
  }
430
475
 
431
476
  /**
@@ -435,12 +480,21 @@ const setGradientMethod = (type) => {
435
480
  * @returns {SVGGradientElement} The existing gradient if found, `null` if not
436
481
  */
437
482
  const findDuplicateGradient = (grad) => {
483
+ if (!grad) {
484
+ return null
485
+ }
486
+ if (!['linearGradient', 'radialGradient'].includes(grad.tagName)) {
487
+ return null
488
+ }
438
489
  const defs = findDefs()
439
490
  const existingGrads = defs.querySelectorAll('linearGradient, radialGradient')
440
491
  let i = existingGrads.length
441
492
  const radAttrs = ['r', 'cx', 'cy', 'fx', 'fy']
442
493
  while (i--) {
443
494
  const og = existingGrads[i]
495
+ if (og.tagName !== grad.tagName) {
496
+ continue
497
+ }
444
498
  if (grad.tagName === 'linearGradient') {
445
499
  if (grad.getAttribute('x1') !== og.getAttribute('x1') ||
446
500
  grad.getAttribute('y1') !== og.getAttribute('y1') ||
@@ -514,10 +568,10 @@ const setPaintMethod = (type, paint) => {
514
568
  svgCanvas.setPaintOpacity(type, p.alpha / 100, true)
515
569
 
516
570
  // now set the current paint object
517
- svgCanvas.setCurProperties(type + '_paint', p)
571
+ svgCanvas.setCurProperties(`${type}_paint`, p)
518
572
  switch (p.type) {
519
573
  case 'solidColor':
520
- svgCanvas.setColor(type, p.solidColor !== 'none' ? '#' + p.solidColor : 'none')
574
+ svgCanvas.setColor(type, p.solidColor !== 'none' ? `#${p.solidColor}` : 'none')
521
575
  break
522
576
  case 'linearGradient':
523
577
  case 'radialGradient':
@@ -601,14 +655,31 @@ const setStrokeAttrMethod = (attr, val) => {
601
655
  }
602
656
  }
603
657
 
658
+ const getSelectedTextElements = () => {
659
+ return svgCanvas.getSelectedElements().filter(el => el?.tagName === 'text')
660
+ }
661
+
662
+ const getChangedTextElements = (textElements, attr, newValue) => {
663
+ const normalizedValue = String(newValue)
664
+ return textElements.filter((elem) => {
665
+ const oldValue = attr === '#text' ? elem.textContent : elem.getAttribute(attr)
666
+ return (oldValue || '') !== normalizedValue
667
+ })
668
+ }
669
+
670
+ const notifyTextChange = (textElements) => {
671
+ if (textElements.length > 0) {
672
+ svgCanvas.call('changed', textElements)
673
+ }
674
+ }
675
+
604
676
  /**
605
677
  * Check if all selected text elements are in bold.
606
678
  * @function module:svgcanvas.SvgCanvas#getBold
607
679
  * @returns {boolean} `true` if all selected elements are bold, `false` otherwise.
608
680
  */
609
681
  const getBoldMethod = () => {
610
- const selectedElements = svgCanvas.getSelectedElements()
611
- const textElements = selectedElements.filter(el => el?.tagName === 'text')
682
+ const textElements = getSelectedTextElements()
612
683
  return textElements.every(el => el.getAttribute('font-weight') === 'bold')
613
684
  }
614
685
 
@@ -619,12 +690,16 @@ const getBoldMethod = () => {
619
690
  * @returns {void}
620
691
  */
621
692
  const setBoldMethod = (b) => {
622
- const selectedElements = svgCanvas.getSelectedElements()
623
- const textElements = selectedElements.filter(el => el?.tagName === 'text')
624
- svgCanvas.changeSelectedAttribute('font-weight', b ? 'bold' : 'normal', textElements)
693
+ const textElements = getSelectedTextElements()
694
+ const value = b ? 'bold' : 'normal'
695
+ const changedTextElements = getChangedTextElements(textElements, 'font-weight', value)
696
+ if (changedTextElements.length > 0) {
697
+ svgCanvas.changeSelectedAttribute('font-weight', value, changedTextElements)
698
+ }
625
699
  if (!textElements.some(el => el.textContent)) {
626
700
  svgCanvas.textActions.setCursor()
627
701
  }
702
+ notifyTextChange(changedTextElements)
628
703
  }
629
704
 
630
705
  /**
@@ -632,8 +707,7 @@ const setBoldMethod = (b) => {
632
707
  * @returns {boolean} Indicates whether or not elements have the text decoration value
633
708
  */
634
709
  const hasTextDecorationMethod = (value) => {
635
- const selectedElements = svgCanvas.getSelectedElements()
636
- const textElements = selectedElements.filter(el => el?.tagName === 'text')
710
+ const textElements = getSelectedTextElements()
637
711
  return textElements.every(el => (el.getAttribute('text-decoration') || '').includes(value))
638
712
  }
639
713
 
@@ -644,8 +718,7 @@ const hasTextDecorationMethod = (value) => {
644
718
  */
645
719
  const addTextDecorationMethod = (value) => {
646
720
  const { ChangeElementCommand, BatchCommand } = svgCanvas.history
647
- const selectedElements = svgCanvas.getSelectedElements()
648
- const textElements = selectedElements.filter(el => el?.tagName === 'text')
721
+ const textElements = getSelectedTextElements()
649
722
 
650
723
  const batchCmd = new BatchCommand()
651
724
  textElements.forEach(elem => {
@@ -653,7 +726,7 @@ const addTextDecorationMethod = (value) => {
653
726
  // Add the new text decoration value if it did not exist
654
727
  if (!oldValue.includes(value)) {
655
728
  batchCmd.addSubCommand(new ChangeElementCommand(elem, { 'text-decoration': oldValue }))
656
- svgCanvas.changeSelectedAttributeNoUndo('text-decoration', (oldValue + ' ' + value).trim(), [elem])
729
+ svgCanvas.changeSelectedAttributeNoUndo('text-decoration', `${oldValue} ${value}`.trim(), [elem])
657
730
  }
658
731
  })
659
732
  if (!batchCmd.isEmpty()) {
@@ -672,8 +745,7 @@ const addTextDecorationMethod = (value) => {
672
745
  */
673
746
  const removeTextDecorationMethod = (value) => {
674
747
  const { ChangeElementCommand, BatchCommand } = svgCanvas.history
675
- const selectedElements = svgCanvas.getSelectedElements()
676
- const textElements = selectedElements.filter(el => el?.tagName === 'text')
748
+ const textElements = getSelectedTextElements()
677
749
 
678
750
  const batchCmd = new BatchCommand()
679
751
  textElements.forEach(elem => {
@@ -696,8 +768,7 @@ const removeTextDecorationMethod = (value) => {
696
768
  * @returns {boolean} `true` if all selected elements are in italics, `false` otherwise.
697
769
  */
698
770
  const getItalicMethod = () => {
699
- const selectedElements = svgCanvas.getSelectedElements()
700
- const textElements = selectedElements.filter(el => el?.tagName === 'text')
771
+ const textElements = getSelectedTextElements()
701
772
  return textElements.every(el => el.getAttribute('font-style') === 'italic')
702
773
  }
703
774
 
@@ -708,12 +779,16 @@ const getItalicMethod = () => {
708
779
  * @returns {void}
709
780
  */
710
781
  const setItalicMethod = (i) => {
711
- const selectedElements = svgCanvas.getSelectedElements()
712
- const textElements = selectedElements.filter(el => el?.tagName === 'text')
713
- svgCanvas.changeSelectedAttribute('font-style', i ? 'italic' : 'normal', textElements)
782
+ const textElements = getSelectedTextElements()
783
+ const value = i ? 'italic' : 'normal'
784
+ const changedTextElements = getChangedTextElements(textElements, 'font-style', value)
785
+ if (changedTextElements.length > 0) {
786
+ svgCanvas.changeSelectedAttribute('font-style', value, changedTextElements)
787
+ }
714
788
  if (!textElements.some(el => el.textContent)) {
715
789
  svgCanvas.textActions.setCursor()
716
790
  }
791
+ notifyTextChange(changedTextElements)
717
792
  }
718
793
 
719
794
  /**
@@ -722,9 +797,12 @@ const setItalicMethod = (i) => {
722
797
  * @returns {void}
723
798
  */
724
799
  const setTextAnchorMethod = (value) => {
725
- const selectedElements = svgCanvas.getSelectedElements()
726
- const textElements = selectedElements.filter(el => el?.tagName === 'text')
727
- svgCanvas.changeSelectedAttribute('text-anchor', value, textElements)
800
+ const textElements = getSelectedTextElements()
801
+ const changedTextElements = getChangedTextElements(textElements, 'text-anchor', value)
802
+ if (changedTextElements.length > 0) {
803
+ svgCanvas.changeSelectedAttribute('text-anchor', value, changedTextElements)
804
+ }
805
+ notifyTextChange(changedTextElements)
728
806
  }
729
807
 
730
808
  /**
@@ -733,12 +811,15 @@ const setTextAnchorMethod = (value) => {
733
811
  * @returns {void}
734
812
  */
735
813
  const setLetterSpacingMethod = (value) => {
736
- const selectedElements = svgCanvas.getSelectedElements()
737
- const textElements = selectedElements.filter(el => el?.tagName === 'text')
738
- svgCanvas.changeSelectedAttribute('letter-spacing', value, textElements)
814
+ const textElements = getSelectedTextElements()
815
+ const changedTextElements = getChangedTextElements(textElements, 'letter-spacing', value)
816
+ if (changedTextElements.length > 0) {
817
+ svgCanvas.changeSelectedAttribute('letter-spacing', value, changedTextElements)
818
+ }
739
819
  if (!textElements.some(el => el.textContent)) {
740
820
  svgCanvas.textActions.setCursor()
741
821
  }
822
+ notifyTextChange(changedTextElements)
742
823
  }
743
824
 
744
825
  /**
@@ -747,12 +828,15 @@ const setLetterSpacingMethod = (value) => {
747
828
  * @returns {void}
748
829
  */
749
830
  const setWordSpacingMethod = (value) => {
750
- const selectedElements = svgCanvas.getSelectedElements()
751
- const textElements = selectedElements.filter(el => el?.tagName === 'text')
752
- svgCanvas.changeSelectedAttribute('word-spacing', value, textElements)
831
+ const textElements = getSelectedTextElements()
832
+ const changedTextElements = getChangedTextElements(textElements, 'word-spacing', value)
833
+ if (changedTextElements.length > 0) {
834
+ svgCanvas.changeSelectedAttribute('word-spacing', value, changedTextElements)
835
+ }
753
836
  if (!textElements.some(el => el.textContent)) {
754
837
  svgCanvas.textActions.setCursor()
755
838
  }
839
+ notifyTextChange(changedTextElements)
756
840
  }
757
841
 
758
842
  /**
@@ -761,12 +845,15 @@ const setWordSpacingMethod = (value) => {
761
845
  * @returns {void}
762
846
  */
763
847
  const setTextLengthMethod = (value) => {
764
- const selectedElements = svgCanvas.getSelectedElements()
765
- const textElements = selectedElements.filter(el => el?.tagName === 'text')
766
- svgCanvas.changeSelectedAttribute('textLength', value, textElements)
848
+ const textElements = getSelectedTextElements()
849
+ const changedTextElements = getChangedTextElements(textElements, 'textLength', value)
850
+ if (changedTextElements.length > 0) {
851
+ svgCanvas.changeSelectedAttribute('textLength', value, changedTextElements)
852
+ }
767
853
  if (!textElements.some(el => el.textContent)) {
768
854
  svgCanvas.textActions.setCursor()
769
855
  }
856
+ notifyTextChange(changedTextElements)
770
857
  }
771
858
 
772
859
  /**
@@ -775,12 +862,15 @@ const setTextLengthMethod = (value) => {
775
862
  * @returns {void}
776
863
  */
777
864
  const setLengthAdjustMethod = (value) => {
778
- const selectedElements = svgCanvas.getSelectedElements()
779
- const textElements = selectedElements.filter(el => el?.tagName === 'text')
780
- svgCanvas.changeSelectedAttribute('lengthAdjust', value, textElements)
865
+ const textElements = getSelectedTextElements()
866
+ const changedTextElements = getChangedTextElements(textElements, 'lengthAdjust', value)
867
+ if (changedTextElements.length > 0) {
868
+ svgCanvas.changeSelectedAttribute('lengthAdjust', value, changedTextElements)
869
+ }
781
870
  if (!textElements.some(el => el.textContent)) {
782
871
  svgCanvas.textActions.setCursor()
783
872
  }
873
+ notifyTextChange(changedTextElements)
784
874
  }
785
875
 
786
876
  /**
@@ -798,13 +888,16 @@ const getFontFamilyMethod = () => {
798
888
  * @returns {void}
799
889
  */
800
890
  const setFontFamilyMethod = (val) => {
801
- const selectedElements = svgCanvas.getSelectedElements()
802
- const textElements = selectedElements.filter(el => el?.tagName === 'text')
891
+ const textElements = getSelectedTextElements()
892
+ const changedTextElements = getChangedTextElements(textElements, 'font-family', val)
803
893
  svgCanvas.setCurText('font_family', val)
804
- svgCanvas.changeSelectedAttribute('font-family', val, textElements)
894
+ if (changedTextElements.length > 0) {
895
+ svgCanvas.changeSelectedAttribute('font-family', val, changedTextElements)
896
+ }
805
897
  if (!textElements.some(el => el.textContent)) {
806
898
  svgCanvas.textActions.setCursor()
807
899
  }
900
+ notifyTextChange(changedTextElements)
808
901
  }
809
902
 
810
903
  /**
@@ -814,8 +907,13 @@ const setFontFamilyMethod = (val) => {
814
907
  * @returns {void}
815
908
  */
816
909
  const setFontColorMethod = (val) => {
910
+ const textElements = getSelectedTextElements()
911
+ const changedTextElements = getChangedTextElements(textElements, 'fill', val)
817
912
  svgCanvas.setCurText('fill', val)
818
- svgCanvas.changeSelectedAttribute('fill', val)
913
+ if (changedTextElements.length > 0) {
914
+ svgCanvas.changeSelectedAttribute('fill', val, changedTextElements)
915
+ }
916
+ notifyTextChange(changedTextElements)
819
917
  }
820
918
 
821
919
  /**
@@ -841,12 +939,16 @@ const getFontSizeMethod = () => {
841
939
  * @returns {void}
842
940
  */
843
941
  const setFontSizeMethod = (val) => {
844
- const selectedElements = svgCanvas.getSelectedElements()
942
+ const textElements = getSelectedTextElements()
943
+ const changedTextElements = getChangedTextElements(textElements, 'font-size', val)
845
944
  svgCanvas.setCurText('font_size', val)
846
- svgCanvas.changeSelectedAttribute('font-size', val)
847
- if (!selectedElements[0]?.textContent) {
945
+ if (changedTextElements.length > 0) {
946
+ svgCanvas.changeSelectedAttribute('font-size', val, changedTextElements)
947
+ }
948
+ if (!textElements.some(el => el.textContent)) {
848
949
  svgCanvas.textActions.setCursor()
849
950
  }
951
+ notifyTextChange(changedTextElements)
850
952
  }
851
953
 
852
954
  /**
@@ -892,32 +994,48 @@ const setImageURLMethod = (val) => {
892
994
  const setsize = (!attrs.width || !attrs.height)
893
995
 
894
996
  const curHref = getHref(elem)
997
+ const hrefChanged = curHref !== val
895
998
 
896
999
  // Do nothing if no URL change or size change
897
- if (curHref === val && !setsize) {
1000
+ if (!hrefChanged && !setsize) {
898
1001
  return
899
1002
  }
900
1003
 
901
1004
  const batchCmd = new BatchCommand('Change Image URL')
902
1005
 
903
- setHref(elem, val)
904
- batchCmd.addSubCommand(new ChangeElementCommand(elem, {
905
- '#href': curHref
906
- }))
1006
+ if (hrefChanged) {
1007
+ setHref(elem, val)
1008
+ batchCmd.addSubCommand(new ChangeElementCommand(elem, {
1009
+ '#href': curHref
1010
+ }))
1011
+ }
1012
+
1013
+ let finalized = false
1014
+ const finalize = () => {
1015
+ if (finalized) { return }
1016
+ finalized = true
1017
+ if (batchCmd.isEmpty()) { return }
1018
+ svgCanvas.addCommandToHistory(batchCmd)
1019
+ svgCanvas.call('changed', [elem])
1020
+ }
1021
+
907
1022
  const img = new Image()
908
- img.onload = function () {
1023
+ img.onload = () => {
909
1024
  const changes = {
910
1025
  width: elem.getAttribute('width'),
911
1026
  height: elem.getAttribute('height')
912
1027
  }
913
- elem.setAttribute('width', this.width)
914
- elem.setAttribute('height', this.height)
1028
+ elem.setAttribute('width', img.width)
1029
+ elem.setAttribute('height', img.height)
915
1030
 
916
- svgCanvas.selectorManager.requestSelector(elem).resize()
1031
+ const selector = svgCanvas.selectorManager.requestSelector(elem)
1032
+ selector && selector.resize()
917
1033
 
918
1034
  batchCmd.addSubCommand(new ChangeElementCommand(elem, changes))
919
- svgCanvas.addCommandToHistory(batchCmd)
920
- svgCanvas.call('changed', [elem])
1035
+ finalize()
1036
+ }
1037
+ img.onerror = () => {
1038
+ finalize()
921
1039
  }
922
1040
  img.src = val
923
1041
  }
@@ -969,15 +1087,27 @@ const setRectRadiusMethod = (val) => {
969
1087
  const { ChangeElementCommand } = svgCanvas.history
970
1088
  const selectedElements = svgCanvas.getSelectedElements()
971
1089
  const selected = selectedElements[0]
972
- if (selected?.tagName === 'rect') {
973
- const r = Number(selected.getAttribute('rx'))
974
- if (r !== val) {
975
- selected.setAttribute('rx', val)
976
- selected.setAttribute('ry', val)
977
- svgCanvas.addCommandToHistory(new ChangeElementCommand(selected, { rx: r, ry: r }, 'Radius'))
978
- svgCanvas.call('changed', [selected])
979
- }
1090
+ if (selected?.tagName !== 'rect') { return }
1091
+
1092
+ const radius = Number(val)
1093
+ if (!Number.isFinite(radius) || radius < 0) {
1094
+ return
980
1095
  }
1096
+
1097
+ const oldRx = selected.getAttribute('rx')
1098
+ const oldRy = selected.getAttribute('ry')
1099
+ const currentRx = Number(oldRx)
1100
+ const currentRy = Number(oldRy)
1101
+ const hasCurrentRx = oldRx !== null && Number.isFinite(currentRx)
1102
+ const hasCurrentRy = oldRy !== null && Number.isFinite(currentRy)
1103
+ const already = (radius === 0 && oldRx === null && oldRy === null) ||
1104
+ (hasCurrentRx && hasCurrentRy && currentRx === radius && currentRy === radius)
1105
+ if (already) { return }
1106
+
1107
+ selected.setAttribute('rx', radius)
1108
+ selected.setAttribute('ry', radius)
1109
+ svgCanvas.addCommandToHistory(new ChangeElementCommand(selected, { rx: oldRx, ry: oldRy }, 'Radius'))
1110
+ svgCanvas.call('changed', [selected])
981
1111
  }
982
1112
 
983
1113
  /**
@@ -1021,7 +1151,9 @@ const setSegTypeMethod = (newType) => {
1021
1151
  */
1022
1152
  const setBackgroundMethod = (color, url) => {
1023
1153
  const bg = getElement('canvasBackground')
1154
+ if (!bg) { return }
1024
1155
  const border = bg.querySelector('rect')
1156
+ if (!border) { return }
1025
1157
  let bgImg = getElement('background_image')
1026
1158
  let bgPattern = getElement('background_pattern')
1027
1159
  border.setAttribute('fill', color === 'chessboard' ? '#fff' : color)