This closes #301, support delete and add radio button form control

- New exported function `DeleteFormControl` has been added
- Update unit tests
- Fix comments was missing after form control added
- Update pull request templates
This commit is contained in:
xuri 2023-07-13 00:03:24 +08:00
parent 2c8dc5c150
commit b667987084
No known key found for this signature in database
GPG Key ID: BA5E5BB1C948EDF7
4 changed files with 212 additions and 79 deletions

View File

@ -21,7 +21,7 @@
<!--- Please describe in detail how you tested your changes. --> <!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, and the tests you ran to --> <!--- Include details of your testing environment, and the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. --> <!--- See how your change affects other areas of the code, etc. -->
## Types of changes ## Types of changes

116
vml.go
View File

@ -28,6 +28,7 @@ type FormControlType byte
const ( const (
FormControlNote FormControlType = iota FormControlNote FormControlType = iota
FormControlButton FormControlButton
FormControlCheckbox
FormControlRadio FormControlRadio
) )
@ -114,8 +115,9 @@ func (f *File) AddComment(sheet string, opts Comment) error {
}) })
} }
// DeleteComment provides the method to delete comment in a sheet by given // DeleteComment provides the method to delete comment in a worksheet by given
// worksheet name. For example, delete the comment in Sheet1!$A$30: // worksheet name and cell reference. For example, delete the comment in
// Sheet1!$A$30:
// //
// err := f.DeleteComment("Sheet1", "A30") // err := f.DeleteComment("Sheet1", "A30")
func (f *File) DeleteComment(sheet, cell string) error { func (f *File) DeleteComment(sheet, cell string) error {
@ -315,6 +317,80 @@ func (f *File) AddFormControl(sheet string, opts FormControl) error {
}) })
} }
// DeleteFormControl provides the method to delete form control in a worksheet
// by given worksheet name and cell reference. For example, delete the form
// control in Sheet1!$A$30:
//
// err := f.DeleteFormControl("Sheet1", "A30")
func (f *File) DeleteFormControl(sheet, cell string) error {
ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}
col, row, err := CellNameToCoordinates(cell)
if err != nil {
return err
}
if ws.LegacyDrawing == nil {
return err
}
sheetRelationshipsDrawingVML := f.getSheetRelationshipsTargetByID(sheet, ws.LegacyDrawing.RID)
vmlID, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(sheetRelationshipsDrawingVML, "../drawings/vmlDrawing"), ".vml"))
drawingVML := strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl")
vml := f.VMLDrawing[drawingVML]
if vml == nil {
vml = &vmlDrawing{
XMLNSv: "urn:schemas-microsoft-com:vml",
XMLNSo: "urn:schemas-microsoft-com:office:office",
XMLNSx: "urn:schemas-microsoft-com:office:excel",
XMLNSmv: "http://macVmlSchemaUri",
ShapeLayout: &xlsxShapeLayout{
Ext: "edit", IDmap: &xlsxIDmap{Ext: "edit", Data: vmlID},
},
ShapeType: &xlsxShapeType{
Stroke: &xlsxStroke{JoinStyle: "miter"},
VPath: &vPath{GradientShapeOK: "t", ConnectType: "rect"},
},
}
// load exist VML shapes from xl/drawings/vmlDrawing%d.vml
d, err := f.decodeVMLDrawingReader(drawingVML)
if err != nil {
return err
}
if d != nil {
vml.ShapeType.ID = d.ShapeType.ID
vml.ShapeType.CoordSize = d.ShapeType.CoordSize
vml.ShapeType.Spt = d.ShapeType.Spt
vml.ShapeType.Path = d.ShapeType.Path
for _, v := range d.Shape {
s := xlsxShape{
ID: v.ID,
Type: v.Type,
Style: v.Style,
Button: v.Button,
Filled: v.Filled,
FillColor: v.FillColor,
InsetMode: v.InsetMode,
Stroked: v.Stroked,
StrokeColor: v.StrokeColor,
Val: v.Val,
}
vml.Shape = append(vml.Shape, s)
}
}
}
for i, sp := range vml.Shape {
var shapeVal decodeShapeVal
if err = xml.Unmarshal([]byte(fmt.Sprintf("<shape>%s</shape>", sp.Val)), &shapeVal); err == nil &&
shapeVal.ClientData.ObjectType != "Note" && shapeVal.ClientData.Column == col-1 && shapeVal.ClientData.Row == row-1 {
vml.Shape = append(vml.Shape[:i], vml.Shape[i+1:]...)
break
}
}
f.VMLDrawing[drawingVML] = vml
return err
}
// countVMLDrawing provides a function to get VML drawing files count storage // countVMLDrawing provides a function to get VML drawing files count storage
// in the folder xl/drawings. // in the folder xl/drawings.
func (f *File) countVMLDrawing() int { func (f *File) countVMLDrawing() int {
@ -380,6 +456,8 @@ func (f *File) addVMLObject(opts vmlOptions) error {
} }
drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" drawingVML := "xl/drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml"
sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml" sheetRelationshipsDrawingVML := "../drawings/vmlDrawing" + strconv.Itoa(vmlID) + ".vml"
sheetXMLPath, _ := f.getSheetXMLPath(opts.Sheet)
sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
if ws.LegacyDrawing != nil { if ws.LegacyDrawing != nil {
// The worksheet already has a VML relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml. // The worksheet already has a VML relationships, use the relationships drawing ../drawings/vmlDrawing%d.vml.
sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(opts.Sheet, ws.LegacyDrawing.RID) sheetRelationshipsDrawingVML = f.getSheetRelationshipsTargetByID(opts.Sheet, ws.LegacyDrawing.RID)
@ -387,13 +465,7 @@ func (f *File) addVMLObject(opts vmlOptions) error {
drawingVML = strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl") drawingVML = strings.ReplaceAll(sheetRelationshipsDrawingVML, "..", "xl")
} else { } else {
// Add first VML drawing for given sheet. // Add first VML drawing for given sheet.
sheetXMLPath, _ := f.getSheetXMLPath(opts.Sheet)
sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "") rID := f.addRels(sheetRels, SourceRelationshipDrawingVML, sheetRelationshipsDrawingVML, "")
if !opts.FormCtrl {
sheetRelationshipsComments := "../comments" + strconv.Itoa(vmlID) + ".xml"
f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "")
}
f.addSheetNameSpace(opts.Sheet, SourceRelationship) f.addSheetNameSpace(opts.Sheet, SourceRelationship)
f.addSheetLegacyDrawing(opts.Sheet, rID) f.addSheetLegacyDrawing(opts.Sheet, rID)
} }
@ -405,6 +477,10 @@ func (f *File) addVMLObject(opts vmlOptions) error {
if err = f.addComment(commentsXML, opts); err != nil { if err = f.addComment(commentsXML, opts); err != nil {
return err return err
} }
if sheetXMLPath, ok := f.getSheetXMLPath(opts.Sheet); ok && f.getSheetComments(filepath.Base(sheetXMLPath)) == "" {
sheetRelationshipsComments := "../comments" + strconv.Itoa(vmlID) + ".xml"
f.addRels(sheetRels, SourceRelationshipComments, sheetRelationshipsComments, "")
}
} }
return f.addContentTypePart(vmlID, "comments") return f.addContentTypePart(vmlID, "comments")
} }
@ -475,8 +551,7 @@ func formCtrlText(opts *vmlOptions) []vmlFont {
return font return font
} }
var ( var formCtrlPresets = map[FormControlType]struct {
formCtrlPresets = map[FormControlType]struct {
objectType string objectType string
filled string filled string
fillColor string fillColor string
@ -489,7 +564,7 @@ var (
noThreeD *string noThreeD *string
firstButton *string firstButton *string
shadow *vShadow shadow *vShadow
}{ }{
FormControlNote: { FormControlNote: {
objectType: "Note", objectType: "Note",
filled: "", filled: "",
@ -528,6 +603,20 @@ var (
firstButton: nil, firstButton: nil,
shadow: nil, shadow: nil,
}, },
FormControlCheckbox: {
objectType: "Checkbox",
filled: "f",
fillColor: "window [65]",
stroked: "f",
strokeColor: "windowText [64]",
strokeButton: "",
fill: nil,
textHAlign: "",
textVAlign: "Center",
noThreeD: stringPtr(""),
firstButton: nil,
shadow: nil,
},
FormControlRadio: { FormControlRadio: {
objectType: "Radio", objectType: "Radio",
filled: "f", filled: "f",
@ -542,8 +631,7 @@ var (
firstButton: stringPtr(""), firstButton: stringPtr(""),
shadow: nil, shadow: nil,
}, },
} }
)
// addDrawingVML provides a function to create VML drawing XML as // addDrawingVML provides a function to create VML drawing XML as
// xl/drawings/vmlDrawing%d.vml by given data ID, XML path and VML options. The // xl/drawings/vmlDrawing%d.vml by given data ID, XML path and VML options. The
@ -634,7 +722,7 @@ func (f *File) addDrawingVML(dataID int, drawingVML string, opts *vmlOptions) er
if opts.FormCtrl { if opts.FormCtrl {
sp.ClientData.FmlaMacro = opts.Macro sp.ClientData.FmlaMacro = opts.Macro
} }
if opts.Type == FormControlRadio && opts.Checked { if (opts.Type == FormControlCheckbox || opts.Type == FormControlRadio) && opts.Checked {
sp.ClientData.Checked = stringPtr("1") sp.ClientData.Checked = stringPtr("1")
} }
s, _ := xml.Marshal(sp) s, _ := xml.Marshal(sp)

View File

@ -172,6 +172,20 @@ type decodeShape struct {
Val string `xml:",innerxml"` Val string `xml:",innerxml"`
} }
// decodeShapeVal defines the structure used to parse the sub-element of the
// shape in the file xl/drawings/vmlDrawing%d.vml.
type decodeShapeVal struct {
ClientData decodeVMLClientData `xml:"ClientData"`
}
// decodeVMLClientData defines the structure used to parse the x:ClientData
// element in the file xl/drawings/vmlDrawing%d.vml.
type decodeVMLClientData struct {
ObjectType string `xml:"ObjectType,attr"`
Column int
Row int
}
// encodeShape defines the structure used to re-serialization shape element. // encodeShape defines the structure used to re-serialization shape element.
type encodeShape struct { type encodeShape struct {
Fill *vFill `xml:"v:fill"` Fill *vFill `xml:"v:fill"`

View File

@ -155,7 +155,7 @@ func TestAddDrawingVML(t *testing.T) {
assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", &vmlOptions{Cell: "A1"}), "XML syntax error on line 1: invalid UTF-8") assert.EqualError(t, f.addDrawingVML(0, "xl/drawings/vmlDrawing1.vml", &vmlOptions{Cell: "A1"}), "XML syntax error on line 1: invalid UTF-8")
} }
func TestAddFormControl(t *testing.T) { func TestFormControl(t *testing.T) {
f := NewFile() f := NewFile()
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
Cell: "D1", Cell: "D1",
@ -185,12 +185,23 @@ func TestAddFormControl(t *testing.T) {
})) }))
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
Cell: "A5", Cell: "A5",
Type: FormControlCheckbox,
Text: "Check Box 1",
Checked: true,
}))
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
Cell: "A6",
Type: FormControlCheckbox,
Text: "Check Box 2",
}))
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
Cell: "A7",
Type: FormControlRadio, Type: FormControlRadio,
Text: "Option Button 1", Text: "Option Button 1",
Checked: true, Checked: true,
})) }))
assert.NoError(t, f.AddFormControl("Sheet1", FormControl{ assert.NoError(t, f.AddFormControl("Sheet1", FormControl{
Cell: "A6", Cell: "A8",
Type: FormControlRadio, Type: FormControlRadio,
Text: "Option Button 2", Text: "Option Button 2",
})) }))
@ -221,4 +232,24 @@ func TestAddFormControl(t *testing.T) {
Macro: "Button1_Click", Macro: "Button1_Click",
}), newNoExistSheetError("SheetN")) }), newNoExistSheetError("SheetN"))
assert.NoError(t, f.Close()) assert.NoError(t, f.Close())
// Test delete form control
f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm"))
assert.NoError(t, err)
assert.NoError(t, f.DeleteFormControl("Sheet1", "D1"))
assert.NoError(t, f.DeleteFormControl("Sheet1", "A1"))
// Test delete form control on not exists worksheet
assert.Equal(t, f.DeleteFormControl("SheetN", "A1"), newNoExistSheetError("SheetN"))
// Test delete form control on not exists worksheet
assert.Equal(t, f.DeleteFormControl("Sheet1", "A"), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestDeleteFormControl.xlsm")))
assert.NoError(t, f.Close())
// Test delete form control with expected element
f, err = OpenFile(filepath.Join("test", "TestAddFormControl.xlsm"))
assert.NoError(t, err)
f.Pkg.Store("xl/drawings/vmlDrawing1.vml", MacintoshCyrillicCharset)
assert.Error(t, f.DeleteFormControl("Sheet1", "A1"), "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
// Test delete form control on a worksheet without form control
f = NewFile()
assert.NoError(t, f.DeleteFormControl("Sheet1", "A1"))
} }