From 7a196928bfa476c754aaa5bef163efb8900966b7 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Wed, 7 May 2025 10:17:40 +0200 Subject: [PATCH 1/7] UI: Add Column Type List --- .../UI/src/Component/Table/Column/Factory.php | 11 +++ .../UI/src/Component/Table/Column/Listing.php | 28 ++++++ .../Component/Table/Column/Factory.php | 5 + .../Component/Table/Column/LinkListing.php | 12 +-- .../Component/Table/Column/Listing.php | 63 +++++++++++++ .../examples/Table/Column/Listing/base.php | 94 +++++++++++++++++++ .../Table/Column/ColumnFactoryTest.php | 1 + .../Component/Table/Column/ColumnTest.php | 13 ++- 8 files changed, 216 insertions(+), 11 deletions(-) create mode 100755 components/ILIAS/UI/src/Component/Table/Column/Listing.php create mode 100755 components/ILIAS/UI/src/Implementation/Component/Table/Column/Listing.php create mode 100755 components/ILIAS/UI/src/examples/Table/Column/Listing/base.php diff --git a/components/ILIAS/UI/src/Component/Table/Column/Factory.php b/components/ILIAS/UI/src/Component/Table/Column/Factory.php index d97c6b92ab7f..579e3113f0b4 100755 --- a/components/ILIAS/UI/src/Component/Table/Column/Factory.php +++ b/components/ILIAS/UI/src/Component/Table/Column/Factory.php @@ -154,4 +154,15 @@ public function linkListing(string $title): LinkListing; * @return \ILIAS\UI\Component\Table\Column\Breadcrumb */ public function breadcrumb(string $title): Breadcrumb; + + /** + * --- + * description: + * purpose: > + * The Listing Column features an Ordered or Unordered Listing. + * + * --- + * @return \ILIAS\UI\Component\Table\Column\Listing + */ + public function listing(string $title): Listing; } diff --git a/components/ILIAS/UI/src/Component/Table/Column/Listing.php b/components/ILIAS/UI/src/Component/Table/Column/Listing.php new file mode 100755 index 000000000000..1d1558f10110 --- /dev/null +++ b/components/ILIAS/UI/src/Component/Table/Column/Listing.php @@ -0,0 +1,28 @@ +lng, $title); } + + public function listing(string $title): Listing + { + return new Listing($this->lng, $title); + } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/Column/LinkListing.php b/components/ILIAS/UI/src/Implementation/Component/Table/Column/LinkListing.php index 3d672128a001..4687a98d3a26 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Table/Column/LinkListing.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/Column/LinkListing.php @@ -26,22 +26,14 @@ use ILIAS\UI\Component\Listing\Unordered; use ILIAS\UI\Component\Component; -class LinkListing extends Column implements C\LinkListing +class LinkListing extends Listing implements C\LinkListing { public function format($value): string|Component { - $listing = $this->toArray($value); - $this->checkArgListElements("value", $listing, [Ordered::class, Unordered::class]); + $value = parent::format($value); $listing_items = $value->getItems(); $this->checkArgListElements("list items", $listing_items, Standard::class); return $value; } - public function getOrderingLabels(): array - { - return [ - $this->asc_label ?? $this->getTitle() . self::SEPERATOR . $this->lng->txt('order_option_alphabetical_ascending'), - $this->desc_label ?? $this->getTitle() . self::SEPERATOR . $this->lng->txt('order_option_alphabetical_descending') - ]; - } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/Column/Listing.php b/components/ILIAS/UI/src/Implementation/Component/Table/Column/Listing.php new file mode 100755 index 000000000000..ddd84b100923 --- /dev/null +++ b/components/ILIAS/UI/src/Implementation/Component/Table/Column/Listing.php @@ -0,0 +1,63 @@ +toArray($value); + $this->checkArgListElements('value', $listing, [Ordered::class, Unordered::class]); + return $value; + } + + public function getOrderingLabels(): array + { + return [ + $this->asc_label ?? $this->getTitle() . self::SEPERATOR . $this->lng->txt('order_option_alphabetical_ascending'), + $this->desc_label ?? $this->getTitle() . self::SEPERATOR . $this->lng->txt('order_option_alphabetical_descending') + ]; + } + + public function withTruncation(?\Closure $truncated_text_closure): self + { + $clone = clone $this; + $clone->truncated_text_closure = $truncated_text_closure; + return $clone; + } + + public function isTruncationEnabled(): bool + { + return $this->header_label !== null; + } + + public function getTruncatedTextClosure(): ?\Closure + { + $this->header_text_closure; + } +} diff --git a/components/ILIAS/UI/src/examples/Table/Column/Listing/base.php b/components/ILIAS/UI/src/examples/Table/Column/Listing/base.php new file mode 100755 index 000000000000..04010f3ef649 --- /dev/null +++ b/components/ILIAS/UI/src/examples/Table/Column/Listing/base.php @@ -0,0 +1,94 @@ + + * ILIAS shows the rendered Component. + * --- + */ +function base(): string +{ + /** @var \ILIAS\DI\Container $DIC */ + global $DIC; + $f = $DIC->ui()->factory(); + $r = $DIC->ui()->renderer(); + + $columns = [ + 'l1' => $f->table()->column()->listing('A list column') + ]; + + $records = [ + [ + 'l1' => $f->listing()->unordered([ + 'Apples', + 'Oranges', + 'Bananas' + ]) + ], + [ + 'l1' => $f->listing()->unordered([ + 'Bun', + 'Croissant', + 'Pumpernickel' + ]) + ] + ]; + + $data_retrieval = new class ($records) implements I\DataRetrieval { + protected array $records; + + public function __construct(array $records) + { + $this->records = $records; + } + + public function getRows( + I\DataRowBuilder $row_builder, + array $visible_column_ids, + Range $range, + Order $order, + ?array $filter_data, + ?array $additional_parameters + ): \Generator { + foreach ($this->records as $idx => $record) { + $row_id = ''; + yield $row_builder->buildDataRow($row_id, $record); + } + } + + public function getTotalRowCount( + ?array $filter_data, + ?array $additional_parameters + ): ?int { + return count($this->records); + } + }; + + $table = $f->table()->data($data_retrieval, 'List Columns', $columns) + ->withRequest($DIC->http()->request()); + return $r->render($table); +} diff --git a/components/ILIAS/UI/tests/Component/Table/Column/ColumnFactoryTest.php b/components/ILIAS/UI/tests/Component/Table/Column/ColumnFactoryTest.php index 8d85257fbf18..5df1a67f979d 100755 --- a/components/ILIAS/UI/tests/Component/Table/Column/ColumnFactoryTest.php +++ b/components/ILIAS/UI/tests/Component/Table/Column/ColumnFactoryTest.php @@ -37,6 +37,7 @@ class ColumnFactoryTest extends AbstractFactoryTestCase "link" => ["context" => false, "rules" => false], "linkListing" => ["context" => false, "rules" => false], "breadcrumb" => ["context" => false, "rules" => false], + "listing" => ["context" => false, "rules" => false] ]; public static string $factory_title = 'ILIAS\\UI\\Component\\Table\\Column\\Factory'; diff --git a/components/ILIAS/UI/tests/Component/Table/Column/ColumnTest.php b/components/ILIAS/UI/tests/Component/Table/Column/ColumnTest.php index 477cc2021d6a..54df8bfe263d 100755 --- a/components/ILIAS/UI/tests/Component/Table/Column/ColumnTest.php +++ b/components/ILIAS/UI/tests/Component/Table/Column/ColumnTest.php @@ -182,6 +182,17 @@ public function __construct() 'value' => 'some string', 'ok' => false ], + [ + 'column' => new Column\Listing($lng, ''), + 'value' => new Listing\Ordered(['1', '2', '3']), + 'ok' => true + ], + [ + 'column' => new Column\Listing($lng, ''), + 'value' => 123, + 'ok' => false + ], + ]; } @@ -191,7 +202,7 @@ public function testDataTableColumnAllowedFormats( mixed $value, bool $ok ): void { - if(! $ok) { + if (! $ok) { $this->expectException(\InvalidArgumentException::class); } $this->assertEquals($value, $column->format($value)); From 9720d8df73421a964b9ba2e551a5738dd844eb1b Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Mon, 11 Aug 2025 09:52:50 +0200 Subject: [PATCH 2/7] UI: Move Truncation to a Convention-Based Approach --- components/ILIAS/UI/UI.php | 2 + .../resources/js/Listing/dist/listing.min.js | 15 ++++ .../UI/resources/js/Listing/rollup.config.js | 42 +++++++++++ .../UI/resources/js/Listing/src/listing.js | 24 +++++++ .../UI/resources/js/Listing/src/truncate.js | 70 +++++++++++++++++++ .../UI/src/Component/Table/Column/Listing.php | 3 - .../Component/Listing/Ordered.php | 18 ++++- .../Component/Listing/Renderer.php | 51 +++++++++++--- .../Component/Listing/Unordered.php | 20 +++++- .../Component/Table/Column/Listing.php | 21 +----- .../examples/Table/Column/Listing/base.php | 3 +- .../default/Listing/tpl.ordered.html | 2 +- .../default/Listing/tpl.unordered.html | 2 +- lang/ilias_de.lang | 6 +- lang/ilias_en.lang | 6 +- 15 files changed, 241 insertions(+), 44 deletions(-) create mode 100644 components/ILIAS/UI/resources/js/Listing/dist/listing.min.js create mode 100755 components/ILIAS/UI/resources/js/Listing/rollup.config.js create mode 100755 components/ILIAS/UI/resources/js/Listing/src/listing.js create mode 100755 components/ILIAS/UI/resources/js/Listing/src/truncate.js diff --git a/components/ILIAS/UI/UI.php b/components/ILIAS/UI/UI.php index 403eb59ea7e8..dae6c96c5cb7 100644 --- a/components/ILIAS/UI/UI.php +++ b/components/ILIAS/UI/UI.php @@ -593,6 +593,8 @@ public function init( new Component\Resource\ComponentJS($this, "js/Input/Field/input.js"); $contribute[Component\Resource\PublicAsset::class] = fn() => new Component\Resource\ComponentJS($this, "js/Item/dist/notification.js"); + $contribute[Component\Resource\PublicAsset::class] = fn() => + new Component\Resource\ComponentJS($this, "js/Listing/dist/listing.min.js"); $contribute[Component\Resource\PublicAsset::class] = fn() => new Component\Resource\ComponentJS($this, "js/MainControls/dist/mainbar.js"); $contribute[Component\Resource\PublicAsset::class] = fn() => diff --git a/components/ILIAS/UI/resources/js/Listing/dist/listing.min.js b/components/ILIAS/UI/resources/js/Listing/dist/listing.min.js new file mode 100644 index 000000000000..acf5bd4b4fa2 --- /dev/null +++ b/components/ILIAS/UI/resources/js/Listing/dist/listing.min.js @@ -0,0 +1,15 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + */ +!function(e){"use strict";function t(e,t,i,c){!function(e,t){const n=e.ownerDocument;t.forEach((t=>{const i=n.createElement("li");i.innerHTML=t,e.appendChild(i)}))}(e,t);const o=document.createElement("button");o.classList.add("btn-link"),o.appendChild(document.createTextNode(`[${c}]`)),o.addEventListener("click",(o=>{o.target.remove(),function(e,t){t.forEach((()=>{e.removeChild(e.lastChild)}))}(e,t),n(e,t,i,c)})),e.insertAdjacentElement("afterend",o)}function n(e,n,i,c){const o=e.ownerDocument,a=o.createElement("button");a.classList.add("btn-link"),a.appendChild(o.createTextNode(`[${i}]`)),a.addEventListener("click",(o=>{o.target.remove(),t(e,n,i,c)})),e.insertAdjacentElement("afterend",a)}e.UI=e.UI||{},e.UI.Listing=e.UI.Listing||{},e.UI.Listing.initTruncation=(e,t,i,c)=>{n(e,t,i,c)}}(il); diff --git a/components/ILIAS/UI/resources/js/Listing/rollup.config.js b/components/ILIAS/UI/resources/js/Listing/rollup.config.js new file mode 100755 index 000000000000..b42c532ae16d --- /dev/null +++ b/components/ILIAS/UI/resources/js/Listing/rollup.config.js @@ -0,0 +1,42 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + */ + +import terser from '@rollup/plugin-terser'; +import copyright from '../../../../../../scripts/Copyright-Checker/copyright.js'; +import preserveCopyright from '../../../../../../scripts/Copyright-Checker/preserveCopyright.js'; + +export default { + input: './src/listing.js', + external: [ + 'ilias', + 'document', + ], + output: { + file: './dist/listing.min.js', + format: 'iife', + banner: copyright, + globals: { + ilias: 'il', + document: 'document', + }, + plugins: [ + terser({ + format: { + comments: preserveCopyright, + }, + }), + ], + }, +}; diff --git a/components/ILIAS/UI/resources/js/Listing/src/listing.js b/components/ILIAS/UI/resources/js/Listing/src/listing.js new file mode 100755 index 000000000000..486564608a15 --- /dev/null +++ b/components/ILIAS/UI/resources/js/Listing/src/listing.js @@ -0,0 +1,24 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + */ + +import il from 'ilias'; +import truncate from './truncate.js'; + +il.UI = il.UI || {}; +il.UI.Listing = il.UI.Listing || {}; + +il.UI.Listing.initTruncation = (listing, additional_items, lang_var_more, lang_var_less) => { + truncate(listing, additional_items, lang_var_more, lang_var_less); +}; diff --git a/components/ILIAS/UI/resources/js/Listing/src/truncate.js b/components/ILIAS/UI/resources/js/Listing/src/truncate.js new file mode 100755 index 000000000000..ff88e036e2b7 --- /dev/null +++ b/components/ILIAS/UI/resources/js/Listing/src/truncate.js @@ -0,0 +1,70 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + */ + +function addEntries( + listingElement, + additionalItems, +) { + const document = listingElement.ownerDocument; + additionalItems.forEach((element) => { + const entry = document.createElement('li'); + entry.innerHTML = element; + listingElement.appendChild(entry); + }); +} + +function removeEntries( + listingElement, + additionalItems, +) { + additionalItems.forEach(() => { + listingElement.removeChild(listingElement.lastChild); + }); +} + +function showAllEntries( + listingElement, + additionalItems, + langVarMore, + langVarLess, +) { + addEntries(listingElement, additionalItems); + const showLessButton = document.createElement('button'); + showLessButton.classList.add('btn-link'); + showLessButton.appendChild(document.createTextNode(`[${langVarLess}]`)); + showLessButton.addEventListener('click', (event) => { + event.target.remove(); + removeEntries(listingElement, additionalItems); + initTruncation(listingElement, additionalItems,langVarMore, langVarLess); + }); + listingElement.insertAdjacentElement('afterend', showLessButton); +} + +export default function initTruncation( + listingElement, + additionalItems, + langVarMore, + langVarLess, +) { + const document = listingElement.ownerDocument; + const showMoreButton = document.createElement('button'); + showMoreButton.classList.add('btn-link'); + showMoreButton.appendChild(document.createTextNode(`[${langVarMore}]`)); + showMoreButton.addEventListener('click', (event) => { + event.target.remove(); + showAllEntries(listingElement, additionalItems,langVarMore, langVarLess); + }); + listingElement.insertAdjacentElement('afterend', showMoreButton); +} diff --git a/components/ILIAS/UI/src/Component/Table/Column/Listing.php b/components/ILIAS/UI/src/Component/Table/Column/Listing.php index 1d1558f10110..fa80d9810da6 100755 --- a/components/ILIAS/UI/src/Component/Table/Column/Listing.php +++ b/components/ILIAS/UI/src/Component/Table/Column/Listing.php @@ -22,7 +22,4 @@ interface Listing extends Column { - public function withTruncation(?\Closure $truncated_text_closure): self; - public function isTruncationEnabled(): bool; - public function getTruncatedTextClosure(): ?\Closure; } diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Ordered.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Ordered.php index 6bbda158f88a..3ae7aade9298 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Ordered.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Ordered.php @@ -21,11 +21,27 @@ namespace ILIAS\UI\Implementation\Component\Listing; use ILIAS\UI\Component as C; +use ILIAS\UI\Implementation\Component\JavaScriptBindable; /** * Class Listing * @package ILIAS\UI\Implementation\Component\Listing\Listing */ -class Ordered extends Listing implements C\Listing\Ordered +class Ordered extends Listing implements C\Listing\Ordered, C\JavaScriptBindable { + use JavaScriptBindable; + + protected bool $truncated = false; + + public function withIsTruncated(bool $truncated = false): self + { + $clone = clone $this; + $clone->truncated = $truncated; + return $clone; + } + + public function isTruncated(): bool + { + return $this->truncated; + } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php index 1a2aced57f96..ac16b4973472 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php @@ -23,6 +23,7 @@ use ILIAS\UI\Implementation\Render\AbstractComponentRenderer; use ILIAS\UI\Renderer as RendererInterface; use ILIAS\UI\Component; +use ILIAS\UI\Implementation\Render\ResourceRegistry; /** * Class Renderer @@ -30,6 +31,7 @@ */ class Renderer extends AbstractComponentRenderer { + private const int TRUNCATION_DISPLAY_LIMIT = 2; /** * @inheritdocs */ @@ -86,17 +88,41 @@ protected function render_simple(Component\Listing\Listing $component, RendererI $tpl = $this->getTemplate($tpl_name, true, true); - if (count($component->getItems()) > 0) { - foreach ($component->getItems() as $item) { - $tpl->setCurrentBlock("item"); - if (is_string($item)) { - $tpl->setVariable("ITEM", $item); - } else { - $tpl->setVariable("ITEM", $default_renderer->render($item)); - } + if ($component->getItems() === []) { + return $tpl->get(); + } + + $is_truncated = $component->isTruncated(); + $nr_of_items = 0; + $additional_elements = []; + foreach ($component->getItems() as $item) { + $tpl->setCurrentBlock("item"); + if (!is_string($item)) { + $item = $default_renderer->render($item); + } + + if (!$is_truncated || $is_truncated && $nr_of_items < self::TRUNCATION_DISPLAY_LIMIT) { + $tpl->setVariable("ITEM", $item); $tpl->parseCurrentBlock(); + $nr_of_items += 1; + continue; } + $additional_elements[] = $item; + } + + $id_attribute = ''; + if ($additional_elements !== []) { + $component = $component->withAdditionalOnLoadCode( + fn($id): string => 'il.UI.Listing.initTruncation(' + . "document.querySelector('#{$id}'), " + . json_encode($additional_elements) . ', ' + . '"' . sprintf($this->txt('show_all_items'), $nr_of_items + count($additional_elements)) . '", ' + . '"' . sprintf($this->txt('hide_items'), count($additional_elements)) . '");' + ); + $id_attribute = " id='{$this->bindJavaScript($component)}'"; } + $tpl->setVariable("ID", $id_attribute); + return $tpl->get(); } @@ -121,4 +147,13 @@ protected function renderProperty( } return $tpl->get(); } + + /** + * @inheritdoc + */ + public function registerResources(ResourceRegistry $registry): void + { + parent::registerResources($registry); + $registry->register('assets/js/listing.min.js'); + } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Unordered.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Unordered.php index d983880eebf6..ef7fd6875a83 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Unordered.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Unordered.php @@ -21,11 +21,29 @@ namespace ILIAS\UI\Implementation\Component\Listing; use ILIAS\UI\Component as C; +use ILIAS\UI\Implementation\Component\JavaScriptBindable; /** * Class Listing * @package ILIAS\UI\Implementation\Component\Listing\Listing */ -class Unordered extends Listing implements C\Listing\Unordered +class Unordered extends Listing implements C\Listing\Unordered, C\JavaScriptBindable { + use JavaScriptBindable; + + protected ?int $limit = null; + + protected bool $truncated = false; + + public function withIsTruncated(bool $truncated = false): self + { + $clone = clone $this; + $clone->truncated = $truncated; + return $clone; + } + + public function isTruncated(): bool + { + return $this->truncated; + } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/Column/Listing.php b/components/ILIAS/UI/src/Implementation/Component/Table/Column/Listing.php index ddd84b100923..489e09648fdd 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Table/Column/Listing.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/Column/Listing.php @@ -27,13 +27,11 @@ class Listing extends Column implements C\Listing { - private ?Closure $truncated_text_closure = null; - public function format($value): string|Component { $listing = $this->toArray($value); $this->checkArgListElements('value', $listing, [Ordered::class, Unordered::class]); - return $value; + return $value->withIsTruncated(true); } public function getOrderingLabels(): array @@ -43,21 +41,4 @@ public function getOrderingLabels(): array $this->desc_label ?? $this->getTitle() . self::SEPERATOR . $this->lng->txt('order_option_alphabetical_descending') ]; } - - public function withTruncation(?\Closure $truncated_text_closure): self - { - $clone = clone $this; - $clone->truncated_text_closure = $truncated_text_closure; - return $clone; - } - - public function isTruncationEnabled(): bool - { - return $this->header_label !== null; - } - - public function getTruncatedTextClosure(): ?\Closure - { - $this->header_text_closure; - } } diff --git a/components/ILIAS/UI/src/examples/Table/Column/Listing/base.php b/components/ILIAS/UI/src/examples/Table/Column/Listing/base.php index 04010f3ef649..40df0df12f61 100755 --- a/components/ILIAS/UI/src/examples/Table/Column/Listing/base.php +++ b/components/ILIAS/UI/src/examples/Table/Column/Listing/base.php @@ -46,7 +46,8 @@ function base(): string 'l1' => $f->listing()->unordered([ 'Apples', 'Oranges', - 'Bananas' + 'Bananas', + 'Pears' ]) ], [ diff --git a/components/ILIAS/UI/src/templates/default/Listing/tpl.ordered.html b/components/ILIAS/UI/src/templates/default/Listing/tpl.ordered.html index c1a592268983..53bf644626e5 100755 --- a/components/ILIAS/UI/src/templates/default/Listing/tpl.ordered.html +++ b/components/ILIAS/UI/src/templates/default/Listing/tpl.ordered.html @@ -1,4 +1,4 @@ -
    +
  1. {ITEM}
  2. diff --git a/components/ILIAS/UI/src/templates/default/Listing/tpl.unordered.html b/components/ILIAS/UI/src/templates/default/Listing/tpl.unordered.html index 594abfe9a720..b5657176ba5f 100755 --- a/components/ILIAS/UI/src/templates/default/Listing/tpl.unordered.html +++ b/components/ILIAS/UI/src/templates/default/Listing/tpl.unordered.html @@ -1,4 +1,4 @@ -
      +
    • {ITEM}
    • diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index 476a103a5fe0..db25315571cc 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -17591,10 +17591,7 @@ ui#:#footer_link_groups#:#Footer Link-Gruppen ui#:#footer_links#:#Footer Links ui#:#footer_permanent_link#:#Footer Permanent Link ui#:#footer_texts#:#Footer Texte -ui#:#image_alt_text#:#Alternativtext -ui#:#image_purpose_decorative#:#Dekoratives Bild -ui#:#image_purpose_informative#:#Informatives Bild -ui#:#image_purpose_user_defined#:#Verwendungszweck +ui#:#hide_items#:#%s ausblenden ui#:#label_fieldselection#:#Spaltenauswahl ui#:#label_fieldselection_refresh#:#Anwenden ui#:#label_modeviewcontrol#:#Anzeigemodus @@ -17615,6 +17612,7 @@ ui#:#presentation_table_expand#:#Alle zeigen ui#:#rating_average#:#Andere bewerteten mit %s von 5 ui#:#reset_stars#:#neutral ui#:#select_node#:#Knoten %s zur Auswahl hinzufügen +ui#:#show_all_items#:#Alle %s anzeigen ui#:#table_posinput_col_title#:#Position ui#:#ui_chars_max#:#Maximum: ui#:#ui_chars_min#:#Minimum: diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index 512dd28c41af..36470f6ac61e 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -17540,10 +17540,7 @@ ui#:#footer_link_groups#:#Footer Link-Groups ui#:#footer_links#:#Footer Links ui#:#footer_permanent_link#:#Footer Permanent Link ui#:#footer_texts#:#Footer Texts -ui#:#image_alt_text#:#Alternate Text -ui#:#image_purpose_decorative#:#Decorative Image -ui#:#image_purpose_informative#:#Informative Image -ui#:#image_purpose_user_defined#:#Image Purpose +ui#:#hide_items#:#Hide last %s ui#:#label_fieldselection#:#Field Selection ui#:#label_fieldselection_refresh#:#Apply ui#:#label_modeviewcontrol#:#view mode @@ -17564,6 +17561,7 @@ ui#:#presentation_table_expand#:#Expand All ui#:#rating_average#:#Others rated %s of 5 ui#:#reset_stars#:#neutral ui#:#select_node#:#Add node %s to selection +ui#:#show_all_items#:#Show all %s ui#:#table_posinput_col_title#:#Position ui#:#ui_chars_max#:#Maximum: ui#:#ui_chars_min#:#Minimum: From 3d5fb8cb0ae8479ec2e4c9ccd9abb325253ca0ea Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Mon, 11 Aug 2025 10:18:07 +0200 Subject: [PATCH 3/7] UI: Fix Tests For Table Columns --- .../tests/Component/Table/Column/ColumnTest.php | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/components/ILIAS/UI/tests/Component/Table/Column/ColumnTest.php b/components/ILIAS/UI/tests/Component/Table/Column/ColumnTest.php index 54df8bfe263d..f72ac2d3997a 100755 --- a/components/ILIAS/UI/tests/Component/Table/Column/ColumnTest.php +++ b/components/ILIAS/UI/tests/Component/Table/Column/ColumnTest.php @@ -144,17 +144,19 @@ public function __construct() return [ [ 'column' => new Column\LinkListing($lng, ''), - 'value' => new Listing\Unordered([(new Link\Standard('label', '#')),(new Link\Standard('label', '#'))]), + 'value' => (new Listing\Unordered([(new Link\Standard('label', '#')),(new Link\Standard('label', '#'))])) + ->withIsTruncated(true), 'ok' => true ], [ 'column' => new Column\LinkListing($lng, ''), - 'value' => new Listing\Unordered(['string', 'string']), + 'value' => (new Listing\Unordered(['string', 'string']))->withIsTruncated(true), 'ok' => false ], [ 'column' => new Column\LinkListing($lng, ''), - 'value' => new Listing\Ordered([(new Link\Standard('label', '#')),(new Link\Standard('label', '#'))]), + 'value' => (new Listing\Ordered([(new Link\Standard('label', '#')),(new Link\Standard('label', '#'))])) + ->withIsTruncated(true), 'ok' => true ], [ @@ -184,7 +186,12 @@ public function __construct() ], [ 'column' => new Column\Listing($lng, ''), - 'value' => new Listing\Ordered(['1', '2', '3']), + 'value' => (new Listing\Ordered(['1', '2', '3']))->withIsTruncated(true), + 'ok' => true + ], + [ + 'column' => new Column\Listing($lng, ''), + 'value' => (new Listing\Unordered(['1', '2', '3']))->withIsTruncated(true), 'ok' => true ], [ @@ -213,7 +220,7 @@ public function testDataTableColumnLinkListingFormat(): void { $col = new Column\LinkListing($this->lng, 'col'); $link = new Link\Standard('label', '#'); - $linklisting = new Listing\Unordered([$link, $link, $link]); + $linklisting = (new Listing\Unordered([$link, $link, $link]))->withIsTruncated(true); $this->assertEquals($linklisting, $col->format($linklisting)); } From 969bf05ae6aff14646d94ab7f16e03f9bf69c833 Mon Sep 17 00:00:00 2001 From: Stephan Kergomard Date: Thu, 19 Feb 2026 14:55:24 +0100 Subject: [PATCH 4/7] UI: ListingColumn fix Tests --- .../UI/src/examples/Table/Column/Listing/base.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/components/ILIAS/UI/src/examples/Table/Column/Listing/base.php b/components/ILIAS/UI/src/examples/Table/Column/Listing/base.php index 40df0df12f61..7dafa2327fa1 100755 --- a/components/ILIAS/UI/src/examples/Table/Column/Listing/base.php +++ b/components/ILIAS/UI/src/examples/Table/Column/Listing/base.php @@ -72,8 +72,9 @@ public function getRows( array $visible_column_ids, Range $range, Order $order, - ?array $filter_data, - ?array $additional_parameters + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters ): \Generator { foreach ($this->records as $idx => $record) { $row_id = ''; @@ -82,8 +83,9 @@ public function getRows( } public function getTotalRowCount( - ?array $filter_data, - ?array $additional_parameters + mixed $additional_viewcontrol_data, + mixed $filter_data, + mixed $additional_parameters ): ?int { return count($this->records); } From 80ccc1c0d6be07df5143ea81e860c614c9ea2b2d Mon Sep 17 00:00:00 2001 From: Thibeau Fuhrer Date: Mon, 29 Jun 2026 12:00:01 +0200 Subject: [PATCH 5/7] [FIX] UI: make ordered/unordered JS bindable --- .../UI/src/Component/Listing/Ordered.php | 4 +- .../UI/src/Component/Listing/Unordered.php | 4 +- .../Component/Listing/Ordered.php | 16 +---- .../Component/Listing/Renderer.php | 72 +++++-------------- .../Component/Listing/Unordered.php | 18 +---- .../default/Listing/tpl.ordered.html | 4 +- .../default/Listing/tpl.unordered.html | 4 +- 7 files changed, 31 insertions(+), 91 deletions(-) diff --git a/components/ILIAS/UI/src/Component/Listing/Ordered.php b/components/ILIAS/UI/src/Component/Listing/Ordered.php index b17dc542cb46..004147c43641 100755 --- a/components/ILIAS/UI/src/Component/Listing/Ordered.php +++ b/components/ILIAS/UI/src/Component/Listing/Ordered.php @@ -20,10 +20,12 @@ namespace ILIAS\UI\Component\Listing; +use ILIAS\UI\Component\JavaScriptBindable; + /** * Interface Ordered * @package ILIAS\UI\Component\Listing */ -interface Ordered extends Listing +interface Ordered extends Listing, JavaScriptBindable { } diff --git a/components/ILIAS/UI/src/Component/Listing/Unordered.php b/components/ILIAS/UI/src/Component/Listing/Unordered.php index c58603a59bf4..49e3bb9752d9 100755 --- a/components/ILIAS/UI/src/Component/Listing/Unordered.php +++ b/components/ILIAS/UI/src/Component/Listing/Unordered.php @@ -20,10 +20,12 @@ namespace ILIAS\UI\Component\Listing; +use ILIAS\UI\Component\JavaScriptBindable; + /** * Interface Unordered * @package ILIAS\UI\Component\Listing */ -interface Unordered extends Listing +interface Unordered extends Listing, JavaScriptBindable { } diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Ordered.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Ordered.php index 3ae7aade9298..6d1f2bc3c6e7 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Ordered.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Ordered.php @@ -27,21 +27,7 @@ * Class Listing * @package ILIAS\UI\Implementation\Component\Listing\Listing */ -class Ordered extends Listing implements C\Listing\Ordered, C\JavaScriptBindable +class Ordered extends Listing implements C\Listing\Ordered { use JavaScriptBindable; - - protected bool $truncated = false; - - public function withIsTruncated(bool $truncated = false): self - { - $clone = clone $this; - $clone->truncated = $truncated; - return $clone; - } - - public function isTruncated(): bool - { - return $this->truncated; - } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php index ac16b4973472..1d8110fcecf2 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php @@ -23,7 +23,6 @@ use ILIAS\UI\Implementation\Render\AbstractComponentRenderer; use ILIAS\UI\Renderer as RendererInterface; use ILIAS\UI\Component; -use ILIAS\UI\Implementation\Render\ResourceRegistry; /** * Class Renderer @@ -31,29 +30,28 @@ */ class Renderer extends AbstractComponentRenderer { - private const int TRUNCATION_DISPLAY_LIMIT = 2; /** * @inheritdocs */ public function render(Component\Component $component, RendererInterface $default_renderer): string { - if ($component instanceof Component\Listing\Descriptive) { - return $this->render_descriptive($component, $default_renderer); + if ($component instanceof Descriptive) { + return $this->renderDescriptive($component, $default_renderer); } - if ($component instanceof Component\Listing\Property) { + if ($component instanceof Property) { return $this->renderProperty($component, $default_renderer); } - if ($component instanceof Component\Listing\Listing) { - return $this->render_simple($component, $default_renderer); + if ($component instanceof Unordered || $component instanceof Ordered) { + return $this->renderSimple($component, $default_renderer); } $this->cannotHandleComponent($component); } - protected function render_descriptive( - Component\Listing\Descriptive $component, + protected function renderDescriptive( + Descriptive $component, RendererInterface $default_renderer ): string { $tpl = $this->getTemplate("tpl.descriptive.html", true, true); @@ -75,53 +73,30 @@ protected function render_descriptive( return $tpl->get(); } - protected function render_simple(Component\Listing\Listing $component, RendererInterface $default_renderer): string + protected function renderSimple(Unordered|Ordered $component, RendererInterface $default_renderer): string { - $tpl_name = ""; - if ($component instanceof Component\Listing\Ordered) { $tpl_name = "tpl.ordered.html"; - } - if ($component instanceof Component\Listing\Unordered) { + } else { $tpl_name = "tpl.unordered.html"; } $tpl = $this->getTemplate($tpl_name, true, true); - if ($component->getItems() === []) { - return $tpl->get(); - } - - $is_truncated = $component->isTruncated(); - $nr_of_items = 0; - $additional_elements = []; - foreach ($component->getItems() as $item) { - $tpl->setCurrentBlock("item"); - if (!is_string($item)) { - $item = $default_renderer->render($item); - } - - if (!$is_truncated || $is_truncated && $nr_of_items < self::TRUNCATION_DISPLAY_LIMIT) { - $tpl->setVariable("ITEM", $item); + if (count($component->getItems()) > 0) { + foreach ($component->getItems() as $item) { + $tpl->setCurrentBlock("item"); + if (is_string($item)) { + $tpl->setVariable("ITEM", $item); + } else { + $tpl->setVariable("ITEM", $default_renderer->render($item)); + } $tpl->parseCurrentBlock(); - $nr_of_items += 1; - continue; } - $additional_elements[] = $item; } - $id_attribute = ''; - if ($additional_elements !== []) { - $component = $component->withAdditionalOnLoadCode( - fn($id): string => 'il.UI.Listing.initTruncation(' - . "document.querySelector('#{$id}'), " - . json_encode($additional_elements) . ', ' - . '"' . sprintf($this->txt('show_all_items'), $nr_of_items + count($additional_elements)) . '", ' - . '"' . sprintf($this->txt('hide_items'), count($additional_elements)) . '");' - ); - $id_attribute = " id='{$this->bindJavaScript($component)}'"; - } - $tpl->setVariable("ID", $id_attribute); + $id = $this->bindJavaScript($component) ?? $this->createId(); + $tpl->setVariable('ID', $id); return $tpl->get(); } @@ -147,13 +122,4 @@ protected function renderProperty( } return $tpl->get(); } - - /** - * @inheritdoc - */ - public function registerResources(ResourceRegistry $registry): void - { - parent::registerResources($registry); - $registry->register('assets/js/listing.min.js'); - } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Unordered.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Unordered.php index ef7fd6875a83..099c75b7c88d 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Unordered.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Unordered.php @@ -27,23 +27,7 @@ * Class Listing * @package ILIAS\UI\Implementation\Component\Listing\Listing */ -class Unordered extends Listing implements C\Listing\Unordered, C\JavaScriptBindable +class Unordered extends Listing implements C\Listing\Unordered { use JavaScriptBindable; - - protected ?int $limit = null; - - protected bool $truncated = false; - - public function withIsTruncated(bool $truncated = false): self - { - $clone = clone $this; - $clone->truncated = $truncated; - return $clone; - } - - public function isTruncated(): bool - { - return $this->truncated; - } } diff --git a/components/ILIAS/UI/src/templates/default/Listing/tpl.ordered.html b/components/ILIAS/UI/src/templates/default/Listing/tpl.ordered.html index 53bf644626e5..588f8990eabf 100755 --- a/components/ILIAS/UI/src/templates/default/Listing/tpl.ordered.html +++ b/components/ILIAS/UI/src/templates/default/Listing/tpl.ordered.html @@ -1,5 +1,5 @@ - +
      1. {ITEM}
      2. -
      \ No newline at end of file +
diff --git a/components/ILIAS/UI/src/templates/default/Listing/tpl.unordered.html b/components/ILIAS/UI/src/templates/default/Listing/tpl.unordered.html index b5657176ba5f..a47565f9864c 100755 --- a/components/ILIAS/UI/src/templates/default/Listing/tpl.unordered.html +++ b/components/ILIAS/UI/src/templates/default/Listing/tpl.unordered.html @@ -1,5 +1,5 @@ - +
  • {ITEM}
  • -
\ No newline at end of file + From 90b6250a17c377300c892e84b459f6b3e7fae9c9 Mon Sep 17 00:00:00 2001 From: Thibeau Fuhrer Date: Tue, 30 Jun 2026 10:13:14 +0200 Subject: [PATCH 6/7] [FIX] UI: remove link listing column, update listing column * Implement listing column using context rendering * Improve a11y of truncation, rename it too * Update JS and avoid client-side rendering --- .../ConsultationHours/BookingTableGUI.php | 6 +- .../Grouping/Table/GroupingHandler.php | 2 +- .../Service/Table/TableAdapterGUI.php | 2 +- .../classes/class.AssignMaterialsTable.php | 2 +- components/ILIAS/UI/UI.php | 10 +++ .../resources/js/Listing/dist/listing.min.js | 2 +- .../UI/resources/js/Listing/rollup.config.js | 1 + .../js/Listing/src/createExpandableList.js | 76 ++++++++++++++++ .../UI/resources/js/Listing/src/listing.js | 11 ++- .../UI/resources/js/Listing/src/truncate.js | 70 --------------- .../UI/src/Component/Table/Column/Factory.php | 18 +--- .../Component/Table/Column/LinkListing.php | 25 ------ .../Listing/ListingRendererFactory.php | 54 ++++++++++++ .../Component/Listing/Renderer.php | 39 +++++---- .../Listing/TableColumnContextRenderer.php | 83 ++++++++++++++++++ .../Component/Table/Column/Factory.php | 9 +- .../Component/Table/Column/LinkListing.php | 39 --------- .../Component/Table/Column/Listing.php | 2 +- .../UI/src/Implementation/Render/FSLoader.php | 6 ++ .../Table/Column/LinkListing/base.php | 86 ------------------- .../Listing/tpl.table_column_context.html | 10 +++ lang/ilias_de.lang | 8 +- lang/ilias_en.lang | 8 +- 23 files changed, 294 insertions(+), 275 deletions(-) create mode 100644 components/ILIAS/UI/resources/js/Listing/src/createExpandableList.js delete mode 100755 components/ILIAS/UI/resources/js/Listing/src/truncate.js delete mode 100755 components/ILIAS/UI/src/Component/Table/Column/LinkListing.php create mode 100644 components/ILIAS/UI/src/Implementation/Component/Listing/ListingRendererFactory.php create mode 100644 components/ILIAS/UI/src/Implementation/Component/Listing/TableColumnContextRenderer.php delete mode 100755 components/ILIAS/UI/src/Implementation/Component/Table/Column/LinkListing.php delete mode 100755 components/ILIAS/UI/src/examples/Table/Column/LinkListing/base.php create mode 100644 components/ILIAS/UI/src/templates/default/Listing/tpl.table_column_context.html diff --git a/components/ILIAS/Calendar/classes/ConsultationHours/BookingTableGUI.php b/components/ILIAS/Calendar/classes/ConsultationHours/BookingTableGUI.php index 594321c5c83f..ff028eb872d0 100644 --- a/components/ILIAS/Calendar/classes/ConsultationHours/BookingTableGUI.php +++ b/components/ILIAS/Calendar/classes/ConsultationHours/BookingTableGUI.php @@ -207,15 +207,15 @@ protected function getColumns(): array 'booking_participant' => $this->ui_factory ->table() ->column() - ->linkListing($this->lng->txt('cal_ch_booking_participants')), + ->listing($this->lng->txt('cal_ch_booking_participants')), 'booking_comment' => $this->ui_factory ->table() ->column() - ->linkListing($this->lng->txt('cal_ch_booking_col_comments')), + ->listing($this->lng->txt('cal_ch_booking_col_comments')), 'booking_location' => $this->ui_factory ->table() ->column() - ->linkListing($this->lng->txt('cal_ch_target_object')) + ->listing($this->lng->txt('cal_ch_target_object')) ]; } diff --git a/components/ILIAS/Course/classes/Grouping/Table/GroupingHandler.php b/components/ILIAS/Course/classes/Grouping/Table/GroupingHandler.php index 97984f5fcfe3..7df6dd0e8543 100755 --- a/components/ILIAS/Course/classes/Grouping/Table/GroupingHandler.php +++ b/components/ILIAS/Course/classes/Grouping/Table/GroupingHandler.php @@ -95,7 +95,7 @@ protected function buildColumns(): array self::COL_DESCRIPTION => $f->text($this->lng->txt('description'))->withIsSortable(true), self::COL_SOURCE => $f->link($this->lng->txt('groupings_source'))->withIsSortable(true), self::COL_UNIQUE_FIELD => $f->text($this->lng->txt('unambiguousness'))->withIsSortable(true), - self::COL_ASSIGNED_OBJS => $f->linkListing($this->lng->txt('groupings_assigned_obj_' . $type))->withIsSortable(true) + self::COL_ASSIGNED_OBJS => $f->listing($this->lng->txt('groupings_assigned_obj_' . $type))->withIsSortable(true) ]; } diff --git a/components/ILIAS/Repository/Service/Table/TableAdapterGUI.php b/components/ILIAS/Repository/Service/Table/TableAdapterGUI.php index 15829b48e271..14e941d83a63 100755 --- a/components/ILIAS/Repository/Service/Table/TableAdapterGUI.php +++ b/components/ILIAS/Repository/Service/Table/TableAdapterGUI.php @@ -137,7 +137,7 @@ public function linkListingColumn( string $title, bool $sortable = false ): self { - $column = $this->ui->factory()->table()->column()->linkListing($title)->withIsSortable($sortable); + $column = $this->ui->factory()->table()->column()->listing($title)->withIsSortable($sortable); $this->addColumn($key, $column); return $this; } diff --git a/components/ILIAS/Skill/Table/classes/class.AssignMaterialsTable.php b/components/ILIAS/Skill/Table/classes/class.AssignMaterialsTable.php index f13a28acb197..d78033815bad 100755 --- a/components/ILIAS/Skill/Table/classes/class.AssignMaterialsTable.php +++ b/components/ILIAS/Skill/Table/classes/class.AssignMaterialsTable.php @@ -100,7 +100,7 @@ protected function getColumns(): array ->withIsSortable(false), "description" => $this->ui_fac->table()->column()->text($this->lng->txt("description")) ->withIsSortable(false), - "resources" => $this->ui_fac->table()->column()->linkListing($this->lng->txt("skmg_materials")) + "resources" => $this->ui_fac->table()->column()->listing($this->lng->txt("skmg_materials")) ->withIsSortable(false) ]; diff --git a/components/ILIAS/UI/UI.php b/components/ILIAS/UI/UI.php index dae6c96c5cb7..57996a1a1040 100644 --- a/components/ILIAS/UI/UI.php +++ b/components/ILIAS/UI/UI.php @@ -551,6 +551,16 @@ public function init( $use[UI\HelpTextRetriever::class], $internal[UI\Implementation\Component\Input\UploadLimitResolver::class], ), + new UI\Implementation\Component\Listing\ListingRendererFactory( + $use[UI\Implementation\FactoryInternal::class], + $internal[UI\Implementation\Render\TemplateFactory::class], + $use[Language\Language::class], + $internal[UI\Implementation\Render\JavaScriptBinding::class], + $use[UI\Implementation\Render\ImagePathResolver::class], + $pull[Data\Factory::class], + $use[UI\HelpTextRetriever::class], + $internal[UI\Implementation\Component\Input\UploadLimitResolver::class], + ), ) ) ); diff --git a/components/ILIAS/UI/resources/js/Listing/dist/listing.min.js b/components/ILIAS/UI/resources/js/Listing/dist/listing.min.js index acf5bd4b4fa2..d93178929a16 100644 --- a/components/ILIAS/UI/resources/js/Listing/dist/listing.min.js +++ b/components/ILIAS/UI/resources/js/Listing/dist/listing.min.js @@ -12,4 +12,4 @@ * https://www.ilias.de * https://github.com/ILIAS-eLearning */ -!function(e){"use strict";function t(e,t,i,c){!function(e,t){const n=e.ownerDocument;t.forEach((t=>{const i=n.createElement("li");i.innerHTML=t,e.appendChild(i)}))}(e,t);const o=document.createElement("button");o.classList.add("btn-link"),o.appendChild(document.createTextNode(`[${c}]`)),o.addEventListener("click",(o=>{o.target.remove(),function(e,t){t.forEach((()=>{e.removeChild(e.lastChild)}))}(e,t),n(e,t,i,c)})),e.insertAdjacentElement("afterend",o)}function n(e,n,i,c){const o=e.ownerDocument,a=o.createElement("button");a.classList.add("btn-link"),a.appendChild(o.createTextNode(`[${i}]`)),a.addEventListener("click",(o=>{o.target.remove(),t(e,n,i,c)})),e.insertAdjacentElement("afterend",a)}e.UI=e.UI||{},e.UI.Listing=e.UI.Listing||{},e.UI.Listing.initTruncation=(e,t,i,c)=>{n(e,t,i,c)}}(il); +!function(t,e){"use strict";function i(t,...e){const i=[...e];return t.replace(/%s/g,(()=>i.shift()??""))}function n(t,e){const n=e.parentElement.querySelector(`[aria-controls="${e.id}"]`);if(!n)throw new Error("Could not find button associated with list.");if(!e.hasAttribute("data-max-items"))throw new Error("Could not find max items attribute.");const a=parseInt(e.getAttribute("data-max-items"),10),r=e.querySelectorAll("li");n.addEventListener("click",(()=>{!function(t,e,n,a){const r=e.hasAttribute("aria-expanded")&&"true"===e.getAttribute("aria-expanded");n.forEach(((t,e)=>{e>a-1&&(r?t.classList.replace("visible","hidden"):t.classList.replace("hidden","visible"))})),r?(e.setAttribute("aria-expanded","false"),e.textContent=i(t.txt("show_x_items"),n.length-a)):(e.setAttribute("aria-expanded","true"),e.textContent=i(t.txt("hide_x_items"),n.length-a))}(t,n,r,a)}))}t.UI=t.UI||{},t.UI.Listing={createExpandableList:i=>n({txt:e=>t.Language.txt(e)},e.getElementById(i))}}(il,document); diff --git a/components/ILIAS/UI/resources/js/Listing/rollup.config.js b/components/ILIAS/UI/resources/js/Listing/rollup.config.js index b42c532ae16d..bd092843e631 100755 --- a/components/ILIAS/UI/resources/js/Listing/rollup.config.js +++ b/components/ILIAS/UI/resources/js/Listing/rollup.config.js @@ -24,6 +24,7 @@ export default { 'document', ], output: { + // file: '../../../../../../public/assets/js/listing.min.js', file: './dist/listing.min.js', format: 'iife', banner: copyright, diff --git a/components/ILIAS/UI/resources/js/Listing/src/createExpandableList.js b/components/ILIAS/UI/resources/js/Listing/src/createExpandableList.js new file mode 100644 index 000000000000..f2381f449c84 --- /dev/null +++ b/components/ILIAS/UI/resources/js/Listing/src/createExpandableList.js @@ -0,0 +1,76 @@ +/** + * This file is part of ILIAS, a powerful learning management system + * published by ILIAS open source e-Learning e.V. + * + * ILIAS is licensed with the GPL-3.0, + * see https://www.gnu.org/licenses/gpl-3.0.en.html + * You should have received a copy of said license along with the + * source code, too. + * + * If this is not the case or you just want to try ILIAS, you'll find + * us at: + * https://www.ilias.de + * https://github.com/ILIAS-eLearning + * + * @author Thibeau Fuhrer + */ + +import sprintf from '../../Core/src/sprintf.js'; + +/** + * @param {{ txt: function(string): string }} + * @param {HTMLButtonElement} button + * @param {HTMLLIElement[]} listItems + * @param {number} maxItemCount + */ +function toggleListItems( + language, + button, + listItems, + maxItemCount, +) { + const isExpanded = button.hasAttribute('aria-expanded') + && button.getAttribute('aria-expanded') === 'true'; + + listItems.forEach((item, index) => { + if (index > (maxItemCount - 1)) { + if (isExpanded) { + item.classList.replace('visible', 'hidden'); + } else { + item.classList.replace('hidden', 'visible'); + } + } + }); + if (isExpanded) { + button.setAttribute('aria-expanded', 'false'); + button.textContent = sprintf(language.txt('show_x_items'), listItems.length - maxItemCount); + } else { + button.setAttribute('aria-expanded', 'true'); + button.textContent = sprintf(language.txt('hide_x_items'), listItems.length - maxItemCount); + } +} + +/** + * @param {{ txt: function(string): string }} + * @param {HTMLUListElement|HTMLOListElement} list + */ +export default function createExpandableList(language, list) { + const button = list.parentElement.querySelector(`[aria-controls="${list.id}"]`); + if (!button) { + throw new Error('Could not find button associated with list.'); + } + if (!list.hasAttribute('data-max-items')) { + throw new Error('Could not find max items attribute.'); + } + const maxItemCount = parseInt(list.getAttribute('data-max-items'), 10); + const listItems = list.querySelectorAll('li'); + + button.addEventListener('click', () => { + toggleListItems( + language, + button, + listItems, + maxItemCount, + ); + }); +} diff --git a/components/ILIAS/UI/resources/js/Listing/src/listing.js b/components/ILIAS/UI/resources/js/Listing/src/listing.js index 486564608a15..5802d3f6c917 100755 --- a/components/ILIAS/UI/resources/js/Listing/src/listing.js +++ b/components/ILIAS/UI/resources/js/Listing/src/listing.js @@ -14,11 +14,14 @@ */ import il from 'ilias'; -import truncate from './truncate.js'; +import document from 'document'; +import createExpandableList from './createExpandableList.js'; il.UI = il.UI || {}; -il.UI.Listing = il.UI.Listing || {}; -il.UI.Listing.initTruncation = (listing, additional_items, lang_var_more, lang_var_less) => { - truncate(listing, additional_items, lang_var_more, lang_var_less); +il.UI.Listing = { + createExpandableList: (id) => createExpandableList( + { txt: (key) => il.Language.txt(key) }, + document.getElementById(id), + ), }; diff --git a/components/ILIAS/UI/resources/js/Listing/src/truncate.js b/components/ILIAS/UI/resources/js/Listing/src/truncate.js deleted file mode 100755 index ff88e036e2b7..000000000000 --- a/components/ILIAS/UI/resources/js/Listing/src/truncate.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * This file is part of ILIAS, a powerful learning management system - * published by ILIAS open source e-Learning e.V. - * - * ILIAS is licensed with the GPL-3.0, - * see https://www.gnu.org/licenses/gpl-3.0.en.html - * You should have received a copy of said license along with the - * source code, too. - * - * If this is not the case or you just want to try ILIAS, you'll find - * us at: - * https://www.ilias.de - * https://github.com/ILIAS-eLearning - */ - -function addEntries( - listingElement, - additionalItems, -) { - const document = listingElement.ownerDocument; - additionalItems.forEach((element) => { - const entry = document.createElement('li'); - entry.innerHTML = element; - listingElement.appendChild(entry); - }); -} - -function removeEntries( - listingElement, - additionalItems, -) { - additionalItems.forEach(() => { - listingElement.removeChild(listingElement.lastChild); - }); -} - -function showAllEntries( - listingElement, - additionalItems, - langVarMore, - langVarLess, -) { - addEntries(listingElement, additionalItems); - const showLessButton = document.createElement('button'); - showLessButton.classList.add('btn-link'); - showLessButton.appendChild(document.createTextNode(`[${langVarLess}]`)); - showLessButton.addEventListener('click', (event) => { - event.target.remove(); - removeEntries(listingElement, additionalItems); - initTruncation(listingElement, additionalItems,langVarMore, langVarLess); - }); - listingElement.insertAdjacentElement('afterend', showLessButton); -} - -export default function initTruncation( - listingElement, - additionalItems, - langVarMore, - langVarLess, -) { - const document = listingElement.ownerDocument; - const showMoreButton = document.createElement('button'); - showMoreButton.classList.add('btn-link'); - showMoreButton.appendChild(document.createTextNode(`[${langVarMore}]`)); - showMoreButton.addEventListener('click', (event) => { - event.target.remove(); - showAllEntries(listingElement, additionalItems,langVarMore, langVarLess); - }); - listingElement.insertAdjacentElement('afterend', showMoreButton); -} diff --git a/components/ILIAS/UI/src/Component/Table/Column/Factory.php b/components/ILIAS/UI/src/Component/Table/Column/Factory.php index 579e3113f0b4..a2199844e8b7 100755 --- a/components/ILIAS/UI/src/Component/Table/Column/Factory.php +++ b/components/ILIAS/UI/src/Component/Table/Column/Factory.php @@ -136,12 +136,13 @@ public function link(string $title): Link; * --- * description: * purpose: > - * The LinkListing Column features an Ordered or Unordered Listing of Standard Links. + * The Listing Column features an Ordered or Unordered Listing. * * --- - * @return \ILIAS\UI\Component\Table\Column\LinkListing + * @param string $title + * @return \ILIAS\UI\Component\Table\Column\Listing */ - public function linkListing(string $title): LinkListing; + public function listing(string $title): Listing; /** * --- @@ -154,15 +155,4 @@ public function linkListing(string $title): LinkListing; * @return \ILIAS\UI\Component\Table\Column\Breadcrumb */ public function breadcrumb(string $title): Breadcrumb; - - /** - * --- - * description: - * purpose: > - * The Listing Column features an Ordered or Unordered Listing. - * - * --- - * @return \ILIAS\UI\Component\Table\Column\Listing - */ - public function listing(string $title): Listing; } diff --git a/components/ILIAS/UI/src/Component/Table/Column/LinkListing.php b/components/ILIAS/UI/src/Component/Table/Column/LinkListing.php deleted file mode 100755 index b02c08c1cc01..000000000000 --- a/components/ILIAS/UI/src/Component/Table/Column/LinkListing.php +++ /dev/null @@ -1,25 +0,0 @@ - + */ +class ListingRendererFactory extends DefaultRendererFactory +{ + /** @var string[] cannonical names of table components */ + protected const array TABLE_COLUMN_CONTEXTS = [ + 'OrderingRowTable', + 'DataRowTable', + ]; + + public function getRendererInContext(Component $component, array $contexts): ComponentRenderer + { + if (!empty(array_intersect(self::TABLE_COLUMN_CONTEXTS, $contexts))) { + return new TableColumnContextRenderer( + $this->ui_factory, + $this->tpl_factory, + $this->lng, + $this->js_binding, + $this->image_path_resolver, + $this->data_factory, + $this->help_text_retriever, + $this->upload_limit_resolver, + ); + } + + return parent::getRendererInContext($component, $contexts); + } +} diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php index 1d8110fcecf2..1707cf3e0128 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php @@ -23,6 +23,7 @@ use ILIAS\UI\Implementation\Render\AbstractComponentRenderer; use ILIAS\UI\Renderer as RendererInterface; use ILIAS\UI\Component; +use ILIAS\UI\Implementation\Render\Template; /** * Class Renderer @@ -36,21 +37,21 @@ class Renderer extends AbstractComponentRenderer public function render(Component\Component $component, RendererInterface $default_renderer): string { if ($component instanceof Descriptive) { - return $this->renderDescriptive($component, $default_renderer); + return $this->renderDescriptiveList($component, $default_renderer); } if ($component instanceof Property) { - return $this->renderProperty($component, $default_renderer); + return $this->renderPropertyList($component, $default_renderer); } if ($component instanceof Unordered || $component instanceof Ordered) { - return $this->renderSimple($component, $default_renderer); + return $this->renderList($component, $default_renderer); } $this->cannotHandleComponent($component); } - protected function renderDescriptive( + protected function renderDescriptiveList( Descriptive $component, RendererInterface $default_renderer ): string { @@ -73,7 +74,7 @@ protected function renderDescriptive( return $tpl->get(); } - protected function renderSimple(Unordered|Ordered $component, RendererInterface $default_renderer): string + protected function renderList(Unordered|Ordered $component, RendererInterface $default_renderer): string { if ($component instanceof Component\Listing\Ordered) { $tpl_name = "tpl.ordered.html"; @@ -83,25 +84,22 @@ protected function renderSimple(Unordered|Ordered $component, RendererInterface $tpl = $this->getTemplate($tpl_name, true, true); - if (count($component->getItems()) > 0) { - foreach ($component->getItems() as $item) { - $tpl->setCurrentBlock("item"); - if (is_string($item)) { - $tpl->setVariable("ITEM", $item); - } else { - $tpl->setVariable("ITEM", $default_renderer->render($item)); - } - $tpl->parseCurrentBlock(); + foreach ($component->getItems() as $item) { + $tpl->setCurrentBlock("item"); + if (is_string($item)) { + $tpl->setVariable("ITEM", $item); + } else { + $tpl->setVariable("ITEM", $default_renderer->render($item)); } + $tpl->parseCurrentBlock(); } - $id = $this->bindJavaScript($component) ?? $this->createId(); - $tpl->setVariable('ID', $id); + $this->bindAndApplyJavaScript($component, $tpl); return $tpl->get(); } - protected function renderProperty( + protected function renderPropertyList( Component\Listing\Property $component, RendererInterface $default_renderer ): string { @@ -109,7 +107,7 @@ protected function renderProperty( foreach ($component->getItems() as $property) { list($label, $value, $show_label) = $property; - if (! is_string($value)) { + if (!is_string($value)) { $value = $default_renderer->render($value); } @@ -122,4 +120,9 @@ protected function renderProperty( } return $tpl->get(); } + + protected function bindAndApplyJavaScript(Component\JavaScriptBindable $component, Template $template): void + { + $template->setVariable('ID', $this->bindJavaScript($component) ?? $this->createId()); + } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/TableColumnContextRenderer.php b/components/ILIAS/UI/src/Implementation/Component/Listing/TableColumnContextRenderer.php new file mode 100644 index 000000000000..0aba961ecca0 --- /dev/null +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/TableColumnContextRenderer.php @@ -0,0 +1,83 @@ + + */ +class TableColumnContextRenderer extends Renderer +{ + protected const int LIST_DISPLAY_LIMIT = 3; + + public function registerResources(ResourceRegistry $registry): void + { + $registry->register('assets/js/listing.min.js'); + } + + protected function renderList(Ordered|Unordered $component, RendererInterface $default_renderer): string + { + if (self::LIST_DISPLAY_LIMIT >= count($component->getItems())) { + return parent::renderList($component, $default_renderer); + } + + $template = $this->getTemplate('tpl.table_column_context.html', true, true); + $template->setVariable('DISPLAY_LIMIT', self::LIST_DISPLAY_LIMIT); + $template->setVariable('SHOW_MORE_LABEL', sprintf( + $this->txt('show_x_items'), + count($component->getItems()) - self::LIST_DISPLAY_LIMIT + )); + + if ($component instanceof Ordered) { + $template->setVariable('LIST_TYPE', 'ol'); + } else { + $template->setVariable('LIST_TYPE', 'ul'); + } + + // array_values() ensures we can use $index for count + foreach (array_values($component->getItems()) as $index => $item) { + $template->setCurrentBlock("item"); + if (is_string($item)) { + $template->setVariable("ITEM", $item); + } else { + $template->setVariable("ITEM", $default_renderer->render($item)); + } + if (self::LIST_DISPLAY_LIMIT > $index) { + $template->setVariable('VISIBILITY', 'visible'); + } else { + $template->setVariable('VISIBILITY', 'hidden'); + } + $template->parseCurrentBlock(); + } + + $enriched_component = $component->withAdditionalOnLoadCode( + static fn($id) => "il.UI.Listing.createExpandableList('$id');", + ); + + $this->bindAndApplyJavaScript($enriched_component, $template); + + $this->toJS('show_x_items'); + $this->toJS('hide_x_items'); + + return $template->get(); + } +} diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/Column/Factory.php b/components/ILIAS/UI/src/Implementation/Component/Table/Column/Factory.php index e52ca6017efc..c46f15baf895 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Table/Column/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/Column/Factory.php @@ -81,18 +81,13 @@ public function link(string $title): Link return new Link($this->lng, $title); } - public function linkListing(string $title): LinkListing + public function listing(string $title): Listing { - return new LinkListing($this->lng, $title); + return new Listing($this->lng, $title); } public function breadcrumb(string $title): I\Breadcrumb { return new Breadcrumb($this->lng, $title); } - - public function listing(string $title): Listing - { - return new Listing($this->lng, $title); - } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/Column/LinkListing.php b/components/ILIAS/UI/src/Implementation/Component/Table/Column/LinkListing.php deleted file mode 100755 index 4687a98d3a26..000000000000 --- a/components/ILIAS/UI/src/Implementation/Component/Table/Column/LinkListing.php +++ /dev/null @@ -1,39 +0,0 @@ -getItems(); - $this->checkArgListElements("list items", $listing_items, Standard::class); - return $value; - } - -} diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/Column/Listing.php b/components/ILIAS/UI/src/Implementation/Component/Table/Column/Listing.php index 489e09648fdd..64e42b060507 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Table/Column/Listing.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/Column/Listing.php @@ -31,7 +31,7 @@ public function format($value): string|Component { $listing = $this->toArray($value); $this->checkArgListElements('value', $listing, [Ordered::class, Unordered::class]); - return $value->withIsTruncated(true); + return $value; } public function getOrderingLabels(): array diff --git a/components/ILIAS/UI/src/Implementation/Render/FSLoader.php b/components/ILIAS/UI/src/Implementation/Render/FSLoader.php index e7e5ff088705..851679081989 100755 --- a/components/ILIAS/UI/src/Implementation/Render/FSLoader.php +++ b/components/ILIAS/UI/src/Implementation/Render/FSLoader.php @@ -28,6 +28,7 @@ use ILIAS\UI\Implementation\Component\MessageBox\MessageBox; use ILIAS\UI\Implementation\Component\Input\Container\Form\Form; use ILIAS\UI\Implementation\Component\Menu\Menu; +use ILIAS\UI\Implementation\Component\Listing\Listing; /** * Loads renderers for components from the file system. @@ -51,6 +52,7 @@ public function __construct( private RendererFactory $message_box_renderer_factory, private RendererFactory $form_renderer_factory, private RendererFactory $menu_renderer_factory, + private RendererFactory $listing_renderer_factory, ) { } @@ -84,6 +86,10 @@ public function getRendererFactoryFor(Component $component): RendererFactory if ($component instanceof Button) { return $this->button_renderer_factory; } + if ($component instanceof Listing) { + return $this->listing_renderer_factory; + } + return $this->default_renderer_factory; } } diff --git a/components/ILIAS/UI/src/examples/Table/Column/LinkListing/base.php b/components/ILIAS/UI/src/examples/Table/Column/LinkListing/base.php deleted file mode 100755 index 5fd76c9f9538..000000000000 --- a/components/ILIAS/UI/src/examples/Table/Column/LinkListing/base.php +++ /dev/null @@ -1,86 +0,0 @@ - - * ILIAS shows the rendered Component. - * --- - */ -function base(): string -{ - global $DIC; - $f = $DIC->ui()->factory(); - $r = $DIC->ui()->renderer(); - - $columns = [ - 'l1' => $f->table()->column()->linkListing("a link list column") - ]; - - $some_link = $f->link()->standard('ILIAS Homepage', 'http://www.ilias.de'); - $some_linklisting = $f->listing()->unordered([$some_link, $some_link, $some_link]); - - $dummy_records = [ - ['l1' => $some_linklisting], - ['l1' => $some_linklisting] - ]; - - $data_retrieval = new class ($dummy_records) implements I\DataRetrieval { - protected array $records; - - public function __construct(array $dummy_records) - { - $this->records = $dummy_records; - } - - public function getRows( - I\DataRowBuilder $row_builder, - array $visible_column_ids, - Range $range, - Order $order, - mixed $additional_viewcontrol_data, - mixed $filter_data, - mixed $additional_parameters - ): \Generator { - foreach ($this->records as $idx => $record) { - $row_id = ''; - yield $row_builder->buildDataRow($row_id, $record); - } - } - - public function getTotalRowCount( - mixed $additional_viewcontrol_data, - mixed $filter_data, - mixed $additional_parameters - ): ?int { - return count($this->records); - } - }; - - $table = $f->table()->data($data_retrieval, 'Link List Columns', $columns) - ->withRequest($DIC->http()->request()); - return $r->render($table); -} diff --git a/components/ILIAS/UI/src/templates/default/Listing/tpl.table_column_context.html b/components/ILIAS/UI/src/templates/default/Listing/tpl.table_column_context.html new file mode 100644 index 000000000000..0cbb547f6c7e --- /dev/null +++ b/components/ILIAS/UI/src/templates/default/Listing/tpl.table_column_context.html @@ -0,0 +1,10 @@ +
+ <{LIST_TYPE} id="{ID}" data-max-items="{DISPLAY_LIMIT}"> + +
  • {ITEM}
  • + + + +
    diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index db25315571cc..98134aad39a5 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -17591,7 +17591,11 @@ ui#:#footer_link_groups#:#Footer Link-Gruppen ui#:#footer_links#:#Footer Links ui#:#footer_permanent_link#:#Footer Permanent Link ui#:#footer_texts#:#Footer Texte -ui#:#hide_items#:#%s ausblenden +ui#:#hide_x_items#:#%s Element(e) ausblenden +ui#:#image_alt_text#:#Alternativtext +ui#:#image_purpose_decorative#:#Dekoratives Bild +ui#:#image_purpose_informative#:#Informatives Bild +ui#:#image_purpose_user_defined#:#Verwendungszweck ui#:#label_fieldselection#:#Spaltenauswahl ui#:#label_fieldselection_refresh#:#Anwenden ui#:#label_modeviewcontrol#:#Anzeigemodus @@ -17612,7 +17616,7 @@ ui#:#presentation_table_expand#:#Alle zeigen ui#:#rating_average#:#Andere bewerteten mit %s von 5 ui#:#reset_stars#:#neutral ui#:#select_node#:#Knoten %s zur Auswahl hinzufügen -ui#:#show_all_items#:#Alle %s anzeigen +ui#:#show_x_items#:#%s Element(e) anzeigen ui#:#table_posinput_col_title#:#Position ui#:#ui_chars_max#:#Maximum: ui#:#ui_chars_min#:#Minimum: diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index 36470f6ac61e..e14bd4551630 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -17540,7 +17540,11 @@ ui#:#footer_link_groups#:#Footer Link-Groups ui#:#footer_links#:#Footer Links ui#:#footer_permanent_link#:#Footer Permanent Link ui#:#footer_texts#:#Footer Texts -ui#:#hide_items#:#Hide last %s +ui#:#hide_x_items#:#Hide %s item(s) +ui#:#image_alt_text#:#Alternate Text +ui#:#image_purpose_decorative#:#Decorative Image +ui#:#image_purpose_informative#:#Informative Image +ui#:#image_purpose_user_defined#:#Image Purpose ui#:#label_fieldselection#:#Field Selection ui#:#label_fieldselection_refresh#:#Apply ui#:#label_modeviewcontrol#:#view mode @@ -17561,7 +17565,7 @@ ui#:#presentation_table_expand#:#Expand All ui#:#rating_average#:#Others rated %s of 5 ui#:#reset_stars#:#neutral ui#:#select_node#:#Add node %s to selection -ui#:#show_all_items#:#Show all %s +ui#:#show_x_items#:#Show %s item(s) ui#:#table_posinput_col_title#:#Position ui#:#ui_chars_max#:#Maximum: ui#:#ui_chars_min#:#Minimum: From 24becf7ce503bab048d8c7bb6309bbcee9a67a0b Mon Sep 17 00:00:00 2001 From: Thibeau Fuhrer Date: Tue, 30 Jun 2026 10:40:16 +0200 Subject: [PATCH 7/7] [FIX] UI: adjust unit tests --- .../Component/Listing/Renderer.php | 5 +- .../default/Listing/tpl.ordered.html | 2 +- .../default/Listing/tpl.unordered.html | 2 +- components/ILIAS/UI/tests/Base.php | 10 ++ .../Table/Column/ColumnFactoryTest.php | 2 +- .../Component/Table/Column/ColumnTest.php | 52 +----- components/ILIAS/UI/tests/InitUIFramework.php | 10 ++ .../ComponentRendererFSLoaderTest.php | 154 ++++++++++++++++++ .../ILIAS/UI/tests/Renderer/FSLoaderTest.php | 3 + 9 files changed, 190 insertions(+), 50 deletions(-) create mode 100755 components/ILIAS/UI/tests/Renderer/ComponentRendererFSLoaderTest.php diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php index 1707cf3e0128..bb6d7223631a 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php @@ -123,6 +123,9 @@ protected function renderPropertyList( protected function bindAndApplyJavaScript(Component\JavaScriptBindable $component, Template $template): void { - $template->setVariable('ID', $this->bindJavaScript($component) ?? $this->createId()); + $id = $this->bindJavaScript($component); + if (null !== $id) { + $template->setVariable('ID', $id); + } } } diff --git a/components/ILIAS/UI/src/templates/default/Listing/tpl.ordered.html b/components/ILIAS/UI/src/templates/default/Listing/tpl.ordered.html index 588f8990eabf..f0797ccc861e 100755 --- a/components/ILIAS/UI/src/templates/default/Listing/tpl.ordered.html +++ b/components/ILIAS/UI/src/templates/default/Listing/tpl.ordered.html @@ -1,4 +1,4 @@ -
      +id="{ID}">
    1. {ITEM}
    2. diff --git a/components/ILIAS/UI/src/templates/default/Listing/tpl.unordered.html b/components/ILIAS/UI/src/templates/default/Listing/tpl.unordered.html index a47565f9864c..3239b2ed9924 100755 --- a/components/ILIAS/UI/src/templates/default/Listing/tpl.unordered.html +++ b/components/ILIAS/UI/src/templates/default/Listing/tpl.unordered.html @@ -1,4 +1,4 @@ -
        +id="{ID}">
      • {ITEM}
      • diff --git a/components/ILIAS/UI/tests/Base.php b/components/ILIAS/UI/tests/Base.php index a82b6f9d7ad7..0412bff2ac42 100755 --- a/components/ILIAS/UI/tests/Base.php +++ b/components/ILIAS/UI/tests/Base.php @@ -479,6 +479,16 @@ public function getDefaultRenderer( $data_factory, $help_text_retriever, $this->getUploadLimitResolver(), + ), + new I\Listing\ListingRendererFactory( + $ui_factory, + $tpl_factory, + $lng, + $js_binding, + $image_path_resolver, + $data_factory, + $help_text_retriever, + $this->getUploadLimitResolver(), ) ) ) diff --git a/components/ILIAS/UI/tests/Component/Table/Column/ColumnFactoryTest.php b/components/ILIAS/UI/tests/Component/Table/Column/ColumnFactoryTest.php index 5df1a67f979d..e916452323aa 100755 --- a/components/ILIAS/UI/tests/Component/Table/Column/ColumnFactoryTest.php +++ b/components/ILIAS/UI/tests/Component/Table/Column/ColumnFactoryTest.php @@ -67,7 +67,7 @@ public static function getColumnTypeProvider(): array [static fn($f) => [Column\StatusIcon::class, $f->statusIcon("")]], [static fn($f) => [Column\Link::class, $f->link("")]], [static fn($f) => [Column\EMail::class, $f->eMail("")]], - [static fn($f) => [Column\LinkListing::class, $f->linkListing("")]] + [static fn($f) => [Column\Listing::class, $f->listing("")]] ]; } diff --git a/components/ILIAS/UI/tests/Component/Table/Column/ColumnTest.php b/components/ILIAS/UI/tests/Component/Table/Column/ColumnTest.php index f72ac2d3997a..ab88d3f0a3dc 100755 --- a/components/ILIAS/UI/tests/Component/Table/Column/ColumnTest.php +++ b/components/ILIAS/UI/tests/Component/Table/Column/ColumnTest.php @@ -142,28 +142,6 @@ public function __construct() } }; return [ - [ - 'column' => new Column\LinkListing($lng, ''), - 'value' => (new Listing\Unordered([(new Link\Standard('label', '#')),(new Link\Standard('label', '#'))])) - ->withIsTruncated(true), - 'ok' => true - ], - [ - 'column' => new Column\LinkListing($lng, ''), - 'value' => (new Listing\Unordered(['string', 'string']))->withIsTruncated(true), - 'ok' => false - ], - [ - 'column' => new Column\LinkListing($lng, ''), - 'value' => (new Listing\Ordered([(new Link\Standard('label', '#')),(new Link\Standard('label', '#'))])) - ->withIsTruncated(true), - 'ok' => true - ], - [ - 'column' => new Column\LinkListing($lng, ''), - 'value' => 123, - 'ok' => false - ], [ 'column' => new Column\Link($lng, ''), 'value' => new Link\Standard('label', '#'), @@ -186,12 +164,12 @@ public function __construct() ], [ 'column' => new Column\Listing($lng, ''), - 'value' => (new Listing\Ordered(['1', '2', '3']))->withIsTruncated(true), + 'value' => (new Listing\Ordered(['1', '2', '3'])), 'ok' => true ], [ 'column' => new Column\Listing($lng, ''), - 'value' => (new Listing\Unordered(['1', '2', '3']))->withIsTruncated(true), + 'value' => (new Listing\Unordered(['1', '2', '3'])), 'ok' => true ], [ @@ -216,35 +194,17 @@ public function testDataTableColumnAllowedFormats( } - public function testDataTableColumnLinkListingFormat(): void + public function testDataTableColumnListingFormat(): void { - $col = new Column\LinkListing($this->lng, 'col'); + $col = new Column\Listing($this->lng, 'col'); $link = new Link\Standard('label', '#'); - $linklisting = (new Listing\Unordered([$link, $link, $link]))->withIsTruncated(true); + $linklisting = (new Listing\Unordered([$link, $link, $link])); $this->assertEquals($linklisting, $col->format($linklisting)); } - public function testDataTableColumnLinkListingFormatAcceptsOnlyLinkListings(): void - { - $this->expectException(\InvalidArgumentException::class); - $col = new Column\LinkListing($this->lng, 'col'); - $linklisting_invalid = new Link\Standard('label', '#'); - $this->assertEquals($linklisting_invalid, $col->format($linklisting_invalid)); - } - - public function testDataTableColumnLinkListingItemsFormatAcceptsOnlyLinks(): void - { - $this->expectException(\InvalidArgumentException::class); - $col = new Column\LinkListing($this->lng, 'col'); - $link = 'some string'; - $linklisting_invalid = new Listing\Unordered([$link, $link, $link]); - $this->assertEquals($linklisting_invalid, $col->format($linklisting_invalid)); - } - public function testDataTableColumnCustomOrderingLabels(): void { - $col = (new Column\LinkListing($this->lng, 'col')) - ->withIsSortable(true) + $col = (new Column\Listing($this->lng, 'col')) ->withOrderingLabels( 'custom label ASC', 'custom label DESC', diff --git a/components/ILIAS/UI/tests/InitUIFramework.php b/components/ILIAS/UI/tests/InitUIFramework.php index 95fae03ffecc..bed11dc6d6be 100755 --- a/components/ILIAS/UI/tests/InitUIFramework.php +++ b/components/ILIAS/UI/tests/InitUIFramework.php @@ -379,6 +379,16 @@ public function getRefreshIntervalInMs(): int $c["help.text_retriever"], $c["ui.upload_limit_resolver"] ), + new ILIAS\UI\Implementation\Component\Listing\ListingRendererFactory( + $c["ui.factory"], + $c["ui.template_factory"], + $c["lng"], + $c["ui.javascript_binding"], + $c["ui.pathresolver"], + $c["ui.data_factory"], + $c["help.text_retriever"], + $c["ui.upload_limit_resolver"], + ), ) ) ); diff --git a/components/ILIAS/UI/tests/Renderer/ComponentRendererFSLoaderTest.php b/components/ILIAS/UI/tests/Renderer/ComponentRendererFSLoaderTest.php new file mode 100755 index 000000000000..9bb3058f0c80 --- /dev/null +++ b/components/ILIAS/UI/tests/Renderer/ComponentRendererFSLoaderTest.php @@ -0,0 +1,154 @@ +getMockBuilder(ILIAS\UI\Implementation\FactoryInternal::class)->disableOriginalConstructor()->getMock(); + $tpl_factory = $this->getMockBuilder(I\Render\TemplateFactory::class)->getMock(); + $lng = $this->getMockBuilder(ILIAS\Language\Language::class)->disableOriginalConstructor()->getMock(); + $js_binding = $this->getMockBuilder(I\Render\JavaScriptBinding::class)->getMock(); + $image_path_resolver = $this->getMockBuilder(ILIAS\UI\Implementation\Render\ImagePathResolver::class) + ->getMock(); + $data_factory = $this->getMockBuilder(ILIAS\Data\Factory::class)->getMock(); + $help_text_retriever = $this->createMock(ILIAS\UI\HelpTextRetriever::class); + $upload_limit_resolver = $this->createMock(ILIAS\UI\Implementation\Component\Input\UploadLimitResolver::class); + + $default_renderer_factory = new I\Render\DefaultRendererFactory( + $ui_factory, + $tpl_factory, + $lng, + $js_binding, + $image_path_resolver, + $data_factory, + $help_text_retriever, + $upload_limit_resolver, + ); + $this->glyph_renderer = $this->createMock(I\Render\RendererFactory::class); + $this->icon_renderer = $this->createMock(I\Render\RendererFactory::class); + $messagebox_renderer = $this->createMock(I\Render\RendererFactory::class); + $form_renderer = $this->createMock(I\Render\RendererFactory::class); + $listing_renderer = $this->createMock(I\Render\RendererFactory::class); + + $field_renderer = $this->createMock(I\Render\RendererFactory::class); + return new FSLoader( + $default_renderer_factory, + $this->glyph_renderer, + $this->icon_renderer, + $field_renderer, + $messagebox_renderer, + $form_renderer, + $listing_renderer, + ); + } + + public function testGetRendererSuccessfully(): void + { + // There should be a renderer for Glyph... + $f = $this->getComponentRendererFSLoader(); + $component = new I\Component\Button\Standard("", ""); + $r = $f->getRendererFor($component, []); + $this->assertInstanceOf(I\Render\ComponentRenderer::class, $r); + } + + public function testGetRendererSuccessfullyExtra(): void + { + // There should be a renderer for Glyph... + $f = $this->getComponentRendererFSLoader(); + $component = new I\Component\Symbol\Glyph\Glyph("up", "up"); + $context = $this->createMock(Component::class); + $renderer = $this->createMock(I\Render\ComponentRenderer::class); + + $context_name = "foo"; + $context + ->expects($this->once()) + ->method("getCanonicalName") + ->willReturn($context_name); + + $this->glyph_renderer + ->expects($this->once()) + ->method("getRendererInContext") + ->with($component, [$context_name]) + ->willReturn($renderer); + + $r = $f->getRendererFor($component, [$context]); + + $this->assertEquals($renderer, $r); + } + + public function testGetRendererUsesRendererFactory(): void + { + $loader = $this->getMockBuilder(ILIAS\UI\Implementation\Render\FSLoader::class) + ->onlyMethods(["getRendererFactoryFor", "getContextNames"]) + ->disableOriginalConstructor() + ->getMock(); + $factory = $this->getMockBuilder(ILIAS\UI\Implementation\Render\RendererFactory::class) + ->getMock(); + + $rendered_component = $this->createMock(ILIAS\UI\Component\Component::class); + + $component1 = $this->createMock(ILIAS\UI\Component\Component::class); + $component2 = $this->createMock(ILIAS\UI\Component\Component::class); + $component_name1 = "COMPONENT 1"; + $component_name2 = "COMPONENT 2"; + + $loader + ->expects($this->once()) + ->method("getContextNames") + ->with([$component1, $component2]) + ->willReturn([$component_name1, $component_name2]); + + $loader + ->expects($this->once()) + ->method("getRendererFactoryFor") + ->with($rendered_component) + ->willReturn($factory); + + $renderer = $this->createMock(ComponentRenderer::class); + $factory + ->expects($this->once()) + ->method("getRendererInContext") + ->with($rendered_component, [$component_name1, $component_name2]) + ->willReturn($renderer); + + $renderer2 = $loader->getRendererFor($rendered_component, [$component1, $component2]); + $this->assertEquals($renderer, $renderer2); + } +} diff --git a/components/ILIAS/UI/tests/Renderer/FSLoaderTest.php b/components/ILIAS/UI/tests/Renderer/FSLoaderTest.php index 689fbfd80d76..634360242a54 100755 --- a/components/ILIAS/UI/tests/Renderer/FSLoaderTest.php +++ b/components/ILIAS/UI/tests/Renderer/FSLoaderTest.php @@ -41,6 +41,7 @@ class FSLoaderTest extends TestCase protected RendererFactory & MockObject $message_box_renderer_factory; protected RendererFactory & MockObject $form_renderer_factory; protected RendererFactory & MockObject $menu_renderer_factory; + protected RendererFactory & MockObject $list_renderer_factory; protected FSLoader $fs_loader; @@ -52,6 +53,7 @@ protected function setUp(): void $this->message_box_renderer_factory = $this->createMock(RendererFactory::class); $this->form_renderer_factory = $this->createMock(RendererFactory::class); $this->menu_renderer_factory = $this->createMock(RendererFactory::class); + $this->list_renderer_factory = $this->createMock(RendererFactory::class); $this->fs_loader = new FSLoader( $this->default_renderer_factory, @@ -60,6 +62,7 @@ protected function setUp(): void $this->message_box_renderer_factory, $this->form_renderer_factory, $this->menu_renderer_factory, + $this->list_renderer_factory, ); parent::setUp();