This commit is contained in:
Thomas Peterson 2025-11-29 22:28:26 +01:00
parent bf986acb49
commit 3fa5276aba
2 changed files with 171 additions and 8 deletions

View File

@ -4,7 +4,7 @@ namespace PHPNative\Ui\Widget;
class FileBrowser extends Container
{
private Table $fileTable;
private VirtualTable $fileTable;
private Label $pathLabel;
private string $currentPath;
private $onFileSelect = null;
@ -17,9 +17,8 @@ class FileBrowser extends Container
public function __construct(string $initialPath = '.', bool $isRemote = false, string $style = '')
{
// Root-Container füllt die verfügbare Höhe im Eltern-Layout (flex-1),
// damit die Tabelle unten eine klar begrenzte Höhe bekommt und
// ihr Body sinnvoll scrollen kann.
// Root-Container füllt die verfügbare Höhe im Eltern-Layout (flex-1)
// und enthält eine VirtualTable für performantes Scrollen.
parent::__construct('w-full flex flex-col flex-1 gap-2 ' . $style);
$this->currentPath = $initialPath;
@ -29,10 +28,10 @@ class FileBrowser extends Container
$this->pathLabel = new Label($initialPath, 'px-3 py-2 bg-gray-200 text-black rounded text-sm font-mono');
$this->addComponent($this->pathLabel);
// File table, die den verbleibenden Platz im FileBrowser nutzt.
// Das eigentliche Scrollen passiert im Body-Container der Table
// (overflow-auto dort).
$this->fileTable = new Table(' flex-1');
// VirtualTable nutzt Paging, um bei vielen Einträgen nur
// einen Teil der Zeilen im UI zu halten das macht Scrollen
// deutlich flüssiger bei großen Verzeichnissen.
$this->fileTable = new VirtualTable(' flex-1');
$this->fileTable->setColumns([
['key' => 'type', 'title' => 'Typ', 'width' => 60],
['key' => 'name', 'title' => 'Name'],

View File

@ -0,0 +1,164 @@
<?php
namespace PHPNative\Ui\Widget;
use PHPNative\Framework\TextRenderer;
class VirtualTable extends Container
{
private Table $innerTable;
private array $rows = [];
private array $columns = [];
private int $pageSize = 200;
private int $currentPage = 0;
private int $totalPages = 1;
private $onRowSelect = null;
private Label $pageInfoLabel;
private Button $prevButton;
private Button $nextButton;
private bool $prevEnabled = false;
private bool $nextEnabled = false;
public function __construct(string $style = '')
{
parent::__construct('flex flex-col w-full' . $style);
$this->innerTable = new Table(' flex-1');
$this->addComponent($this->innerTable);
$pagination = new Container('flex flex-row items-center justify-end gap-2 mt-1');
$this->pageInfoLabel = new Label('', 'text-xs text-gray-600');
$this->prevButton = new Button('←', 'px-2 py-1 text-xs bg-gray-200 rounded hover:bg-gray-300');
$this->nextButton = new Button('→', 'px-2 py-1 text-xs bg-gray-200 rounded hover:bg-gray-300');
$virtualTable = $this;
$this->prevButton->setOnClick(function () use ($virtualTable) {
$virtualTable->goToPreviousPage();
});
$this->nextButton->setOnClick(function () use ($virtualTable) {
$virtualTable->goToNextPage();
});
$pagination->addComponent($this->pageInfoLabel);
$pagination->addComponent($this->prevButton);
$pagination->addComponent($this->nextButton);
$this->addComponent($pagination);
}
public function layout(null|TextRenderer $textRenderer = null): void
{
parent::layout($textRenderer);
$this->updatePaginationLabel();
}
public function setColumns(array $columns): void
{
$this->columns = $columns;
$this->innerTable->setColumns($columns);
}
public function setData(array $data): void
{
$this->rows = array_values($data);
$this->currentPage = 0;
$this->recalculateTotalPages();
$this->updatePageData();
}
public function setOnRowSelect(callable $callback): void
{
$this->onRowSelect = $callback;
$virtualTable = $this;
$this->innerTable->setOnRowSelect(function ($index, $row) use ($virtualTable) {
if ($virtualTable->onRowSelect !== null) {
$globalIndex = ($virtualTable->currentPage * $virtualTable->pageSize) + $index;
($virtualTable->onRowSelect)($globalIndex, $row);
}
});
}
public function getSelectedRow(): null|array
{
$selected = $this->innerTable->getSelectedRow();
if ($selected === null) {
return null;
}
return $selected;
}
private function recalculateTotalPages(): void
{
$rowCount = count($this->rows);
$this->totalPages = max(1, (int) ceil($rowCount / $this->pageSize));
if ($this->currentPage >= $this->totalPages) {
$this->currentPage = $this->totalPages - 1;
}
}
private function updatePageData(): void
{
$offset = $this->currentPage * $this->pageSize;
$pageRows = array_slice($this->rows, $offset, $this->pageSize);
$this->innerTable->setData($pageRows, false);
$this->updatePaginationLabel();
}
private function updatePaginationLabel(): void
{
$totalRows = count($this->rows);
if ($totalRows === 0) {
$this->pageInfoLabel->setText('Keine Einträge');
$this->prevEnabled = false;
$this->nextEnabled = false;
$this->prevButton->setStyle('px-2 py-1 text-xs bg-gray-100 rounded text-gray-400');
$this->nextButton->setStyle('px-2 py-1 text-xs bg-gray-100 rounded text-gray-400');
return;
}
$start = ($this->currentPage * $this->pageSize) + 1;
$end = min($totalRows, ($this->currentPage + 1) * $this->pageSize);
$this->pageInfoLabel->setText(sprintf('Zeige %d%d von %d', $start, $end, $totalRows));
$this->prevEnabled = $this->currentPage > 0;
$this->nextEnabled = ($this->currentPage + 1) < $this->totalPages;
$this->prevButton->setStyle(
$this->prevEnabled
? 'px-2 py-1 text-xs bg-gray-200 rounded hover:bg-gray-300'
: 'px-2 py-1 text-xs bg-gray-100 rounded text-gray-400'
);
$this->nextButton->setStyle(
$this->nextEnabled
? 'px-2 py-1 text-xs bg-gray-200 rounded hover:bg-gray-300'
: 'px-2 py-1 text-xs bg-gray-100 rounded text-gray-400'
);
}
private function goToPreviousPage(): void
{
if ($this->currentPage <= 0 || !$this->prevEnabled) {
return;
}
$this->currentPage--;
$this->updatePageData();
$this->markDirty(true);
}
private function goToNextPage(): void
{
if (($this->currentPage + 1) >= $this->totalPages || !$this->nextEnabled) {
return;
}
$this->currentPage++;
$this->updatePageData();
$this->markDirty(true);
}
}