From 4ac0c47ac764156d636b7ac07453dc59870dde03 Mon Sep 17 00:00:00 2001 From: oleibman Date: Sun, 14 Nov 2021 10:06:46 -0800 Subject: [PATCH] Support Data Validations in More Versions of Excel (#2377) * Support Data Validations in More Versions of Excel Attempt to deal with #2368, this time for good. Some deleted code was accidentally restored just before release 19, causing errors in spreadsheets with Data Validations. PR #2369 removed the duplicated code, and the fix was confirmed in current versions of Excel for Windows, Google sheets, and other versions of Excel. However, there were problems reported in earlier version of Excel for Windows, and some, versions of Excel for Mac, not all but including a recent one. This change, which is simpler than the original (no need for extLst) fix for DataValidations, is tested with Excel 2007 and Excel 2003 as well as more recent versions. I do not have a Mac on which to test. * Multiple Identical Data Validation Lists Using the same Data Validation List in multiple places on a worksheet caused them all to be merged into the same range. This was because sqref was not part of the hash code; it is now, avoiding this problem. * Must Write Data Validations Before Hyperlinks See discussion in #2389. --- src/PhpSpreadsheet/Cell/DataValidation.php | 1 + src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 30 +++---- .../Writer/Xlsx/Issue2368Test.php | 78 ++++++++++++++++++ tests/data/Writer/XLSX/issue.2368new.xlsx | Bin 0 -> 9914 bytes 4 files changed, 89 insertions(+), 20 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/Xlsx/Issue2368Test.php create mode 100644 tests/data/Writer/XLSX/issue.2368new.xlsx diff --git a/src/PhpSpreadsheet/Cell/DataValidation.php b/src/PhpSpreadsheet/Cell/DataValidation.php index 79d70a98..7ee53eae 100644 --- a/src/PhpSpreadsheet/Cell/DataValidation.php +++ b/src/PhpSpreadsheet/Cell/DataValidation.php @@ -460,6 +460,7 @@ class DataValidation $this->error . $this->promptTitle . $this->prompt . + $this->sqref . __CLASS__ ); } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 466e8b00..286ebe7e 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -85,6 +85,9 @@ class Worksheet extends WriterPart // conditionalFormatting $this->writeConditionalFormatting($objWriter, $worksheet); + // dataValidations + $this->writeDataValidations($objWriter, $worksheet); + // hyperlinks $this->writeHyperlinks($objWriter, $worksheet); @@ -118,8 +121,6 @@ class Worksheet extends WriterPart // ConditionalFormattingRuleExtensionList // (Must be inserted last. Not insert last, an Excel parse error will occur) $this->writeExtLst($objWriter, $worksheet); - // dataValidations - $this->writeDataValidations($objWriter, $worksheet); $objWriter->endElement(); @@ -681,16 +682,11 @@ class Worksheet extends WriterPart // Write data validations? if (!empty($dataValidationCollection)) { $dataValidationCollection = Coordinate::mergeRangesInCollection($dataValidationCollection); - $objWriter->startElement('extLst'); - $objWriter->startElement('ext'); - $objWriter->writeAttribute('uri', '{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}'); - $objWriter->writeAttribute('xmlns:x14', 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main'); - $objWriter->startElement('x14:dataValidations'); + $objWriter->startElement('dataValidations'); $objWriter->writeAttribute('count', count($dataValidationCollection)); - $objWriter->writeAttribute('xmlns:xm', 'http://schemas.microsoft.com/office/excel/2006/main'); foreach ($dataValidationCollection as $coordinate => $dv) { - $objWriter->startElement('x14:dataValidation'); + $objWriter->startElement('dataValidation'); if ($dv->getType() != '') { $objWriter->writeAttribute('type', $dv->getType()); @@ -705,7 +701,6 @@ class Worksheet extends WriterPart } $objWriter->writeAttribute('allowBlank', ($dv->getAllowBlank() ? '1' : '0')); - // showDropDown is really hideDropDown Excel renders as true = hide, false = show $objWriter->writeAttribute('showDropDown', (!$dv->getShowDropDown() ? '1' : '0')); $objWriter->writeAttribute('showInputMessage', ($dv->getShowInputMessage() ? '1' : '0')); $objWriter->writeAttribute('showErrorMessage', ($dv->getShowErrorMessage() ? '1' : '0')); @@ -723,24 +718,19 @@ class Worksheet extends WriterPart $objWriter->writeAttribute('prompt', $dv->getPrompt()); } + $objWriter->writeAttribute('sqref', $dv->getSqref() ?? $coordinate); + if ($dv->getFormula1() !== '') { - $objWriter->startElement('x14:formula1'); - $objWriter->writeElement('xm:f', $dv->getFormula1()); - $objWriter->endElement(); + $objWriter->writeElement('formula1', $dv->getFormula1()); } if ($dv->getFormula2() !== '') { - $objWriter->startElement('x14:formula2'); - $objWriter->writeElement('xm:f', $dv->getFormula2()); - $objWriter->endElement(); + $objWriter->writeElement('formula2', $dv->getFormula2()); } - $objWriter->writeElement('xm:sqref', $dv->getSqref() ?? $coordinate); $objWriter->endElement(); } - $objWriter->endElement(); // dataValidations - $objWriter->endElement(); // ext - $objWriter->endElement(); // extLst + $objWriter->endElement(); } } diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue2368Test.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue2368Test.php new file mode 100644 index 00000000..8915f5df --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/Issue2368Test.php @@ -0,0 +1,78 @@ +getActiveSheet(); + $validation = $sheet->getDataValidation('A1:A10'); + $validation->setType(DataValidation::TYPE_LIST); + $validation->setShowDropDown(true); + $validation->setFormula1('"Option 1, Option 2"'); + + $outputFilename = File::temporaryFilename(); + $writer = new Writer($spreadsheet); + $writer->save($outputFilename); + $zipfile = "zip://$outputFilename#xl/worksheets/sheet1.xml"; + $contents = file_get_contents($zipfile); + unlink($outputFilename); + $spreadsheet->disconnectWorksheets(); + if ($contents === false) { + self::fail('Unable to open file'); + } else { + self::assertSame(0, substr_count($contents, '')); + self::assertSame(2, substr_count($contents, 'dataValidations')); // start and end tags + } + } + + public function testMultipleRange(): void + { + // DataValidations which were identical except for sqref were incorrectly merged. + $filename = 'tests/data/Writer/XLSX/issue.2368new.xlsx'; + $reader = IOFactory::createReader('Xlsx'); + $spreadsheet = $reader->load($filename); + $sheet = $spreadsheet->getActiveSheet(); + $validations = $sheet->getDataValidationCollection(); + /** @var string[] */ + $ranges = []; + foreach ($validations as $validation) { + $ranges[] = $validation->getSqref(); + } + self::assertContains('A1:A5', $ranges); + self::assertContains('A10:A14', $ranges); + self::assertContains('A20:A24', $ranges); + self::assertSame('"yes,no"', $sheet->getCell('A3')->getDataValidation()->getFormula1()); + self::assertSame('"yes,no"', $sheet->getCell('A10')->getDataValidation()->getFormula1()); + self::assertSame('"yes,no"', $sheet->getCell('A24')->getDataValidation()->getFormula1()); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + $spreadsheet->disconnectWorksheets(); + + $sheet2 = $reloadedSpreadsheet->getActiveSheet(); + $validation2 = $sheet2->getDataValidationCollection(); + /** @var string[] */ + $range2 = []; + foreach ($validation2 as $validation) { + $range2[] = $validation->getSqref(); + } + self::assertContains('A1:A5', $range2); + self::assertContains('A10:A14', $range2); + self::assertContains('A20:A24', $range2); + self::assertSame('"yes,no"', $sheet2->getCell('A3')->getDataValidation()->getFormula1()); + self::assertSame('"yes,no"', $sheet2->getCell('A10')->getDataValidation()->getFormula1()); + self::assertSame('"yes,no"', $sheet2->getCell('A24')->getDataValidation()->getFormula1()); + + $reloadedSpreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/data/Writer/XLSX/issue.2368new.xlsx b/tests/data/Writer/XLSX/issue.2368new.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..dbd693f6d5677396267dea15befb591cba36060f GIT binary patch literal 9914 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 literal 0 HcmV?d00001