@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.
- 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 +247 -115
- package/core/event.js +171 -29
- package/core/history.js +105 -50
- 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 +95 -24
- 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 +410 -15
- package/core/sanitize.js +51 -18
- package/core/select.js +44 -20
- package/core/selected-elem.js +159 -38
- package/core/selection.js +16 -6
- package/core/svg-exec.js +99 -27
- package/core/svgroot.js +1 -1
- package/core/text-actions.js +363 -307
- package/core/undo.js +47 -15
- package/core/units.js +8 -6
- package/core/utilities.js +282 -170
- package/dist/svgcanvas.js +40021 -55172
- 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 +11 -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,9 +86,10 @@ 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
|
-
if (firstItem.type ===
|
|
92
|
+
if (firstItem.type === SVGTransform.SVG_TRANSFORM_TRANSLATE) {
|
|
90
93
|
tlist.replaceItem(xform, 0)
|
|
91
94
|
} else {
|
|
92
95
|
tlist.insertItemBefore(xform, 0)
|
|
@@ -96,6 +99,25 @@ const updateTransformList = (svgRoot, element, dx, dy) => {
|
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
101
|
|
|
102
|
+
// Consolidate an element's current transform list into a single matrix,
|
|
103
|
+
// returning an undo command that restores `oldTransform`. Used for groups
|
|
104
|
+
// (whose transform must stay on the group rather than be flattened into
|
|
105
|
+
// their children) and as a fallback when recalculateDimensions() doesn't
|
|
106
|
+
// recognize the transform-list shape a drag interaction left behind.
|
|
107
|
+
const consolidateTransform = (svgRoot, elem, tlist, oldTransform) => {
|
|
108
|
+
const consolidatedMatrix = transformListToTransform(tlist).matrix
|
|
109
|
+
|
|
110
|
+
while (tlist.numberOfItems > 0) {
|
|
111
|
+
tlist.removeItem(0)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const newTransform = svgRoot.createSVGTransform()
|
|
115
|
+
newTransform.setMatrix(consolidatedMatrix)
|
|
116
|
+
tlist.appendItem(newTransform)
|
|
117
|
+
|
|
118
|
+
return new ChangeElementCommand(elem, { transform: oldTransform })
|
|
119
|
+
}
|
|
120
|
+
|
|
99
121
|
/**
|
|
100
122
|
*
|
|
101
123
|
* @param {MouseEvent} evt
|
|
@@ -145,6 +167,31 @@ const mouseMoveEvent = (evt) => {
|
|
|
145
167
|
let tlist
|
|
146
168
|
switch (svgCanvas.getCurrentMode()) {
|
|
147
169
|
case 'select': {
|
|
170
|
+
// Insert dummy transform on first mouse move (drag start), not on click.
|
|
171
|
+
// This avoids creating multiple transforms that trigger unwanted flattening.
|
|
172
|
+
if (!svgCanvas.hasDragStartTransform && selectedElements.length > 0) {
|
|
173
|
+
// Store original transforms BEFORE adding the drag transform (for undo)
|
|
174
|
+
svgCanvas.dragStartTransforms = new Map()
|
|
175
|
+
for (const selectedElement of selectedElements) {
|
|
176
|
+
if (!selectedElement) { continue }
|
|
177
|
+
// Capture the transform attribute before we modify it
|
|
178
|
+
svgCanvas.dragStartTransforms.set(selectedElement, selectedElement.getAttribute('transform') || '')
|
|
179
|
+
const slist = getTransformList(selectedElement)
|
|
180
|
+
if (!slist) { continue }
|
|
181
|
+
// The dummy must already be a translate (not the default identity
|
|
182
|
+
// matrix) so updateTransformList's `firstItem.type === 2` check
|
|
183
|
+
// recognizes and replaces it in place on the very first mousemove,
|
|
184
|
+
// instead of leaving it behind as a stray extra transform item.
|
|
185
|
+
const dummy = svgRoot.createSVGTransform()
|
|
186
|
+
dummy.setTranslate(0, 0)
|
|
187
|
+
if (slist.numberOfItems) {
|
|
188
|
+
slist.insertItemBefore(dummy, 0)
|
|
189
|
+
} else {
|
|
190
|
+
slist.appendItem(dummy)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
svgCanvas.hasDragStartTransform = true
|
|
194
|
+
}
|
|
148
195
|
// we temporarily use a translate on the element(s) being dragged
|
|
149
196
|
// this transform is removed upon mousing up and the element is
|
|
150
197
|
// relocated to the new location
|
|
@@ -222,6 +269,7 @@ const mouseMoveEvent = (evt) => {
|
|
|
222
269
|
// while the mouse is down, when mouse goes up, we use this to recalculate
|
|
223
270
|
// the shape's coordinates
|
|
224
271
|
tlist = getTransformList(selected)
|
|
272
|
+
if (!tlist) { break }
|
|
225
273
|
const hasMatrix = hasMatrixTransform(tlist)
|
|
226
274
|
box = hasMatrix ? svgCanvas.getInitBbox() : getBBox(selected)
|
|
227
275
|
let left = box.x
|
|
@@ -548,10 +596,21 @@ const mouseMoveEvent = (evt) => {
|
|
|
548
596
|
*
|
|
549
597
|
* @returns {void}
|
|
550
598
|
*/
|
|
551
|
-
const mouseOutEvent = () => {
|
|
599
|
+
const mouseOutEvent = (evt) => {
|
|
552
600
|
const { $id } = svgCanvas
|
|
553
601
|
if (svgCanvas.getCurrentMode() !== 'select' && svgCanvas.getStarted()) {
|
|
554
|
-
const event = new
|
|
602
|
+
const event = new MouseEvent('mouseup', {
|
|
603
|
+
bubbles: true,
|
|
604
|
+
cancelable: true,
|
|
605
|
+
clientX: evt?.clientX ?? 0,
|
|
606
|
+
clientY: evt?.clientY ?? 0,
|
|
607
|
+
button: evt?.button ?? 0,
|
|
608
|
+
buttons: evt?.buttons ?? 0,
|
|
609
|
+
altKey: evt?.altKey ?? false,
|
|
610
|
+
ctrlKey: evt?.ctrlKey ?? false,
|
|
611
|
+
metaKey: evt?.metaKey ?? false,
|
|
612
|
+
shiftKey: evt?.shiftKey ?? false
|
|
613
|
+
})
|
|
555
614
|
$id('svgcanvas').dispatchEvent(event)
|
|
556
615
|
}
|
|
557
616
|
}
|
|
@@ -637,10 +696,73 @@ const mouseUpEvent = (evt) => {
|
|
|
637
696
|
}
|
|
638
697
|
svgCanvas.selectorManager.requestSelector(selected).showGrips(true)
|
|
639
698
|
}
|
|
640
|
-
// always recalculate dimensions to strip off stray identity transforms
|
|
641
|
-
svgCanvas.recalculateAllSelectedDimensions()
|
|
642
699
|
// if it was being dragged/resized
|
|
643
700
|
if (realX !== svgCanvas.getRStartX() || realY !== svgCanvas.getRStartY()) {
|
|
701
|
+
// Only recalculate dimensions after actual dragging/resizing to avoid
|
|
702
|
+
// unwanted transform flattening on simple clicks
|
|
703
|
+
|
|
704
|
+
// Create a single batch command for all moved elements
|
|
705
|
+
const batchCmd = new BatchCommand('position')
|
|
706
|
+
|
|
707
|
+
selectedElements.forEach((elem) => {
|
|
708
|
+
if (!elem) return
|
|
709
|
+
|
|
710
|
+
const tlist = getTransformList(elem)
|
|
711
|
+
if (!tlist || tlist.numberOfItems === 0) return
|
|
712
|
+
|
|
713
|
+
// Get the transform from BEFORE this interaction started. `dragStartTransforms`
|
|
714
|
+
// is only populated for 'select'-mode (move) drags; 'resize'/'rotate' drags fall
|
|
715
|
+
// through to this same code with dragStartTransforms left null, so fall back to
|
|
716
|
+
// the value captured at mousedown via setStartTransform, which correctly reflects
|
|
717
|
+
// the prior transform (e.g. a matrix left by an earlier move) regardless of mode.
|
|
718
|
+
const oldTransform = svgCanvas.dragStartTransforms?.has(elem)
|
|
719
|
+
? svgCanvas.dragStartTransforms.get(elem)
|
|
720
|
+
: (svgCanvas.getStartTransform() || '')
|
|
721
|
+
|
|
722
|
+
// Check if the first transform is a translate (the drag transform we added)
|
|
723
|
+
const firstTransform = tlist.getItem(0)
|
|
724
|
+
const hasDragTranslate = firstTransform.type === SVGTransform.SVG_TRANSFORM_TRANSLATE
|
|
725
|
+
|
|
726
|
+
// recalculateDimensions() returns null for groups (their transform must stay
|
|
727
|
+
// on the group itself), so consolidate those directly into one matrix. For
|
|
728
|
+
// every other element type, recalculateDimensions() already understands the
|
|
729
|
+
// translate/scale/translate shape produced by a resize (and plain translate
|
|
730
|
+
// from a move), and flattening into it keeps the shape's real geometry
|
|
731
|
+
// attributes (x/y/width/height/rx/ry/points/d/etc.) in sync - which is what
|
|
732
|
+
// drives the coordinate/size panel inputs and keeps stroke width from being
|
|
733
|
+
// visually distorted by a leftover scale matrix.
|
|
734
|
+
const isGroup = elem.tagName === 'g' || elem.tagName === 'a'
|
|
735
|
+
|
|
736
|
+
if (isGroup && hasDragTranslate) {
|
|
737
|
+
batchCmd.addSubCommand(consolidateTransform(svgCanvas.getSvgRoot(), elem, tlist, oldTransform))
|
|
738
|
+
return
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const cmd = svgCanvas.recalculateDimensions(elem)
|
|
742
|
+
if (cmd) {
|
|
743
|
+
batchCmd.addSubCommand(cmd)
|
|
744
|
+
} else if (tlist.numberOfItems > 1 && hasDragTranslate) {
|
|
745
|
+
// recalculateDimensions() didn't recognize this transform-list shape;
|
|
746
|
+
// fall back to consolidating into one matrix so the transform isn't lost.
|
|
747
|
+
batchCmd.addSubCommand(consolidateTransform(svgCanvas.getSvgRoot(), elem, tlist, oldTransform))
|
|
748
|
+
} else {
|
|
749
|
+
// recalculateDimensions returned null and there's nothing left to consolidate
|
|
750
|
+
// Check if the transform actually changed and record it manually
|
|
751
|
+
const newTransform = elem.getAttribute('transform') || ''
|
|
752
|
+
if (newTransform !== oldTransform) {
|
|
753
|
+
batchCmd.addSubCommand(new ChangeElementCommand(elem, { transform: oldTransform }))
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
if (!batchCmd.isEmpty()) {
|
|
759
|
+
svgCanvas.addCommandToHistory(batchCmd)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Clear the stored transforms AND reset the flag together
|
|
763
|
+
svgCanvas.dragStartTransforms = null
|
|
764
|
+
svgCanvas.hasDragStartTransform = false
|
|
765
|
+
|
|
644
766
|
const len = selectedElements.length
|
|
645
767
|
for (let i = 0; i < len; ++i) {
|
|
646
768
|
if (!selectedElements[i]) { break }
|
|
@@ -799,6 +921,8 @@ const mouseUpEvent = (evt) => {
|
|
|
799
921
|
svgCanvas.textActions.mouseUp(evt, mouseX, mouseY)
|
|
800
922
|
break
|
|
801
923
|
case 'rotate': {
|
|
924
|
+
svgCanvas.hasDragStartTransform = false
|
|
925
|
+
svgCanvas.dragStartTransforms = null
|
|
802
926
|
keep = true
|
|
803
927
|
element = null
|
|
804
928
|
svgCanvas.setCurrentMode('select')
|
|
@@ -812,8 +936,13 @@ const mouseUpEvent = (evt) => {
|
|
|
812
936
|
break
|
|
813
937
|
} default:
|
|
814
938
|
// This could occur in an extension
|
|
939
|
+
svgCanvas.hasDragStartTransform = false
|
|
940
|
+
svgCanvas.dragStartTransforms = null
|
|
815
941
|
break
|
|
816
942
|
}
|
|
943
|
+
// Reset drag flag after any mouseUp
|
|
944
|
+
svgCanvas.hasDragStartTransform = false
|
|
945
|
+
svgCanvas.dragStartTransforms = null
|
|
817
946
|
|
|
818
947
|
/**
|
|
819
948
|
* The main (left) mouse button is released (anywhere).
|
|
@@ -979,7 +1108,13 @@ const mouseDownEvent = (evt) => {
|
|
|
979
1108
|
svgCanvas.cloneSelectedElements(0, 0)
|
|
980
1109
|
}
|
|
981
1110
|
|
|
982
|
-
|
|
1111
|
+
// Get screenCTM from the first child group of svgcontent
|
|
1112
|
+
// Note: svgcontent itself has x/y offset attributes, so we use its first child
|
|
1113
|
+
const svgContent = $id('svgcontent')
|
|
1114
|
+
const rootGroup = svgContent?.querySelector('g')
|
|
1115
|
+
const screenCTM = rootGroup?.getScreenCTM?.()
|
|
1116
|
+
if (!screenCTM) { return }
|
|
1117
|
+
svgCanvas.setRootSctm(screenCTM.inverse())
|
|
983
1118
|
|
|
984
1119
|
const pt = transformPoint(evt.clientX, evt.clientY, svgCanvas.getrootSctm())
|
|
985
1120
|
const mouseX = pt.x * zoom
|
|
@@ -1039,12 +1174,22 @@ const mouseDownEvent = (evt) => {
|
|
|
1039
1174
|
svgCanvas.setStartTransform(mouseTarget.getAttribute('transform'))
|
|
1040
1175
|
|
|
1041
1176
|
const tlist = getTransformList(mouseTarget)
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1177
|
+
|
|
1178
|
+
// Consolidate transforms for non-group elements to simplify dragging
|
|
1179
|
+
// For elements with multiple transforms (e.g., after ungrouping), consolidate them
|
|
1180
|
+
// into a single matrix so the dummy translate can be properly applied during drag
|
|
1181
|
+
if (tlist?.numberOfItems > 1 && mouseTarget.tagName !== 'g' && mouseTarget.tagName !== 'a') {
|
|
1182
|
+
// Compute the consolidated matrix from all transforms
|
|
1183
|
+
const consolidatedMatrix = transformListToTransform(tlist).matrix
|
|
1184
|
+
|
|
1185
|
+
// Clear the transform list and add a single matrix transform
|
|
1186
|
+
while (tlist.numberOfItems > 0) {
|
|
1187
|
+
tlist.removeItem(0)
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const newTransform = svgCanvas.getSvgRoot().createSVGTransform()
|
|
1191
|
+
newTransform.setMatrix(consolidatedMatrix)
|
|
1192
|
+
tlist.appendItem(newTransform)
|
|
1048
1193
|
}
|
|
1049
1194
|
switch (svgCanvas.getCurrentMode()) {
|
|
1050
1195
|
case 'select':
|
|
@@ -1067,19 +1212,9 @@ const mouseDownEvent = (evt) => {
|
|
|
1067
1212
|
}
|
|
1068
1213
|
// else if it's a path, go into pathedit mode in mouseup
|
|
1069
1214
|
|
|
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
|
-
}
|
|
1215
|
+
// Note: Dummy transform insertion moved to mouseMove to avoid triggering
|
|
1216
|
+
// recalculateDimensions on simple clicks. The dummy transform is only needed
|
|
1217
|
+
// when actually starting a drag operation.
|
|
1083
1218
|
} else if (!rightClick) {
|
|
1084
1219
|
svgCanvas.clearSelection()
|
|
1085
1220
|
svgCanvas.setCurrentMode('multiselect')
|
|
@@ -1105,13 +1240,14 @@ const mouseDownEvent = (evt) => {
|
|
|
1105
1240
|
}
|
|
1106
1241
|
assignAttributes(svgCanvas.getRubberBox(), {
|
|
1107
1242
|
x: realX * zoom,
|
|
1108
|
-
y:
|
|
1243
|
+
y: realY * zoom,
|
|
1109
1244
|
width: 0,
|
|
1110
1245
|
height: 0,
|
|
1111
1246
|
display: 'inline'
|
|
1112
1247
|
}, 100)
|
|
1113
1248
|
break
|
|
1114
1249
|
case 'resize': {
|
|
1250
|
+
if (!tlist) { break }
|
|
1115
1251
|
svgCanvas.setStarted(true)
|
|
1116
1252
|
svgCanvas.setStartX(x)
|
|
1117
1253
|
svgCanvas.setStartY(y)
|
|
@@ -1339,7 +1475,13 @@ const DOMMouseScrollEvent = (e) => {
|
|
|
1339
1475
|
|
|
1340
1476
|
e.preventDefault()
|
|
1341
1477
|
|
|
1342
|
-
|
|
1478
|
+
// Get screenCTM from the first child group of svgcontent
|
|
1479
|
+
// Note: svgcontent itself has x/y offset attributes, so we use its first child
|
|
1480
|
+
const svgContent = $id('svgcontent')
|
|
1481
|
+
const rootGroup = svgContent?.querySelector('g')
|
|
1482
|
+
const screenCTM = rootGroup?.getScreenCTM?.()
|
|
1483
|
+
if (!screenCTM) { return }
|
|
1484
|
+
svgCanvas.setRootSctm(screenCTM.inverse())
|
|
1343
1485
|
|
|
1344
1486
|
const workarea = document.getElementById('workarea')
|
|
1345
1487
|
const scrbar = 15
|
package/core/history.js
CHANGED
|
@@ -6,7 +6,63 @@
|
|
|
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'
|
|
11
|
+
import { getTransformList, transformListToTransform, transformPoint } from './math.js'
|
|
12
|
+
|
|
13
|
+
// Attributes that affect an element's bounding box. Only these require
|
|
14
|
+
// recalculating the rotation center when changed.
|
|
15
|
+
export const BBOX_AFFECTING_ATTRS = new Set([
|
|
16
|
+
'x', 'y', 'x1', 'y1', 'x2', 'y2',
|
|
17
|
+
'cx', 'cy', 'r', 'rx', 'ry',
|
|
18
|
+
'width', 'height', 'd', 'points'
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Relocate rotation center after a bbox-affecting attribute change.
|
|
23
|
+
* Uses the transform list API to update only the rotation entry,
|
|
24
|
+
* preserving compound transforms (translate, scale, etc.).
|
|
25
|
+
* @param {Element} elem - SVG element
|
|
26
|
+
* @param {string[]} changedAttrs - attribute names that were changed
|
|
27
|
+
*/
|
|
28
|
+
function relocateRotationCenter (elem, changedAttrs) {
|
|
29
|
+
const hasBboxChange = changedAttrs.some(attr => BBOX_AFFECTING_ATTRS.has(attr))
|
|
30
|
+
if (!hasBboxChange) return
|
|
31
|
+
|
|
32
|
+
const angle = getRotationAngle(elem)
|
|
33
|
+
if (!angle) return
|
|
34
|
+
|
|
35
|
+
const tlist = getTransformList(elem)
|
|
36
|
+
let n = tlist.numberOfItems
|
|
37
|
+
while (n--) {
|
|
38
|
+
const xform = tlist.getItem(n)
|
|
39
|
+
if (xform.type === 4) { // SVG_TRANSFORM_ROTATE
|
|
40
|
+
// Compute bbox BEFORE removing the rotation so we can bail out
|
|
41
|
+
// safely if getBBox returns nothing (avoids losing the rotation).
|
|
42
|
+
const box = getBBox(elem)
|
|
43
|
+
if (!box) return
|
|
44
|
+
|
|
45
|
+
tlist.removeItem(n)
|
|
46
|
+
|
|
47
|
+
// Transform bbox center through only post-rotation transforms.
|
|
48
|
+
// After removeItem(n), what was at n+1 is now at n.
|
|
49
|
+
let centerMatrix
|
|
50
|
+
if (n < tlist.numberOfItems) {
|
|
51
|
+
centerMatrix = transformListToTransform(tlist, n, tlist.numberOfItems - 1).matrix
|
|
52
|
+
} else {
|
|
53
|
+
centerMatrix = elem.ownerSVGElement.createSVGMatrix() // identity
|
|
54
|
+
}
|
|
55
|
+
const center = transformPoint(
|
|
56
|
+
box.x + box.width / 2, box.y + box.height / 2, centerMatrix
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const newrot = elem.ownerSVGElement.createSVGTransform()
|
|
60
|
+
newrot.setRotate(angle, center.x, center.y)
|
|
61
|
+
tlist.insertItemBefore(newrot, n)
|
|
62
|
+
break
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
10
66
|
|
|
11
67
|
/**
|
|
12
68
|
* Group: Undo/Redo history management.
|
|
@@ -140,7 +196,7 @@ export class MoveElementCommand extends Command {
|
|
|
140
196
|
constructor (elem, oldNextSibling, oldParent, text) {
|
|
141
197
|
super()
|
|
142
198
|
this.elem = elem
|
|
143
|
-
this.text = text ?
|
|
199
|
+
this.text = text ? `Move ${elem.tagName} to ${text}` : `Move ${elem.tagName}`
|
|
144
200
|
this.oldNextSibling = oldNextSibling
|
|
145
201
|
this.oldParent = oldParent
|
|
146
202
|
this.newNextSibling = elem.nextSibling
|
|
@@ -155,7 +211,11 @@ export class MoveElementCommand extends Command {
|
|
|
155
211
|
*/
|
|
156
212
|
apply (handler) {
|
|
157
213
|
super.apply(handler, () => {
|
|
158
|
-
|
|
214
|
+
const reference =
|
|
215
|
+
this.newNextSibling && this.newNextSibling.parentNode === this.newParent
|
|
216
|
+
? this.newNextSibling
|
|
217
|
+
: null
|
|
218
|
+
this.elem = this.newParent.insertBefore(this.elem, reference)
|
|
159
219
|
})
|
|
160
220
|
}
|
|
161
221
|
|
|
@@ -167,7 +227,11 @@ export class MoveElementCommand extends Command {
|
|
|
167
227
|
*/
|
|
168
228
|
unapply (handler) {
|
|
169
229
|
super.unapply(handler, () => {
|
|
170
|
-
|
|
230
|
+
const reference =
|
|
231
|
+
this.oldNextSibling && this.oldNextSibling.parentNode === this.oldParent
|
|
232
|
+
? this.oldNextSibling
|
|
233
|
+
: null
|
|
234
|
+
this.elem = this.oldParent.insertBefore(this.elem, reference)
|
|
171
235
|
})
|
|
172
236
|
}
|
|
173
237
|
}
|
|
@@ -184,7 +248,7 @@ export class InsertElementCommand extends Command {
|
|
|
184
248
|
constructor (elem, text) {
|
|
185
249
|
super()
|
|
186
250
|
this.elem = elem
|
|
187
|
-
this.text = text ||
|
|
251
|
+
this.text = text || `Create ${elem.tagName}`
|
|
188
252
|
this.parent = elem.parentNode
|
|
189
253
|
this.nextSibling = this.elem.nextSibling
|
|
190
254
|
}
|
|
@@ -197,7 +261,11 @@ export class InsertElementCommand extends Command {
|
|
|
197
261
|
*/
|
|
198
262
|
apply (handler) {
|
|
199
263
|
super.apply(handler, () => {
|
|
200
|
-
|
|
264
|
+
const reference =
|
|
265
|
+
this.nextSibling && this.nextSibling.parentNode === this.parent
|
|
266
|
+
? this.nextSibling
|
|
267
|
+
: null
|
|
268
|
+
this.elem = this.parent.insertBefore(this.elem, reference)
|
|
201
269
|
})
|
|
202
270
|
}
|
|
203
271
|
|
|
@@ -229,7 +297,7 @@ export class RemoveElementCommand extends Command {
|
|
|
229
297
|
constructor (elem, oldNextSibling, oldParent, text) {
|
|
230
298
|
super()
|
|
231
299
|
this.elem = elem
|
|
232
|
-
this.text = text ||
|
|
300
|
+
this.text = text || `Delete ${elem.tagName}`
|
|
233
301
|
this.nextSibling = oldNextSibling
|
|
234
302
|
this.parent = oldParent
|
|
235
303
|
}
|
|
@@ -255,10 +323,11 @@ export class RemoveElementCommand extends Command {
|
|
|
255
323
|
*/
|
|
256
324
|
unapply (handler) {
|
|
257
325
|
super.unapply(handler, () => {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
326
|
+
const reference =
|
|
327
|
+
this.nextSibling && this.nextSibling.parentNode === this.parent
|
|
328
|
+
? this.nextSibling
|
|
329
|
+
: null
|
|
330
|
+
this.parent.insertBefore(this.elem, reference) // Don't use `before` or `prepend` as `reference` may be `null`
|
|
262
331
|
})
|
|
263
332
|
}
|
|
264
333
|
}
|
|
@@ -284,7 +353,7 @@ export class ChangeElementCommand extends Command {
|
|
|
284
353
|
constructor (elem, attrs, text) {
|
|
285
354
|
super()
|
|
286
355
|
this.elem = elem
|
|
287
|
-
this.text = text ?
|
|
356
|
+
this.text = text ? `Change ${elem.tagName} ${text}` : `Change ${elem.tagName}`
|
|
288
357
|
this.newValues = {}
|
|
289
358
|
this.oldValues = attrs
|
|
290
359
|
for (const attr in attrs) {
|
|
@@ -308,19 +377,21 @@ export class ChangeElementCommand extends Command {
|
|
|
308
377
|
super.apply(handler, () => {
|
|
309
378
|
let bChangedTransform = false
|
|
310
379
|
Object.entries(this.newValues).forEach(([attr, value]) => {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
380
|
+
const isNullishOrEmpty = value === null || value === undefined || value === ''
|
|
381
|
+
if (attr === '#text') {
|
|
382
|
+
this.elem.textContent = value === null || value === undefined ? '' : String(value)
|
|
383
|
+
} else if (attr === '#href') {
|
|
384
|
+
if (isNullishOrEmpty) {
|
|
385
|
+
this.elem.removeAttribute('href')
|
|
386
|
+
this.elem.removeAttributeNS(NS.XLINK, 'href')
|
|
316
387
|
} else {
|
|
317
|
-
this.elem
|
|
388
|
+
setHref(this.elem, String(value))
|
|
318
389
|
}
|
|
319
|
-
} else if (
|
|
320
|
-
this.elem.textContent = ''
|
|
321
|
-
} else {
|
|
390
|
+
} else if (isNullishOrEmpty) {
|
|
322
391
|
this.elem.setAttribute(attr, '')
|
|
323
392
|
this.elem.removeAttribute(attr)
|
|
393
|
+
} else {
|
|
394
|
+
this.elem.setAttribute(attr, value)
|
|
324
395
|
}
|
|
325
396
|
|
|
326
397
|
if (attr === 'transform') { bChangedTransform = true }
|
|
@@ -328,16 +399,7 @@ export class ChangeElementCommand extends Command {
|
|
|
328
399
|
|
|
329
400
|
// relocate rotational transform, if necessary
|
|
330
401
|
if (!bChangedTransform) {
|
|
331
|
-
|
|
332
|
-
if (angle) {
|
|
333
|
-
const bbox = getBBox(this.elem)
|
|
334
|
-
const cx = bbox.x + bbox.width / 2
|
|
335
|
-
const cy = bbox.y + bbox.height / 2
|
|
336
|
-
const rotate = ['rotate(', angle, ' ', cx, ',', cy, ')'].join('')
|
|
337
|
-
if (rotate !== this.elem.getAttribute('transform')) {
|
|
338
|
-
this.elem.setAttribute('transform', rotate)
|
|
339
|
-
}
|
|
340
|
-
}
|
|
402
|
+
relocateRotationCenter(this.elem, Object.keys(this.newValues))
|
|
341
403
|
}
|
|
342
404
|
})
|
|
343
405
|
}
|
|
@@ -352,33 +414,26 @@ export class ChangeElementCommand extends Command {
|
|
|
352
414
|
super.unapply(handler, () => {
|
|
353
415
|
let bChangedTransform = false
|
|
354
416
|
Object.entries(this.oldValues).forEach(([attr, value]) => {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
417
|
+
const isNullishOrEmpty = value === null || value === undefined || value === ''
|
|
418
|
+
if (attr === '#text') {
|
|
419
|
+
this.elem.textContent = value === null || value === undefined ? '' : String(value)
|
|
420
|
+
} else if (attr === '#href') {
|
|
421
|
+
if (isNullishOrEmpty) {
|
|
422
|
+
this.elem.removeAttribute('href')
|
|
423
|
+
this.elem.removeAttributeNS(NS.XLINK, 'href')
|
|
360
424
|
} else {
|
|
361
|
-
this.elem
|
|
425
|
+
setHref(this.elem, String(value))
|
|
362
426
|
}
|
|
363
|
-
} else if (
|
|
364
|
-
this.elem.textContent = ''
|
|
365
|
-
} else {
|
|
427
|
+
} else if (isNullishOrEmpty) {
|
|
366
428
|
this.elem.removeAttribute(attr)
|
|
429
|
+
} else {
|
|
430
|
+
this.elem.setAttribute(attr, value)
|
|
367
431
|
}
|
|
368
432
|
if (attr === 'transform') { bChangedTransform = true }
|
|
369
433
|
})
|
|
370
434
|
// relocate rotational transform, if necessary
|
|
371
435
|
if (!bChangedTransform) {
|
|
372
|
-
|
|
373
|
-
if (angle) {
|
|
374
|
-
const bbox = getBBox(this.elem)
|
|
375
|
-
const cx = bbox.x + bbox.width / 2
|
|
376
|
-
const cy = bbox.y + bbox.height / 2
|
|
377
|
-
const rotate = ['rotate(', angle, ' ', cx, ',', cy, ')'].join('')
|
|
378
|
-
if (rotate !== this.elem.getAttribute('transform')) {
|
|
379
|
-
this.elem.setAttribute('transform', rotate)
|
|
380
|
-
}
|
|
381
|
-
}
|
|
436
|
+
relocateRotationCenter(this.elem, Object.keys(this.oldValues))
|
|
382
437
|
}
|
|
383
438
|
})
|
|
384
439
|
}
|
|
@@ -602,7 +657,7 @@ export class UndoManager {
|
|
|
602
657
|
const p = this.undoChangeStackPointer--
|
|
603
658
|
const changeset = this.undoableChangeStack[p]
|
|
604
659
|
const { attrName } = changeset
|
|
605
|
-
const batchCmd = new BatchCommand(
|
|
660
|
+
const batchCmd = new BatchCommand(`Change ${attrName}`)
|
|
606
661
|
let i = changeset.elements.length
|
|
607
662
|
while (i--) {
|
|
608
663
|
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
|