Skip to content

Commit 6faf828

Browse files
author
MarkBaker
committed
Fix issues with updating Conditional Formatting when inserting/deleting rows/columns
- Update existing ranges, expanding if necessary, rather than trying to clone for each individual cell - Update conditions, so that cell references and formulae are maintained correctly Note that absolute as well as relative cell references should be updated in conditions
1 parent 78c27c0 commit 6faf828

File tree

4 files changed

+110
-34
lines changed

4 files changed

+110
-34
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
3939

4040
### Fixed
4141

42+
- Update Conditional Formatting ranges and rule conditions when inserting/deleting rows/columns [Issue #2678](https://github.com/PHPOffice/PhpSpreadsheet/issues/2678) [PR #2689](https://github.com/PHPOffice/PhpSpreadsheet/pull/2689)
4243
- Allow `INDIRECT()` to accept row/column ranges as well as cell ranges [PR #2687](https://github.com/PHPOffice/PhpSpreadsheet/pull/2687)
4344
- Fix bug when deleting cells with hyperlinks, where the hyperlink was then being "inherited" by whatever cell moved to that cell address.
4445
- Fix bug in Conditional Formatting in the Xls Writer that resulted in a broken file when there were multiple conditional ranges in a worksheet.

src/PhpSpreadsheet/CellReferenceHelper.php

+5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ public function __construct(string $beforeCellAddress = 'A1', int $numberOfColum
4343
$this->beforeRow = (int) $beforeRow;
4444
}
4545

46+
public function beforeCellAddress(): string
47+
{
48+
return $this->beforeCellAddress;
49+
}
50+
4651
public function refreshRequired(string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): bool
4752
{
4853
return $this->beforeCellAddress !== $beforeCellAddress ||

src/PhpSpreadsheet/ReferenceHelper.php

+65-34
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
66
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
77
use PhpOffice\PhpSpreadsheet\Cell\DataType;
8+
use PhpOffice\PhpSpreadsheet\Style\Conditional;
89
use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter;
910
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
1011

@@ -198,6 +199,45 @@ protected function adjustHyperlinks($worksheet, $numberOfColumns, $numberOfRows)
198199
}
199200
}
200201

202+
/**
203+
* Update conditional formatting styles when inserting/deleting rows/columns.
204+
*
205+
* @param Worksheet $worksheet The worksheet that we're editing
206+
* @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion)
207+
* @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion)
208+
*/
209+
protected function adjustConditionalFormatting($worksheet, $numberOfColumns, $numberOfRows): void
210+
{
211+
$aStyles = $worksheet->getConditionalStylesCollection();
212+
($numberOfColumns > 0 || $numberOfRows > 0)
213+
? uksort($aStyles, [self::class, 'cellReverseSort'])
214+
: uksort($aStyles, [self::class, 'cellSort']);
215+
216+
foreach ($aStyles as $cellAddress => $cfRules) {
217+
$worksheet->removeConditionalStyles($cellAddress);
218+
$newReference = $this->updateCellReference($cellAddress);
219+
220+
foreach ($cfRules as &$cfRule) {
221+
/** @var Conditional $cfRule */
222+
$conditions = $cfRule->getConditions();
223+
foreach ($conditions as &$condition) {
224+
if (is_string($condition)) {
225+
$condition = $this->updateFormulaReferences(
226+
$condition,
227+
$this->cellReferenceHelper->beforeCellAddress(),
228+
$numberOfColumns,
229+
$numberOfRows,
230+
$worksheet->getTitle(),
231+
true
232+
);
233+
}
234+
}
235+
$cfRule->setConditions($conditions);
236+
}
237+
$worksheet->setConditionalStyles($newReference, $cfRules);
238+
}
239+
}
240+
201241
/**
202242
* Update data validations when inserting/deleting rows/columns.
203243
*
@@ -442,6 +482,9 @@ function ($coordinate) use ($allCoordinates) {
442482
// Update worksheet: hyperlinks
443483
$this->adjustHyperlinks($worksheet, $numberOfColumns, $numberOfRows);
444484

485+
// Update worksheet: conditional formatting styles
486+
$this->adjustConditionalFormatting($worksheet, $numberOfColumns, $numberOfRows);
487+
445488
// Update worksheet: data validations
446489
$this->adjustDataValidations($worksheet, $numberOfColumns, $numberOfRows);
447490

@@ -505,8 +548,14 @@ function ($coordinate) use ($allCoordinates) {
505548
*
506549
* @return string Updated formula
507550
*/
508-
public function updateFormulaReferences($formula = '', $beforeCellAddress = 'A1', $numberOfColumns = 0, $numberOfRows = 0, $worksheetName = '')
509-
{
551+
public function updateFormulaReferences(
552+
$formula = '',
553+
$beforeCellAddress = 'A1',
554+
$numberOfColumns = 0,
555+
$numberOfRows = 0,
556+
$worksheetName = '',
557+
bool $includeAbsoluteReferences = false
558+
) {
510559
if (
511560
$this->cellReferenceHelper === null ||
512561
$this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows)
@@ -528,8 +577,8 @@ public function updateFormulaReferences($formula = '', $beforeCellAddress = 'A1'
528577
foreach ($matches as $match) {
529578
$fromString = ($match[2] > '') ? $match[2] . '!' : '';
530579
$fromString .= $match[3] . ':' . $match[4];
531-
$modified3 = substr($this->updateCellReference('$A' . $match[3]), 2);
532-
$modified4 = substr($this->updateCellReference('$A' . $match[4]), 2);
580+
$modified3 = substr($this->updateCellReference('$A' . $match[3], $includeAbsoluteReferences), 2);
581+
$modified4 = substr($this->updateCellReference('$A' . $match[4], $includeAbsoluteReferences), 2);
533582

534583
if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) {
535584
if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) {
@@ -553,8 +602,8 @@ public function updateFormulaReferences($formula = '', $beforeCellAddress = 'A1'
553602
foreach ($matches as $match) {
554603
$fromString = ($match[2] > '') ? $match[2] . '!' : '';
555604
$fromString .= $match[3] . ':' . $match[4];
556-
$modified3 = substr($this->updateCellReference($match[3] . '$1'), 0, -2);
557-
$modified4 = substr($this->updateCellReference($match[4] . '$1'), 0, -2);
605+
$modified3 = substr($this->updateCellReference($match[3] . '$1', $includeAbsoluteReferences), 0, -2);
606+
$modified4 = substr($this->updateCellReference($match[4] . '$1', $includeAbsoluteReferences), 0, -2);
558607

559608
if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) {
560609
if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) {
@@ -578,8 +627,8 @@ public function updateFormulaReferences($formula = '', $beforeCellAddress = 'A1'
578627
foreach ($matches as $match) {
579628
$fromString = ($match[2] > '') ? $match[2] . '!' : '';
580629
$fromString .= $match[3] . ':' . $match[4];
581-
$modified3 = $this->updateCellReference($match[3]);
582-
$modified4 = $this->updateCellReference($match[4]);
630+
$modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences);
631+
$modified4 = $this->updateCellReference($match[4], $includeAbsoluteReferences);
583632

584633
if ($match[3] . $match[4] !== $modified3 . $modified4) {
585634
if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) {
@@ -606,7 +655,7 @@ public function updateFormulaReferences($formula = '', $beforeCellAddress = 'A1'
606655
$fromString = ($match[2] > '') ? $match[2] . '!' : '';
607656
$fromString .= $match[3];
608657

609-
$modified3 = $this->updateCellReference($match[3]);
658+
$modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences);
610659
if ($match[3] !== $modified3) {
611660
if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) {
612661
$toString = ($match[2] > '') ? $match[2] . '!' : '';
@@ -786,18 +835,18 @@ private function updateRowRangesAllWorksheets(string $formula, int $numberOfRows
786835
*
787836
* @return string Updated cell range
788837
*/
789-
private function updateCellReference($cellReference = 'A1')
838+
private function updateCellReference($cellReference = 'A1', bool $includeAbsoluteReferences = false)
790839
{
791840
// Is it in another worksheet? Will not have to update anything.
792841
if (strpos($cellReference, '!') !== false) {
793842
return $cellReference;
794843
// Is it a range or a single cell?
795844
} elseif (!Coordinate::coordinateIsRange($cellReference)) {
796845
// Single cell
797-
return $this->cellReferenceHelper->updateCellReference($cellReference);
846+
return $this->cellReferenceHelper->updateCellReference($cellReference, $includeAbsoluteReferences);
798847
} elseif (Coordinate::coordinateIsRange($cellReference)) {
799848
// Range
800-
return $this->updateCellRange($cellReference);
849+
return $this->updateCellRange($cellReference, $includeAbsoluteReferences);
801850
}
802851

803852
// Return original
@@ -839,7 +888,7 @@ public function updateNamedFormulas(Spreadsheet $spreadsheet, $oldName = '', $ne
839888
*
840889
* @return string Updated cell range
841890
*/
842-
private function updateCellRange(string $cellRange = 'A1:A1'): string
891+
private function updateCellRange(string $cellRange = 'A1:A1', bool $includeAbsoluteReferences = false): string
843892
{
844893
if (!Coordinate::coordinateIsRange($cellRange)) {
845894
throw new Exception('Only cell ranges may be passed to this method.');
@@ -853,14 +902,14 @@ private function updateCellRange(string $cellRange = 'A1:A1'): string
853902
for ($j = 0; $j < $jc; ++$j) {
854903
if (ctype_alpha($range[$i][$j])) {
855904
$range[$i][$j] = Coordinate::coordinateFromString(
856-
$this->cellReferenceHelper->updateCellReference($range[$i][$j] . '1')
905+
$this->cellReferenceHelper->updateCellReference($range[$i][$j] . '1', $includeAbsoluteReferences)
857906
)[0];
858907
} elseif (ctype_digit($range[$i][$j])) {
859908
$range[$i][$j] = Coordinate::coordinateFromString(
860-
$this->cellReferenceHelper->updateCellReference('A' . $range[$i][$j])
909+
$this->cellReferenceHelper->updateCellReference('A' . $range[$i][$j], $includeAbsoluteReferences)
861910
)[1];
862911
} else {
863-
$range[$i][$j] = $this->cellReferenceHelper->updateCellReference($range[$i][$j]);
912+
$range[$i][$j] = $this->cellReferenceHelper->updateCellReference($range[$i][$j], $includeAbsoluteReferences);
864913
}
865914
}
866915
}
@@ -985,17 +1034,8 @@ private function duplicateStylesByColumn(Worksheet $worksheet, int $beforeColumn
9851034
$coordinate = $beforeColumnName . $i;
9861035
if ($worksheet->cellExists($coordinate)) {
9871036
$xfIndex = $worksheet->getCell($coordinate)->getXfIndex();
988-
$conditionalStyles = $worksheet->conditionalStylesExists($coordinate) ?
989-
$worksheet->getConditionalStyles($coordinate) : false;
9901037
for ($j = $beforeColumn; $j <= $beforeColumn - 1 + $numberOfColumns; ++$j) {
9911038
$worksheet->getCellByColumnAndRow($j, $i)->setXfIndex($xfIndex);
992-
if ($conditionalStyles) {
993-
$cloned = [];
994-
foreach ($conditionalStyles as $conditionalStyle) {
995-
$cloned[] = clone $conditionalStyle;
996-
}
997-
$worksheet->setConditionalStyles(Coordinate::stringFromColumnIndex($j) . $i, $cloned);
998-
}
9991039
}
10001040
}
10011041
}
@@ -1009,17 +1049,8 @@ private function duplicateStylesByRow(Worksheet $worksheet, int $beforeColumn, i
10091049
$coordinate = Coordinate::stringFromColumnIndex($i) . ($beforeRow - 1);
10101050
if ($worksheet->cellExists($coordinate)) {
10111051
$xfIndex = $worksheet->getCell($coordinate)->getXfIndex();
1012-
$conditionalStyles = $worksheet->conditionalStylesExists($coordinate) ?
1013-
$worksheet->getConditionalStyles($coordinate) : false;
10141052
for ($j = $beforeRow; $j <= $beforeRow - 1 + $numberOfRows; ++$j) {
10151053
$worksheet->getCell(Coordinate::stringFromColumnIndex($i) . $j)->setXfIndex($xfIndex);
1016-
if ($conditionalStyles) {
1017-
$cloned = [];
1018-
foreach ($conditionalStyles as $conditionalStyle) {
1019-
$cloned[] = clone $conditionalStyle;
1020-
}
1021-
$worksheet->setConditionalStyles(Coordinate::stringFromColumnIndex($i) . $j, $cloned);
1022-
}
10231054
}
10241055
}
10251056
}

tests/PhpSpreadsheetTests/ReferenceHelperTest.php

+39
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PhpOffice\PhpSpreadsheet\Comment;
88
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
99
use PhpOffice\PhpSpreadsheet\Spreadsheet;
10+
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\Wizard;
1011
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
1112
use PHPUnit\Framework\TestCase;
1213

@@ -296,4 +297,42 @@ function (Hyperlink $value) {
296297

297298
self::assertSame(['A3' => 'https://phpspreadsheet.readthedocs.io/en/latest/'], $hyperlinks);
298299
}
300+
301+
public function testInsertRowsWithConditionalFormatting(): void
302+
{
303+
$spreadsheet = new Spreadsheet();
304+
$sheet = $spreadsheet->getActiveSheet();
305+
$sheet->fromArray([[1, 2, 3, 4], [3, 4, 5, 6], [5, 6, 7, 8], [7, 8, 9, 10], [9, 10, 11, 12]], null, 'C3', true);
306+
$sheet->getCell('H5')->setValue(5);
307+
308+
$cellRange = 'C3:F7';
309+
$conditionalStyles = [];
310+
$wizardFactory = new Wizard($cellRange);
311+
/** @var Wizard\CellValue $cellWizard */
312+
$cellWizard = $wizardFactory->newRule(Wizard::CELL_VALUE);
313+
314+
$cellWizard->equals('$H$5', Wizard::VALUE_TYPE_CELL);
315+
$conditionalStyles[] = $cellWizard->getConditional();
316+
317+
$cellWizard->greaterThan('$H$5', Wizard::VALUE_TYPE_CELL);
318+
$conditionalStyles[] = $cellWizard->getConditional();
319+
320+
$cellWizard->lessThan('$H$5', Wizard::VALUE_TYPE_CELL);
321+
$conditionalStyles[] = $cellWizard->getConditional();
322+
323+
$spreadsheet->getActiveSheet()
324+
->getStyle($cellWizard->getCellRange())
325+
->setConditionalStyles($conditionalStyles);
326+
$sheet->insertNewRowBefore(4, 2);
327+
328+
$styles = $sheet->getConditionalStylesCollection();
329+
// verify that the conditional range has been updated
330+
self::assertSame('C3:F9', array_keys($styles)[0]);
331+
// verify that the conditions have been updated
332+
foreach ($styles as $style) {
333+
foreach ($style as $conditions) {
334+
self::assertSame('$H$7', $conditions->getConditions()[0]);
335+
}
336+
}
337+
}
299338
}

0 commit comments

Comments
 (0)