@svgedit/svgcanvas 7.4.1 → 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/core/elem-get-set.js +86 -38
- package/core/event.js +53 -26
- package/core/history.js +57 -22
- package/core/sanitize.js +5 -4
- package/core/selected-elem.js +13 -7
- package/core/text-actions.js +39 -4
- package/core/undo.js +27 -10
- package/dist/svgcanvas.js +40089 -33447
- package/dist/svgcanvas.js.map +1 -1
- package/package.json +1 -1
- package/svgcanvas.js +3 -1
package/core/elem-get-set.js
CHANGED
|
@@ -655,14 +655,31 @@ const setStrokeAttrMethod = (attr, val) => {
|
|
|
655
655
|
}
|
|
656
656
|
}
|
|
657
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
|
+
|
|
658
676
|
/**
|
|
659
677
|
* Check if all selected text elements are in bold.
|
|
660
678
|
* @function module:svgcanvas.SvgCanvas#getBold
|
|
661
679
|
* @returns {boolean} `true` if all selected elements are bold, `false` otherwise.
|
|
662
680
|
*/
|
|
663
681
|
const getBoldMethod = () => {
|
|
664
|
-
const
|
|
665
|
-
const textElements = selectedElements.filter(el => el?.tagName === 'text')
|
|
682
|
+
const textElements = getSelectedTextElements()
|
|
666
683
|
return textElements.every(el => el.getAttribute('font-weight') === 'bold')
|
|
667
684
|
}
|
|
668
685
|
|
|
@@ -673,12 +690,16 @@ const getBoldMethod = () => {
|
|
|
673
690
|
* @returns {void}
|
|
674
691
|
*/
|
|
675
692
|
const setBoldMethod = (b) => {
|
|
676
|
-
const
|
|
677
|
-
const
|
|
678
|
-
|
|
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
|
+
}
|
|
679
699
|
if (!textElements.some(el => el.textContent)) {
|
|
680
700
|
svgCanvas.textActions.setCursor()
|
|
681
701
|
}
|
|
702
|
+
notifyTextChange(changedTextElements)
|
|
682
703
|
}
|
|
683
704
|
|
|
684
705
|
/**
|
|
@@ -686,8 +707,7 @@ const setBoldMethod = (b) => {
|
|
|
686
707
|
* @returns {boolean} Indicates whether or not elements have the text decoration value
|
|
687
708
|
*/
|
|
688
709
|
const hasTextDecorationMethod = (value) => {
|
|
689
|
-
const
|
|
690
|
-
const textElements = selectedElements.filter(el => el?.tagName === 'text')
|
|
710
|
+
const textElements = getSelectedTextElements()
|
|
691
711
|
return textElements.every(el => (el.getAttribute('text-decoration') || '').includes(value))
|
|
692
712
|
}
|
|
693
713
|
|
|
@@ -698,8 +718,7 @@ const hasTextDecorationMethod = (value) => {
|
|
|
698
718
|
*/
|
|
699
719
|
const addTextDecorationMethod = (value) => {
|
|
700
720
|
const { ChangeElementCommand, BatchCommand } = svgCanvas.history
|
|
701
|
-
const
|
|
702
|
-
const textElements = selectedElements.filter(el => el?.tagName === 'text')
|
|
721
|
+
const textElements = getSelectedTextElements()
|
|
703
722
|
|
|
704
723
|
const batchCmd = new BatchCommand()
|
|
705
724
|
textElements.forEach(elem => {
|
|
@@ -726,8 +745,7 @@ const addTextDecorationMethod = (value) => {
|
|
|
726
745
|
*/
|
|
727
746
|
const removeTextDecorationMethod = (value) => {
|
|
728
747
|
const { ChangeElementCommand, BatchCommand } = svgCanvas.history
|
|
729
|
-
const
|
|
730
|
-
const textElements = selectedElements.filter(el => el?.tagName === 'text')
|
|
748
|
+
const textElements = getSelectedTextElements()
|
|
731
749
|
|
|
732
750
|
const batchCmd = new BatchCommand()
|
|
733
751
|
textElements.forEach(elem => {
|
|
@@ -750,8 +768,7 @@ const removeTextDecorationMethod = (value) => {
|
|
|
750
768
|
* @returns {boolean} `true` if all selected elements are in italics, `false` otherwise.
|
|
751
769
|
*/
|
|
752
770
|
const getItalicMethod = () => {
|
|
753
|
-
const
|
|
754
|
-
const textElements = selectedElements.filter(el => el?.tagName === 'text')
|
|
771
|
+
const textElements = getSelectedTextElements()
|
|
755
772
|
return textElements.every(el => el.getAttribute('font-style') === 'italic')
|
|
756
773
|
}
|
|
757
774
|
|
|
@@ -762,12 +779,16 @@ const getItalicMethod = () => {
|
|
|
762
779
|
* @returns {void}
|
|
763
780
|
*/
|
|
764
781
|
const setItalicMethod = (i) => {
|
|
765
|
-
const
|
|
766
|
-
const
|
|
767
|
-
|
|
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
|
+
}
|
|
768
788
|
if (!textElements.some(el => el.textContent)) {
|
|
769
789
|
svgCanvas.textActions.setCursor()
|
|
770
790
|
}
|
|
791
|
+
notifyTextChange(changedTextElements)
|
|
771
792
|
}
|
|
772
793
|
|
|
773
794
|
/**
|
|
@@ -776,9 +797,12 @@ const setItalicMethod = (i) => {
|
|
|
776
797
|
* @returns {void}
|
|
777
798
|
*/
|
|
778
799
|
const setTextAnchorMethod = (value) => {
|
|
779
|
-
const
|
|
780
|
-
const
|
|
781
|
-
|
|
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)
|
|
782
806
|
}
|
|
783
807
|
|
|
784
808
|
/**
|
|
@@ -787,12 +811,15 @@ const setTextAnchorMethod = (value) => {
|
|
|
787
811
|
* @returns {void}
|
|
788
812
|
*/
|
|
789
813
|
const setLetterSpacingMethod = (value) => {
|
|
790
|
-
const
|
|
791
|
-
const
|
|
792
|
-
|
|
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
|
+
}
|
|
793
819
|
if (!textElements.some(el => el.textContent)) {
|
|
794
820
|
svgCanvas.textActions.setCursor()
|
|
795
821
|
}
|
|
822
|
+
notifyTextChange(changedTextElements)
|
|
796
823
|
}
|
|
797
824
|
|
|
798
825
|
/**
|
|
@@ -801,12 +828,15 @@ const setLetterSpacingMethod = (value) => {
|
|
|
801
828
|
* @returns {void}
|
|
802
829
|
*/
|
|
803
830
|
const setWordSpacingMethod = (value) => {
|
|
804
|
-
const
|
|
805
|
-
const
|
|
806
|
-
|
|
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
|
+
}
|
|
807
836
|
if (!textElements.some(el => el.textContent)) {
|
|
808
837
|
svgCanvas.textActions.setCursor()
|
|
809
838
|
}
|
|
839
|
+
notifyTextChange(changedTextElements)
|
|
810
840
|
}
|
|
811
841
|
|
|
812
842
|
/**
|
|
@@ -815,12 +845,15 @@ const setWordSpacingMethod = (value) => {
|
|
|
815
845
|
* @returns {void}
|
|
816
846
|
*/
|
|
817
847
|
const setTextLengthMethod = (value) => {
|
|
818
|
-
const
|
|
819
|
-
const
|
|
820
|
-
|
|
848
|
+
const textElements = getSelectedTextElements()
|
|
849
|
+
const changedTextElements = getChangedTextElements(textElements, 'textLength', value)
|
|
850
|
+
if (changedTextElements.length > 0) {
|
|
851
|
+
svgCanvas.changeSelectedAttribute('textLength', value, changedTextElements)
|
|
852
|
+
}
|
|
821
853
|
if (!textElements.some(el => el.textContent)) {
|
|
822
854
|
svgCanvas.textActions.setCursor()
|
|
823
855
|
}
|
|
856
|
+
notifyTextChange(changedTextElements)
|
|
824
857
|
}
|
|
825
858
|
|
|
826
859
|
/**
|
|
@@ -829,12 +862,15 @@ const setTextLengthMethod = (value) => {
|
|
|
829
862
|
* @returns {void}
|
|
830
863
|
*/
|
|
831
864
|
const setLengthAdjustMethod = (value) => {
|
|
832
|
-
const
|
|
833
|
-
const
|
|
834
|
-
|
|
865
|
+
const textElements = getSelectedTextElements()
|
|
866
|
+
const changedTextElements = getChangedTextElements(textElements, 'lengthAdjust', value)
|
|
867
|
+
if (changedTextElements.length > 0) {
|
|
868
|
+
svgCanvas.changeSelectedAttribute('lengthAdjust', value, changedTextElements)
|
|
869
|
+
}
|
|
835
870
|
if (!textElements.some(el => el.textContent)) {
|
|
836
871
|
svgCanvas.textActions.setCursor()
|
|
837
872
|
}
|
|
873
|
+
notifyTextChange(changedTextElements)
|
|
838
874
|
}
|
|
839
875
|
|
|
840
876
|
/**
|
|
@@ -852,13 +888,16 @@ const getFontFamilyMethod = () => {
|
|
|
852
888
|
* @returns {void}
|
|
853
889
|
*/
|
|
854
890
|
const setFontFamilyMethod = (val) => {
|
|
855
|
-
const
|
|
856
|
-
const
|
|
891
|
+
const textElements = getSelectedTextElements()
|
|
892
|
+
const changedTextElements = getChangedTextElements(textElements, 'font-family', val)
|
|
857
893
|
svgCanvas.setCurText('font_family', val)
|
|
858
|
-
|
|
894
|
+
if (changedTextElements.length > 0) {
|
|
895
|
+
svgCanvas.changeSelectedAttribute('font-family', val, changedTextElements)
|
|
896
|
+
}
|
|
859
897
|
if (!textElements.some(el => el.textContent)) {
|
|
860
898
|
svgCanvas.textActions.setCursor()
|
|
861
899
|
}
|
|
900
|
+
notifyTextChange(changedTextElements)
|
|
862
901
|
}
|
|
863
902
|
|
|
864
903
|
/**
|
|
@@ -868,8 +907,13 @@ const setFontFamilyMethod = (val) => {
|
|
|
868
907
|
* @returns {void}
|
|
869
908
|
*/
|
|
870
909
|
const setFontColorMethod = (val) => {
|
|
910
|
+
const textElements = getSelectedTextElements()
|
|
911
|
+
const changedTextElements = getChangedTextElements(textElements, 'fill', val)
|
|
871
912
|
svgCanvas.setCurText('fill', val)
|
|
872
|
-
|
|
913
|
+
if (changedTextElements.length > 0) {
|
|
914
|
+
svgCanvas.changeSelectedAttribute('fill', val, changedTextElements)
|
|
915
|
+
}
|
|
916
|
+
notifyTextChange(changedTextElements)
|
|
873
917
|
}
|
|
874
918
|
|
|
875
919
|
/**
|
|
@@ -895,12 +939,16 @@ const getFontSizeMethod = () => {
|
|
|
895
939
|
* @returns {void}
|
|
896
940
|
*/
|
|
897
941
|
const setFontSizeMethod = (val) => {
|
|
898
|
-
const
|
|
942
|
+
const textElements = getSelectedTextElements()
|
|
943
|
+
const changedTextElements = getChangedTextElements(textElements, 'font-size', val)
|
|
899
944
|
svgCanvas.setCurText('font_size', val)
|
|
900
|
-
|
|
901
|
-
|
|
945
|
+
if (changedTextElements.length > 0) {
|
|
946
|
+
svgCanvas.changeSelectedAttribute('font-size', val, changedTextElements)
|
|
947
|
+
}
|
|
948
|
+
if (!textElements.some(el => el.textContent)) {
|
|
902
949
|
svgCanvas.textActions.setCursor()
|
|
903
950
|
}
|
|
951
|
+
notifyTextChange(changedTextElements)
|
|
904
952
|
}
|
|
905
953
|
|
|
906
954
|
/**
|
package/core/event.js
CHANGED
|
@@ -89,7 +89,7 @@ const updateTransformList = (svgRoot, element, dx, dy) => {
|
|
|
89
89
|
if (!tlist) { return }
|
|
90
90
|
if (tlist.numberOfItems) {
|
|
91
91
|
const firstItem = tlist.getItem(0)
|
|
92
|
-
if (firstItem.type ===
|
|
92
|
+
if (firstItem.type === SVGTransform.SVG_TRANSFORM_TRANSLATE) {
|
|
93
93
|
tlist.replaceItem(xform, 0)
|
|
94
94
|
} else {
|
|
95
95
|
tlist.insertItemBefore(xform, 0)
|
|
@@ -99,6 +99,25 @@ const updateTransformList = (svgRoot, element, dx, dy) => {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
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
|
+
|
|
102
121
|
/**
|
|
103
122
|
*
|
|
104
123
|
* @param {MouseEvent} evt
|
|
@@ -159,10 +178,16 @@ const mouseMoveEvent = (evt) => {
|
|
|
159
178
|
svgCanvas.dragStartTransforms.set(selectedElement, selectedElement.getAttribute('transform') || '')
|
|
160
179
|
const slist = getTransformList(selectedElement)
|
|
161
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)
|
|
162
187
|
if (slist.numberOfItems) {
|
|
163
|
-
slist.insertItemBefore(
|
|
188
|
+
slist.insertItemBefore(dummy, 0)
|
|
164
189
|
} else {
|
|
165
|
-
slist.appendItem(
|
|
190
|
+
slist.appendItem(dummy)
|
|
166
191
|
}
|
|
167
192
|
}
|
|
168
193
|
svgCanvas.hasDragStartTransform = true
|
|
@@ -685,41 +710,43 @@ const mouseUpEvent = (evt) => {
|
|
|
685
710
|
const tlist = getTransformList(elem)
|
|
686
711
|
if (!tlist || tlist.numberOfItems === 0) return
|
|
687
712
|
|
|
688
|
-
// Get the transform from BEFORE
|
|
689
|
-
|
|
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() || '')
|
|
690
721
|
|
|
691
722
|
// Check if the first transform is a translate (the drag transform we added)
|
|
692
723
|
const firstTransform = tlist.getItem(0)
|
|
693
|
-
const hasDragTranslate = firstTransform.type ===
|
|
694
|
-
|
|
695
|
-
//
|
|
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.
|
|
696
734
|
const isGroup = elem.tagName === 'g' || elem.tagName === 'a'
|
|
697
735
|
|
|
698
|
-
|
|
699
|
-
|
|
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 }))
|
|
736
|
+
if (isGroup && hasDragTranslate) {
|
|
737
|
+
batchCmd.addSubCommand(consolidateTransform(svgCanvas.getSvgRoot(), elem, tlist, oldTransform))
|
|
714
738
|
return
|
|
715
739
|
}
|
|
716
740
|
|
|
717
|
-
// For non-group elements with simple transforms, try recalculateDimensions
|
|
718
741
|
const cmd = svgCanvas.recalculateDimensions(elem)
|
|
719
742
|
if (cmd) {
|
|
720
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))
|
|
721
748
|
} else {
|
|
722
|
-
// recalculateDimensions returned null
|
|
749
|
+
// recalculateDimensions returned null and there's nothing left to consolidate
|
|
723
750
|
// Check if the transform actually changed and record it manually
|
|
724
751
|
const newTransform = elem.getAttribute('transform') || ''
|
|
725
752
|
if (newTransform !== oldTransform) {
|
package/core/history.js
CHANGED
|
@@ -8,6 +8,61 @@
|
|
|
8
8
|
|
|
9
9
|
import { NS } from './namespaces.js'
|
|
10
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
|
+
}
|
|
11
66
|
|
|
12
67
|
/**
|
|
13
68
|
* Group: Undo/Redo history management.
|
|
@@ -344,17 +399,7 @@ export class ChangeElementCommand extends Command {
|
|
|
344
399
|
|
|
345
400
|
// relocate rotational transform, if necessary
|
|
346
401
|
if (!bChangedTransform) {
|
|
347
|
-
|
|
348
|
-
if (angle) {
|
|
349
|
-
const bbox = getBBox(this.elem)
|
|
350
|
-
if (!bbox) return
|
|
351
|
-
const cx = bbox.x + bbox.width / 2
|
|
352
|
-
const cy = bbox.y + bbox.height / 2
|
|
353
|
-
const rotate = ['rotate(', angle, ' ', cx, ',', cy, ')'].join('')
|
|
354
|
-
if (rotate !== this.elem.getAttribute('transform')) {
|
|
355
|
-
this.elem.setAttribute('transform', rotate)
|
|
356
|
-
}
|
|
357
|
-
}
|
|
402
|
+
relocateRotationCenter(this.elem, Object.keys(this.newValues))
|
|
358
403
|
}
|
|
359
404
|
})
|
|
360
405
|
}
|
|
@@ -388,17 +433,7 @@ export class ChangeElementCommand extends Command {
|
|
|
388
433
|
})
|
|
389
434
|
// relocate rotational transform, if necessary
|
|
390
435
|
if (!bChangedTransform) {
|
|
391
|
-
|
|
392
|
-
if (angle) {
|
|
393
|
-
const bbox = getBBox(this.elem)
|
|
394
|
-
if (!bbox) return
|
|
395
|
-
const cx = bbox.x + bbox.width / 2
|
|
396
|
-
const cy = bbox.y + bbox.height / 2
|
|
397
|
-
const rotate = ['rotate(', angle, ' ', cx, ',', cy, ')'].join('')
|
|
398
|
-
if (rotate !== this.elem.getAttribute('transform')) {
|
|
399
|
-
this.elem.setAttribute('transform', rotate)
|
|
400
|
-
}
|
|
401
|
-
}
|
|
436
|
+
relocateRotationCenter(this.elem, Object.keys(this.oldValues))
|
|
402
437
|
}
|
|
403
438
|
})
|
|
404
439
|
}
|
package/core/sanitize.js
CHANGED
|
@@ -11,6 +11,7 @@ import { getHref, getRefElem, setHref, getUrlFromAttr } from './utilities.js'
|
|
|
11
11
|
import { warn } from '../common/logger.js'
|
|
12
12
|
|
|
13
13
|
const REVERSE_NS = getReverseNS()
|
|
14
|
+
const FONT_ATTRIBUTES = ['font-family', 'font-size', 'font-stretch', 'font-style', 'font-weight']
|
|
14
15
|
|
|
15
16
|
// Todo: Split out into core attributes, presentation attributes, etc. so consistent
|
|
16
17
|
/**
|
|
@@ -39,7 +40,7 @@ const svgWhiteList_ = {
|
|
|
39
40
|
feOffset: ['dx', 'in', 'dy', 'result'],
|
|
40
41
|
filter: ['color-interpolation-filters', 'filterRes', 'filterUnits', 'height', 'href', 'primitiveUnits', 'requiredFeatures', 'width', 'x', 'xlink:href', 'y'],
|
|
41
42
|
foreignObject: ['font-size', 'height', 'opacity', 'requiredFeatures', 'width', 'x', 'y'],
|
|
42
|
-
g: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', '
|
|
43
|
+
g: [...FONT_ATTRIBUTES, 'clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'text-anchor'],
|
|
43
44
|
image: [
|
|
44
45
|
'clip-path', 'clip-rule', 'filter', 'height', 'mask', 'opacity',
|
|
45
46
|
'preserveAspectRatio', 'requiredFeatures', 'systemLanguage', 'viewBox',
|
|
@@ -60,11 +61,11 @@ const svgWhiteList_ = {
|
|
|
60
61
|
style: ['type'],
|
|
61
62
|
svg: ['clip-path', 'clip-rule', 'enable-background', 'filter', 'height', 'mask', 'preserveAspectRatio', 'requiredFeatures', 'systemLanguage', 'version', 'viewBox', 'width', 'x', 'xmlns', 'xmlns:se', 'xmlns:xlink', 'xmlns:oi', 'oi:animations', 'y', 'stroke-linejoin', 'fill-rule', 'aria-label', 'stroke-width', 'fill-rule', 'xml:space'],
|
|
62
63
|
switch: ['requiredFeatures', 'systemLanguage'],
|
|
63
|
-
symbol: ['fill', 'fill-opacity', 'fill-rule', 'filter', '
|
|
64
|
-
text: ['clip-path', 'clip-rule', 'dominant-baseline', 'fill', 'fill-opacity', 'fill-rule', 'filter', '
|
|
64
|
+
symbol: [...FONT_ATTRIBUTES, 'fill', 'fill-opacity', 'fill-rule', 'filter', 'opacity', 'overflow', 'preserveAspectRatio', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'viewBox', 'width', 'height'],
|
|
65
|
+
text: [...FONT_ATTRIBUTES, 'clip-path', 'clip-rule', 'dominant-baseline', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'text-anchor', 'letter-spacing', 'word-spacing', 'text-decoration', 'textLength', 'lengthAdjust', 'x', 'xml:space', 'y'],
|
|
65
66
|
textPath: ['dominant-baseline', 'href', 'method', 'requiredFeatures', 'spacing', 'startOffset', 'systemLanguage', 'xlink:href'],
|
|
66
67
|
title: [],
|
|
67
|
-
tspan: ['clip-path', 'clip-rule', 'dx', 'dy', 'dominant-baseline', 'fill', 'fill-opacity', 'fill-rule', 'filter', '
|
|
68
|
+
tspan: [...FONT_ATTRIBUTES, 'clip-path', 'clip-rule', 'dx', 'dy', 'dominant-baseline', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'mask', 'opacity', 'requiredFeatures', 'rotate', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'text-anchor', 'textLength', 'x', 'xml:space', 'y'],
|
|
68
69
|
use: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'height', 'href', 'mask', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'width', 'x', 'xlink:href', 'y', 'overflow'],
|
|
69
70
|
// Filter Primitives
|
|
70
71
|
feComponentTransfer: ['in', 'result'],
|
package/core/selected-elem.js
CHANGED
|
@@ -1069,6 +1069,7 @@ const convertToGroup = elem => {
|
|
|
1069
1069
|
tlist.appendItem(xform)
|
|
1070
1070
|
recalculateDimensions(elem)
|
|
1071
1071
|
svgCanvas.call('selected', [elem])
|
|
1072
|
+
return elem
|
|
1072
1073
|
} else if (dataStorage.has($elem, 'symbol')) {
|
|
1073
1074
|
elem = dataStorage.get($elem, 'symbol')
|
|
1074
1075
|
if (!elem) {
|
|
@@ -1203,8 +1204,10 @@ const convertToGroup = elem => {
|
|
|
1203
1204
|
}
|
|
1204
1205
|
|
|
1205
1206
|
svgCanvas.addCommandToHistory(batchCmd)
|
|
1207
|
+
return g
|
|
1206
1208
|
} else {
|
|
1207
1209
|
warn('Unexpected element to ungroup:', elem, 'selected-elem')
|
|
1210
|
+
return null
|
|
1208
1211
|
}
|
|
1209
1212
|
}
|
|
1210
1213
|
|
|
@@ -1222,11 +1225,14 @@ const ungroupSelectedElement = () => {
|
|
|
1222
1225
|
return
|
|
1223
1226
|
}
|
|
1224
1227
|
if (dataStorage.has(g, 'gsvg') || dataStorage.has(g, 'symbol')) {
|
|
1225
|
-
// Is svg, so
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1228
|
+
// Is svg (e.g. imported content), so convert to a real group first, then
|
|
1229
|
+
// fall through below to actually ungroup/flatten that new group - a
|
|
1230
|
+
// single Ungroup action on imported content should ungroup it, not just
|
|
1231
|
+
// silently swap its <use>/gsvg wrapper for a <g> that still needs a
|
|
1232
|
+
// second Ungroup to do anything.
|
|
1233
|
+
g = convertToGroup(g)
|
|
1234
|
+
if (!g) { return }
|
|
1235
|
+
} else if (g.tagName === 'use') {
|
|
1230
1236
|
// Somehow doesn't have data set, so retrieve
|
|
1231
1237
|
const href = getHref(g)
|
|
1232
1238
|
if (!href || !href.startsWith('#')) {
|
|
@@ -1240,8 +1246,8 @@ const ungroupSelectedElement = () => {
|
|
|
1240
1246
|
}
|
|
1241
1247
|
dataStorage.put(g, 'symbol', symbol)
|
|
1242
1248
|
dataStorage.put(g, 'ref', symbol)
|
|
1243
|
-
convertToGroup(g)
|
|
1244
|
-
return
|
|
1249
|
+
g = convertToGroup(g)
|
|
1250
|
+
if (!g) { return }
|
|
1245
1251
|
}
|
|
1246
1252
|
const parentsA = getParents(g.parentNode, 'a')
|
|
1247
1253
|
if (parentsA?.length) {
|
package/core/text-actions.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { NS } from './namespaces.js'
|
|
9
|
-
import { transformPoint,
|
|
9
|
+
import { transformPoint, matrixMultiply, getTransformList, transformListToTransform } from './math.js'
|
|
10
10
|
import {
|
|
11
11
|
assignAttributes,
|
|
12
12
|
getElement,
|
|
@@ -44,6 +44,40 @@ class TextActions {
|
|
|
44
44
|
#lastY = null
|
|
45
45
|
#allowDbl = false
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Get the accumulated transformation matrix from the element up to the SVG content element.
|
|
49
|
+
* This includes transforms from all parent groups, fixing the issue where text cursor
|
|
50
|
+
* appears in the wrong position when editing text inside a transformed group.
|
|
51
|
+
* @param {Element} elem - The element to get the accumulated matrix for
|
|
52
|
+
* @returns {SVGMatrix|null} The accumulated transformation matrix, or null if none
|
|
53
|
+
* @private
|
|
54
|
+
*/
|
|
55
|
+
#getAccumulatedMatrix = (elem) => {
|
|
56
|
+
const svgContent = svgCanvas.getSvgContent()
|
|
57
|
+
const matrices = []
|
|
58
|
+
|
|
59
|
+
let current = elem
|
|
60
|
+
while (current && current !== svgContent && current.nodeType === 1) {
|
|
61
|
+
const tlist = getTransformList(current)
|
|
62
|
+
if (tlist && tlist.numberOfItems > 0) {
|
|
63
|
+
const matrix = transformListToTransform(tlist).matrix
|
|
64
|
+
matrices.unshift(matrix) // Add to beginning to maintain correct order
|
|
65
|
+
}
|
|
66
|
+
current = current.parentNode
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (matrices.length === 0) {
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (matrices.length === 1) {
|
|
74
|
+
return matrices[0]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Multiply all matrices together
|
|
78
|
+
return matrixMultiply(...matrices)
|
|
79
|
+
}
|
|
80
|
+
|
|
47
81
|
/**
|
|
48
82
|
*
|
|
49
83
|
* @param {Integer} index
|
|
@@ -526,11 +560,12 @@ class TextActions {
|
|
|
526
560
|
const str = this.#curtext.textContent
|
|
527
561
|
const len = str.length
|
|
528
562
|
|
|
529
|
-
const xform = this.#curtext.getAttribute('transform')
|
|
530
|
-
|
|
531
563
|
this.#textbb = utilsGetBBox(this.#curtext)
|
|
532
564
|
|
|
533
|
-
|
|
565
|
+
// Calculate accumulated transform matrix including all parent groups
|
|
566
|
+
// This fixes the issue where text cursor appears in wrong position
|
|
567
|
+
// when editing text inside a group with transforms
|
|
568
|
+
this.#matrix = this.#getAccumulatedMatrix(this.#curtext)
|
|
534
569
|
|
|
535
570
|
this.#chardata = []
|
|
536
571
|
this.#chardata.length = len
|