diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 665b2e18..378a9a5a 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Style\Conditional; use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter; +use PhpOffice\PhpSpreadsheet\Worksheet\Table; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class ReferenceHelper @@ -497,6 +498,9 @@ class ReferenceHelper // Update worksheet: autofilter $this->adjustAutoFilter($worksheet, $beforeCellAddress, $numberOfColumns); + // Update worksheet: table + $this->adjustTable($worksheet, $beforeCellAddress, $numberOfColumns); + // Update worksheet: freeze pane if ($worksheet->getFreezePane()) { $splitCell = $worksheet->getFreezePane() ?? ''; @@ -1026,6 +1030,85 @@ class ReferenceHelper } while ($startColID !== $endColID); } + private function adjustTable(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void + { + $tableCollection = $worksheet->getTableCollection(); + + foreach ($tableCollection as $table) { + $tableRange = $table->getRange(); + if (!empty($tableRange)) { + if ($numberOfColumns !== 0) { + $tableColumns = $table->getColumns(); + if (count($tableColumns) > 0) { + $column = ''; + $row = 0; + sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row); + $columnIndex = Coordinate::columnIndexFromString($column); + [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($tableRange); + if ($columnIndex <= $rangeEnd[0]) { + if ($numberOfColumns < 0) { + $this->adjustTableDeleteRules($columnIndex, $numberOfColumns, $tableColumns, $table); + } + $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0]; + + // Shuffle columns in table range + if ($numberOfColumns > 0) { + $this->adjustTableInsert($startCol, $numberOfColumns, $rangeEnd[0], $table); + } else { + $this->adjustTableDelete($startCol, $numberOfColumns, $rangeEnd[0], $table); + } + } + } + } + + $table->setRange($this->updateCellReference($tableRange)); + } + } + } + + private function adjustTableDeleteRules(int $columnIndex, int $numberOfColumns, array $tableColumns, Table $table): void + { + // If we're actually deleting any columns that fall within the table range, + // then we delete any rules for those columns + $deleteColumn = $columnIndex + $numberOfColumns - 1; + $deleteCount = abs($numberOfColumns); + + for ($i = 1; $i <= $deleteCount; ++$i) { + $columnName = Coordinate::stringFromColumnIndex($deleteColumn + 1); + if (isset($tableColumns[$columnName])) { + $table->clearColumn($columnName); + } + ++$deleteColumn; + } + } + + private function adjustTableInsert(int $startCol, int $numberOfColumns, int $rangeEnd, Table $table): void + { + $startColRef = $startCol; + $endColRef = $rangeEnd; + $toColRef = $rangeEnd + $numberOfColumns; + + do { + $table->shiftColumn(Coordinate::stringFromColumnIndex($endColRef), Coordinate::stringFromColumnIndex($toColRef)); + --$endColRef; + --$toColRef; + } while ($startColRef <= $endColRef); + } + + private function adjustTableDelete(int $startCol, int $numberOfColumns, int $rangeEnd, Table $table): void + { + // For delete, we shuffle from beginning to end to avoid overwriting + $startColID = Coordinate::stringFromColumnIndex($startCol); + $toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns); + $endColID = Coordinate::stringFromColumnIndex($rangeEnd + 1); + + do { + $table->shiftColumn($startColID, $toColID); + ++$startColID; + ++$toColID; + } while ($startColID !== $endColID); + } + private function duplicateStylesByColumn(Worksheet $worksheet, int $beforeColumn, int $beforeRow, int $highestRow, int $numberOfColumns): void { $beforeColumnName = Coordinate::stringFromColumnIndex($beforeColumn - 1); diff --git a/src/PhpSpreadsheet/Worksheet/Table.php b/src/PhpSpreadsheet/Worksheet/Table.php index 950fe74c..04a34e6f 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -380,6 +380,36 @@ class Table return $this; } + /** + * Shift an Table Column Rule to a different column. + * + * Note: This method bypasses validation of the destination column to ensure it is within this Table range. + * Nor does it verify whether any column rule already exists at $toColumn, but will simply override any existing value. + * Use with caution. + * + * @param string $fromColumn Column name (e.g. A) + * @param string $toColumn Column name (e.g. B) + * + * @return $this + */ + public function shiftColumn($fromColumn, $toColumn) + { + $fromColumn = strtoupper($fromColumn); + $toColumn = strtoupper($toColumn); + + if (($fromColumn !== null) && (isset($this->columns[$fromColumn])) && ($toColumn !== null)) { + $this->columns[$fromColumn]->setTable(); + $this->columns[$fromColumn]->setColumnIndex($toColumn); + $this->columns[$toColumn] = $this->columns[$fromColumn]; + $this->columns[$toColumn]->setTable($this); + unset($this->columns[$fromColumn]); + + ksort($this->columns); + } + + return $this; + } + /** * Get table Style. * diff --git a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php index 106955a8..1b1edb83 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php @@ -208,6 +208,64 @@ class TableTest extends SetupTeardown } } + public function testRemoveColumns(): void + { + $sheet = $this->getSheet(); + $sheet->fromArray(range('H', 'O'), null, 'H2'); + $table = new Table(self::INITIAL_RANGE); + $table->getColumn('L')->setShowFilterButton(false); + $sheet->addTable($table); + + $sheet->removeColumn('K', 2); + $result = $table->getRange(); + self::assertEquals('H2:M256', $result); + + // Check that the prop that was set for column L is no longer set + self::assertTrue($table->getColumn('L')->getShowFilterButton()); + } + + public function testRemoveRows(): void + { + $sheet = $this->getSheet(); + $sheet->fromArray(range('H', 'O'), null, 'H2'); + $table = new Table(self::INITIAL_RANGE); + $sheet->addTable($table); + + $sheet->removeRow(42, 128); + $result = $table->getRange(); + self::assertEquals('H2:O128', $result); + } + + public function testInsertColumns(): void + { + $sheet = $this->getSheet(); + $sheet->fromArray(range('H', 'O'), null, 'H2'); + $table = new Table(self::INITIAL_RANGE); + $table->getColumn('N')->setShowFilterButton(false); + $sheet->addTable($table); + + $sheet->insertNewColumnBefore('N', 3); + $result = $table->getRange(); + self::assertEquals('H2:R256', $result); + + // Check that column N no longer has a prop + self::assertTrue($table->getColumn('N')->getShowFilterButton()); + // Check that the prop originally set in column N has been moved to column Q + self::assertFalse($table->getColumn('Q')->getShowFilterButton()); + } + + public function testInsertRows(): void + { + $sheet = $this->getSheet(); + $sheet->fromArray(range('H', 'O'), null, 'H2'); + $table = new Table(self::INITIAL_RANGE); + $sheet->addTable($table); + + $sheet->insertNewRowBefore(3, 4); + $result = $table->getRange(); + self::assertEquals('H2:O260', $result); + } + public function testGetInvalidColumnOffset(): void { $this->expectException(PhpSpreadsheetException::class);