658 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			658 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| namespace nur\v\bs3\vc;
 | |
| 
 | |
| use Exception;
 | |
| use Iterator;
 | |
| use nur\A;
 | |
| use nur\b\coll\BaseArray;
 | |
| use nur\b\params\IParametrable;
 | |
| use nur\b\params\Tparametrable1;
 | |
| use nur\b\ValueException;
 | |
| use nur\data\types\md_utils;
 | |
| use nur\data\types\Metadata;
 | |
| use nur\func;
 | |
| use nur\iter;
 | |
| use nur\SL;
 | |
| use nur\v\base\ComponentPrintable;
 | |
| use nur\v\v;
 | |
| use nur\v\vo;
 | |
| 
 | |
| /**
 | |
|  * Class CTable: affiche un tableau de données
 | |
|  */
 | |
| class CTable extends ComponentPrintable implements IParametrable {
 | |
|   use Tparametrable1;
 | |
| 
 | |
|   const COLS = null;
 | |
|   const EXCLUDE_COLS = null;
 | |
|   const ADD_COLS = null;
 | |
|   const HEADERS = null;
 | |
|   const ADD_HEADERS = null;
 | |
|   /** @var array schéma des données à afficher */
 | |
|   const SCHEMA = null;
 | |
|   const TABLE_CLASS = "table-bordered";
 | |
|   const AUTOPRINT = null;
 | |
|   /** @var ?array schéma des informations supplémentaires */
 | |
|   const DATA_SCHEMA = null;
 | |
| 
 | |
|   /**
 | |
|    * retourner la valeur de COLS construite dynamiquement. cette méthode n'est
 | |
|    * appelée que si c'est nécessaire
 | |
|    */
 | |
|   protected function COLS(): ?array {
 | |
|     return null;
 | |
|   }
 | |
|   /**
 | |
|    * retourner la valeur de HEADERS construite dynamiquement. cette méthode
 | |
|    * n'est appelée que si c'est nécessaire
 | |
|    */
 | |
|   protected function HEADERS(): ?array {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   function __construct(?iterable $rows=null, ?array $params=null) {
 | |
|     self::set_parametrable_params_defaults($params, [
 | |
|       "cols" => static::COLS,
 | |
|       "exclude_cols" => static::EXCLUDE_COLS,
 | |
|       "add_cols" => static::ADD_COLS,
 | |
|       "headers" => static::HEADERS,
 | |
|       "add_headers" => static::ADD_HEADERS,
 | |
|       "schema" => static::SCHEMA,
 | |
|       "table_class" => static::TABLE_CLASS,
 | |
|       "autoprint" => static::AUTOPRINT,
 | |
|       "data_schema" => static::DATA_SCHEMA,
 | |
|     ]);
 | |
|     if ($rows === null) $rows = $this->defaultRows();
 | |
|     A::set_nn($params, "rows", $rows);
 | |
|     [$params, $data] = $this->splitParametrableParams($params);
 | |
|     if ($data) A::merge($params["data"], $data);
 | |
|     $this->initParametrableParams($params);
 | |
|     if ($this->ppAutoprint) $this->print();
 | |
|   }
 | |
| 
 | |
|   protected function defaultRows(): ?iterable {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   const PARAMETRABLE_PARAMS_SCHEMA = [
 | |
|     "rows" => ["?iterable", null, "source des lignes à afficher"],
 | |
|     "filter_func" => ["?callable", null, "fonction permettant de filter les éléments à afficher"],
 | |
|     "map_func" => ["?callable", null, "fonction permettant de mapper les éléments"],
 | |
|     "map" => ["?array", null, "tableau permettant de sélectionner les éléments"],
 | |
|     "cols" => ["?array", null, "colonnes à extraire et à afficher"],
 | |
|     "exclude_cols" => ["?array", null, "colonnes à exclure de la liste calculée automatiquement"],
 | |
|     "add_cols" => ["?array", null, "colonnes à ajouter à la liste calculée automatiquement"],
 | |
|     "headers" => ["?array", null, "en-têtes correspondant aux colonnes"],
 | |
|     "add_headers" => ["?array", null, "en-têtes à ajouter à la liste calculée automatiquement"],
 | |
|     "schema" => ["?array", null, "schéma des données à afficher"],
 | |
|     "table_class" => ["?array", null, "classe CSS du tableau"],
 | |
|     "table_attrs" => ["?array", null, "autres attributs à placer sur le tableau"],
 | |
|     "before_table" => ["?content", null, "Contenu à afficher avant le tableau s'il y a des lignes à afficher"],
 | |
|     "row_func" => ["?callable", null, "fonction avec la signature (\$vs, \$row, \$rawRow) retournant la ligne à afficher"],
 | |
|     "col_func" => ["?callable", null, "fonction avec la signature (\$vs, \$value, \$col, \$index, \$row, \$rawRow) retournant la colonne à afficher"],
 | |
|     "after_table" => ["?content", null, "Contenu à afficher après le tableau s'il y a des lignes à afficher"],
 | |
|     "no_data" => ["?content", null, "Contenu à afficher s'il n'y a pas de lignes à afficher"],
 | |
|     "on_exception" => ["?callable", null, "fonction à appeler en cas d'exception"],
 | |
|     "autoprint" => ["bool", false, "faut-il afficher automatiquement le tableau"],
 | |
|     "data" => ["?array", null, "donnés supplémentaires utilisées pour l'affichage"],
 | |
|     "data_schema" => ["?array", null, "schéma des données supplémentaire"],
 | |
|   ];
 | |
| 
 | |
| 
 | |
|   const COL_SCHEMA = [
 | |
|     "name" => ["?string", null, "Nom de la colonne"],
 | |
|     "pkey" => ["?string", null, "Clé permettant de calculer la valeur effective de la colonne"],
 | |
|   ];
 | |
|   private static $col_md;
 | |
| 
 | |
|   const HEADER_SCHEMA = [
 | |
|     "name" => ["string", null, "Nom de l'en-tête"],
 | |
|     "span" => ["?int", null, "nombre de colonnes que cet en-tête doit occuper"],
 | |
|     "label" => ["?string", null, "Libellé de l'en-tête",
 | |
|       "desc" => "vaut [name] par défaut",
 | |
|     ],
 | |
|   ];
 | |
|   private static $header_md;
 | |
| 
 | |
|   /** @var Metadata schéma des données */
 | |
|   protected $md;
 | |
| 
 | |
|   function pp_setSchema($schema): void {
 | |
|     $this->md = Metadata::with($schema);
 | |
|   }
 | |
| 
 | |
|   /** @var ?iterable */
 | |
|   protected $ppRows;
 | |
| 
 | |
|   /** @var array */
 | |
|   protected $filterCtx;
 | |
| 
 | |
|   function pp_setFilterFunc($filterFunc): void {
 | |
|     if ($filterFunc === null) $this->filterCtx = null;
 | |
|     else $this->filterCtx = func::_prepare($filterFunc);
 | |
|   }
 | |
| 
 | |
|   /** @var array */
 | |
|   protected $mapCtx;
 | |
| 
 | |
|   function pp_setMapFunc($mapFunc): void {
 | |
|     if ($mapFunc === null) $this->mapCtx = null;
 | |
|     else $this->mapCtx = func::_prepare($mapFunc);
 | |
|   }
 | |
| 
 | |
|   /** @var ?array */
 | |
|   protected $ppMap;
 | |
| 
 | |
|   protected $ppCols;
 | |
| 
 | |
|   protected $ppExcludeCols;
 | |
| 
 | |
|   protected $ppAddCols;
 | |
| 
 | |
|   protected $ppHeaders;
 | |
| 
 | |
|   protected $ppAddHeaders;
 | |
| 
 | |
|   /** @var array classe CSS du tableau */
 | |
|   protected $ppTableClass;
 | |
| 
 | |
|   protected function getTableClass(): array {
 | |
|     return $this->ppTableClass;
 | |
|   }
 | |
| 
 | |
|   /** @var array */
 | |
|   protected $ppTableAttrs;
 | |
| 
 | |
|   /** @var array|string */
 | |
|   protected $ppBeforeTable;
 | |
| 
 | |
|   /** @var array */
 | |
|   protected $rowCtx;
 | |
| 
 | |
|   function pp_setRowFunc($rowFunc): void {
 | |
|     if ($rowFunc === null) $this->rowCtx = null;
 | |
|     else $this->rowCtx = func::_prepare($rowFunc);
 | |
|   }
 | |
| 
 | |
|   /** @var array */
 | |
|   protected $colCtx;
 | |
| 
 | |
|   function pp_setColFunc($colFunc): void {
 | |
|     if ($colFunc === null) $this->colCtx = null;
 | |
|     else $this->colCtx = func::_prepare($colFunc);
 | |
|   }
 | |
| 
 | |
|   /** @var array|string */
 | |
|   protected $ppAfterTable;
 | |
| 
 | |
|   /** @var array|string */
 | |
|   protected $ppNoData;
 | |
| 
 | |
|   /** @var callable */
 | |
|   protected $ppOnException;
 | |
| 
 | |
|   /** @var bool */
 | |
|   protected $ppAutoprint;
 | |
| 
 | |
|   /** @var Metadata */
 | |
|   protected $dataSchema;
 | |
| 
 | |
|   function pp_setDataSchema($dataSchema): void {
 | |
|     $this->dataSchema = Metadata::with($dataSchema);
 | |
|   }
 | |
| 
 | |
|   /** @var mixed données supplémentaires utilisées pour l'affichage */
 | |
|   protected $data;
 | |
| 
 | |
|   function pp_setData(?array $data): void {
 | |
|     $this->data = $data;
 | |
|   }
 | |
| 
 | |
|   protected function afterSetParametrableParams(array $modifiedKeys, ?Metadata $md=null): void {
 | |
|     if (self::was_parametrable_param_modified($modifiedKeys
 | |
|       , "cols", "exclude_cols", "add_cols"
 | |
|       , "headers", "add_headers")) {
 | |
|       $this->cols = null;
 | |
|       $this->headers = null;
 | |
|     }
 | |
|     if (self::was_parametrable_param_modified($modifiedKeys, "data", "data_schema")) {
 | |
|       if ($this->dataSchema !== null) {
 | |
|         $this->dataSchema->ensureSchema($this->data);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /** @var array liste des colonnes à extraire des lignes */
 | |
|   protected $cols;
 | |
| 
 | |
|   /** @var array informations sur les colonnes à extraire des lignes */
 | |
|   protected $colInfos;
 | |
| 
 | |
|   /** @var array en-têtes correspondant aux colonnes extraites */
 | |
|   protected $headers;
 | |
| 
 | |
|   protected function updateColInfos(?array &$cols, ?array &$colInfos): void {
 | |
|     # extraire les données de colonnes pour que $cols soit juste une liste de
 | |
|     # colonnes
 | |
|     if ($cols === null) return;
 | |
|     if ($colInfos === null) $colInfos = [];
 | |
|     $colKeys = [];
 | |
|     $index = 0;
 | |
|     foreach ($cols as $key => $col) {
 | |
|       if ($key === $index) {
 | |
|         $index++;
 | |
|         if ($col === null) {
 | |
|           # colonne "nulle", à prendre telle quelle
 | |
|           $colKeys[] = null;
 | |
|         } elseif (!array_key_exists($col, $colInfos)) {
 | |
|           $colKeys[] = $col;
 | |
|           $colInfos[$col] = null;
 | |
|         }
 | |
|       } else {
 | |
|         if (is_array($col) && array_key_exists("name", $col)) {
 | |
|           $key = $col["name"];
 | |
|         }
 | |
|         if (!array_key_exists($key, $colInfos)) $colKeys[] = $key;
 | |
|         $colInfos[$key] = $col;
 | |
|       }
 | |
|     }
 | |
|     $cols = $colKeys;
 | |
|   }
 | |
| 
 | |
|   protected function resolveColsHeaders($row): bool {
 | |
|     if ($this->cols !== null || $this->headers !== null) return false;
 | |
|     $schema = $this->md;
 | |
| 
 | |
|     $cols = $this->ppCols;
 | |
|     $colsExcludes = $this->ppExcludeCols;
 | |
|     if ($cols === null) $cols = $this->COLS();
 | |
|     if ($cols === null && $schema !== null) {
 | |
|       $cols = A::xselect($schema->getKeys(), null, $colsExcludes);
 | |
|     }
 | |
|     $this->updateColInfos($cols, $colInfos);
 | |
| 
 | |
|     $headers = $this->ppHeaders;
 | |
|     $sfields = null;
 | |
|     if ($headers === null) $headers = $this->HEADERS();
 | |
|     if ($headers === null) {
 | |
|       if ($schema !== null) {
 | |
|         # ici, $cols est forcément défini, à cause de la commande ci-dessus
 | |
|         $headers = [];
 | |
|         $sfields = $schema->getSfields();
 | |
|         foreach ($cols as $col) {
 | |
|           $sfield = A::get($sfields, $col);
 | |
|           $headers[] = $sfield !== null? $sfield["header"]: $col;
 | |
|         }
 | |
|       } elseif ($cols !== null) {
 | |
|         $headers = $cols;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     $skipRow = false;
 | |
|     if ($cols === null || $headers === null) {
 | |
|       # si pas de colonnes ou de headers, prendre la première ligne si
 | |
|       # c'est une séquence ou ses clés si c'est un tableau associatif
 | |
|       if (A::is_seq($row)) {
 | |
|         $skipRow = true;
 | |
|         $keys = A::xselect($row, null, $colsExcludes);
 | |
|         if ($headers === null) $headers = $keys;
 | |
|         if ($cols === null) $cols = array_keys($keys);
 | |
|       } else {
 | |
|         $row = A::xselect_keys($row, null, $colsExcludes);
 | |
|         if ($headers === null) $headers = array_keys($row);
 | |
|         if ($cols === null) $cols = array_keys($row);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     # ces cas ne devraient pas se produire puisqu'ils devraient être récupérés
 | |
|     # par la section no_contents, mais bon... au cas où
 | |
|     if ($headers === null) $headers = [];
 | |
|     if ($cols === null) $cols = [];
 | |
| 
 | |
|     $addCols = $this->ppAddCols;
 | |
|     $this->updateColInfos($addCols, $colInfos);
 | |
|     $addHeaders = $this->ppAddHeaders;
 | |
|     if ($addCols !== null && $addHeaders !== null) {
 | |
|       # si on fournit les deux, les intégrer sans modification
 | |
|       A::merge($cols, $addCols);
 | |
|       A::merge($headers, $addHeaders);
 | |
|     } elseif ($addCols !== null) {
 | |
|       # si on ne donne que des colonnes supplémentaires, inférer les en-têtes
 | |
|       # à rajouter
 | |
|       foreach ($addCols as $col) {
 | |
|         $cols[] = $col;
 | |
|         if ($col === null) {
 | |
|           $headers[] = "";
 | |
|         } elseif ($sfields !== null) {
 | |
|           $sfield = A::get($sfields, $col);
 | |
|           $headers[] = $sfield !== null? $sfield["header"]: $col;
 | |
|         } else {
 | |
|           $headers[] = $col;
 | |
|         }
 | |
|       }
 | |
|     } elseif ($addHeaders !== null) {
 | |
|       # si on ne donne que des en-têtes supplémentaires, rajouter des colonnes
 | |
|       # nulles pour compenser
 | |
|       foreach ($addHeaders as $header) {
 | |
|         $cols[] = null;
 | |
|         $headers[] = $header;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     md_utils::ensure_md(self::$col_md, self::COL_SCHEMA)
 | |
|       ->eachEnsureSchema($colInfos);
 | |
|     md_utils::ensure_md(self::$header_md, self::HEADER_SCHEMA)
 | |
|       ->eachEnsureSchema($headers);
 | |
| 
 | |
|     $this->cols = $cols;
 | |
|     $this->colInfos = $colInfos;
 | |
|     $this->headers = $headers;
 | |
|     return $skipRow;
 | |
|   }
 | |
| 
 | |
|   protected function _rewindRows(): bool {
 | |
|     return iter::rewind($this->ppRows);
 | |
|   }
 | |
| 
 | |
|   protected function _validRow(): bool {
 | |
|     return iter::valid($this->ppRows);
 | |
|   }
 | |
| 
 | |
|   protected function _currentRow(&$key=null) {
 | |
|     return iter::current($this->ppRows, $key);
 | |
|   }
 | |
| 
 | |
|   protected function _nextRow(): void {
 | |
|     iter::next($this->ppRows);
 | |
|   }
 | |
| 
 | |
|   protected function nextRow(): ?array {
 | |
|     $filterCtx = $this->filterCtx;
 | |
|     $mapCtx = $this->mapCtx;
 | |
|     $map = $this->ppMap;
 | |
|     $row = $this->rawRow;
 | |
|     if ($filterCtx !== null) {
 | |
|       if (!func::_call($filterCtx, [$row, $this->rowKey, $this->rowIndex])) return null;
 | |
|     }
 | |
|     if ($mapCtx !== null) {
 | |
|       $row = func::_call($mapCtx, [$row, $this->rowKey, $this->rowIndex]);
 | |
|     } elseif ($map !== null) {
 | |
|       # si la valeur est séquentielle, c'est une clé dans $row
 | |
|       # si la valeur est associative et que c'est une fonction, elle est appelée
 | |
|       # avec la valeur
 | |
|       $row = A::with($row);
 | |
|       $index = 0;
 | |
|       $mapped = [];
 | |
|       foreach ($map as $key => $value) {
 | |
|         if ($key === $index) {
 | |
|           $index++;
 | |
|           if ($value === null) $mapped[] = null;
 | |
|           else $mapped[$value] = A::get($row, $value);
 | |
|         } elseif (is_callable($value)) {
 | |
|           $mapped[$key] = func::call($value, A::get($row, $key), $key, $row, $this->rowKey, $this->rowIndex);
 | |
|         } else {
 | |
|           $mapped[$key] = $value;
 | |
|         }
 | |
|       }
 | |
|       $row = $mapped;
 | |
|     } else {
 | |
|       $row = A::with($row);
 | |
|     }
 | |
|     return $row;
 | |
|   }
 | |
| 
 | |
|   protected function getRowValues(array $row): array {
 | |
|     $havePkey = false;
 | |
|     if ($this->colInfos !== null) {
 | |
|       foreach ($this->colInfos as $colInfo) {
 | |
|         if ($colInfo["pkey"] !== null) {
 | |
|           $havePkey = true;
 | |
|           break;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     if (!$havePkey) return $row;
 | |
|     $cooked = [];
 | |
|     foreach ($this->cols as $col) {
 | |
|       if ($col === null) {
 | |
|         $cooked[] = null;
 | |
|       } else {
 | |
|         $colInfo = A::get($this->colInfos, $col);
 | |
|         $pkey = $colInfo !== null? $colInfo["pkey"]: null;
 | |
|         if ($pkey !== null) $value = A::pget($row, $pkey);
 | |
|         else $value = $row[$col];
 | |
|         $cooked[$col] = $value;
 | |
|       }
 | |
|     }
 | |
|     # compléter avec les autres champs tels quels
 | |
|     foreach ($row as $key => $value) {
 | |
|       if (!array_key_exists($key, $cooked)) {
 | |
|         $cooked[$key] = $value;
 | |
|       }
 | |
|     }
 | |
|     return $cooked;
 | |
|   }
 | |
| 
 | |
|   protected $results;
 | |
| 
 | |
|   protected function ensureRowSchema(array &$row): void {
 | |
|     $md = $this->md;
 | |
|     if ($md !== null) {
 | |
|       $md->ensureSchema($row);
 | |
|       $md->verifix($row, $this->results, false, true);
 | |
|       #XXX il faut garder les champs "autres"!
 | |
|       #$row = $md->format($row);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   protected function cookRow(array $row): array {
 | |
|     $cooked = $this->getRowValues($row);
 | |
|     $this->ensureRowSchema($cooked);
 | |
|     return $cooked;
 | |
|   }
 | |
| 
 | |
|   function print(): void {
 | |
|     try {
 | |
|       $this->_rewindRows();
 | |
|       $this->rowIndex = 0;
 | |
|       $haveRows = false;
 | |
|       while ($this->_validRow()) {
 | |
|         $this->rawRow = $this->_currentRow($this->rowKey);
 | |
|         try {
 | |
|           $this->origRow = $this->nextRow();
 | |
|           if ($this->origRow === null) continue;
 | |
| 
 | |
|           $skipRow = false;
 | |
|           if (!$haveRows) $skipRow = $this->resolveColsHeaders($this->origRow);
 | |
|           if (!$skipRow) {
 | |
|             $this->row = $this->cookRow($this->origRow);
 | |
|             if (!$haveRows) {
 | |
|               $haveRows = true;
 | |
|               vo::print($this->beforeTable());
 | |
|               vo::stable(["class" => "table", $this->table()]);
 | |
| 
 | |
|               vo::sthead($this->thead());
 | |
|               $this->printBeforeHeader();
 | |
|               $this->printHeader($this->headers);
 | |
|               $this->printAfterHeader();
 | |
|               vo::ethead();
 | |
| 
 | |
|               vo::stbody($this->tbody());
 | |
|             }
 | |
|             $this->printBeforeRow();
 | |
|             $this->printRow($this->row);
 | |
|             $this->printAfterRow();
 | |
|             $this->rowIndex++;
 | |
|           }
 | |
|         } finally {
 | |
|           $this->_nextRow();
 | |
|         }
 | |
|       }
 | |
|       if ($haveRows) {
 | |
|         vo::etbody();
 | |
|         vo::etable();
 | |
|         vo::print($this->afterTable());
 | |
|       } else {
 | |
|         vo::print($this->noData());
 | |
|       }
 | |
|     } catch (Exception $e) {
 | |
|       $onException = $this->ppOnException;
 | |
|       if ($onException !== null) func::call($onException, $e);
 | |
|       else throw $e;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /** contenu à afficher avant le tableau s'il y a des lignes à afficher */
 | |
|   function beforeTable(): ?iterable {
 | |
|     return $this->ppBeforeTable;
 | |
|   }
 | |
| 
 | |
|   /** contenu à afficher après le tableau s'il y a des lignes à afficher */
 | |
|   function afterTable(): ?iterable {
 | |
|     return $this->ppAfterTable;
 | |
|   }
 | |
| 
 | |
|   /** contenu à afficher s'il n'y a pas de lignes à afficher */
 | |
|   function noData(): ?iterable {
 | |
|     return $this->ppNoData;
 | |
|   }
 | |
| 
 | |
|   function table(): ?iterable {
 | |
|     return SL::merge(["class" => $this->getTableClass()], $this->ppTableAttrs);
 | |
|   }
 | |
| 
 | |
|   function thead(): ?iterable {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   /** contenu à afficher avant la ligne */
 | |
|   function beforeHeader(): ?iterable {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   function printBeforeHeader(): void {
 | |
|     $beforeHeaderContents = $this->beforeHeader();
 | |
|     if ($beforeHeaderContents !== null) vo::tr($beforeHeaderContents);
 | |
|   }
 | |
| 
 | |
|   /** contenu à afficher après la ligne */
 | |
|   function afterHeader(): ?iterable {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   function printAfterHeader(): void {
 | |
|     $afterHeaderContents = $this->afterHeader();
 | |
|     if ($afterHeaderContents !== null) vo::tr($afterHeaderContents);
 | |
|   }
 | |
| 
 | |
|   function printHeader(array $headers): void {
 | |
|     vo::print($this->headerTr($headers));
 | |
|   }
 | |
| 
 | |
|   function headerTr(array $headers): array {
 | |
|     return v::tr($this->headerRow($headers));
 | |
|   }
 | |
| 
 | |
|   function headerRow(array $headers): ?iterable {
 | |
|     $vs = [];
 | |
|     foreach ($headers as $header) {
 | |
|       $label = $header["label"];
 | |
|       if ($label === null) $label = $header["name"];
 | |
|       $colspan = $header["span"];
 | |
|       $vs[] = v::th(["colspan" => $colspan, $label]);
 | |
|     }
 | |
|     return $vs;
 | |
|   }
 | |
| 
 | |
|   function tbody(): ?iterable {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   /** @var int index de la ligne courante */
 | |
|   protected $rowIndex;
 | |
| 
 | |
|   /** @var string|int clé de la ligne courante */
 | |
|   protected $rowKey;
 | |
| 
 | |
|   /** @var array la ligne originale, avant le mapping */
 | |
|   protected $rawRow;
 | |
| 
 | |
|   /** @var array la ligne mappée avant sa "cuisine" */
 | |
|   protected $origRow;
 | |
| 
 | |
|   /** @var array la ligne cuisinée */
 | |
|   protected $row;
 | |
| 
 | |
|   /** contenu à afficher avant la ligne */
 | |
|   function beforeRow(): ?iterable {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   function printBeforeRow(): void {
 | |
|     $beforeRowContents = $this->beforeRow();
 | |
|     if ($beforeRowContents !== null) vo::tr($beforeRowContents);
 | |
|   }
 | |
| 
 | |
|   /** contenu à afficher après la ligne */
 | |
|   function afterRow(): ?iterable {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   function printAfterRow(): void {
 | |
|     $afterRowContents = $this->afterRow();
 | |
|     if ($afterRowContents !== null) vo::tr($afterRowContents);
 | |
|   }
 | |
| 
 | |
|   function printRow(array $row): void {
 | |
|     vo::print($this->rowTr($row));
 | |
|   }
 | |
| 
 | |
|   function rowTr(array $row): array {
 | |
|     $vs = $this->row($row);
 | |
|     if ($this->rowCtx !== null) {
 | |
|       $vs = func::_call($this->rowCtx, [$vs, $row, $this->rawRow]);
 | |
|     }
 | |
|     return v::tr($vs);
 | |
|   }
 | |
| 
 | |
|   function row(array $row): ?iterable {
 | |
|     $vs = [];
 | |
|     $this->index = 0;
 | |
|     foreach ($this->cols as $this->col) {
 | |
|       if ($this->col === null) $value = null;
 | |
|       else $value = $row[$this->col];
 | |
|       $vs[] = $this->colTd($value);
 | |
|       $this->index++;
 | |
|     }
 | |
|     return $vs;
 | |
|   }
 | |
| 
 | |
|   /** @var int index de la colonne courante */
 | |
|   protected $index;
 | |
| 
 | |
|   /** @var string|int clé de la colonne courante */
 | |
|   protected $col;
 | |
| 
 | |
|   function colTd($value): ?array {
 | |
|     $vs = $this->col($value);
 | |
|     if ($this->colCtx !== null) {
 | |
|       $vs = func::_call($this->colCtx, [$vs, $value, $this->col, $this->index, $this->row, $this->rawRow]);
 | |
|       if ($vs === false) return null;
 | |
|     } else {
 | |
|       $result = A::get($this->results, $this->col);
 | |
|       $valid = $result === null || $result["valid"];
 | |
|       $vs = [
 | |
|         "class" => ["danger" => !$valid],
 | |
|         $vs,
 | |
|       ];
 | |
|     }
 | |
|     return v::td($vs);
 | |
|   }
 | |
| 
 | |
|   function col($value): ?iterable {
 | |
|     #XXX formatter selon le schéma, ou en utilisant des méthodes définies dans
 | |
|     # cette classe
 | |
|     return q($value);
 | |
|   }
 | |
| }
 |