@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/CHANGES.md +6 -0
- package/common/browser.js +104 -37
- package/common/logger.js +151 -0
- package/common/util.js +96 -155
- package/core/blur-event.js +106 -42
- package/core/clear.js +13 -3
- package/core/coords.js +214 -90
- package/core/copy-elem.js +27 -13
- package/core/dataStorage.js +84 -21
- package/core/draw.js +80 -40
- package/core/elem-get-set.js +161 -77
- package/core/event.js +143 -28
- package/core/history.js +51 -31
- package/core/historyrecording.js +4 -2
- package/core/json.js +54 -12
- package/core/layer.js +11 -17
- package/core/math.js +102 -23
- package/core/namespaces.js +5 -5
- package/core/paint.js +100 -23
- package/core/paste-elem.js +58 -19
- package/core/path-actions.js +812 -791
- package/core/path-method.js +236 -37
- package/core/path.js +45 -10
- package/core/recalculate.js +438 -24
- package/core/sanitize.js +71 -34
- package/core/select.js +44 -20
- package/core/selected-elem.js +146 -31
- package/core/selection.js +16 -6
- package/core/svg-exec.js +103 -29
- package/core/svgroot.js +1 -1
- package/core/text-actions.js +327 -306
- package/core/undo.js +20 -5
- package/core/units.js +8 -6
- package/core/utilities.js +316 -203
- package/dist/svgcanvas.js +31616 -53281
- package/dist/svgcanvas.js.map +1 -1
- package/package.json +55 -54
- package/publish.md +1 -6
- package/svgcanvas.d.ts +225 -0
- package/svgcanvas.js +9 -9
- package/vite.config.mjs +20 -0
- package/rollup.config.mjs +0 -38
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
|
|
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
|
-
|
|
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
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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:
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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 ?
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
|
333
|
+
setHref(this.elem, String(value))
|
|
318
334
|
}
|
|
319
|
-
} else if (
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
|
380
|
+
setHref(this.elem, String(value))
|
|
362
381
|
}
|
|
363
|
-
} else if (
|
|
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(
|
|
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]
|
package/core/historyrecording.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
131
|
+
opacity,
|
|
96
132
|
style: 'pointer-events:inherit'
|
|
97
133
|
}, 100)
|
|
98
134
|
}
|
|
99
|
-
assignAttributes(shape,
|
|
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
|
-
|
|
144
|
+
const childNode = addSVGElementsFromJson(child)
|
|
145
|
+
if (childNode) {
|
|
146
|
+
shape.append(childNode)
|
|
147
|
+
}
|
|
106
148
|
})
|
|
107
149
|
}
|
|
108
150
|
|