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 403eb59ea7e8..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], + ), ) ) ); @@ -593,6 +603,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..d93178929a16 --- /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(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 new file mode 100755 index 000000000000..bd092843e631 --- /dev/null +++ b/components/ILIAS/UI/resources/js/Listing/rollup.config.js @@ -0,0 +1,43 @@ +/** + * 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: '../../../../../../public/assets/js/listing.min.js', + 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/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 new file mode 100755 index 000000000000..5802d3f6c917 --- /dev/null +++ b/components/ILIAS/UI/resources/js/Listing/src/listing.js @@ -0,0 +1,27 @@ +/** + * 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 document from 'document'; +import createExpandableList from './createExpandableList.js'; + +il.UI = il.UI || {}; + +il.UI.Listing = { + createExpandableList: (id) => createExpandableList( + { txt: (key) => il.Language.txt(key) }, + document.getElementById(id), + ), +}; 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/Component/Table/Column/Factory.php b/components/ILIAS/UI/src/Component/Table/Column/Factory.php index d97c6b92ab7f..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; /** * --- diff --git a/components/ILIAS/UI/src/Component/Table/Column/LinkListing.php b/components/ILIAS/UI/src/Component/Table/Column/Listing.php similarity index 94% rename from components/ILIAS/UI/src/Component/Table/Column/LinkListing.php rename to components/ILIAS/UI/src/Component/Table/Column/Listing.php index b02c08c1cc01..fa80d9810da6 100755 --- a/components/ILIAS/UI/src/Component/Table/Column/LinkListing.php +++ b/components/ILIAS/UI/src/Component/Table/Column/Listing.php @@ -20,6 +20,6 @@ namespace ILIAS\UI\Component\Table\Column; -interface LinkListing extends Column +interface Listing extends Column { } diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/ListingRendererFactory.php b/components/ILIAS/UI/src/Implementation/Component/Listing/ListingRendererFactory.php new file mode 100644 index 000000000000..8ffbb8a2c766 --- /dev/null +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/ListingRendererFactory.php @@ -0,0 +1,54 @@ + + */ +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/Ordered.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Ordered.php index 6bbda158f88a..6d1f2bc3c6e7 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Ordered.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Ordered.php @@ -21,6 +21,7 @@ namespace ILIAS\UI\Implementation\Component\Listing; use ILIAS\UI\Component as C; +use ILIAS\UI\Implementation\Component\JavaScriptBindable; /** * Class Listing @@ -28,4 +29,5 @@ */ class Ordered extends Listing implements C\Listing\Ordered { + use JavaScriptBindable; } diff --git a/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Renderer.php index 1a2aced57f96..bb6d7223631a 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 @@ -35,23 +36,23 @@ class Renderer extends AbstractComponentRenderer */ 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->renderDescriptiveList($component, $default_renderer); } - if ($component instanceof Component\Listing\Property) { - return $this->renderProperty($component, $default_renderer); + if ($component instanceof Property) { + return $this->renderPropertyList($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->renderList($component, $default_renderer); } $this->cannotHandleComponent($component); } - protected function render_descriptive( - Component\Listing\Descriptive $component, + protected function renderDescriptiveList( + Descriptive $component, RendererInterface $default_renderer ): string { $tpl = $this->getTemplate("tpl.descriptive.html", true, true); @@ -73,34 +74,32 @@ protected function render_descriptive( return $tpl->get(); } - protected function render_simple(Component\Listing\Listing $component, RendererInterface $default_renderer): string + protected function renderList(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 (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(); } + + $this->bindAndApplyJavaScript($component, $tpl); + return $tpl->get(); } - protected function renderProperty( + protected function renderPropertyList( Component\Listing\Property $component, RendererInterface $default_renderer ): string { @@ -108,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); } @@ -121,4 +120,12 @@ protected function renderProperty( } return $tpl->get(); } + + protected function bindAndApplyJavaScript(Component\JavaScriptBindable $component, Template $template): void + { + $id = $this->bindJavaScript($component); + if (null !== $id) { + $template->setVariable('ID', $id); + } + } } 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/Listing/Unordered.php b/components/ILIAS/UI/src/Implementation/Component/Listing/Unordered.php index d983880eebf6..099c75b7c88d 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Listing/Unordered.php +++ b/components/ILIAS/UI/src/Implementation/Component/Listing/Unordered.php @@ -21,6 +21,7 @@ namespace ILIAS\UI\Implementation\Component\Listing; use ILIAS\UI\Component as C; +use ILIAS\UI\Implementation\Component\JavaScriptBindable; /** * Class Listing @@ -28,4 +29,5 @@ */ class Unordered extends Listing implements C\Listing\Unordered { + use JavaScriptBindable; } 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 7d8aaeb666e1..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,9 +81,9 @@ 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 diff --git a/components/ILIAS/UI/src/Implementation/Component/Table/Column/LinkListing.php b/components/ILIAS/UI/src/Implementation/Component/Table/Column/Listing.php similarity index 81% rename from components/ILIAS/UI/src/Implementation/Component/Table/Column/LinkListing.php rename to components/ILIAS/UI/src/Implementation/Component/Table/Column/Listing.php index 3d672128a001..64e42b060507 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Table/Column/LinkListing.php +++ b/components/ILIAS/UI/src/Implementation/Component/Table/Column/Listing.php @@ -21,19 +21,16 @@ namespace ILIAS\UI\Implementation\Component\Table\Column; use ILIAS\UI\Component\Table\Column as C; -use ILIAS\UI\Component\Link\Standard; use ILIAS\UI\Component\Listing\Ordered; use ILIAS\UI\Component\Listing\Unordered; use ILIAS\UI\Component\Component; -class LinkListing extends Column implements C\LinkListing +class Listing extends Column implements C\Listing { public function format($value): string|Component { $listing = $this->toArray($value); - $this->checkArgListElements("value", $listing, [Ordered::class, Unordered::class]); - $listing_items = $value->getItems(); - $this->checkArgListElements("list items", $listing_items, Standard::class); + $this->checkArgListElements('value', $listing, [Ordered::class, Unordered::class]); return $value; } 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/Listing/base.php similarity index 69% rename from components/ILIAS/UI/src/examples/Table/Column/LinkListing/base.php rename to components/ILIAS/UI/src/examples/Table/Column/Listing/base.php index 5fd76c9f9538..7dafa2327fa1 100755 --- a/components/ILIAS/UI/src/examples/Table/Column/LinkListing/base.php +++ b/components/ILIAS/UI/src/examples/Table/Column/Listing/base.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\UI\examples\Table\Column\LinkListing; +namespace ILIAS\UI\examples\Table\Column\Listing; use ILIAS\UI\Component\Table as I; use ILIAS\Data\Range; @@ -32,28 +32,39 @@ */ function base(): string { + /** @var \ILIAS\DI\Container $DIC */ global $DIC; $f = $DIC->ui()->factory(); $r = $DIC->ui()->renderer(); $columns = [ - 'l1' => $f->table()->column()->linkListing("a link list column") + 'l1' => $f->table()->column()->listing('A 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] + $records = [ + [ + 'l1' => $f->listing()->unordered([ + 'Apples', + 'Oranges', + 'Bananas', + 'Pears' + ]) + ], + [ + 'l1' => $f->listing()->unordered([ + 'Bun', + 'Croissant', + 'Pumpernickel' + ]) + ] ]; - $data_retrieval = new class ($dummy_records) implements I\DataRetrieval { + $data_retrieval = new class ($records) implements I\DataRetrieval { protected array $records; - public function __construct(array $dummy_records) + public function __construct(array $records) { - $this->records = $dummy_records; + $this->records = $records; } public function getRows( @@ -80,7 +91,7 @@ public function getTotalRowCount( } }; - $table = $f->table()->data($data_retrieval, 'Link List Columns', $columns) + $table = $f->table()->data($data_retrieval, 'List Columns', $columns) ->withRequest($DIC->http()->request()); return $r->render($table); } 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..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,5 +1,5 @@ -
    +id="{ID}">
  1. {ITEM}
  2. -
\ No newline at end of file + 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/components/ILIAS/UI/src/templates/default/Listing/tpl.unordered.html b/components/ILIAS/UI/src/templates/default/Listing/tpl.unordered.html index 594abfe9a720..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,5 +1,5 @@ - \ No newline at end of file + 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 8d85257fbf18..e916452323aa 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'; @@ -66,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 477cc2021d6a..ab88d3f0a3dc 100755 --- a/components/ILIAS/UI/tests/Component/Table/Column/ColumnTest.php +++ b/components/ILIAS/UI/tests/Component/Table/Column/ColumnTest.php @@ -143,45 +143,41 @@ public function __construct() }; return [ [ - 'column' => new Column\LinkListing($lng, ''), - 'value' => new Listing\Unordered([(new Link\Standard('label', '#')),(new Link\Standard('label', '#'))]), + 'column' => new Column\Link($lng, ''), + 'value' => new Link\Standard('label', '#'), 'ok' => true ], [ - 'column' => new Column\LinkListing($lng, ''), - 'value' => new Listing\Unordered(['string', 'string']), + 'column' => new Column\Link($lng, ''), + 'value' => 'some string', 'ok' => false ], [ - 'column' => new Column\LinkListing($lng, ''), - 'value' => new Listing\Ordered([(new Link\Standard('label', '#')),(new Link\Standard('label', '#'))]), + 'column' => new Column\StatusIcon($lng, ''), + 'value' => new StandardIcon('', '', 'small', false), 'ok' => true ], [ - 'column' => new Column\LinkListing($lng, ''), - 'value' => 123, + 'column' => new Column\StatusIcon($lng, ''), + 'value' => 'some string', 'ok' => false ], [ - 'column' => new Column\Link($lng, ''), - 'value' => new Link\Standard('label', '#'), + 'column' => new Column\Listing($lng, ''), + 'value' => (new Listing\Ordered(['1', '2', '3'])), 'ok' => true ], [ - 'column' => new Column\Link($lng, ''), - 'value' => 'some string', - 'ok' => false - ], - [ - 'column' => new Column\StatusIcon($lng, ''), - 'value' => new StandardIcon('', '', 'small', false), + 'column' => new Column\Listing($lng, ''), + 'value' => (new Listing\Unordered(['1', '2', '3'])), 'ok' => true ], [ - 'column' => new Column\StatusIcon($lng, ''), - 'value' => 'some string', + 'column' => new Column\Listing($lng, ''), + 'value' => 123, 'ok' => false ], + ]; } @@ -191,42 +187,24 @@ public function testDataTableColumnAllowedFormats( mixed $value, bool $ok ): void { - if(! $ok) { + if (! $ok) { $this->expectException(\InvalidArgumentException::class); } $this->assertEquals($value, $column->format($value)); } - 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]); + $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(); diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index 476a103a5fe0..98134aad39a5 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -17591,6 +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#:#hide_x_items#:#%s Element(e) ausblenden ui#:#image_alt_text#:#Alternativtext ui#:#image_purpose_decorative#:#Dekoratives Bild ui#:#image_purpose_informative#:#Informatives Bild @@ -17615,6 +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_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 512dd28c41af..e14bd4551630 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -17540,6 +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#:#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 @@ -17564,6 +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_x_items#:#Show %s item(s) ui#:#table_posinput_col_title#:#Position ui#:#ui_chars_max#:#Maximum: ui#:#ui_chars_min#:#Minimum: