From afefec876f0aed81e2d88558c19b6bd258cd22a5 Mon Sep 17 00:00:00 2001 From: Thibeau Fuhrer Date: Mon, 2 Nov 2026 10:42:30 +0100 Subject: [PATCH 1/4] [REFACTOR] UI: improve `Table` internal/public typing Improves the typing of the `Table` component family. We strictly consider the internal interface for actions and columns, because there is no explicit rendering for them. In the future we might extend the public interfaces so custom implementations become possible. --- .../Component/Table/AbstractTable.php | 81 ++++--- .../Implementation/Component/Table/Data.php | 41 ++-- .../Component/Table/DataRow.php | 15 +- .../Component/Table/DataRowBuilder.php | 9 +- .../Component/Table/Ordering.php | 26 ++- .../Component/Table/OrderingRowBuilder.php | 7 +- .../Component/Table/Renderer.php | 201 +++++++++--------- .../Component/Table/RowBuilder.php | 15 +- .../Table/TableViewControlFieldSelection.php | 18 +- .../Table/TableViewControlOrdering.php | 2 +- .../UI/tests/Component/Table/DataTest.php | 2 +- .../tests/Component/Table/TableTestBase.php | 4 +- 12 files changed, 197 insertions(+), 224 deletions(-) diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/AbstractTable.php b/components/ILIAS/UI/src/Implementation/Component/Table/AbstractTable.php index ab36db7b522f..dc858feeaf88 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Table/AbstractTable.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/AbstractTable.php @@ -20,60 +20,52 @@ namespace ILIAS\UI\Implementation\Component\Table; -use ILIAS\UI\Component\Table as T; -use ILIAS\UI\Component\Table\Column\Column; -use ILIAS\UI\Component\Table\Action\Action; +use ILIAS\UI\Implementation\Component as I; +use ILIAS\UI\Component as C; use Psr\Http\Message\ServerRequestInterface; -use ILIAS\UI\Implementation\Component\SignalGeneratorInterface; -use ILIAS\UI\Component\Signal; -use ILIAS\UI\Implementation\Component\JavaScriptBindable; -use ILIAS\UI\Component\JavaScriptBindable as JSBindable; -use ILIAS\UI\Component\Input\ViewControl; -use ILIAS\UI\Component\Input\Container\ViewControl as ViewControlContainer; -use ILIAS\UI\Implementation\Component\Input\ArrayInputData; - -abstract class AbstractTable extends Table implements JSBindable + +abstract class AbstractTable extends Table implements C\JavaScriptBindable { - use JavaScriptBindable; + use I\JavaScriptBindable; /** - * @var array + * @var array */ protected $columns = []; /** - * @var array + * @var array */ protected $actions_single = []; /** - * @var array + * @var array */ protected $actions_multi = []; /** - * @var array + * @var array */ protected $actions_std = []; protected ?string $id = null; - protected Signal $multi_action_signal; - protected Signal $selection_signal; - protected Signal $async_action_signal; + protected C\Signal $multi_action_signal; + protected C\Signal $selection_signal; + protected C\Signal $async_action_signal; protected ?ServerRequestInterface $request = null; /** - * @param array $columns + * @param array $columns */ public function __construct( - SignalGeneratorInterface $signal_generator, - protected ViewControl\Factory $view_control_factory, - protected ViewControlContainer\Factory $view_control_container_factory, - protected \ArrayAccess $storage, + I\SignalGeneratorInterface $signal_generator, + protected I\Input\ViewControl\Factory $view_control_factory, + protected I\Input\Container\ViewControl\Factory $view_control_container_factory, + protected \ILIAS\UI\Storage $storage, string $title, - array $columns + array $columns, ) { - $this->checkArgListElements('columns', $columns, [Column::class]); + $this->checkArgListElements('columns', $columns, [I\Table\Column\Column::class]); if ($columns === []) { throw new \InvalidArgumentException('cannot construct a table without columns.'); } @@ -86,8 +78,8 @@ public function __construct( } /** - * @param array $columns - * @return array + * @param array $columns + * @return array */ private function enumerateColumns(array $columns): array { @@ -100,7 +92,7 @@ private function enumerateColumns(array $columns): array } /** - * @return array + * @return array */ public function getColumns(): array { @@ -112,18 +104,20 @@ public function getColumns(): array */ public function withActions(array $actions): static { - $this->checkArgListElements('actions', $actions, [T\Action\Action::class]); + // check actions against internal interface, will not be + // deletaged to rendering chain and cannot be customised + $this->checkArgListElements('actions', $actions, [I\Table\Action\Action::class]); $clone = clone $this; foreach ($actions as $id => $action) { switch (true) { - case ($action instanceof T\Action\Single): + case ($action instanceof I\Table\Action\Single): $clone->actions_single[$id] = $action; break; - case ($action instanceof T\Action\Multi): + case ($action instanceof I\Table\Action\Multi): $clone->actions_multi[$id] = $action; break; - case ($action instanceof T\Action\Standard): + case ($action instanceof I\Table\Action\Standard): $clone->actions_std[$id] = $action; break; } @@ -142,17 +136,17 @@ public function getRequest(): ?ServerRequestInterface return $this->request; } - public function getMultiActionSignal(): Signal + public function getMultiActionSignal(): C\Signal { return $this->multi_action_signal; } - public function getSelectionSignal(): Signal + public function getSelectionSignal(): C\Signal { return $this->selection_signal; } - public function getAsyncActionSignal(): Signal + public function getAsyncActionSignal(): C\Signal { return $this->async_action_signal; } @@ -168,7 +162,7 @@ public function hasMultiActions(): bool } /** - * @return array + * @return array */ public function getMultiActions(): array { @@ -176,7 +170,7 @@ public function getMultiActions(): array } /** - * @return array + * @return array */ public function getSingleActions(): array { @@ -184,7 +178,7 @@ public function getSingleActions(): array } /** - * @return array + * @return array */ public function getAllActions(): array { @@ -232,15 +226,14 @@ protected function getId(): ?string } protected function applyValuesToViewcontrols( - ViewControlContainer\ViewControl $view_controls, + I\Input\Container\ViewControl\ViewControl $view_controls, ServerRequestInterface $request - ): ViewControlContainer\ViewControl { - $stored_values = new ArrayInputData($this->getStorageData() ?? []); + ): I\Input\Container\ViewControl\ViewControl { + $stored_values = new I\Input\ArrayInputData($this->getStorageData() ?? []); $view_controls = $view_controls ->withStoredInput($stored_values) ->withRequest($request); $this->setStorageData($view_controls->getComponentInternalValues()); return $view_controls; } - } diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/Data.php b/components/ILIAS/UI/src/Implementation/Component/Table/Data.php index e8b6d544366e..7fe7f1d56ba0 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Table/Data.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/Data.php @@ -20,23 +20,14 @@ namespace ILIAS\UI\Implementation\Component\Table; -use ILIAS\UI\Component\Table as T; -use ILIAS\UI\Component\Table\Column\Column; -use ILIAS\UI\Component\Table\Action\Action; -use Psr\Http\Message\ServerRequestInterface; -use ILIAS\UI\Implementation\Component\SignalGeneratorInterface; -use ILIAS\UI\Component\Signal; -use ILIAS\UI\Implementation\Component\JavaScriptBindable; -use ILIAS\UI\Component\JavaScriptBindable as JSBindable; -use ILIAS\Data\Factory as DataFactory; -use ILIAS\UI\Component\Input\ViewControl; -use ILIAS\UI\Component\Input\Container\ViewControl as ViewControlContainer; +use ILIAS\UI\Implementation\Component as I; +use ILIAS\UI\Component as C; use ILIAS\Data\Range; use ILIAS\Data\Order; -class Data extends AbstractTable implements T\Data +class Data extends AbstractTable implements C\Table\Data { - use JavaScriptBindable; + use I\JavaScriptBindable; use TableViewControlFieldSelection; use TableViewControlPagination; use TableViewControlOrdering; @@ -50,21 +41,21 @@ class Data extends AbstractTable implements T\Data protected mixed $filter = null; protected mixed $additional_parameters = null; protected mixed $additional_viewcontrol_data = null; - protected ?ViewControlContainer\ViewControlInput $additional_view_control = null; + protected C\Input\Container\ViewControl\ViewControlInput|null $additional_view_control = null; /** - * @param array $columns + * @param array $columns */ public function __construct( - SignalGeneratorInterface $signal_generator, - ViewControl\Factory $view_control_factory, - ViewControlContainer\Factory $view_control_container_factory, - protected DataFactory $data_factory, + I\SignalGeneratorInterface $signal_generator, + I\Input\ViewControl\Factory $view_control_factory, + I\Input\Container\ViewControl\Factory $view_control_container_factory, + protected \ILIAS\Data\Factory $data_factory, protected DataRowBuilder $data_row_builder, string $title, array $columns, - protected T\DataRetrieval $data_retrieval, - \ArrayAccess $storage + protected C\Table\DataRetrieval $data_retrieval, + \ILIAS\UI\Storage $storage ) { parent::__construct( $signal_generator, @@ -79,7 +70,7 @@ public function __construct( $this->initViewControlpagination(); } - public function getDataRetrieval(): T\DataRetrieval + public function getDataRetrieval(): C\Table\DataRetrieval { return $this->data_retrieval; } @@ -130,7 +121,7 @@ public function getRowBuilder(): DataRowBuilder } /** - * @return array + * @return array{0: self, 1: I\Input\Container\ViewControl\ViewControl} */ public function applyViewControls( mixed $filter_data, @@ -179,7 +170,7 @@ public function applyViewControls( ]; } - protected function getViewControls(?int $total_count = null): ViewControlContainer\ViewControl + protected function getViewControls(?int $total_count = null): I\Input\Container\ViewControl\ViewControl { $view_controls = [ self::VIEWCONTROL_KEY_PAGINATION => $this->getViewControlPagination($total_count), @@ -196,7 +187,7 @@ protected function getViewControls(?int $total_count = null): ViewControlContain } public function withAdditionalViewControl( - ViewControlContainer\ViewControlInput $view_control + C\Input\Container\ViewControl\ViewControlInput $view_control ): self { $clone = clone $this; $clone->additional_view_control = $view_control diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/DataRow.php b/components/ILIAS/UI/src/Implementation/Component/Table/DataRow.php index 04b3c1e5d50c..e8b8414de569 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Table/DataRow.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/DataRow.php @@ -20,13 +20,12 @@ namespace ILIAS\UI\Implementation\Component\Table; -use ILIAS\UI\Component\Table as T; -use ILIAS\UI\Component\Component; -use ILIAS\UI\Implementation\Component\ComponentHelper; +use ILIAS\UI\Implementation\Component as I; +use ILIAS\UI\Component as C; -class DataRow implements T\DataRow +class DataRow implements C\Table\DataRow { - use ComponentHelper; + use I\ComponentHelper; /** * @var array @@ -37,8 +36,8 @@ class DataRow implements T\DataRow * The records's key is the column-id of the table. * Its value will be formatted by the respective colum type's format-method. * - * @param array $columns - * @param array $actions + * @param array $columns + * @param array $actions * @param array $record */ public function __construct( @@ -91,7 +90,7 @@ function (string $id): bool { ); } - public function getCellContent(string $col_id): string|Component + public function getCellContent(string $col_id): C\Component|string { if (!array_key_exists($col_id, $this->record)) { return ''; diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/DataRowBuilder.php b/components/ILIAS/UI/src/Implementation/Component/Table/DataRowBuilder.php index a000b6e8197e..671643a711d4 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Table/DataRowBuilder.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/DataRowBuilder.php @@ -20,16 +20,15 @@ namespace ILIAS\UI\Implementation\Component\Table; -use ILIAS\UI\Component\Table as T; -use ILIAS\UI\Component\Table\Column\Column; -use ILIAS\UI\Component\Table\Action\Action; +use ILIAS\UI\Implementation\Component as I; +use ILIAS\UI\Component as C; -class DataRowBuilder extends RowBuilder implements T\DataRowBuilder +class DataRowBuilder extends RowBuilder implements C\Table\DataRowBuilder { /** * @param array $record */ - public function buildDataRow(string $id, array $record): T\DataRow + public function buildDataRow(string $id, array $record): DataRow { return new DataRow( $this->row_actions !== [], diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/Ordering.php b/components/ILIAS/UI/src/Implementation/Component/Table/Ordering.php index 855007dbd3d5..ebe7d9a18794 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Table/Ordering.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/Ordering.php @@ -20,13 +20,11 @@ namespace ILIAS\UI\Implementation\Component\Table; -use ILIAS\UI\Component\Table as T; -use ILIAS\UI\Implementation\Component\SignalGeneratorInterface; +use ILIAS\UI\Implementation\Component as I; +use ILIAS\UI\Component as C; use ILIAS\Data\URI; -use ILIAS\UI\Component\Input\ViewControl; -use ILIAS\UI\Component\Input\Container\ViewControl as ViewControlContainer; -class Ordering extends AbstractTable implements T\Ordering +class Ordering extends AbstractTable implements C\Table\Ordering { use TableViewControlFieldSelection; @@ -36,15 +34,15 @@ class Ordering extends AbstractTable implements T\Ordering protected bool $ordering_disabled = false; public function __construct( - SignalGeneratorInterface $signal_generator, - ViewControl\Factory $view_control_factory, - ViewControlContainer\Factory $view_control_container_factory, + I\SignalGeneratorInterface $signal_generator, + I\Input\ViewControl\Factory $view_control_factory, + I\Input\Container\ViewControl\Factory $view_control_container_factory, protected OrderingRowBuilder $row_builder, string $title, array $columns, - protected T\OrderingRetrieval $binding, + protected C\Table\OrderingRetrieval $binding, protected URI $target_url, - \ArrayAccess $storage + \ILIAS\UI\Storage $storage ) { parent::__construct( $signal_generator, @@ -65,7 +63,7 @@ public function getRowBuilder(): OrderingRowBuilder ->withVisibleColumns($this->getVisibleColumns()); } - public function getDataBinding(): T\OrderingRetrieval + public function getDataBinding(): C\Table\OrderingRetrieval { return $this->binding; } @@ -90,7 +88,7 @@ public function getTargetURL(): ?URI public function getData(): array { if (!$request = $this->getRequest()) { - return null; + return []; } $ordered = $request->getParsedBody(); asort($ordered, SORT_NUMERIC); @@ -98,7 +96,7 @@ public function getData(): array } /** - * @return array + * @return array */ public function applyViewControls(): array { @@ -118,7 +116,7 @@ public function applyViewControls(): array ]; } - protected function getViewControls(): ViewControlContainer\ViewControl + protected function getViewControls(): I\Input\Container\ViewControl\ViewControl { $view_controls = [ self::VIEWCONTROL_KEY_FIELDSELECTION => $this->getViewControlFieldSelection(), diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/OrderingRowBuilder.php b/components/ILIAS/UI/src/Implementation/Component/Table/OrderingRowBuilder.php index 7ccaece6bd09..290dbe1cd734 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Table/OrderingRowBuilder.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/OrderingRowBuilder.php @@ -20,14 +20,15 @@ namespace ILIAS\UI\Implementation\Component\Table; -use ILIAS\UI\Component\Table as T; +use ILIAS\UI\Implementation\Component as I; +use ILIAS\UI\Component as C; -class OrderingRowBuilder extends RowBuilder implements T\OrderingRowBuilder +class OrderingRowBuilder extends RowBuilder implements C\Table\OrderingRowBuilder { /** * @param array $record */ - public function buildOrderingRow(string $id, array $record): T\OrderingRow + public function buildOrderingRow(string $id, array $record): OrderingRow { return new OrderingRow( $this->row_actions !== [], diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Table/Renderer.php index f123cb08a2c2..11cd115b974c 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Table/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/Renderer.php @@ -22,49 +22,47 @@ use ILIAS\UI\Implementation\Render\AbstractComponentRenderer; use ILIAS\UI\Implementation\Render\ResourceRegistry; -use ILIAS\UI\Renderer as RendererInterface; -use ILIAS\UI\Component; use ILIAS\UI\Implementation\Render\Template; +use ILIAS\UI\Renderer as RendererInterface; +use ILIAS\UI\Implementation\Component as I; +use ILIAS\UI\Component as C; use ILIAS\Data\Order; use ILIAS\Data\URI; -use ILIAS\UI\Implementation\Component\Table\Action\Action; -use ILIAS\UI\Implementation\Component\Input\ViewControl\Pagination; -use ILIAS\UI\Implementation\Component\Input\NameSource; +use ILIAS\UI\URLBuilder; class Renderer extends AbstractComponentRenderer { + protected const string KEY_ACTION_ID = 'action_id'; + protected const string KEY_ROW_ID = 'row_id'; + /** * @inheritdoc */ - public function render(Component\Component $component, RendererInterface $default_renderer): string + public function render(C\Component $component, RendererInterface $default_renderer): string { - if ($component instanceof Component\Table\Presentation) { + if ($component instanceof Presentation) { return $this->renderPresentationTable($component, $default_renderer); } - if ($component instanceof Component\Table\PresentationRow) { + if ($component instanceof PresentationRow) { return $this->renderPresentationRow($component, $default_renderer); } - if ($component instanceof Component\Table\Data) { + if ($component instanceof Data) { return $this->renderDataTable($component, $default_renderer); } - if ($component instanceof Component\Table\DataRow - && !$component instanceof Component\Table\OrderingRow - ) { + if ($component instanceof DataRow && !$component instanceof OrderingRow) { return $this->renderDataRow($component, $default_renderer); } - if ($component instanceof Component\Table\Ordering) { + if ($component instanceof Ordering) { return $this->renderOrderingTable($component, $default_renderer); } - if ($component instanceof Component\Table\OrderingRow) { + if ($component instanceof OrderingRow) { return $this->renderOrderingRow($component, $default_renderer); } $this->cannotHandleComponent($component); } - protected function renderPresentationTable( - Component\Table\Presentation $component, - RendererInterface $default_renderer - ): string { + protected function renderPresentationTable(Presentation $component, RendererInterface $default_renderer): string + { $tpl = $this->getTemplate("tpl.presentationtable.html", true, true); $tpl->setVariable("TITLE", $component->getTitle()); $expcollapsebtns = []; @@ -72,13 +70,13 @@ protected function renderPresentationTable( $sig_ta_expand = clone $sig_ta; $sig_ta_expand->addOption('expand', true); $expcollapsebtns[] = $this->getUIFactory()->button() - ->standard($this->txt('presentation_table_expand'), '') - ->withOnClick($sig_ta_expand); + ->standard($this->txt('presentation_table_expand'), '') + ->withOnClick($sig_ta_expand); $sig_ta_collapse = clone $sig_ta; $sig_ta_collapse->addOption('expand', false); $expcollapsebtns[] = $this->getUIFactory()->button() - ->standard($this->txt('presentation_table_collapse'), '') - ->withOnClick($sig_ta_collapse); + ->standard($this->txt('presentation_table_collapse'), '') + ->withOnClick($sig_ta_collapse); $component = $component->withAdditionalOnLoadCode( static fn($id) => " il.UI.table.presentation.init('{$id}'); @@ -122,10 +120,8 @@ protected function renderPresentationTable( return $tpl->get(); } - protected function renderPresentationRow( - Component\Table\PresentationRow $component, - RendererInterface $default_renderer - ): string { + protected function renderPresentationRow(PresentationRow $component, RendererInterface $default_renderer): string + { $f = $this->getUIFactory(); $tpl = $this->getTemplate("tpl.presentationrow.html", true, true); @@ -136,11 +132,11 @@ protected function renderPresentationRow( $id = $this->bindJavaScript($component); $expander = $f->button()->shy('', '')->withSymbol($f->symbol()->glyph()->expand()) - ->withOnClick($sig_show); + ->withOnClick($sig_show); $collapser = $f->button()->shy('', '')->withSymbol($f->symbol()->glyph()->collapse()) - ->withOnClick($sig_hide); + ->withOnClick($sig_hide); $shy_expander = $f->button()->shy($this->txt("presentation_table_more"), "") - ->withOnClick($sig_show); + ->withOnClick($sig_show); $tpl->setVariable("ID", $id); $tpl->setVariable("EXPANDER", $default_renderer->render($expander)); @@ -204,7 +200,7 @@ protected function renderPresentationRow( return $tpl->get(); } - public function renderDataTable(Component\Table\Data $component, RendererInterface $default_renderer): string + public function renderDataTable(Data $component, RendererInterface $default_renderer): string { $tpl = $this->getTemplate("tpl.datatable.html", true, true); $component = $this->registerActions($component); @@ -269,7 +265,7 @@ public function renderDataTable(Component\Table\Data $component, RendererInterfa $sortation_view_control = array_filter( $view_controls->getInputs(), - static fn($i): bool => $i instanceof Component\Input\ViewControl\Sortation + static fn($i): bool => $i instanceof C\Input\ViewControl\Sortation ); if ($sortation_view_control) { $sortation_signal = array_shift($sortation_view_control)->getInternalSignal(); @@ -283,9 +279,9 @@ public function renderDataTable(Component\Table\Data $component, RendererInterfa protected function renderTableHeader( RendererInterface $default_renderer, - Component\Table\Data $component, + Data $component, Template $tpl, - ?Component\Signal $sortation_signal, + ?C\Signal $sortation_signal, int $compensate_col_index, ): void { $order = $component->getOrder(); @@ -321,12 +317,12 @@ protected function renderTableHeader( if ($col_id === $sort_col) { $sortation_glyph = $this->getUIFactory()->button()->shy('', '') - ->withSymbol( - $sort_direction === Order::ASC ? - $glyph_factory->sortAscending() : - $glyph_factory->sortDescending() - ) - ->withOnClick($sort_signal); + ->withSymbol( + $sort_direction === Order::ASC ? + $glyph_factory->sortAscending() : + $glyph_factory->sortDescending() + ) + ->withOnClick($sort_signal); $tpl->setVariable('COL_SORTATION', $sortation); $tpl->setVariable('COL_SORTATION_GLYPH', $default_renderer->render($sortation_glyph)); } @@ -340,7 +336,7 @@ protected function renderTableHeader( protected function renderActionsHeader( RendererInterface $default_renderer, - Component\Table\Table $component, + AbstractTable $component, Template $tpl, int $compensate_col_count, ): void { @@ -356,17 +352,17 @@ protected function renderActionsHeader( $sig_all = clone $signal; $sig_all->addOption('select', true); $select_all = $f->button()->shy('', '') - ->withSymbol($glyph_factory->add()) - ->withOnClick($sig_all); + ->withSymbol($glyph_factory->add()) + ->withOnClick($sig_all); $signal->addOption('select', false); $select_none = $f->button()->shy('', '') - ->withSymbol($glyph_factory->close()) - ->withOnClick($signal); + ->withSymbol($glyph_factory->close()) + ->withOnClick($signal); $tpl->setVariable('SELECTION_CONTROL_SELECT', $default_renderer->render($select_all)); $tpl->setVariable('SELECTION_CONTROL_DESELECT', $default_renderer->render($select_none)); } - if ($component instanceof Component\Table\Ordering) { + if ($component instanceof Ordering) { if (!$component->isOrderingDisabled() && !$component->hasMultiActions()) { $tpl->touchBlock('header_rowselection_cell'); } @@ -377,7 +373,7 @@ protected function renderActionsHeader( * Renders a full-width cell with a single message within, indication there is no * data to display. This is achieved using a colspan attribute. */ - protected function renderFullWidthDataCell(Component\Table\Data $component, Template $tpl, string $content): void + protected function renderFullWidthDataCell(Data $component, Template $tpl, string $content): void { $cell_tpl = $this->getTemplate('tpl.datacell.html', true, true); $cell_tpl->setCurrentBlock('cell'); @@ -393,10 +389,10 @@ protected function renderFullWidthDataCell(Component\Table\Data $component, Temp $tpl->parseCurrentBlock(); } - protected function registerActions(Component\Table\Table $component): Component\Table\Table + protected function registerActions(AbstractTable $component): AbstractTable { - $opt_action_id = Action::OPT_ACTIONID; - $opt_row_id = Action::OPT_ROWID; + $opt_action_id = self::KEY_ACTION_ID; + $opt_row_id = self::KEY_ROW_ID; if ($component->hasMultiActions()) { $component = $component->withAdditionalOnLoadCode( @@ -406,10 +402,12 @@ protected function registerActions(Component\Table\Table $component): Component\ $actions = []; foreach ($component->getAllActions() as $action_id => $action) { - $component = $component->withAdditionalOnLoadCode($this->getActionRegistration((string) $action_id, $action)); + $component = $component->withAdditionalOnLoadCode( + $this->getActionRegistration((string) $action_id, $action) + ); if ($action->isAsync()) { $signal = clone $component->getAsyncActionSignal(); - $signal->addOption(Action::OPT_ACTIONID, $action_id); + $signal->addOption(self::KEY_ACTION_ID, $action_id); $action = $action->withSignalTarget($signal); } $actions[$action_id] = $action; @@ -418,8 +416,7 @@ protected function registerActions(Component\Table\Table $component): Component\ $component = $component ->withAdditionalOnLoadCode( - static fn($id): string => - "il.UI.table.data.init('{$id}','{$opt_action_id}','{$opt_row_id}');" + static fn($id): string => "il.UI.table.data.init('{$id}','{$opt_action_id}','{$opt_row_id}');" ) ->withAdditionalOnLoadCode($this->getAsyncActionHandler($component->getAsyncActionSignal())) ->withAdditionalOnLoadCode($this->getMultiActionHandler($component->getMultiActionSignal())) @@ -444,15 +441,18 @@ protected function appendTableRows( } } - protected function renderEmptyPresentationRow(Template $tpl, RendererInterface $default_renderer, string $content): void - { + protected function renderEmptyPresentationRow( + Template $tpl, + RendererInterface $default_renderer, + string $content + ): void { $row_tpl = $this->getTemplate('tpl.presentationrow_empty.html', true, true); $row_tpl->setVariable('CONTENT', $content); $tpl->setVariable('ROW', $row_tpl->get()); } /** - * @param array $actions + * @param array $actions */ protected function buildMultiActionsAllObjectsModal( array $actions, @@ -471,12 +471,13 @@ protected function buildMultiActionsAllObjectsModal( "" )->withRequired(true); $submit = $f->button()->primary($this->txt('datatable_multiactionmodal_apply'), '') - ->withOnLoadCode( - static fn($id): string => "$('#{$id}').click(function() { il.UI.table.data.get('{$table_id}').doActionForAll(this); return false; });" - ); + ->withOnLoadCode( + static fn($id + ): string => "$('#{$id}').click(function() { il.UI.table.data.get('{$table_id}').doActionForAll(this); return false; });" + ); $modal = $f->modal() - ->roundtrip($this->txt('datatable_multiactionmodal_title'), [$msg, $select]) - ->withActionButtons([$submit]); + ->roundtrip($this->txt('datatable_multiactionmodal_title'), [$msg, $select]) + ->withActionButtons([$submit]); return $modal; } @@ -487,13 +488,13 @@ protected function getUrlTooLongWarning(): \ILIAS\UI\Component\MessageBox\Messag } /** - * @param array $actions + * @param array $actions */ protected function buildMultiActionsDropdown( array $actions, - Component\Signal $action_signal, - Component\Signal $modal_signal, - ): ?\ILIAS\UI\Component\Dropdown\Dropdown { + C\Signal $action_signal, + C\Signal $modal_signal + ): ?I\Dropdown\Dropdown { if ($actions === []) { return null; } @@ -503,17 +504,19 @@ protected function buildMultiActionsDropdown( $all_obj_buttons = []; foreach ($actions as $action_id => $act) { $signal = clone $action_signal; - $signal->addOption(Action::OPT_ACTIONID, $action_id); + $signal->addOption(self::KEY_ACTION_ID, $action_id); $buttons[] = $f->button()->shy($act->getLabel(), $signal); } $buttons[] = $f->divider()->horizontal(); - $buttons[] = $f->button()->shy($this->txt('datatable_multiactionmodal_listentry'), '#')->withOnClick($modal_signal); + $buttons[] = $f->button()->shy($this->txt('datatable_multiactionmodal_listentry'), '#')->withOnClick( + $modal_signal + ); return $f->dropdown()->standard($buttons)->withLabel($this->txt('datatable_multiaction_label')); } - protected function getAsyncActionHandler(Component\Signal $action_signal): \Closure + protected function getAsyncActionHandler(C\Signal $action_signal): \Closure { return static function ($id) use ($action_signal): string { return " @@ -523,7 +526,8 @@ protected function getAsyncActionHandler(Component\Signal $action_signal): \Clos });"; }; } - protected function getMultiActionHandler(Component\Signal $action_signal): \Closure + + protected function getMultiActionHandler(C\Signal $action_signal): \Closure { return static function ($id) use ($action_signal): string { return " @@ -534,7 +538,7 @@ protected function getMultiActionHandler(Component\Signal $action_signal): \Clos }; } - protected function getSelectionHandler(Component\Signal $selection_signal): \Closure + protected function getSelectionHandler(C\Signal $selection_signal): \Closure { return static function ($id) use ($selection_signal): string { return " @@ -546,10 +550,8 @@ protected function getSelectionHandler(Component\Signal $selection_signal): \Clo }; } - protected function getActionRegistration( - string $action_id, - Action $action - ): \Closure { + protected function getActionRegistration(string $action_id, I\Table\Action\Action $action): \Closure + { $async = $action->isAsync() ? 'true' : 'false'; $url_builder_js = $action->getURLBuilderJS(); $tokens_js = $action->getURLBuilderTokensJS(); @@ -561,30 +563,29 @@ protected function getActionRegistration( }; } - public function renderDataRow(Component\Table\DataRow $component, RendererInterface $default_renderer): string + public function renderDataRow(DataRow $component, RendererInterface $default_renderer): string { $cell_tpl = $this->getTemplate("tpl.datacell.html", true, true); $this->fillCells($component, $cell_tpl, $default_renderer); - return $cell_tpl->get(); } - public function renderOrderingRow(Component\Table\OrderingRow $component, RendererInterface $default_renderer): string + public function renderOrderingRow(OrderingRow $component, RendererInterface $default_renderer): string { $cell_tpl = $this->getTemplate("tpl.orderingcell.html", true, true); $this->fillCells($component, $cell_tpl, $default_renderer); - if ($component->isOrderingDisabled()) { return $cell_tpl->get(); } - $namesource = new class () implements NameSource { + $namesource = new class () implements I\Input\NameSource { public function getNewName(): string { return ''; } + public function getNewDedicatedName(string $dedicated_name): string { return $dedicated_name; @@ -593,20 +594,16 @@ public function getNewDedicatedName(string $dedicated_name): string $numeric_label = $this->txt("ui_table_order"); $input = $this->getUIFactory()->input()->field()->numeric($numeric_label) - ->withDedicatedName($component->getId()) - ->withNameFrom($namesource) - ->withValue($component->getPosition() * 10); + ->withDedicatedName($component->getId()) + ->withNameFrom($namesource) + ->withValue($component->getPosition() * 10); $cell_tpl->setVariable('ORDER_INPUT', $default_renderer->render($input)); return $cell_tpl->get(); } - - protected function fillCells( - Component\Table\DataRow $row, - Template $cell_tpl, - RendererInterface $default_renderer - ) { + protected function fillCells(DataRow $row, Template $cell_tpl, RendererInterface $default_renderer): void + { $cols = $row->getColumns(); foreach ($cols as $col_id => $column) { if ($column->isHighlighted()) { @@ -615,7 +612,7 @@ protected function fillCells( $cell_tpl->setCurrentBlock('cell'); $cell_tpl->setVariable('COL_TYPE', strtolower($column->getType())); $cell_content = $row->getCellContent($col_id); - if ($cell_content instanceof Component\Component) { + if ($cell_content instanceof C\Component) { $cell_content = $default_renderer->render($cell_content); } $cell_tpl->setVariable('CELL_CONTENT', $cell_content); @@ -634,7 +631,7 @@ protected function fillCells( $cell_tpl->setVariable('ACTION_CONTENT', $default_renderer->render($row_actions_dropdown)); } - if ($row instanceof Component\Table\OrderingRow) { + if ($row instanceof OrderingRow) { if (!$row->isOrderingDisabled()) { $drag_handle = $this->getUIFactory()->symbol()->glyph()->dragHandle(); $drag_handle = $default_renderer->render($drag_handle); @@ -644,9 +641,9 @@ protected function fillCells( } /** - * @param array $actions + * @param array $actions */ - protected function getSingleActionsForRow(string $row_id, array $actions): \ILIAS\UI\Component\Dropdown\Standard + protected function getSingleActionsForRow(string $row_id, array $actions): I\Dropdown\Dropdown { $f = $this->getUIFactory(); $buttons = []; @@ -661,8 +658,7 @@ protected function getSingleActionsForRow(string $row_id, array $actions): \ILIA return $f->dropdown()->standard($buttons); } - - public function renderOrderingTable(Component\Table\Ordering $component, RendererInterface $default_renderer): string + public function renderOrderingTable(Ordering $component, RendererInterface $default_renderer): string { $tpl = $this->getTemplate("tpl.orderingtable.html", true, true); $component = $this->registerActions($component); @@ -674,7 +670,6 @@ public function renderOrderingTable(Component\Table\Ordering $component, Rendere array_keys($component->getVisibleColumns()), ); - if (!$component->isOrderingDisabled()) { $component = $component->withAdditionalOnLoadCode( static fn($id): string => "il.UI.table.ordering.init('{$id}');" @@ -706,12 +701,12 @@ public function renderOrderingTable(Component\Table\Ordering $component, Rendere if (!$component->isOrderingDisabled()) { $submit = $this->getUIFactory()->button()->standard($this->txt('sorting_save'), "") - ->withOnLoadCode(static fn($id) => "document.getElementById('$id').addEventListener('click', - function() {document.querySelector('#$tableid form.c-table-ordering__form').submit();return false;});"); + ->withOnLoadCode(static fn($id) => "document.getElementById('$id').addEventListener('click', + function() {document.querySelector('#$tableid form.c-table-ordering__form').submit();return false;});" + ); $tpl->setVariable('FORM_BUTTONS', $default_renderer->render($submit)); $tpl->setVariable('POS_INPUT_TITLE', $this->txt('table_posinput_col_title')); - } $tpl->setVariable('ID', $tableid); @@ -773,17 +768,17 @@ public function registerResources(ResourceRegistry $registry): void $registry->register('assets/js/modal.min.js'); } - protected function registerSignals(Component\Table\PresentationRow $component): Component\JavaScriptBindable + protected function registerSignals(PresentationRow $component): C\JavaScriptBindable { $show = $component->getShowSignal(); $close = $component->getCloseSignal(); $toggle = $component->getToggleSignal(); $table_id = $component->getTableId(); return $component->withAdditionalOnLoadCode( - static fn($id): string => - "$(document).on('$show', function() { il.UI.table.presentation.get('$table_id').expandRow('$id'); return false; });" . - "$(document).on('$close', function() { il.UI.table.presentation.get('$table_id').collapseRow('$id'); return false; });" . - "$(document).on('$toggle', function() { il.UI.table.presentation.get('$table_id').toggleRow('$id'); return false; });" + static fn($id + ): string => "$(document).on('$show', function() { il.UI.table.presentation.get('$table_id').expandRow('$id'); return false; });" . + "$(document).on('$close', function() { il.UI.table.presentation.get('$table_id').collapseRow('$id'); return false; });" . + "$(document).on('$toggle', function() { il.UI.table.presentation.get('$table_id').toggleRow('$id'); return false; });" ); } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/RowBuilder.php b/components/ILIAS/UI/src/Implementation/Component/Table/RowBuilder.php index 234bfca1b390..87d4015fc21c 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Table/RowBuilder.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/RowBuilder.php @@ -20,26 +20,23 @@ namespace ILIAS\UI\Implementation\Component\Table; -use ILIAS\UI\Component\Table as T; -use ILIAS\UI\Component\Table\Column\Column; -use ILIAS\UI\Component\Table\Action\Action; +use ILIAS\UI\Implementation\Component as I; +use ILIAS\UI\Component as C; abstract class RowBuilder { protected bool $table_has_multiactions = false; /** - * @var array + * @var array */ protected array $row_actions = []; /** - * @var array + * @var array */ protected array $columns = []; - //abstract function buildRow(string $id, array $record); - public function withMultiActionsPresent(bool $flag): self { $clone = clone $this; @@ -48,7 +45,7 @@ public function withMultiActionsPresent(bool $flag): self } /** - * @param array $row_actions + * @param array $row_actions */ public function withSingleActions(array $row_actions): self { @@ -57,7 +54,7 @@ public function withSingleActions(array $row_actions): self return $clone; } /** - * @param array $columns + * @param array $columns */ public function withVisibleColumns(array $columns): self { diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/TableViewControlFieldSelection.php b/components/ILIAS/UI/src/Implementation/Component/Table/TableViewControlFieldSelection.php index fb782d46fe23..b0f1521968ba 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Table/TableViewControlFieldSelection.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/TableViewControlFieldSelection.php @@ -20,8 +20,8 @@ namespace ILIAS\UI\Implementation\Component\Table; -use ILIAS\UI\Component\Input\ViewControl\FieldSelection; -use ILIAS\UI\Component\Table\Column\Column; +use ILIAS\UI\Implementation\Component as I; +use ILIAS\UI\Component as C; trait TableViewControlFieldSelection { @@ -31,14 +31,14 @@ trait TableViewControlFieldSelection protected ?array $selected_optional_column_ids = null; /** - * @param array $columns + * @param array $columns */ protected function initViewControlFieldSelection(array $columns): void { $this->selected_optional_column_ids = $this->filterVisibleColumnIds($columns); } - protected function getViewControlFieldSelection(): ?FieldSelection + protected function getViewControlFieldSelection(): ?I\Input\ViewControl\FieldSelection { $optional_cols = $this->getOptionalColumns(); if ($optional_cols === []) { @@ -54,7 +54,7 @@ protected function getViewControlFieldSelection(): ?FieldSelection } /** - * @param array $columns + * @param array $columns * @return array */ protected function filterVisibleColumnIds(array $columns): array @@ -89,7 +89,7 @@ public function getSelectedOptionalColumns(): array } /** - * @return array + * @return array */ protected function getOptionalColumns(): array { @@ -100,7 +100,7 @@ protected function getOptionalColumns(): array } /** - * @return array + * @return array */ protected function getInitiallyVisibleColumns(): array { @@ -111,14 +111,14 @@ protected function getInitiallyVisibleColumns(): array } /** - * @return array + * @return array */ public function getVisibleColumns(): array { $visible_optional_columns = $this->getSelectedOptionalColumns(); return array_filter( $this->getColumns(), - fn(Column $col, string $col_id): bool => !$col->isOptional() || in_array($col_id, $visible_optional_columns, true), + fn(I\Table\Column\Column $col, string $col_id): bool => !$col->isOptional() || in_array($col_id, $visible_optional_columns, true), ARRAY_FILTER_USE_BOTH ); } diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/TableViewControlOrdering.php b/components/ILIAS/UI/src/Implementation/Component/Table/TableViewControlOrdering.php index 52df155e3d67..6a0536c7fd20 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Table/TableViewControlOrdering.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/TableViewControlOrdering.php @@ -47,7 +47,7 @@ private function initialOrder(): string return array_key_first($sortable_visible_cols); } - protected function getViewControlOrdering(int|null $total_count): Sortation|ViewControl\Group + protected function getViewControlOrdering(int|null $total_count): ViewControl\Sortation|ViewControl\Group { $sortable_visible_cols = array_filter( $this->getVisibleColumns(), diff --git a/components/ILIAS/UI/tests/Component/Table/DataTest.php b/components/ILIAS/UI/tests/Component/Table/DataTest.php index 631f90185d3b..75bce58d9e54 100755 --- a/components/ILIAS/UI/tests/Component/Table/DataTest.php +++ b/components/ILIAS/UI/tests/Component/Table/DataTest.php @@ -239,7 +239,7 @@ public function testDataTableWithIdAndStorage(): void $table = new class ($storage) extends C\Table\Data { public function __construct( - protected \ArrayAccess $storage, + protected \ILIAS\UI\Storage $storage, ) { } public function mockGetStorageData(): ?array diff --git a/components/ILIAS/UI/tests/Component/Table/TableTestBase.php b/components/ILIAS/UI/tests/Component/Table/TableTestBase.php index 4371fd652ff1..748b98a2e4ae 100755 --- a/components/ILIAS/UI/tests/Component/Table/TableTestBase.php +++ b/components/ILIAS/UI/tests/Component/Table/TableTestBase.php @@ -87,9 +87,9 @@ protected function getTableFactory(): C\Table\Factory ); } - protected function getMockStorage(): ArrayAccess + protected function getMockStorage(): \ILIAS\UI\Storage { - return new class () implements ArrayAccess { + return new class () implements \ILIAS\UI\Storage { protected array $data = []; public function offsetExists(mixed $offset): bool { From 5388c07cc6a4f6f9a24346998571c7da358e0880 Mon Sep 17 00:00:00 2001 From: Thibeau Fuhrer Date: Fri, 6 Nov 2026 13:03:01 +0100 Subject: [PATCH 2/4] [DOCUMENTATION] UI: table refactoring roadmap entry --- components/ILIAS/UI/docs/ROADMAP.md | 20 +++++++++++++++++++ .../default/Table/tpl.datatable.html | 3 +++ .../default/Table/tpl.orderingtable.html | 9 ++++++--- .../Client/Table/Data/data.table.test.js | 3 ++- 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/components/ILIAS/UI/docs/ROADMAP.md b/components/ILIAS/UI/docs/ROADMAP.md index e0f1486372a8..2205c425ecee 100755 --- a/components/ILIAS/UI/docs/ROADMAP.md +++ b/components/ILIAS/UI/docs/ROADMAP.md @@ -406,6 +406,26 @@ contain an object that is exported from a `.js` file, which can be imported dire Alternatively, we build this as a string and store this in an `.php` artifact and ship its content via our component renderers, so it is available as global state (e.g. `il.UI.Config`). +### Refactoring `UI\Component\Table\Data` and `Ordering` components (advanced, ~8d) + +The Data and Ordering Table have accumulated some structural debt on both server and client, which makes it increasingly +hard to maintain and extend it. We should therefore refactor these components internally and focus on the following +aspects: + +- Remove the unnecessary `Table\AbstractTable` and `Table\Table` inheritance level. +- Refine the public interface of `Table\Action` and `Table\Column` so consumers can contribute custom implementations in +theory. Today they expose too little information for the UI framework to process them and since they are passed to +Tables by composition, we should also support less concrete implementations than our internals. A symptom of this is the +check that given Actions and Columns are an instanfe of our implementation. +- Simplify the rendering by reusing more UI components internally, e.g. for warning dialog and replace implementation of +`Input\NameSource` with proper context rendering. +- Stop carrying around Action information between rendering steps via the composable DTO. This can most likely be +avoided by delegating more of this work to the client. +- Modularise the current client-side facility which has grown into a god object. Create smaller more managable modules +e.g. for selection, action handling, async rendering, navigation. +- Modernise the client-side architecture to fit modern patterns and remove the use of globals. Remove dynamic DOM +lookups (which potentially happen many times) and follow correctness on construction. + ## Long Term ### Mark Some Components as Internal diff --git a/components/ILIAS/UI/src/templates/default/Table/tpl.datatable.html b/components/ILIAS/UI/src/templates/default/Table/tpl.datatable.html index b3cbd734bfa4..ce6a84fbe873 100755 --- a/components/ILIAS/UI/src/templates/default/Table/tpl.datatable.html +++ b/components/ILIAS/UI/src/templates/default/Table/tpl.datatable.html @@ -83,4 +83,7 @@

{TITLE}

+ + {DIALOG} + diff --git a/components/ILIAS/UI/src/templates/default/Table/tpl.orderingtable.html b/components/ILIAS/UI/src/templates/default/Table/tpl.orderingtable.html index f79fdfcd63ba..58c21170f2f2 100644 --- a/components/ILIAS/UI/src/templates/default/Table/tpl.orderingtable.html +++ b/components/ILIAS/UI/src/templates/default/Table/tpl.orderingtable.html @@ -76,16 +76,16 @@

{TITLE}

- + + + + {DIALOG} + diff --git a/components/ILIAS/UI/tests/Client/Table/Data/data.table.test.js b/components/ILIAS/UI/tests/Client/Table/Data/data.table.test.js index 0d608f2e591f..98d6f1e944b9 100755 --- a/components/ILIAS/UI/tests/Client/Table/Data/data.table.test.js +++ b/components/ILIAS/UI/tests/Client/Table/Data/data.table.test.js @@ -93,9 +93,10 @@ describe('Data Table', () => { const dt = f.get('tid'); strict.equal(dt.registerAction instanceof Function, true); strict.equal(dt.selectAll instanceof Function, true); - strict.equal(dt.doSingleAction instanceof Function, true); strict.equal(dt.doMultiAction instanceof Function, true); strict.equal(dt.doActionForAll instanceof Function, true); strict.equal(dt.doAction instanceof Function, true); + strict.equal(dt.collectSelectedRowIds instanceof Function, true); + strict.equal(dt.selectAll instanceof Function, true); }); }); From 3179136029cf547fc640a0745eee1b9249a47b5b Mon Sep 17 00:00:00 2001 From: Nils Haagen Date: Thu, 24 Oct 2024 11:47:14 +0200 Subject: [PATCH 3/4] [FEATURE] UI: make Table Action support Prompt's Change `UI\Component\Table\Action\Factory` method signatures to also accept `UI\Component\Prompt\Prompt`. This changes the behaviour of Table Actions conceptually, since Prompts are asynchronous by default. This makes `UI\Component\Table\Action\Action::withAsync()` obsolete, which is why this method has been deprecated. To incorporate Prompt's into Table Action's they were modified to work with an `UI\URLBuilder` instead of an `ILIAS\Data\URI`. --- .../UI/resources/js/Table/dist/table.min.js | 2 +- .../resources/js/Table/src/datatable.class.js | 29 +++-- .../js/Table/src/datatable.factory.js | 10 +- .../ILIAS/UI/resources/js/Table/src/table.js | 11 +- .../ILIAS/UI/src/Component/Prompt/Factory.php | 7 +- .../ILIAS/UI/src/Component/Prompt/Prompt.php | 26 +++- .../UI/src/Component/Table/Action/Action.php | 9 +- .../UI/src/Component/Table/Action/Factory.php | 52 +++++--- .../Component/Prompt/Factory.php | 6 +- .../Component/Prompt/Prompt.php | 32 +++-- .../Component/Prompt/Renderer.php | 1 - .../Component/Table/Action/Action.php | 80 ++++++------ .../Component/Table/Action/Factory.php | 22 ++-- .../Component/Table/Renderer.php | 88 +++++++------ .../UI/src/examples/Prompt/Standard/base.php | 3 +- .../examples/Prompt/Standard/file_upload.php | 2 +- .../UI/src/examples/Prompt/Standard/form.php | 2 +- .../Standard/multi_entity_confirmation.php | 2 +- .../examples/Prompt/Standard/parameters.php | 11 +- .../Standard/single_entity_confirmation.php | 2 +- .../src/examples/Prompt/State/Close/base.php | 2 +- .../examples/Prompt/State/Redirect/base.php | 2 +- .../src/examples/Prompt/State/Show/base.php | 2 +- .../src/examples/Table/Action/Multi/base.php | 34 ++--- .../src/examples/Table/Action/Single/base.php | 34 ++--- .../examples/Table/Action/Standard/base.php | 46 ++++--- .../ILIAS/UI/src/examples/Table/Data/base.php | 54 ++++---- .../default/Table/tpl.datatable.html | 68 +++++----- .../default/Table/tpl.orderingtable.html | 106 ++++++++-------- .../UI/tests/Component/Prompt/PromptTest.php | 61 +++++++-- .../Component/Table/Action/ActionTest.php | 46 ++++--- .../Component/Table/DataRendererTest.php | 119 ++++++++++++++---- .../UI/tests/Component/Table/DataTest.php | 1 - 33 files changed, 574 insertions(+), 398 deletions(-) diff --git a/components/ILIAS/UI/resources/js/Table/dist/table.min.js b/components/ILIAS/UI/resources/js/Table/dist/table.min.js index d02c2c73e55e..df00de012610 100644 --- a/components/ILIAS/UI/resources/js/Table/dist/table.min.js +++ b/components/ILIAS/UI/resources/js/Table/dist/table.min.js @@ -12,4 +12,4 @@ * https://www.ilias.de * https://github.com/ILIAS-eLearning */ -!function(e,t){"use strict";class s{#e;#t;#s;#i;#o;#a;#n;#r;constructor(e,t,s,i){if(this.#s=document.getElementById(i),null===this.#s)throw new Error(`Could not find a DataTable for id '${i}'.`);if(this.#n=this.#s.getElementsByTagName("table").item(0),null===this.#n)throw new Error("There is no in the component's HTML.");this.#i=this.#s.getElementsByClassName("c-table-data__async_modal_container").item(0),this.#o=this.#s.getElementsByClassName("c-table-data__async_message").item(0),this.#a=this.#o.getElementsByClassName("c-table-data__async_messageresponse").item(0),this.#e=e,this.#t={actionId:t,rowId:s},this.#r={},this.#s.addEventListener("keydown",e=>this.navigateCellsWithArrowKeys(e));const o=this.#n.getElementsByClassName("c-table-data__row-selector");for(let e=0;ethis.selectionChange())}}registerAction(e,t,s,i){this.#r[e]={async:t,urlBuilder:s,urlTokens:i}}selectionChange(){this.#l(!this.#c())}#c(){const{urlBuilder:e}=this.#r[Object.keys(this.#r).at(0)],{urlTokens:t}=this.#r[Object.keys(this.#r).at(0)],s=t.values().next().value,i=this.collectSelectedRowIds();i.push(i[0]),e.writeParameter(s,i);try{e.getUrl().toString()}catch(e){return!1}return!0}#d(){const e=this.#s.querySelector("dialog.c-table-data__multiaction-warning");il.UI.modal.showModal(e,{},{id:e.id})}#l(e){this.#n.querySelectorAll(".c-table-data__row-selector").forEach(t=>{t.disabled=!0===e&&!t.checked})}selectAll(e){const t=this.#n.getElementsByClassName("c-table-data__row-selector"),s=this.#n.getElementsByClassName("c-table-data__selection_all").item(0),i=this.#n.getElementsByClassName("c-table-data__selection_none").item(0);for(let s=0;s{t.checked&&e.push(t.value)}),e}doMultiAction(e){this.doAction(e,this.collectSelectedRowIds())}doSingleAction(e){const t=e.options[this.#t.rowId];this.doAction(e,[t])}doActionForAll(e){const t=e.parentNode.parentNode.parentNode,s=t.getElementsByClassName("close").item(0),i=t.getElementsByClassName("modal-body")[0].getElementsByTagName("select")[0].value;if(i in this.#r){const e=this.#t.actionId,t={options:{}};t.options[e]=i,s.click(),this.doAction(t,["ALL_OBJECTS"])}}doAction(e,t){const s=e.options[this.#t.actionId],i=this.#r[s],o=i.urlTokens.values().next().value;i.urlBuilder.writeParameter(o,t);const a=decodeURI(i.urlBuilder.getUrl().toString());i.async?this.asyncAction(a):window.location.href=a}asyncAction(e){this.#e.ajax({url:e,dataType:"html"}).done(e=>{if("SCRIPT"===this.#e(e).first().prop("tagName"))this.#e.globalEval(this.#e(e).first().text());else{let t;this.#e(e).first().hasClass("c-modal")?(this.#i.innerHTML=e,t=this.#i.firstChild):(this.#a.innerHTML=e,t=this.#o);this.#e(`
${e}
`).find("[data-replace-marker='script']").each((e,t)=>this.#e.globalEval(t.innerHTML)),il.UI.modal.showModal(t,{},{id:t.id})}})}navigateCellsWithArrowKeys(e){if(37!==e.which&&38!==e.which&&39!==e.which&&40!==e.which)return;const t=e.target.closest("td, th"),s=t.closest("tr");let{cellIndex:i}=t,{rowIndex:o}=s;switch(e.which){case 37:i-=1;break;case 39:i+=1;break;case 38:o-=1;break;case 40:o+=1}o<0||i<0||o>=this.#n.rows.length||i>=s.cells.length||this.focusCell(t,o,i)}focusCell(e,t,s){const i=this.#n.rows[t].cells[s];i.focus(),e.setAttribute("tabindex",-1),i.setAttribute("tabindex",0)}}class i{#s;constructor(e){if(this.#s=document.getElementById(e),null===this.#s)throw new Error(`Could not find a PresentationTable for id '${e}'.`)}expandRow(e){const t=this.#s.querySelector(`#${e}`);t.classList.remove("collapsed"),t.classList.add("expanded")}collapseRow(e){const t=this.#s.querySelector(`#${e}`);t.classList.remove("expanded"),t.classList.add("collapsed")}toggleRow(e){this.#s.querySelector(`#${e}`).classList.contains("expanded")?this.collapseRow(e):this.expandRow(e)}expandAll(e){const t=this.#s.querySelectorAll(".il-table-presentation-row");e.options.expand?t.forEach(e=>this.expandRow(e.id)):t.forEach(e=>this.collapseRow(e.id))}}class o{#s;#n;#h;#g;#w;#m;constructor(e){if(this.#s=document.getElementById(e),null===this.#s)throw new Error(`Could not find a OrderingTable for id '${e}'.`);if(this.#n=this.#s.getElementsByTagName("table").item(0),null===this.#n)throw new Error("There is no
in the component's HTML.");this.#p(),this.#h.forEach(e=>this.#u(e)),this.#y()}#y(){this.#n.querySelectorAll('input[type="number"]').forEach(e=>{e.addEventListener("keydown",e=>{"Enter"===e.key&&e.preventDefault()})})}#p(){this.#h=Array.from(this.#n.rows),this.#h.shift(),this.#h.pop()}#u(e){e.addEventListener("dragstart",e=>this.dragstart(e)),e.addEventListener("dragover",e=>this.dragover(e)),e.addEventListener("dragend",e=>this.dragend(e)),e.addEventListener("touchstart",e=>this.touchstart(e)),e.addEventListener("touchmove",e=>this.touchmove(e)),e.addEventListener("touchend",e=>this.touchend(e)),e.addEventListener("touchcancel",e=>this.touchend(e))}dragstart(e){this.#s.classList.add("dragInProgress"),this.#g=e.target.closest("tr"),e.dataTransfer.clearData(),e.dataTransfer.setData("text/html",this.#g.outerHTML),e.dataTransfer.setData("text/plain",this.#g.textContent.replace(/[\n\r]+|[\s]{2,}/g," ").trim()),this.#w=this.#g.cloneNode(!0),this.#w.classList.add("c-table-data__row--drag-image"),this.#w.style.top="-9999px",this.#s.appendChild(this.#w),e.dataTransfer.setDragImage(this.#w,0,0),this.#m=this.#g.cloneNode(!0),this.#m.addEventListener("dragover",e=>this.dragover(e)),this.#m.classList.add("c-table-data__row--placeholder"),Array.from(this.#m.getElementsByTagName("td")).forEach(e=>{e.innerHTML=""}),this.#g.classList.add("c-table-data__row--drag-origin")}dragover(e){if(!this.#b())return;e.preventDefault(),e.dataTransfer.effectAllowed="copyMove";const t=e.target.closest("tr");t&&t!==this.#m&&(this.#h.indexOf(t)>this.#h.indexOf(this.#g)?t.after(this.#m):t.before(this.#m))}dragend(e){e.preventDefault(),this.#w&&this.#s.contains(this.#w)&&(this.#s.removeChild(this.#w),this.#w=null),this.#m.replaceWith(this.#g),this.#g.classList.remove("c-table-data__row--drag-origin"),this.#g.classList.add("c-table-data__row--drag-settle"),this.#g.addEventListener("animationend",()=>this.#R(),{once:!0}),this.#s.classList.remove("dragInProgress"),this.#p(),this.#f()}#R(){this.#g.classList.remove("c-table-data__row--drag-settle")}#b(){return this.#h.includes(this.#g)}touchstart(e){this.#g=e.target.closest("tr"),this.#m=this.#g.cloneNode(!0),this.#m.classList.add("c-table-data__row--placeholder"),Array.from(this.#m.getElementsByTagName("td")).forEach(e=>{e.innerHTML=""}),this.#w=this.#g.cloneNode(!0),this.#w.classList.add("c-table-data__row--touch-drag-image"),this.#s.appendChild(this.#w),this.#g.classList.add("c-table-data__row--drag-origin")}touchmove(e){e.preventDefault();const t=e.touches[0];this.#w.style.left=`${t.clientX+-50}px`,this.#w.style.top=`${t.clientY}px`;const s=document.elementFromPoint(t.clientX,t.clientY)?.closest("tr");s&&this.#h.includes(s)&&(this.#h.indexOf(s)>this.#h.indexOf(this.#g)?s.after(this.#m):s.before(this.#m)),t.clientY<100?this.#v(-8):t.clientY>window.innerHeight-100?this.#v(8):this.#E()}touchend(){this.#s.removeChild(this.#w),this.#m.replaceWith(this.#g),this.#g.classList.remove("c-table-data__row--drag-origin"),this.#p(),this.#f()}#f(){let e=10;this.#n.querySelectorAll('input[type="number"]').forEach(t=>{t.value=e,e+=10})}#v(e){this.scrollInterval||(this.scrollInterval=setInterval(()=>{window.scrollBy(0,e)},16))}#E(){this.scrollInterval&&(clearInterval(this.scrollInterval),this.scrollInterval=null)}}e.UI=e.UI||{},e.UI.table=e.UI.table||{},e.UI.table.data=new class{#e;#I=[];constructor(e){this.#e=e}init(e,t,i){if(void 0!==this.#I[e])throw new Error(`DataTable with id '${e}' has already been initialized.`);this.#I[e]=new s(this.#e,t,i,e)}get(e){return this.#I[e]??null}}(t),e.UI.table.presentation=new class{#I=[];init(e){if(void 0!==this.#I[e])throw new Error(`PresentationTable with input-id '${e}' has already been initialized.`);this.#I[e]=new i(e)}get(e){return this.#I[e]??null}},e.UI.table.ordering=new class{#I=[];init(e){if(void 0!==this.#I[e])throw new Error(`OrderingTable with id '${e}' has already been initialized.`);this.#I[e]=new o(e)}get(e){return this.#I[e]??null}}}(il,$); +!function(e,t){"use strict";class s{#e;#t;#s;#i;#a;#o;#n;#r;#l;constructor(e,t,s,i,a){if(this.#i=document.getElementById(a),null===this.#i)throw new Error(`Could not find a DataTable for id '${a}'.`);if(this.#r=this.#i.getElementsByTagName("table").item(0),null===this.#r)throw new Error("There is no
in the component's HTML.");this.#a=this.#i.getElementsByClassName("c-table-data__async_modal_container").item(0),this.#o=this.#i.getElementsByClassName("c-table-data__async_message").item(0),this.#n=this.#o.getElementsByClassName("c-table-data__async_messageresponse").item(0),this.#e=e,this.#t=t,this.#s={actionId:s,rowId:i},this.#l={},this.#i.addEventListener("keydown",e=>this.navigateCellsWithArrowKeys(e));const o=this.#r.getElementsByClassName("c-table-data__row-selector");for(let e=0;ethis.selectionChange())}}registerAction(e,t,s,i,a){this.#l[e]={async:t,signalId:s,urlBuilder:i,urlTokens:a}}selectionChange(){this.#c(!this.#h())}#h(){const{urlBuilder:e}=this.#l[Object.keys(this.#l).at(0)],{urlTokens:t}=this.#l[Object.keys(this.#l).at(0)],s=t.values().next().value,i=this.collectSelectedRowIds();i.push(i[0]),e.writeParameter(s,i);try{e.getUrl().toString()}catch(e){return!1}return!0}#d(){const e=this.#i.querySelector("dialog.c-table-data__multiaction-warning");il.UI.modal.showModal(e,{},{id:e.id})}#c(e){this.#r.querySelectorAll(".c-table-data__row-selector")?.forEach(t=>{t.disabled=!0===e&&!t.checked})}selectAll(e){const t=this.#r.getElementsByClassName("c-table-data__row-selector"),s=this.#r.getElementsByClassName("c-table-data__selection_all").item(0),i=this.#r.getElementsByClassName("c-table-data__selection_none").item(0);for(let s=0;s{t.checked&&e.push(t.value)}),e}doMultiAction(e){this.doAction(e,this.collectSelectedRowIds())}doSingleAction(e){const t=e.options[this.#s.rowId];this.doAction(e,[t])}doActionForAll(e){const t=e.parentNode.parentNode.parentNode,s=t.getElementsByClassName("close").item(0),i=t.getElementsByClassName("modal-body")[0].getElementsByTagName("select")[0].value;if(i in this.#l){const e=this.#s.actionId,t={options:{}};t.options[e]=i,s.click(),this.doAction(t,["ALL_OBJECTS"])}}doAction(e,t){const s=e.options[this.#s.actionId],i=this.#l[s],a=i.urlTokens.values().next().value;i.urlBuilder.writeParameter(a,t);const o=decodeURI(i.urlBuilder.getUrl().toString());""===i.signalId?window.location.href=o:i.async?this.asyncAction(o):(e.options.url=o,this.#t.dispatch(this.#i,i.signalId,e))}asyncAction(e){this.#e.ajax({url:e,dataType:"html"}).done(e=>{if("SCRIPT"===this.#e(e).first().prop("tagName"))this.#e.globalEval(this.#e(e).first().text());else{let t;this.#e(e).first().hasClass("c-modal")?(this.#a.innerHTML=e,t=this.#a.firstChild):(this.#n.innerHTML=e,t=this.#o);this.#e(`
${e}
`).find("[data-replace-marker='script']").each((e,t)=>this.#e.globalEval(t.innerHTML)),il.UI.modal.showModal(t,{},{id:t.id})}})}navigateCellsWithArrowKeys(e){if(37!==e.which&&38!==e.which&&39!==e.which&&40!==e.which)return;const t=e.target.closest("td, th"),s=t.closest("tr");let{cellIndex:i}=t,{rowIndex:a}=s;switch(e.which){case 37:i-=1;break;case 39:i+=1;break;case 38:a-=1;break;case 40:a+=1}a<0||i<0||a>=this.#r.rows.length||i>=s.cells.length||this.focusCell(t,a,i)}focusCell(e,t,s){const i=this.#r.rows[t].cells[s];i.focus(),e.setAttribute("tabindex",-1),i.setAttribute("tabindex",0)}}class i{#i;constructor(e){if(this.#i=document.getElementById(e),null===this.#i)throw new Error(`Could not find a PresentationTable for id '${e}'.`)}expandRow(e){const t=this.#i.querySelector(`#${e}`);t.classList.remove("collapsed"),t.classList.add("expanded")}collapseRow(e){const t=this.#i.querySelector(`#${e}`);t.classList.remove("expanded"),t.classList.add("collapsed")}toggleRow(e){this.#i.querySelector(`#${e}`).classList.contains("expanded")?this.collapseRow(e):this.expandRow(e)}expandAll(e){const t=this.#i.querySelectorAll(".il-table-presentation-row");e.options.expand?t.forEach(e=>this.expandRow(e.id)):t.forEach(e=>this.collapseRow(e.id))}}class a{#i;#r;#g;#w;#m;#p;constructor(e){if(this.#i=document.getElementById(e),null===this.#i)throw new Error(`Could not find a OrderingTable for id '${e}'.`);if(this.#r=this.#i.getElementsByTagName("table").item(0),null===this.#r)throw new Error("There is no
in the component's HTML.");this.#u(),this.#g.forEach(e=>this.#y(e)),this.#b()}#b(){this.#r.querySelectorAll('input[type="number"]').forEach(e=>{e.addEventListener("keydown",e=>{"Enter"===e.key&&e.preventDefault()})})}#u(){this.#g=Array.from(this.#r.rows),this.#g.shift(),this.#g.pop()}#y(e){e.addEventListener("dragstart",e=>this.dragstart(e)),e.addEventListener("dragover",e=>this.dragover(e)),e.addEventListener("dragend",e=>this.dragend(e)),e.addEventListener("touchstart",e=>this.touchstart(e)),e.addEventListener("touchmove",e=>this.touchmove(e)),e.addEventListener("touchend",e=>this.touchend(e)),e.addEventListener("touchcancel",e=>this.touchend(e))}dragstart(e){this.#i.classList.add("dragInProgress"),this.#w=e.target.closest("tr"),e.dataTransfer.clearData(),e.dataTransfer.setData("text/html",this.#w.outerHTML),e.dataTransfer.setData("text/plain",this.#w.textContent.replace(/[\n\r]+|[\s]{2,}/g," ").trim()),this.#m=this.#w.cloneNode(!0),this.#m.classList.add("c-table-data__row--drag-image"),this.#m.style.top="-9999px",this.#i.appendChild(this.#m),e.dataTransfer.setDragImage(this.#m,0,0),this.#p=this.#w.cloneNode(!0),this.#p.addEventListener("dragover",e=>this.dragover(e)),this.#p.classList.add("c-table-data__row--placeholder"),Array.from(this.#p.getElementsByTagName("td")).forEach(e=>{e.innerHTML=""}),this.#w.classList.add("c-table-data__row--drag-origin")}dragover(e){if(!this.#R())return;e.preventDefault(),e.dataTransfer.effectAllowed="copyMove";const t=e.target.closest("tr");t&&t!==this.#p&&(this.#g.indexOf(t)>this.#g.indexOf(this.#w)?t.after(this.#p):t.before(this.#p))}dragend(e){e.preventDefault(),this.#m&&this.#i.contains(this.#m)&&(this.#i.removeChild(this.#m),this.#m=null),this.#p.replaceWith(this.#w),this.#w.classList.remove("c-table-data__row--drag-origin"),this.#w.classList.add("c-table-data__row--drag-settle"),this.#w.addEventListener("animationend",()=>this.#f(),{once:!0}),this.#i.classList.remove("dragInProgress"),this.#u(),this.#v()}#f(){this.#w.classList.remove("c-table-data__row--drag-settle")}#R(){return this.#g.includes(this.#w)}touchstart(e){this.#w=e.target.closest("tr"),this.#p=this.#w.cloneNode(!0),this.#p.classList.add("c-table-data__row--placeholder"),Array.from(this.#p.getElementsByTagName("td")).forEach(e=>{e.innerHTML=""}),this.#m=this.#w.cloneNode(!0),this.#m.classList.add("c-table-data__row--touch-drag-image"),this.#i.appendChild(this.#m),this.#w.classList.add("c-table-data__row--drag-origin")}touchmove(e){e.preventDefault();const t=e.touches[0];this.#m.style.left=`${t.clientX+-50}px`,this.#m.style.top=`${t.clientY}px`;const s=document.elementFromPoint(t.clientX,t.clientY)?.closest("tr");s&&this.#g.includes(s)&&(this.#g.indexOf(s)>this.#g.indexOf(this.#w)?s.after(this.#p):s.before(this.#p)),t.clientY<100?this.#I(-8):t.clientY>window.innerHeight-100?this.#I(8):this.#E()}touchend(){this.#i.removeChild(this.#m),this.#p.replaceWith(this.#w),this.#w.classList.remove("c-table-data__row--drag-origin"),this.#u(),this.#v()}#v(){let e=10;this.#r.querySelectorAll('input[type="number"]').forEach(t=>{t.value=e,e+=10})}#I(e){this.scrollInterval||(this.scrollInterval=setInterval(()=>{window.scrollBy(0,e)},16))}#E(){this.scrollInterval&&(clearInterval(this.scrollInterval),this.scrollInterval=null)}}const o=new class{#e;constructor(e){this.#e=e}dispatch(e,t,s){this.#e(e).trigger(t,s)}register(e,t,s){this.#e(e).on(t,s)}}(t);e.UI=e.UI||{},e.UI.table=e.UI.table||{},e.UI.table.data=new class{#e;#t;#_=[];constructor(e,t){this.#e=e,this.#t=t}init(e,t,i){if(void 0!==this.#_[e])throw new Error(`DataTable with id '${e}' has already been initialized.`);this.#_[e]=new s(this.#e,this.#t,t,i,e)}get(e){return this.#_[e]??null}}(t,o),e.UI.table.presentation=new class{#_=[];init(e){if(void 0!==this.#_[e])throw new Error(`PresentationTable with input-id '${e}' has already been initialized.`);this.#_[e]=new i(e)}get(e){return this.#_[e]??null}},e.UI.table.ordering=new class{#_=[];init(e){if(void 0!==this.#_[e])throw new Error(`OrderingTable with id '${e}' has already been initialized.`);this.#_[e]=new a(e)}get(e){return this.#_[e]??null}}}(il,$); diff --git a/components/ILIAS/UI/resources/js/Table/src/datatable.class.js b/components/ILIAS/UI/resources/js/Table/src/datatable.class.js index 7b68fb9015dd..4685680c907d 100755 --- a/components/ILIAS/UI/resources/js/Table/src/datatable.class.js +++ b/components/ILIAS/UI/resources/js/Table/src/datatable.class.js @@ -40,7 +40,12 @@ export default class DataTable { #jquery; /** - * @type {actionId: string, rowId: string} + * @type {JQueryEventDispatcher} + */ + #eventDispatcher; + + /** + * @type {actionId: string, rowId: string, async: string} */ #signalConstants; @@ -70,18 +75,19 @@ export default class DataTable { #table; /** - * @type {array} + * @type {array} */ #actionsRegistry; /** * @param {jQuery} jquery + * @param {JQueryEventDispatcher} eventDispatcher * @param {string} optActionId * @param {string} optRowId * @param {string} tableId * @throws {Error} if DOM element is missing */ - constructor(jquery, optActionId, optRowId, componentId) { + constructor(jquery, eventDispatcher, optActionId, optRowId, componentId) { this.#component = document.getElementById(componentId); if (this.#component === null) { throw new Error(`Could not find a DataTable for id '${componentId}'.`); @@ -95,6 +101,7 @@ export default class DataTable { this.#responseContent = this.#responseContainer.getElementsByClassName('c-table-data__async_messageresponse').item(0); this.#jquery = jquery; + this.#eventDispatcher = eventDispatcher; this.#signalConstants = { actionId: optActionId, rowId: optRowId, @@ -113,13 +120,15 @@ export default class DataTable { /** * @param {string} actionId * @param {bool} async + * @param {string} signalId * @param {URLBuilder} urlBuilder * @param {Map} urlTokens * @return {void} */ - registerAction(actionId, async, urlBuilder, urlTokens) { + registerAction(actionId, async, signalId, urlBuilder, urlTokens) { this.#actionsRegistry[actionId] = { async, + signalId, urlBuilder, urlTokens, }; @@ -164,7 +173,7 @@ export default class DataTable { * @return {void} */ #disableRowSelection(flag) { - this.#table.querySelectorAll('.c-table-data__row-selector').forEach( + this.#table.querySelectorAll('.c-table-data__row-selector')?.forEach( (chk) => { chk.disabled = (flag === true && !chk.checked); }, @@ -207,7 +216,7 @@ export default class DataTable { collectSelectedRowIds() { const ret = []; - this.#table.querySelectorAll('.c-table-data__row-selector').forEach( + this.#table.querySelectorAll('.c-table-data__row-selector')?.forEach( (chk) => { if (chk.checked) { ret.push(chk.value); @@ -249,7 +258,6 @@ export default class DataTable { const k = this.#signalConstants.actionId; const signalData = { options: {} }; signalData.options[k] = selectedAction; - modalClose.click(); this.doAction(signalData, ['ALL_OBJECTS']); } @@ -267,10 +275,13 @@ export default class DataTable { action.urlBuilder.writeParameter(token, rowIds); const target = decodeURI(action.urlBuilder.getUrl().toString()); - if (!action.async) { + if (action.signalId === '') { window.location.href = target; - } else { + } else if (action.async) { this.asyncAction(target); + } else { + signalData.options.url = target; + this.#eventDispatcher.dispatch(this.#component, action.signalId, signalData); } } diff --git a/components/ILIAS/UI/resources/js/Table/src/datatable.factory.js b/components/ILIAS/UI/resources/js/Table/src/datatable.factory.js index 09ae03437346..36169a3000e4 100755 --- a/components/ILIAS/UI/resources/js/Table/src/datatable.factory.js +++ b/components/ILIAS/UI/resources/js/Table/src/datatable.factory.js @@ -21,6 +21,11 @@ export default class DataTableFactory { */ #jquery; + /** + * @type {JQueryEventDispatcher} + */ + #eventDispatcher; + /** * @type {Array} */ @@ -28,9 +33,11 @@ export default class DataTableFactory { /** * @param {jQuery} jquery + * @param {JQueryEventDispatcher} eventDispatcher */ - constructor(jquery) { + constructor(jquery, eventDispatcher) { this.#jquery = jquery; + this.#eventDispatcher = eventDispatcher; } /** @@ -47,6 +54,7 @@ export default class DataTableFactory { this.#instances[tableId] = new DataTable( this.#jquery, + this.#eventDispatcher, optActionId, optRowId, tableId, diff --git a/components/ILIAS/UI/resources/js/Table/src/table.js b/components/ILIAS/UI/resources/js/Table/src/table.js index 0f7b92f49aa7..f95b054857d5 100755 --- a/components/ILIAS/UI/resources/js/Table/src/table.js +++ b/components/ILIAS/UI/resources/js/Table/src/table.js @@ -15,12 +15,15 @@ import il from 'il'; import $ from 'jquery'; -import DataTableFactory from './datatable.factory'; -import PresentationTableFactory from './presentationtable.factory'; -import OrderingTableFactory from './orderingtable.factory'; +import DataTableFactory from './datatable.factory.js'; +import PresentationTableFactory from './presentationtable.factory.js'; +import OrderingTableFactory from './orderingtable.factory.js'; +import JQueryEventDispatcher from '../../Core/src/jqueryeventdispatcher.js'; + +const eventDispatcher = new JQueryEventDispatcher($); il.UI = il.UI || {}; il.UI.table = il.UI.table || {}; -il.UI.table.data = new DataTableFactory($); +il.UI.table.data = new DataTableFactory($, eventDispatcher); il.UI.table.presentation = new PresentationTableFactory(); il.UI.table.ordering = new OrderingTableFactory(); diff --git a/components/ILIAS/UI/src/Component/Prompt/Factory.php b/components/ILIAS/UI/src/Component/Prompt/Factory.php index 036db44c7625..7d6dc9ce01c1 100644 --- a/components/ILIAS/UI/src/Component/Prompt/Factory.php +++ b/components/ILIAS/UI/src/Component/Prompt/Factory.php @@ -20,7 +20,7 @@ namespace ILIAS\UI\Component\Prompt; -use ILIAS\Data\URI; +use ILIAS\UI\URLBuilder; interface Factory { @@ -46,11 +46,12 @@ interface Factory * usage: * 1: > * The server MUST answer with an Prompt State Component - * to a request to the url provided to the Prompt. + * to a request to the url provided to the Prompt via the URLBuilder. * --- + * @param \ILIAS\UI\URLBuilder $async_url_builder * @return \ILIAS\UI\Component\Prompt\Prompt */ - public function standard(URI $async_url): Prompt; + public function standard(URLBuilder $async_url_builder): Prompt; /** * --- diff --git a/components/ILIAS/UI/src/Component/Prompt/Prompt.php b/components/ILIAS/UI/src/Component/Prompt/Prompt.php index 3acccee53a84..3cf2799aa920 100644 --- a/components/ILIAS/UI/src/Component/Prompt/Prompt.php +++ b/components/ILIAS/UI/src/Component/Prompt/Prompt.php @@ -24,6 +24,8 @@ use ILIAS\UI\Component\JavaScriptBindable; use ILIAS\UI\Component\Signal; use ILIAS\Data\URI; +use ILIAS\UI\URLBuilderToken; +use ILIAS\UI\URLBuilder; interface Prompt extends Component, JavaScriptBindable { @@ -33,12 +35,30 @@ interface Prompt extends Component, JavaScriptBindable public function withTitle(string $title): static; /** - * Get the signal to load and show this Prompt. + * Get a Prompt like this, but alter a parameter of the URLBuilder + * with another value. The parameter is identified using a token. + * + * This alters the Signal returned by {@see self::getShowSignal()}. */ - public function getShowSignal(?URI $uri = null): Signal; + public function withParameter(URLBuilderToken $token, string|array $value): static; /** - * Get the signal to close this Prompt. + * Get the URLBuilder used to load Prompt State's for this Prompt. + * + * This is on the public interface so other UI components can access + * the endpoint of the Prompt as well. + */ + public function getUrlBuilder(): URLBuilder; + + /** + * Get the Signal to show this Prompt and load its Prompt State. + * + * Alter this Signal using {@see self::withParameter()}. + */ + public function getShowSignal(): Signal; + + /** + * Get the Signal to close this Prompt. */ public function getCloseSignal(): Signal; } diff --git a/components/ILIAS/UI/src/Component/Table/Action/Action.php b/components/ILIAS/UI/src/Component/Table/Action/Action.php index 76bac41562d9..7c67eef0ac47 100755 --- a/components/ILIAS/UI/src/Component/Table/Action/Action.php +++ b/components/ILIAS/UI/src/Component/Table/Action/Action.php @@ -20,10 +20,13 @@ namespace ILIAS\UI\Component\Table\Action; -use ILIAS\UI\Component\Signal; +use ILIAS\UI\Component as C; use ILIAS\Data\URI; -interface Action extends \ILIAS\UI\Component\Component +interface Action extends C\Component { - public function withAsync(bool $async = true): self; + /** + * @deprecated construct the Action using a {@see C\Prompt\Prompt} instead. + */ + public function withAsync(bool $is_async = true): self; } diff --git a/components/ILIAS/UI/src/Component/Table/Action/Factory.php b/components/ILIAS/UI/src/Component/Table/Action/Factory.php index 4a85b530db8d..b8fe24b15783 100755 --- a/components/ILIAS/UI/src/Component/Table/Action/Factory.php +++ b/components/ILIAS/UI/src/Component/Table/Action/Factory.php @@ -20,6 +20,7 @@ namespace ILIAS\UI\Component\Table\Action; +use ILIAS\UI\Component as C; use ILIAS\UI\URLBuilder; use ILIAS\UI\URLBuilderToken; @@ -29,47 +30,68 @@ interface Factory * --- * description: * purpose: > - * The Standard Action applies to both a single and multiple records. - * A typical example would be "delete". - * + * The Standard Action applies to both a single and multiple records. + * A typical example would be "delete". + * effect: > + * When a Standard Action created with a Prompt is operated, an asynchronous + * request to the Prompt's endpoint is triggered, carrying the row id(s) + * inside the given token. When created with an URLBuilder, the request + * is performed synchronous instead. * --- + * @param string $label + * @param \ILIAS\UI\Component\Prompt\Prompt|URLBuilder $target + * @param URLBuilderToken $row_id_parameter * @return \ILIAS\UI\Component\Table\Action\Standard */ public function standard( string $label, - URLBuilder $url_builder, - URLBuilderToken $row_id_parameter + C\Prompt\Prompt|URLBuilder $target, + URLBuilderToken $row_id_parameter, ): Standard; /** * --- * description: * purpose: > - * The Single Action applies to a single record only. - * A typical example would be "edit". - * + * The Single Action applies to a single record only. + * A typical example would be "edit". + * effect: > + * When a Single Action created with a Prompt is operated, an asynchronous + * request to the Prompt's endpoint is triggered, carrying the row id(s) + * inside the given token. When created with an URLBuilder, the request + * is performed synchronous instead. * --- + * @param string $label + * @param \ILIAS\UI\Component\Prompt\Prompt|URLBuilder $target + * @param URLBuilderToken $row_id_parameter * @return \ILIAS\UI\Component\Table\Action\Single */ public function single( string $label, - URLBuilder $url_builder, - URLBuilderToken $row_id_parameter + C\Prompt\Prompt|URLBuilder $target, + URLBuilderToken $row_id_parameter, ): Single; /** * --- * description: * purpose: > - * The Multi Action can only be used with more than one record. - * A typical example would be "compare". - * + * The Multi Action can only be used with more than one record. + * A typical example would be "compare". + * effect: > + * When a Multi Action created with a Prompt is operated, an asynchronous + * request to the Prompt's endpoint is triggered, carrying the row id(s) + * inside the given token. When created with an URLBuilder, the request + * is performed synchronous instead. * --- + * @param string $label + * @param \ILIAS\UI\Component\Prompt\Prompt|URLBuilder $target + * @param URLBuilderToken $row_id_parameter * @return \ILIAS\UI\Component\Table\Action\Multi */ public function multi( string $label, - URLBuilder $url_builder, - URLBuilderToken $row_id_parameter + C\Prompt\Prompt|URLBuilder $target, + URLBuilderToken $row_id_parameter, ): Multi; } diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/Factory.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/Factory.php index cac13f4109f4..2b669fb79865 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/Factory.php @@ -20,9 +20,9 @@ namespace ILIAS\UI\Implementation\Component\Prompt; -use ILIAS\Data\URI; use ILIAS\UI\Component\Prompt as I; use ILIAS\UI\Implementation\Component\SignalGeneratorInterface; +use ILIAS\UI\URLBuilder; class Factory implements I\Factory { @@ -32,9 +32,9 @@ public function __construct( ) { } - public function standard(URI $async_url): Prompt + public function standard(URLBuilder $async_url_builder): Prompt { - return new Prompt($this->signal_generator, $async_url); + return new Prompt($this->signal_generator, $async_url_builder); } public function state(): State\Factory diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/Prompt.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/Prompt.php index 5f8e99ecf9de..5055b5834e1d 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/Prompt.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/Prompt.php @@ -26,9 +26,10 @@ use ILIAS\UI\Implementation\Component\ComponentHelper; use ILIAS\UI\Implementation\Component\JavaScriptBindable; use ILIAS\UI\Implementation\Component\SignalGenerator; +use ILIAS\UI\URLBuilderToken; +use ILIAS\UI\URLBuilder; use ILIAS\Data\URI; - class Prompt implements I\Prompt { use ComponentHelper; @@ -40,17 +41,12 @@ class Prompt implements I\Prompt public function __construct( SignalGeneratorInterface $signal_generator, - protected URI $async_url + protected URLBuilder $async_url_builder, ) { $this->show_signal = $signal_generator->create(); $this->close_signal = $signal_generator->create(); } - public function getAsyncUrl(): URI - { - return $this->async_url; - } - public function getTitle(): ?string { return $this->title; @@ -63,11 +59,29 @@ public function withTitle(string $title): static return $clone; } + public function withParameter(URLBuilderToken $token, string|array $value): static + { + $clone = clone $this; + $clone->async_url_builder = $clone->async_url_builder->withParameter($token, $value); + return $clone; + } + + public function getUrlBuilder(): URLBuilder + { + return $this->async_url_builder; + } + + /** + * @param URI|null $uri @deprecated, legacy usages should fail loudly since another endpoint + * would be used, which most likely results in weird behaviour. + */ public function getShowSignal(?URI $uri = null): Signal { - $target = $uri ?? $this->async_url; + if (null !== $uri) { + throw new \LogicException('Prompt target must be changed using withParameter() instead.'); + } $signal = clone $this->show_signal; - $signal->addOption('url', $target->__toString()); + $signal = $signal->withOptions(['url' => (string) $this->getUrlBuilder()->buildURI()]); return $signal; } diff --git a/components/ILIAS/UI/src/Implementation/Component/Prompt/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Prompt/Renderer.php index c9b7bba56fd1..ddfc55c3b643 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Prompt/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Prompt/Renderer.php @@ -60,7 +60,6 @@ protected function renderPrompt(Prompt $component, RendererInterface $default_re $show_signal = $component->getShowSignal(); $close_signal = $component->getCloseSignal(); - $url = $component->getAsyncUrl()->__toString(); $component = $component->withAdditionalOnLoadCode( fn($id) => "(function() { const prompt = il.UI.Prompt.createPrompt(document.getElementById('$id')); diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/Action/Action.php b/components/ILIAS/UI/src/Implementation/Component/Table/Action/Action.php index aca56b4b433f..06c88ba57d90 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Table/Action/Action.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/Action/Action.php @@ -21,32 +21,23 @@ namespace ILIAS\UI\Implementation\Component\Table\Action; use ILIAS\UI\Implementation\Component\ComponentHelper; -use ILIAS\UI\Component\Table\Action as I; -use ILIAS\UI\Component\Signal; +use ILIAS\UI\Component as C; use ILIAS\Data\URI; use ILIAS\UI\URLBuilder; use ILIAS\UI\URLBuilderToken; -abstract class Action implements I\Action +abstract class Action implements C\Table\Action\Action { use ComponentHelper; - /** - * JS needs to know about the type of an action - * and where to find the options (in case of signal) - * Theses constants are passed to il.UI.table.data.init - */ - public const OPT_ACTIONID = 'actId'; - public const OPT_ROWID = 'rowid'; - protected Signal|URI $target; - protected bool $async = false; + protected bool $is_legacy_async = false; + protected ?C\Signal $legacy_async_signal = null; public function __construct( protected string $label, - protected URLBuilder $url_builder, - protected URLBuilderToken $row_id_parameter + protected C\Prompt\Prompt|URLBuilder $target, + protected URLBuilderToken $row_id_parameter, ) { - $this->target = $url_builder->buildURI(); } public function getLabel(): string @@ -54,55 +45,62 @@ public function getLabel(): string return $this->label; } - public function getTarget(): Signal|URI + public function getTarget(): C\Prompt\Prompt|URLBuilder { return $this->target; } - public function withSignalTarget(Signal $target): self + /** + * @inheritDoc + */ + public function withAsync(bool $is_async = true): self { + if ($this->target instanceof C\Prompt\Prompt) { + throw new \LogicException('Action already migrated to Prompt and is async by default.'); + } $clone = clone $this; - $clone->target = $target; + $clone->is_legacy_async = $is_async; return $clone; } - public function withAsync(bool $async = true): self + /** + * @deprecated use {@see self::getTarget() to determine behaviour} + */ + public function isTargetLegacyAsync(): bool + { + return $this->is_legacy_async; + } + + /** + * @deprecated do not pass information between rendering steps via DTO! + */ + public function withLegacyAsyncSignal(C\Signal $signal): static { $clone = clone $this; - $clone->async = $async; + $clone->legacy_async_signal = $signal; return $clone; } - public function isAsync(): bool + /** + * @deprecated do not pass information between rendering steps via DTO! + */ + public function getLegacyAsyncSignal(): C\Signal { - return $this->async; + return $this->legacy_async_signal; } public function withRowId(string $row_id): self { $clone = clone $this; - $target = $clone->getTarget(); - - if ($target instanceof Signal) { - $target->addOption('rowid', $row_id); - } - if ($target instanceof URI) { - $target = $this->url_builder->withParameter( - $this->row_id_parameter, - [$row_id] - ) - ->buildURI(); - } - $clone->target = $target; + $clone->target = $clone->target->withParameter( + $this->getRowIdParameter(), + [$row_id] + ); return $clone; } - public function getURLBuilderJS(): string - { - return $this->url_builder->renderObject([$this->row_id_parameter]); - } - public function getURLBuilderTokensJS(): string + public function getRowIdParameter(): URLBuilderToken { - return $this->url_builder->renderTokens([$this->row_id_parameter]); + return $this->row_id_parameter; } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/Action/Factory.php b/components/ILIAS/UI/src/Implementation/Component/Table/Action/Factory.php index 122f22782b3c..c52771533a3e 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Table/Action/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/Action/Factory.php @@ -22,31 +22,31 @@ use ILIAS\UI\URLBuilder; use ILIAS\UI\URLBuilderToken; -use ILIAS\UI\Component\Table\Action as I; +use ILIAS\UI\Component as C; -class Factory implements I\Factory +class Factory implements C\Table\Action\Factory { public function standard( string $label, - URLBuilder $url_builder, - URLBuilderToken $row_id_parameter + C\Prompt\Prompt|URLBuilder $target, + URLBuilderToken $row_id_parameter, ): Standard { - return new Standard($label, $url_builder, $row_id_parameter); + return new Standard($label, $target, $row_id_parameter); } public function single( string $label, - URLBuilder $url_builder, - URLBuilderToken $row_id_parameter + C\Prompt\Prompt|URLBuilder $target, + URLBuilderToken $row_id_parameter, ): Single { - return new Single($label, $url_builder, $row_id_parameter); + return new Single($label, $target, $row_id_parameter); } public function multi( string $label, - URLBuilder $url_builder, - URLBuilderToken $row_id_parameter + C\Prompt\Prompt|URLBuilder $target, + URLBuilderToken $row_id_parameter, ): Multi { - return new Multi($label, $url_builder, $row_id_parameter); + return new Multi($label, $target, $row_id_parameter); } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Table/Renderer.php index 11cd115b974c..fb916661db2c 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Table/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/Renderer.php @@ -203,7 +203,8 @@ protected function renderPresentationRow(PresentationRow $component, RendererInt public function renderDataTable(Data $component, RendererInterface $default_renderer): string { $tpl = $this->getTemplate("tpl.datatable.html", true, true); - $component = $this->registerActions($component); + + [$component, $prompts] = $this->registerActions($component); [$component, $view_controls] = $component->applyViewControls( $component->getFilter(), @@ -238,6 +239,7 @@ public function renderDataTable(Data $component, RendererInterface $default_rend $tpl->setVariable('TITLE', $component->getTitle()); $tpl->setVariable('COL_COUNT', (string) $component->getColumnCount() + $compensate_col_count); $tpl->setVariable('VIEW_CONTROLS', $default_renderer->render($view_controls)); + $tpl->setVariable('DIALOG', $default_renderer->render($prompts)); $sortation_signal = null; // if the generator is empty, and thus invalid, we render an empty row. @@ -389,31 +391,52 @@ protected function renderFullWidthDataCell(Data $component, Template $tpl, strin $tpl->parseCurrentBlock(); } - protected function registerActions(AbstractTable $component): AbstractTable - { - $opt_action_id = self::KEY_ACTION_ID; - $opt_row_id = self::KEY_ROW_ID; + /** + * @return array{0: AbstractTable, 1: C\Prompt\Prompt[]} + */ + protected function registerActions(AbstractTable $component): array + { if ($component->hasMultiActions()) { $component = $component->withAdditionalOnLoadCode( static fn($id): string => "il.UI.table.data.get('{$id}').selectAll(false);" ); } + $prompts = []; $actions = []; foreach ($component->getAllActions() as $action_id => $action) { - $component = $component->withAdditionalOnLoadCode( - $this->getActionRegistration((string) $action_id, $action) - ); - if ($action->isAsync()) { + $target = $action->getTarget(); + $signal = null; + if ($target instanceof C\Prompt\Prompt) { + $url_builder = $target->getUrlBuilder(); + $signal = $target->getShowSignal(); + $prompts[] = $target; + } else { + $url_builder = $target; + } + + if ($action->isTargetLegacyAsync()) { $signal = clone $component->getAsyncActionSignal(); - $signal->addOption(self::KEY_ACTION_ID, $action_id); - $action = $action->withSignalTarget($signal); + $signal = $signal->withOptions([self::KEY_ACTION_ID => $action_id]); + $action = $action->withLegacyAsyncSignal($signal); } + + $url_builder_js = $url_builder->renderObject([$action->getRowIdParameter()]); + $tokens_js = $url_builder->renderTokens([$action->getRowIdParameter()]); + $is_legacy_async_js = $action->isTargetLegacyAsync() ? 'true' : 'false'; + + $component = $component->withAdditionalOnLoadCode( + static fn($id): string => + "il.UI.table.data.get('{$id}').registerAction('{$action_id}', {$is_legacy_async_js}, '{$signal}', {$url_builder_js}, {$tokens_js});" + ); + $actions[$action_id] = $action; } $component = $component->withActions($actions); + $opt_action_id = self::KEY_ACTION_ID; + $opt_row_id = self::KEY_ROW_ID; $component = $component ->withAdditionalOnLoadCode( static fn($id): string => "il.UI.table.data.init('{$id}','{$opt_action_id}','{$opt_row_id}');" @@ -422,7 +445,7 @@ protected function registerActions(AbstractTable $component): AbstractTable ->withAdditionalOnLoadCode($this->getMultiActionHandler($component->getMultiActionSignal())) ->withAdditionalOnLoadCode($this->getSelectionHandler($component->getSelectionSignal())); - return $component; + return [$component, $prompts]; } protected function appendTableRows( @@ -472,7 +495,8 @@ protected function buildMultiActionsAllObjectsModal( )->withRequired(true); $submit = $f->button()->primary($this->txt('datatable_multiactionmodal_apply'), '') ->withOnLoadCode( - static fn($id + static fn( + $id ): string => "$('#{$id}').click(function() { il.UI.table.data.get('{$table_id}').doActionForAll(this); return false; });" ); $modal = $f->modal() @@ -550,19 +574,6 @@ protected function getSelectionHandler(C\Signal $selection_signal): \Closure }; } - protected function getActionRegistration(string $action_id, I\Table\Action\Action $action): \Closure - { - $async = $action->isAsync() ? 'true' : 'false'; - $url_builder_js = $action->getURLBuilderJS(); - $tokens_js = $action->getURLBuilderTokensJS(); - - return static function ($id) use ($action_id, $async, $url_builder_js, $tokens_js): string { - return " - il.UI.table.data.get('{$id}').registerAction('{$action_id}', {$async}, {$url_builder_js}, {$tokens_js}); - "; - }; - } - public function renderDataRow(DataRow $component, RendererInterface $default_renderer): string { $cell_tpl = $this->getTemplate("tpl.datacell.html", true, true); @@ -647,13 +658,17 @@ protected function getSingleActionsForRow(string $row_id, array $actions): I\Dro { $f = $this->getUIFactory(); $buttons = []; - foreach ($actions as $act) { - $act = $act->withRowId($row_id); - $target = $act->getTarget(); - if ($target instanceof URI) { - $target = (string) $target; + foreach ($actions as $action) { + $action = $action->withRowId($row_id); + $target = $action->getTarget(); + if ($target instanceof C\Prompt\Prompt) { + $btn_action = $target->getShowSignal(); + } elseif ($action->isTargetLegacyAsync()) { + $btn_action = $action->getLegacyAsyncSignal(); + } else { + $btn_action = (string) $target->buildURI(); } - $buttons[] = $f->button()->shy($act->getLabel(), $target); + $buttons[] = $f->button()->shy($action->getLabel(), $btn_action); } return $f->dropdown()->standard($buttons); } @@ -661,8 +676,8 @@ protected function getSingleActionsForRow(string $row_id, array $actions): I\Dro public function renderOrderingTable(Ordering $component, RendererInterface $default_renderer): string { $tpl = $this->getTemplate("tpl.orderingtable.html", true, true); - $component = $this->registerActions($component); + [$component, $prompts] = $this->registerActions($component); [$component, $view_controls] = $component->applyViewControls(); $rows = $component->getDataBinding()->getRows( @@ -701,7 +716,8 @@ public function renderOrderingTable(Ordering $component, RendererInterface $defa if (!$component->isOrderingDisabled()) { $submit = $this->getUIFactory()->button()->standard($this->txt('sorting_save'), "") - ->withOnLoadCode(static fn($id) => "document.getElementById('$id').addEventListener('click', + ->withOnLoadCode( + static fn($id) => "document.getElementById('$id').addEventListener('click', function() {document.querySelector('#$tableid form.c-table-ordering__form').submit();return false;});" ); @@ -714,6 +730,7 @@ function() {document.querySelector('#$tableid form.c-table-ordering__form').subm $tpl->setVariable('TITLE', $component->getTitle()); $tpl->setVariable('COL_COUNT', (string) $component->getColumnCount() + $compensate_col_count); $tpl->setVariable('VIEW_CONTROLS', $default_renderer->render($view_controls)); + $tpl->setVariable('DIALOG', $default_renderer->render($prompts)); $columns = $component->getVisibleColumns(); foreach ($columns as $col_id => $col) { @@ -775,7 +792,8 @@ protected function registerSignals(PresentationRow $component): C\JavaScriptBind $toggle = $component->getToggleSignal(); $table_id = $component->getTableId(); return $component->withAdditionalOnLoadCode( - static fn($id + static fn( + $id ): string => "$(document).on('$show', function() { il.UI.table.presentation.get('$table_id').expandRow('$id'); return false; });" . "$(document).on('$close', function() { il.UI.table.presentation.get('$table_id').collapseRow('$id'); return false; });" . "$(document).on('$toggle', function() { il.UI.table.presentation.get('$table_id').toggleRow('$id'); return false; });" diff --git a/components/ILIAS/UI/src/examples/Prompt/Standard/base.php b/components/ILIAS/UI/src/examples/Prompt/Standard/base.php index 48533b5b2439..6cd53c6e5371 100644 --- a/components/ILIAS/UI/src/examples/Prompt/Standard/base.php +++ b/components/ILIAS/UI/src/examples/Prompt/Standard/base.php @@ -61,8 +61,7 @@ function base() //build the prompt $query_namespace = ['prompt', 'example0']; list($url_builder, $token) = $url_builder->acquireParameters($query_namespace, "show"); - $url_builder = $url_builder->withParameter($token, "true"); - $prompt = $factory->prompt()->standard($url_builder->buildURI()); + $prompt = $factory->prompt()->standard($url_builder)->withParameter($token, "true"); //build the endpoint returning the wrapped message $query = $DIC->http()->wrapper()->query(); diff --git a/components/ILIAS/UI/src/examples/Prompt/Standard/file_upload.php b/components/ILIAS/UI/src/examples/Prompt/Standard/file_upload.php index ea651e851c69..c378302a60df 100644 --- a/components/ILIAS/UI/src/examples/Prompt/Standard/file_upload.php +++ b/components/ILIAS/UI/src/examples/Prompt/Standard/file_upload.php @@ -104,7 +104,7 @@ function file_upload(): string } // create a Standard Prompt that requests the simulated GET endpoint - $prompt = $factory->prompt()->standard($form_get_url->buildURI())->withTitle('Upload file'); + $prompt = $factory->prompt()->standard($form_get_url)->withTitle('Upload file'); // create a Button that triggers the Standard Prompt $button = $factory->button()->primary('Upload file', $prompt->getShowSignal()); diff --git a/components/ILIAS/UI/src/examples/Prompt/Standard/form.php b/components/ILIAS/UI/src/examples/Prompt/Standard/form.php index 7ed682b3fc03..bf3424cb0827 100644 --- a/components/ILIAS/UI/src/examples/Prompt/Standard/form.php +++ b/components/ILIAS/UI/src/examples/Prompt/Standard/form.php @@ -57,7 +57,7 @@ function form() $query_namespace = ['prompt', 'example2']; list($url_builder, $action_token) = $url_builder->acquireParameters($query_namespace, "action"); $url_builder = $url_builder->withParameter($action_token, "form"); - $prompt = $factory->prompt()->standard($url_builder->buildURI()); + $prompt = $factory->prompt()->standard($url_builder); //fill the state according to (query-)parameters $query = $DIC->http()->wrapper()->query(); diff --git a/components/ILIAS/UI/src/examples/Prompt/Standard/multi_entity_confirmation.php b/components/ILIAS/UI/src/examples/Prompt/Standard/multi_entity_confirmation.php index 173df99faba4..5a84b59ff38e 100644 --- a/components/ILIAS/UI/src/examples/Prompt/Standard/multi_entity_confirmation.php +++ b/components/ILIAS/UI/src/examples/Prompt/Standard/multi_entity_confirmation.php @@ -151,7 +151,7 @@ function multi_entity_confirmation(): string } // create a Standard Prompt that requests the simulated GET endpoint - $prompt = $factory->prompt()->standard($confirmation_get_url->buildURI())->withTitle('Confirm deletion'); + $prompt = $factory->prompt()->standard($confirmation_get_url)->withTitle('Confirm deletion'); // create a Button that triggers the Standard Prompt $button = $factory->button()->primary('Delete entities', $prompt->getShowSignal()); diff --git a/components/ILIAS/UI/src/examples/Prompt/Standard/parameters.php b/components/ILIAS/UI/src/examples/Prompt/Standard/parameters.php index 8d802625d072..ce22f6c1c18c 100644 --- a/components/ILIAS/UI/src/examples/Prompt/Standard/parameters.php +++ b/components/ILIAS/UI/src/examples/Prompt/Standard/parameters.php @@ -57,8 +57,7 @@ function parameters() list($url_builder, $action_token, $amount_token) = $url_builder->acquireParameters($query_namespace, "action", "amount"); $url_builder = $url_builder->withParameter($action_token, "showprompt"); - $default_uri = $url_builder->withParameter($amount_token, "1")->buildURI(); - $prompt = $factory->prompt()->standard($default_uri); + $prompt = $factory->prompt()->standard($url_builder)->withParameter($amount_token, "1"); //build some endpoint $query = $DIC->http()->wrapper()->query(); @@ -117,11 +116,11 @@ function parameters() $show_button = $factory->button()->standard('Show Simple Prompt', $prompt->getShowSignal()); $close_button = $factory->button()->standard('close Simple Prompt', $prompt->getCloseSignal()); - $uri = $url_builder->withParameter($amount_token, "8")->buildURI(); - $show_button8 = $factory->button()->standard('Show Prompt with Parameter', $prompt->getShowSignal($uri)); + $prompt = $prompt->withParameter($amount_token, "8"); + $show_button8 = $factory->button()->standard('Show Prompt with Parameter', $prompt->getShowSignal()); - $uri = $url_builder->withParameter($amount_token, "78")->buildURI(); - $show_button78 = $factory->button()->standard('Show Prompt with a lot of Items', $prompt->getShowSignal($uri)); + $prompt = $prompt->withParameter($amount_token, "78"); + $show_button78 = $factory->button()->standard('Show Prompt with a lot of Items', $prompt->getShowSignal()); return $renderer->render([ $prompt, diff --git a/components/ILIAS/UI/src/examples/Prompt/Standard/single_entity_confirmation.php b/components/ILIAS/UI/src/examples/Prompt/Standard/single_entity_confirmation.php index 84b76e9eb7d9..b0d2da65b14d 100644 --- a/components/ILIAS/UI/src/examples/Prompt/Standard/single_entity_confirmation.php +++ b/components/ILIAS/UI/src/examples/Prompt/Standard/single_entity_confirmation.php @@ -165,7 +165,7 @@ function single_entity_confirmation(): string } // create a Standard Prompt that requests the simulated GET endpoint - $prompt = $factory->prompt()->standard($confirmation_get_url->buildURI())->withTitle('Confirm deletion'); + $prompt = $factory->prompt()->standard($confirmation_get_url)->withTitle('Confirm deletion'); // create a Button that triggers the Standard Prompt $button = $factory->button()->primary('Delete entity', $prompt->getShowSignal()); diff --git a/components/ILIAS/UI/src/examples/Prompt/State/Close/base.php b/components/ILIAS/UI/src/examples/Prompt/State/Close/base.php index 8e93b89f6008..48aadeb9539a 100644 --- a/components/ILIAS/UI/src/examples/Prompt/State/Close/base.php +++ b/components/ILIAS/UI/src/examples/Prompt/State/Close/base.php @@ -71,7 +71,7 @@ function base() } //render prompt and button - $prompt = $factory->prompt()->standard($url_builder->buildURI()); + $prompt = $factory->prompt()->standard($url_builder); return $renderer->render([ $factory->button()->standard('Show Prompt', $prompt->getShowSignal()), $prompt diff --git a/components/ILIAS/UI/src/examples/Prompt/State/Redirect/base.php b/components/ILIAS/UI/src/examples/Prompt/State/Redirect/base.php index 09b980fce2eb..b3bfa956eef1 100644 --- a/components/ILIAS/UI/src/examples/Prompt/State/Redirect/base.php +++ b/components/ILIAS/UI/src/examples/Prompt/State/Redirect/base.php @@ -72,7 +72,7 @@ function base() } //render prompt and button - $prompt = $factory->prompt()->standard($url_builder->buildURI()); + $prompt = $factory->prompt()->standard($url_builder); return $renderer->render([ $factory->button()->standard('Show Prompt', $prompt->getShowSignal()), $prompt diff --git a/components/ILIAS/UI/src/examples/Prompt/State/Show/base.php b/components/ILIAS/UI/src/examples/Prompt/State/Show/base.php index f307c9599d86..9e1415644bac 100644 --- a/components/ILIAS/UI/src/examples/Prompt/State/Show/base.php +++ b/components/ILIAS/UI/src/examples/Prompt/State/Show/base.php @@ -62,7 +62,7 @@ function base() } //build the prompt - $prompt = $factory->prompt()->standard($url_builder->buildURI()); + $prompt = $factory->prompt()->standard($url_builder); $show_button = $factory->button()->standard('Show Prompt', $prompt->getShowSignal()); diff --git a/components/ILIAS/UI/src/examples/Table/Action/Multi/base.php b/components/ILIAS/UI/src/examples/Table/Action/Multi/base.php index df1cce9b0c14..21e9cfafb098 100755 --- a/components/ILIAS/UI/src/examples/Table/Action/Multi/base.php +++ b/components/ILIAS/UI/src/examples/Table/Action/Multi/base.php @@ -60,7 +60,7 @@ function base() 'do something else', $url_builder->withParameter($action_token, "do_something_else"), $id_token - )->withAsync(), + ), ]; $table = getExampleTable($f) @@ -75,30 +75,14 @@ function base() if ($query->has($action_token->getName())) { $action = $query->retrieve($action_token->getName(), $refinery->to()->string()); $ids = $query->retrieve($id_token->getName(), $refinery->custom()->transformation(fn($v) => $v)); - - if ($action === 'do_something_else') { - $items = []; - foreach ($ids as $id) { - $items[] = $f->modal()->interruptiveItem()->keyValue($id, $id_token->getName(), $id); - } - echo($r->renderAsync([ - $f->modal()->interruptive( - 'do something else', - 'affected items', - '#' - )->withAffectedItems($items) - ])); - exit(); - } else { - $items = $f->listing()->characteristicValue()->text( - [ - 'table_action' => $action, - 'id' => print_r($ids, true), - ] - ); - $result[] = $f->divider()->horizontal(); - $result[] = $items; - } + $items = $f->listing()->characteristicValue()->text( + [ + 'table_action' => $action, + 'id' => print_r($ids, true), + ] + ); + $result[] = $f->divider()->horizontal(); + $result[] = $items; } return $r->render($result); diff --git a/components/ILIAS/UI/src/examples/Table/Action/Single/base.php b/components/ILIAS/UI/src/examples/Table/Action/Single/base.php index f1266471b847..888d589d5db7 100755 --- a/components/ILIAS/UI/src/examples/Table/Action/Single/base.php +++ b/components/ILIAS/UI/src/examples/Table/Action/Single/base.php @@ -60,7 +60,7 @@ function base() 'do something else', $url_builder->withParameter($action_token, "do_something_else"), $id_token - )->withAsync(), + ), ]; $table = getExampleTable($f) @@ -74,30 +74,14 @@ function base() if ($query->has($action_token->getName())) { $action = $query->retrieve($action_token->getName(), $refinery->to()->string()); $ids = $query->retrieve($id_token->getName(), $refinery->custom()->transformation(fn($v) => $v)); - - if ($action === 'do_something_else') { - $items = []; - foreach ($ids as $id) { - $items[] = $f->modal()->interruptiveItem()->keyValue($id, $id_token->getName(), $id); - } - echo($r->renderAsync([ - $f->modal()->interruptive( - 'do something else', - 'affected items', - '#' - )->withAffectedItems($items) - ])); - exit(); - } else { - $items = $f->listing()->characteristicValue()->text( - [ - 'table_action' => $action, - 'id' => print_r($ids, true), - ] - ); - $result[] = $f->divider()->horizontal(); - $result[] = $items; - } + $items = $f->listing()->characteristicValue()->text( + [ + 'table_action' => $action, + 'id' => print_r($ids, true), + ] + ); + $result[] = $f->divider()->horizontal(); + $result[] = $items; } return $r->render($result); } diff --git a/components/ILIAS/UI/src/examples/Table/Action/Standard/base.php b/components/ILIAS/UI/src/examples/Table/Action/Standard/base.php index a927bef9436d..2bdc9545e014 100755 --- a/components/ILIAS/UI/src/examples/Table/Action/Standard/base.php +++ b/components/ILIAS/UI/src/examples/Table/Action/Standard/base.php @@ -61,10 +61,12 @@ function base() $id_token ), $f->table()->action()->standard( - 'do something else', - $url_builder->withParameter($action_token, "do_something_else"), + 'open a prompt', + $f->prompt()->standard( + $url_builder->withParameter($action_token, "something_prompt") + ), $id_token - )->withAsync(), + ), ]; $table = getExampleTable($f) @@ -80,28 +82,22 @@ function base() $action = $query->retrieve($action_token->getName(), $refinery->to()->string()); $ids = $query->retrieve($id_token->getName(), $refinery->custom()->transformation(fn($v) => $v)); - if ($action === 'do_something_else') { - $items = []; - foreach ($ids as $id) { - $items[] = $f->modal()->interruptiveItem()->keyValue($id, $id_token->getName(), $id); - } - echo($r->renderAsync([ - $f->modal()->interruptive( - 'do something else', - 'affected items', - '#' - )->withAffectedItems($items) - ])); - exit(); - } else { - $items = $f->listing()->characteristicValue()->text( - [ - 'table_action' => $action, - 'id' => print_r($ids, true), - ] - ); - $result[] = $f->divider()->horizontal(); - $result[] = $items; + switch ($action) { + case 'something_prompt': + $message = $f->messageBox()->success('some message box in a prompt'); + $response = $f->prompt()->state()->show($message); + echo($r->renderAsync($response)); + exit(); + + default: + $items = $f->listing()->characteristicValue()->text( + [ + 'table_action' => $action, + 'id' => print_r($ids, true), + ] + ); + $result[] = $f->divider()->horizontal(); + $result[] = $items; } } diff --git a/components/ILIAS/UI/src/examples/Table/Data/base.php b/components/ILIAS/UI/src/examples/Table/Data/base.php index 4c2cc503725b..854dc539be6f 100755 --- a/components/ILIAS/UI/src/examples/Table/Data/base.php +++ b/components/ILIAS/UI/src/examples/Table/Data/base.php @@ -123,16 +123,25 @@ function base() 'Remove Student', $url_builder->withParameter($action_parameter_token, "delete"), $row_id_token - ) - /** - * An async Action will trigger an AJAX-call to the action's target - * and display the results in a modal-layer over the Table. - * Parameters are passed to the call, but you will have to completely - * build the contents of the response. DO NOT render an entire page ;) - */ - ->withAsync(), + ), + /** + * A prompt Action will open a Prompt and load its url, expecting a Prompt State in return. + */ + 'status' => + $f->table()->action()->standard( + 'Status', + $f->prompt()->standard( + $url_builder->withParameter($action_parameter_token, "status") + ), + $row_id_token + ), + /** + * An async Action will trigger an AJAX-call to the action's target + * and display the results in a modal-layer over the Table. + * THIS IS DEPRECATED in 11. Please use Prompts instead. + */ 'info' => - $f->table()->action()->standard( //in both + $f->table()->action()->standard( 'Info', $url_builder->withParameter($action_parameter_token, "info"), $row_id_token @@ -242,10 +251,9 @@ protected function getRecords(?Range $range = null, ?Order $order = null): array } }; - /** * setup the Table and hand over the request; - * with an ID for the table, parameters will be stored throughout url changes + * with an Id for the table, parameters will be stored throughout url changes */ $table = $f->table() ->data($data_retrieval, 'a data table', $columns) @@ -277,25 +285,19 @@ protected function getRecords(?Range $range = null, ?Order $order = null): array 'id' => print_r($ids, true), ]); - /** take care of the async-call; 'delete'-action asks for it. */ - if ($action === 'delete') { - $items = []; - foreach ($ids as $id) { - $items[] = $f->modal()->interruptiveItem()->keyValue($id, $row_id_token->getName(), $id); - } - echo($r->renderAsync([ - $f->modal()->interruptive( - 'Deletion', - 'You are about to delete items!', - '#' - )->withAffectedItems($items) - ->withAdditionalOnLoadCode(static fn($id): string => "console.log('ASYNC JS');") - ])); + if ($action === 'status') { + $ids = array_map(fn($id) => $f->button()->shy($id, '#'), $ids); + $message = $f->messageBox()->info('some message box in a prompt-dialog') + ->withLinks($ids); + + $response = $f->prompt()->state()->show($message); + echo($r->renderAsync($response)); exit(); } + if ($action === 'info') { echo( - $r->render($f->messageBox()->info('an info message:
  • ' . implode('
  • ', $ids))) + $r->render($f->messageBox()->info('an (asynch) info message:
  • ' . implode('
  • ', $ids))) . '' ); exit(); diff --git a/components/ILIAS/UI/src/templates/default/Table/tpl.datatable.html b/components/ILIAS/UI/src/templates/default/Table/tpl.datatable.html index ce6a84fbe873..a65165d0f102 100755 --- a/components/ILIAS/UI/src/templates/default/Table/tpl.datatable.html +++ b/components/ILIAS/UI/src/templates/default/Table/tpl.datatable.html @@ -9,46 +9,46 @@

    {TITLE}

  • - + - - - + + + - - - + + + - - - - + + + + - - - {CELLS} - - - - - - - + + + {CELLS} + + + + + + +
    -
    {SELECTION_CONTROL_SELECT}
    -
    {SELECTION_CONTROL_DESELECT}
    -
    +
    {SELECTION_CONTROL_SELECT}
    +
    {SELECTION_CONTROL_DESELECT}
    +
    aria-sort="{COL_SORTATION}"> -
    - - {COL_SORTATION_GLYPH} - - {COL_TITLE} -
    -
    aria-sort="{COL_SORTATION}"> +
    + + {COL_SORTATION_GLYPH} + + {COL_TITLE} +
    +
    {COL_TITLE_ACTION}
    {COL_TITLE_ACTION}
    -
    {MULTI_ACTION_TRIGGERER}
    - {MULTI_ACTION_ALL_MODAL} -
    +
    {MULTI_ACTION_TRIGGERER}
    + {MULTI_ACTION_ALL_MODAL} +
    diff --git a/components/ILIAS/UI/src/templates/default/Table/tpl.orderingtable.html b/components/ILIAS/UI/src/templates/default/Table/tpl.orderingtable.html index 58c21170f2f2..24f3fe2098f5 100644 --- a/components/ILIAS/UI/src/templates/default/Table/tpl.orderingtable.html +++ b/components/ILIAS/UI/src/templates/default/Table/tpl.orderingtable.html @@ -11,61 +11,61 @@

    {TITLE}

    - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - {CELLS} - - - - - - + {CELLS} + + + + + + - - + + + + @@ -76,16 +76,16 @@

    {TITLE}

    - +
    - {DRAG_HINT} - -
    {SELECTION_CONTROL_SELECT}
    -
    {SELECTION_CONTROL_DESELECT}
    - -
    -
    - {POS_INPUT_TITLE} -
    -
    -
    - {COL_TITLE} -
    -
    {COL_TITLE_ACTION}
    + {DRAG_HINT} + +
    {SELECTION_CONTROL_SELECT}
    +
    {SELECTION_CONTROL_DESELECT}
    + +
    +
    + {POS_INPUT_TITLE} +
    +
    +
    + {COL_TITLE} +
    +
    {COL_TITLE_ACTION}
    -
    -
    -
    {MULTI_ACTION_TRIGGERER}
    -
    -
    + +
    +
    +
    +
    {MULTI_ACTION_TRIGGERER}
    +
    +
    {FORM_BUTTONS} -
    -