From c49b31c2f04a4ef9a76a2a36d4bf437b8d0aa5e7 Mon Sep 17 00:00:00 2001 From: Thibeau Fuhrer Date: Tue, 19 May 2026 11:03:16 +0200 Subject: [PATCH 1/4] [FEATURE] UI: add `Symbol\Glyph` variations. * Copy: to indicate information can be copied (to the clipboard) * Share: to indicate information can be shared * QR-code: to indicate a QR-code can be shown --- .../UI/src/Component/Symbol/Glyph/Factory.php | 84 +++++++++++++++++++ .../UI/src/Component/Symbol/Glyph/Glyph.php | 3 + .../Component/Symbol/Glyph/Factory.php | 15 ++++ .../Component/Symbol/Glyph/Glyph.php | 3 + .../src/examples/Symbol/Glyph/Copy/copy.php | 51 +++++++++++ .../examples/Symbol/Glyph/QrCode/qr_code.php | 51 +++++++++++ .../src/examples/Symbol/Glyph/Share/share.php | 51 +++++++++++ .../templates/default/Symbol/tpl.glyph.html | 3 + lang/ilias_de.lang | 3 + lang/ilias_en.lang | 5 +- 10 files changed, 268 insertions(+), 1 deletion(-) create mode 100755 components/ILIAS/UI/src/examples/Symbol/Glyph/Copy/copy.php create mode 100755 components/ILIAS/UI/src/examples/Symbol/Glyph/QrCode/qr_code.php create mode 100755 components/ILIAS/UI/src/examples/Symbol/Glyph/Share/share.php diff --git a/components/ILIAS/UI/src/Component/Symbol/Glyph/Factory.php b/components/ILIAS/UI/src/Component/Symbol/Glyph/Factory.php index d145c1a27433..9e6260618b31 100755 --- a/components/ILIAS/UI/src/Component/Symbol/Glyph/Factory.php +++ b/components/ILIAS/UI/src/Component/Symbol/Glyph/Factory.php @@ -1631,4 +1631,88 @@ public function checked(): Glyph; * @return \ILIAS\UI\Component\Symbol\Glyph\Glyph */ public function unchecked(): Glyph; + + /** + * --- + * description: + * purpose: > + * The Copy Glyph is used to indicate the possibility of copying content + * (to the computers clipboard). + * composition: > + * The Copy Glyph uses the glyphicon-copy. + * effect: > + * When placed in a Button or Link, clicking copies the associated content + * (to the computers clipboard). + * rivals: + * Share Glyph: > + * This Glyph SHOULD be used if the information is meant to be shared + * with other/more means of transportaiton than just copying. + * + * context: + * - Transfering the permanent link of a Page inside the Footer. + * + * rules: + * accessibility: + * 1: > + * The aria-label SHOULD be 'Copy'. + * --- + * @return \ILIAS\UI\Component\Symbol\Glyph\Glyph + */ + public function copy(): Glyph; + + /** + * --- + * description: + * purpose: > + * The QR Code Glyph is used to indicate the possibility of displaying or generating + * a QR code for some given information. + * composition: > + * The QR Code Glyph uses the glyphicon-qrcode. + * effect: > + * When placed in a Button or Link, clicking opens a Modal or Prompt displaying + * the QR code for the given information. + * + * context: + * - Transfering the permanent link of a Page inside the Footer. + * + * rules: + * accessibility: + * 1: > + * The aria-label SHOULD be 'Show QR-code'. + * --- + * @return \ILIAS\UI\Component\Symbol\Glyph\Glyph + */ + public function qrCode(): Glyph; + + /** + * --- + * description: + * purpose: > + * The Share Glyph is used to indicate the possibility of sharing information + * with other persons. + * composition: > + * The Share Glyph uses the glyphicon-share. + * effect: > + * When placed in a Button or Link, clicking opens a dialog where the information + * can be shared, e.g. using the Web Share API. + * rivals: + * Copy Glyph: > + * This Glyph SHOULD be used if the information is only copied. + * QR Code Glyph: > + * This Glyph SHOULD be used if the information is only shared using a QR-code. + * + * context: + * - Transfering the permanent link of a Page inside the Footer. + * + * background: > + * https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API + * + * rules: + * accessibility: + * 1: > + * The aria-label SHOULD be 'Share'. + * --- + * @return \ILIAS\UI\Component\Symbol\Glyph\Glyph + */ + public function share(): Glyph; } diff --git a/components/ILIAS/UI/src/Component/Symbol/Glyph/Glyph.php b/components/ILIAS/UI/src/Component/Symbol/Glyph/Glyph.php index 40bc0fe6b384..d725fbe4994e 100755 --- a/components/ILIAS/UI/src/Component/Symbol/Glyph/Glyph.php +++ b/components/ILIAS/UI/src/Component/Symbol/Glyph/Glyph.php @@ -86,6 +86,9 @@ interface Glyph extends Symbol public const DRAG_HANDLE = "dragHandle"; public const CHECKED = "checked"; public const UNCHECKED = "unchecked"; + public const COPY = 'copy'; + public const QR_CODE = 'qrCode'; + public const SHARE = 'share'; /** * Get the type of the glyph. diff --git a/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Factory.php b/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Factory.php index c55ad835f9cd..679f61dff6a7 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Factory.php @@ -323,4 +323,19 @@ public function unchecked(): G\Glyph { return new Glyph(G\Glyph::UNCHECKED, "unchecked"); } + + public function copy(): G\Glyph + { + return new Glyph(G\Glyph::COPY, 'copy'); + } + + public function qrCode(): G\Glyph + { + return new Glyph(G\Glyph::QR_CODE, 'show_qr_code'); + } + + public function share(): G\Glyph + { + return new Glyph(G\Glyph::SHARE, 'share'); + } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Glyph.php b/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Glyph.php index 2f5836091f39..76842a0494b0 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Glyph.php +++ b/components/ILIAS/UI/src/Implementation/Component/Symbol/Glyph/Glyph.php @@ -91,6 +91,9 @@ class Glyph implements C\Symbol\Glyph\Glyph self::DRAG_HANDLE, self::CHECKED, self::UNCHECKED, + self::COPY, + self::QR_CODE, + self::SHARE, ]; private string $type; diff --git a/components/ILIAS/UI/src/examples/Symbol/Glyph/Copy/copy.php b/components/ILIAS/UI/src/examples/Symbol/Glyph/Copy/copy.php new file mode 100755 index 000000000000..e421dbf5187c --- /dev/null +++ b/components/ILIAS/UI/src/examples/Symbol/Glyph/Copy/copy.php @@ -0,0 +1,51 @@ + + * Example for rendering a Copy Glyph. + * + * expected output: > + * Standard: + * ILIAS shows a monochrome copy symbol on a grey background. + * + * Highlighted: + * ILIAS shows the same symbol, but it's highlighted particularly. + * --- + */ +function copy(): string +{ + global $DIC; + $f = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + + $glyph = $f->symbol()->glyph()->copy(); + + //Showcase the various states of this Glyph + $list = $f->listing()->descriptive([ + "Standard" => $glyph, + "Highlighted" => $glyph->withHighlight(), + ]); + + return $renderer->render($list); +} diff --git a/components/ILIAS/UI/src/examples/Symbol/Glyph/QrCode/qr_code.php b/components/ILIAS/UI/src/examples/Symbol/Glyph/QrCode/qr_code.php new file mode 100755 index 000000000000..720a6910cba1 --- /dev/null +++ b/components/ILIAS/UI/src/examples/Symbol/Glyph/QrCode/qr_code.php @@ -0,0 +1,51 @@ + + * Example for rendering a QR Code Glyph. + * + * expected output: > + * Standard: + * ILIAS shows a monochrome QR-Code symbol on a grey background. + * + * Highlighted: + * ILIAS shows the same symbol, but it's highlighted particularly. + * --- + */ +function qr_code(): string +{ + global $DIC; + $f = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + + $glyph = $f->symbol()->glyph()->qrCode(); + + //Showcase the various states of this Glyph + $list = $f->listing()->descriptive([ + "Standard" => $glyph, + "Highlighted" => $glyph->withHighlight(), + ]); + + return $renderer->render($list); +} diff --git a/components/ILIAS/UI/src/examples/Symbol/Glyph/Share/share.php b/components/ILIAS/UI/src/examples/Symbol/Glyph/Share/share.php new file mode 100755 index 000000000000..89c8f7f2dd57 --- /dev/null +++ b/components/ILIAS/UI/src/examples/Symbol/Glyph/Share/share.php @@ -0,0 +1,51 @@ + + * Example for rendering a Share Glyph. + * + * expected output: > + * Standard: + * ILIAS shows a monochrome share symbol on a grey background. + * + * Highlighted: + * ILIAS shows the same symbol, but it's highlighted particularly. + * --- + */ +function share(): string +{ + global $DIC; + $f = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + + $glyph = $f->symbol()->glyph()->share(); + + //Showcase the various states of this Glyph + $list = $f->listing()->descriptive([ + "Standard" => $glyph, + "Highlighted" => $glyph->withHighlight(), + ]); + + return $renderer->render($list); +} diff --git a/components/ILIAS/UI/src/templates/default/Symbol/tpl.glyph.html b/components/ILIAS/UI/src/templates/default/Symbol/tpl.glyph.html index 324913696fd8..187a5bd20989 100755 --- a/components/ILIAS/UI/src/templates/default/Symbol/tpl.glyph.html +++ b/components/ILIAS/UI/src/templates/default/Symbol/tpl.glyph.html @@ -59,6 +59,9 @@ glyphicon-dragHandle glyphicon-checked glyphicon-unchecked + glyphicon-copy + glyphicon-qrcode + glyphicon-share " aria-hidden="true"> diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index bd0edfc49499..7ba9cd4d91c1 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -17510,6 +17510,7 @@ ui#:#2stars#:#zwei von fünf Sternen ui#:#3stars#:#drei von fünf Sternen ui#:#4stars#:#vier von fünf Sternen ui#:#5stars#:#fünf von fünf Sternen +ui#:#copy#:#Copy ui#:#datatable_close_warning#:#OK ui#:#datatable_multiaction_label#:#Sammelaktionen ui#:#datatable_multiactionmodal_actionlabel#:#Aktion für alle Einträge @@ -17551,6 +17552,8 @@ 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#:#share#:#Share +ui#:#show_qr_code#:#Show QR-code 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 b6abfd46ef9b..2727884aa310 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -13392,7 +13392,7 @@ pd#:#pd_view_select_at_least_one#:#You have to select at least one view, either pdesk#:#bookmark_moved_ok#:#Bookmark has been moved. pdesk#:#bookmark_select_target#:#Please select target. poll#:#poll_absolute#:#Current Votes -poll#:#poll_activation_online_info#:#Make this poll accessible to users. +poll#:#poll_activation_online_info#:#Make this poll accessible to users. poll#:#poll_add#:#Add Poll poll#:#poll_anonymous_warning#:#This is an anonymous poll. Individual votes are recorded, but your name will not be shown in the poll results. poll#:#poll_answer#:#Answer @@ -17462,6 +17462,7 @@ ui#:#2stars#:#two of five stars ui#:#3stars#:#three of five stars ui#:#4stars#:#four of five stars ui#:#5stars#:#five of five stars +ui#:#copy#:#Copy ui#:#datatable_close_warning#:#OK ui#:#datatable_multiaction_label#:#Bulk Actions ui#:#datatable_multiactionmodal_actionlabel#:#Action for All Entries @@ -17503,6 +17504,8 @@ 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#:#share#:#Share +ui#:#show_qr_code#:#Show QR-code ui#:#table_posinput_col_title#:#Position ui#:#ui_chars_max#:#Maximum: ui#:#ui_chars_min#:#Minimum: From be4cd544411e2ae65dae999a767422a4c3a1734e Mon Sep 17 00:00:00 2001 From: Thibeau Fuhrer Date: Tue, 19 May 2026 16:36:20 +0200 Subject: [PATCH 2/4] [FEATURE] UI: add `UI\Component\Transfer\Link` component and family. * Add `UI\Component\Transfer` component family, for components to transfer specific information from one medium or context to another, using different means of transportation. * Add `UI\Component\Transfer\Link` component, for transferring links such as the permanent-link. --- components/ILIAS/UI/UI.php | 12 ++ components/ILIAS/UI/docs/ROADMAP.md | 31 +++ .../ILIAS/UI/resources/js/Core/src/sleep.js | 37 ++++ .../js/Transfer/dist/transfer.min.js | 15 ++ .../UI/resources/js/Transfer/rollup.config.js | 47 +++++ .../UI/resources/js/Transfer/src/constants.js | 37 ++++ .../js/Transfer/src/createLinkTransfer.js | 75 ++++++++ .../js/Transfer/src/createTransferButton.js | 116 ++++++++++++ .../UI/resources/js/Transfer/src/facade.js | 30 +++ .../Transfer/src/performClipboardTransfer.js | 24 +++ .../Transfer/src/performWebShareTransfer.js | 28 +++ .../UI/src/Component/Transfer/Factory.php | 74 ++++++++ .../HasAdditionalTransferMechanism.php | 27 +++ .../ILIAS/UI/src/Component/Transfer/Link.php | 24 +++ .../UI/src/Component/Transfer/Transfer.php | 28 +++ .../Component/Transfer/TransferMechanism.php | 36 ++++ components/ILIAS/UI/src/Factory.php | 36 ++++ .../Button/ButtonRendererFactory.php | 1 + .../Container/Form/FormRendererFactory.php | 6 +- .../Input/Field/FieldRendererFactory.php | 9 +- .../Component/Menu/MenuRendererFactory.php | 1 + .../MessageBox/MessageBoxRendererFactory.php | 6 +- .../Component/Transfer/Factory.php | 32 ++++ .../HasAdditionalTransferMechanisms.php | 68 +++++++ .../Component/Transfer/Link.php | 51 +++++ .../Component/Transfer/Renderer.php | 179 ++++++++++++++++++ .../ILIAS/UI/src/Implementation/Factory.php | 5 + .../UI/src/Implementation/FactoryInternal.php | 3 + .../Render/AbstractComponentRenderer.php | 6 + .../Render/DefaultRendererFactory.php | 2 + .../UI/src/examples/Transfer/Link/base.php | 50 +++++ .../with_additional_transfer_mechanism.php | 51 +++++ .../templates/default/Transfer/tpl.link.html | 11 ++ .../default/Transfer/tpl.transfer_button.html | 14 ++ components/ILIAS/UI/tests/Base.php | 3 + components/ILIAS/UI/tests/InitUIFramework.php | 19 +- lang/ilias_de.lang | 8 + lang/ilias_en.lang | 8 + .../Transfer/_ui-component_transfer-link.scss | 29 +++ templates/default/070-components/_index.scss | 1 + templates/default/delos.css | 21 ++ 41 files changed, 1248 insertions(+), 13 deletions(-) create mode 100644 components/ILIAS/UI/resources/js/Core/src/sleep.js create mode 100644 components/ILIAS/UI/resources/js/Transfer/dist/transfer.min.js create mode 100644 components/ILIAS/UI/resources/js/Transfer/rollup.config.js create mode 100644 components/ILIAS/UI/resources/js/Transfer/src/constants.js create mode 100644 components/ILIAS/UI/resources/js/Transfer/src/createLinkTransfer.js create mode 100644 components/ILIAS/UI/resources/js/Transfer/src/createTransferButton.js create mode 100644 components/ILIAS/UI/resources/js/Transfer/src/facade.js create mode 100644 components/ILIAS/UI/resources/js/Transfer/src/performClipboardTransfer.js create mode 100644 components/ILIAS/UI/resources/js/Transfer/src/performWebShareTransfer.js create mode 100644 components/ILIAS/UI/src/Component/Transfer/Factory.php create mode 100644 components/ILIAS/UI/src/Component/Transfer/HasAdditionalTransferMechanism.php create mode 100644 components/ILIAS/UI/src/Component/Transfer/Link.php create mode 100644 components/ILIAS/UI/src/Component/Transfer/Transfer.php create mode 100644 components/ILIAS/UI/src/Component/Transfer/TransferMechanism.php create mode 100644 components/ILIAS/UI/src/Implementation/Component/Transfer/Factory.php create mode 100644 components/ILIAS/UI/src/Implementation/Component/Transfer/HasAdditionalTransferMechanisms.php create mode 100644 components/ILIAS/UI/src/Implementation/Component/Transfer/Link.php create mode 100644 components/ILIAS/UI/src/Implementation/Component/Transfer/Renderer.php create mode 100644 components/ILIAS/UI/src/examples/Transfer/Link/base.php create mode 100644 components/ILIAS/UI/src/examples/Transfer/Link/with_additional_transfer_mechanism.php create mode 100644 components/ILIAS/UI/src/templates/default/Transfer/tpl.link.html create mode 100644 components/ILIAS/UI/src/templates/default/Transfer/tpl.transfer_button.html create mode 100644 templates/default/070-components/UI-framework/Transfer/_ui-component_transfer-link.scss diff --git a/components/ILIAS/UI/UI.php b/components/ILIAS/UI/UI.php index 2deadccaab62..9032206b83e8 100644 --- a/components/ILIAS/UI/UI.php +++ b/components/ILIAS/UI/UI.php @@ -214,6 +214,7 @@ public function init( $internal[UI\Implementation\Component\Entity\Factory::class], $internal[UI\Implementation\Component\Prompt\Factory::class], $internal[UI\Implementation\Component\Navigation\Factory::class], + $internal[UI\Implementation\Component\Transfer\Factory::class], ); $internal[UI\Implementation\Component\Counter\Factory::class] = static fn() => @@ -480,6 +481,9 @@ public function init( $use[UI\Storage::class], ); + $internal[UI\Implementation\Component\Transfer\Factory::class] = static fn() => + new UI\Implementation\Component\Transfer\Factory(); + $internal[UI\Implementation\DefaultRenderer::class] = static fn() => new UI\Implementation\DefaultRenderer( $internal[UI\Implementation\Render\Loader::class], @@ -500,6 +504,7 @@ public function init( $pull[Data\Factory::class], $use[UI\HelpTextRetriever::class], $internal[UI\Implementation\Component\Input\UploadLimitResolver::class], + $pull[Refinery\Factory::class], ), new UI\Implementation\Component\Button\ButtonRendererFactory( $use[UI\Implementation\FactoryInternal::class], @@ -510,6 +515,7 @@ public function init( $pull[Data\Factory::class], $use[UI\HelpTextRetriever::class], $internal[UI\Implementation\Component\Input\UploadLimitResolver::class], + $pull[Refinery\Factory::class], ), new UI\Implementation\Component\Input\Field\FieldRendererFactory( $use[UI\Implementation\FactoryInternal::class], @@ -520,6 +526,7 @@ public function init( $pull[Data\Factory::class], $use[UI\HelpTextRetriever::class], $internal[UI\Implementation\Component\Input\UploadLimitResolver::class], + $pull[Refinery\Factory::class], ), new UI\Implementation\Component\MessageBox\MessageBoxRendererFactory( $use[UI\Implementation\FactoryInternal::class], @@ -530,6 +537,7 @@ public function init( $pull[Data\Factory::class], $use[UI\HelpTextRetriever::class], $internal[UI\Implementation\Component\Input\UploadLimitResolver::class], + $pull[Refinery\Factory::class], ), new UI\Implementation\Component\Input\Container\Form\FormRendererFactory( $use[UI\Implementation\FactoryInternal::class], @@ -540,6 +548,7 @@ public function init( $pull[Data\Factory::class], $use[UI\HelpTextRetriever::class], $internal[UI\Implementation\Component\Input\UploadLimitResolver::class], + $pull[Refinery\Factory::class], ), new UI\Implementation\Component\Menu\MenuRendererFactory( $use[UI\Implementation\FactoryInternal::class], @@ -550,6 +559,7 @@ public function init( $pull[Data\Factory::class], $use[UI\HelpTextRetriever::class], $internal[UI\Implementation\Component\Input\UploadLimitResolver::class], + $pull[Refinery\Factory::class], ), ) ) @@ -637,6 +647,8 @@ public function init( new Component\Resource\ComponentJS($this, "js/Input/ViewControl/dist/input.viewcontrols.min.js"); $contribute[Component\Resource\PublicAsset::class] = fn() => new Component\Resource\ComponentJS($this, "js/MathJax/mathjax_config.js"); + $contribute[Component\Resource\PublicAsset::class] = fn() => + new Component\Resource\ComponentJS($this, "js/Transfer/dist/transfer.min.js"); /* those are contributed by MediaObjects diff --git a/components/ILIAS/UI/docs/ROADMAP.md b/components/ILIAS/UI/docs/ROADMAP.md index 24b18d5d43e6..c2c8acf54190 100755 --- a/components/ILIAS/UI/docs/ROADMAP.md +++ b/components/ILIAS/UI/docs/ROADMAP.md @@ -327,6 +327,37 @@ with the file upload library, where working around these constraints made mainte more expensive than it needed to be. To avoid the same fate here, we should replace the library with a custom implementation sometime soon. +### Streamline template-embedding mechanisms (expert, ~5d) + +There are several different problems that occur during the rendering of a component where solutions require +to embed content into the components actual template: + +* `UI\Component\HasHelpTopics`: tooltip embedding, encapsulated in a dedicated renderer class +* `UI\Component\JavaScriptBindable`: JavaScript embedding (currently only an ID attribute, but this will change in the +future), implemented entirely inside the `UI\Render\AbstractComponentRenderer`. +* Dynamic heading levels: not yet implemented, but will require to embed the correct heading-level (h1-h6). +* Possibly others: there are probably other solutions which are not (yet) implemented centrally. + +These mechanisms should be analysed and streamlined so there is a single documented mechanism that can be used for +all of the above solutions to embed something into the actual template of a component. During this analysis it must +be considered, that we may change our template engine soon and that a migration of the new mechanism to a possible +engine like Twig or Blade is possible. Its even possible this solution becomes obsolete if we switch to such template +engines. + +### Introduce client-side configuration object (advanced, ~5d) + +Client-side configuration values, such as debounce delays or visibility durations, are currently hardcoded as inline +constants – scattered across our JavaScript code-base. For configuration values like this on the server we have already +a concept in place, where we hide values like this behind dedicated interfaces, so potentially the system can offer +real implementations to alter some values. This pattern was established during the migration of the UI framework towards +the new component bootstrap mechanism. Since configuration values on the client are currently left behind, we should +create a concept for the integration of these values into the bootstrap mechanism as well. The difficulty here lies in +how we transfer these values from the server to our client in a sophisticated manner. Since values like this do not +change once the application is built, it only makes sense to compile them into an artifact. Ideally this artifact would +contain an object that is exported from a `.js` file, which can be imported directly by our ES6 modules (components). +Alternatively, we build this as a string and store this in an `.php` artifact and ship its content via our component +renderers, so it is available as global state (e.g. `il.UI.Config`). + ## Long Term ### Mark Some Components as Internal diff --git a/components/ILIAS/UI/resources/js/Core/src/sleep.js b/components/ILIAS/UI/resources/js/Core/src/sleep.js new file mode 100644 index 000000000000..ee6815f6e504 --- /dev/null +++ b/components/ILIAS/UI/resources/js/Core/src/sleep.js @@ -0,0 +1,37 @@ +/** + * 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 + */ + +/** + * Pauses the execution of an async function for the given duration. + * + * @example: + * async function someAction() { + * // stops someAction() for 2 seconds: + * await sleep(2_000); + * } + * + * @param {number} durationInMs + * @returns {Promise} + */ +export default function sleep(durationInMs) { + if (durationInMs <= 0) { + throw new TypeError('Duration must be greater than 0.'); + } + return new Promise((resolve) => { + setTimeout(resolve, durationInMs); + }); +} diff --git a/components/ILIAS/UI/resources/js/Transfer/dist/transfer.min.js b/components/ILIAS/UI/resources/js/Transfer/dist/transfer.min.js new file mode 100644 index 000000000000..97a6d645b558 --- /dev/null +++ b/components/ILIAS/UI/resources/js/Transfer/dist/transfer.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,r,t){"use strict";const n="hidden",a="visible",o="clipboard",s="web-share",c="data-transfer-status",i="data-transfer-type",u=`[${i}]`,f=`[${c}="default"]`,l=`[${c}="success"]`,w=`[${c}="failure"]`;const d=1300,y=new Set;function h(e){return e.classList.replace(n,a)}function p(e){return e.classList.replace(a,n)}async function b(e,r){var t;p(e),h(r),await(t=d,new Promise((e=>{setTimeout(e,t)}))),p(r),h(e)}function T(e,r,t,n){const a=t.querySelector(f),o=t.querySelector(l),s=t.querySelector(w);if(!a||!o||!s)throw new Error("Transfer: one or more status elements not found.");t.addEventListener("click",(()=>{!async function(e,r,t,n,a,o,s){if(!y.has(t))try{y.add(t),await r(n),await b(a,o)}catch(e){await b(a,s)}finally{y.delete(t)}}(0,r,t,n,a,o,s)}))}function m(e,r,t){switch(t){case o:return r=>function(e,r){return e.clipboard.writeText(r)}(e,r);case s:return t=>function(e,r){if(!e.canShare(r))throw new Error("Transfer: web share api is not available.");return e.share(r)}(e,function(e,r){return{text:e.labels[0]?.textContent??"",url:r}}(r,t));default:throw new Error(`Transfer: unknown transfer type '${t}'.`)}}e.UI=e.UI||{},e.UI.Transfer={createLinkTransfer:e=>function(e,r){const t=r.querySelector(".c-transfer__payload");if(!t)throw new Error("Transfer: payload input not found.");Array.from(r.querySelectorAll(u)).forEach((r=>{const n=r.getAttribute(i);T(0,m(e,t,n),r,t.value??"")}))}(r.navigator,t.getElementById(e))}}(il,window,document); diff --git a/components/ILIAS/UI/resources/js/Transfer/rollup.config.js b/components/ILIAS/UI/resources/js/Transfer/rollup.config.js new file mode 100644 index 000000000000..d0c294c842e1 --- /dev/null +++ b/components/ILIAS/UI/resources/js/Transfer/rollup.config.js @@ -0,0 +1,47 @@ +/** + * 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 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/facade.js', + external: [ + 'ilias', + 'document', + 'window', + ], + output: { + // file: '../../../../../../public/assets/js/transfer.min.js', + file: './dist/transfer.min.js', + format: 'iife', + banner: copyright, + globals: { + ilias: 'il', + document: 'document', + window: 'window', + }, + plugins: [ + terser({ + format: { + comments: preserveCopyright, + }, + }), + ], + }, +}; diff --git a/components/ILIAS/UI/resources/js/Transfer/src/constants.js b/components/ILIAS/UI/resources/js/Transfer/src/constants.js new file mode 100644 index 000000000000..e1e246c7efb5 --- /dev/null +++ b/components/ILIAS/UI/resources/js/Transfer/src/constants.js @@ -0,0 +1,37 @@ +/** + * 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 + */ + +export const HIDDEN_CLASS = 'hidden'; +export const VISIBLE_CLASS = 'visible'; + +export const DEFAULT_STATUS = 'default'; +export const SUCCESS_STATUS = 'success'; +export const FAILURE_STATUS = 'failure'; + +export const CLIPBOARD_TRANSFER_TYPE = 'clipboard'; +export const WEB_SHARE_TRANSFER_TYPE = 'web-share'; +export const QR_CODE_TRANSFER_TYPE = 'qr-code'; + +export const TRANSFER_STATUS_ATTRIBUTE = 'data-transfer-status'; +export const TRANSFER_TYPE_ATTRIBUTE = 'data-transfer-type'; + +export const TRANSFER_SELECTOR = '.c-transfer'; +export const PAYLOAD_SELECTOR = `${TRANSFER_SELECTOR}__payload`; +export const TRANSFER_BUTTON_SELECTOR = `[${TRANSFER_TYPE_ATTRIBUTE}]`; +export const DEFAULT_STATUS_SELECTOR = `[${TRANSFER_STATUS_ATTRIBUTE}="${DEFAULT_STATUS}"]`; +export const SUCCESS_STATUS_SELECTOR = `[${TRANSFER_STATUS_ATTRIBUTE}="${SUCCESS_STATUS}"]`; +export const FAILURE_STATUS_SELECTOR = `[${TRANSFER_STATUS_ATTRIBUTE}="${FAILURE_STATUS}"]`; diff --git a/components/ILIAS/UI/resources/js/Transfer/src/createLinkTransfer.js b/components/ILIAS/UI/resources/js/Transfer/src/createLinkTransfer.js new file mode 100644 index 000000000000..a040822b948d --- /dev/null +++ b/components/ILIAS/UI/resources/js/Transfer/src/createLinkTransfer.js @@ -0,0 +1,75 @@ +/** + * 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 * as CONSTANTS from './constants.js'; +import createTransferButton from './createTransferButton.js'; +import performClipboardTransfer from './performClipboardTransfer.js'; +import performWebShareTransfer from './performWebShareTransfer.js'; + +/** + * @param {HTMLInputElement} payloadInput + * @param {string} payload + * @returns {{text: string, url: string}} + */ +function createWebShareTransferPayload(payloadInput, payload) { + return { + text: payloadInput.labels[0]?.textContent ?? '', + url: payload, + }; +} + +/** + * @param {Navigator} navigator + * @param {HTMLInputElement} payloadInput + * @param {string} transferType + * @returns {function(string): Promise} + */ +function createTransferCallbackForType(navigator, payloadInput, transferType) { + switch (transferType) { + case CONSTANTS.CLIPBOARD_TRANSFER_TYPE: + return (payload) => performClipboardTransfer(navigator, payload); + case CONSTANTS.WEB_SHARE_TRANSFER_TYPE: + return (payload) => performWebShareTransfer( + navigator, + createWebShareTransferPayload(payloadInput, payload), + ); + default: + throw new Error(`Transfer: unknown transfer type '${transferType}'.`); + } +} + +/** + * @param {Navigator} navigator + * @param {HTMLElement} transferElement + */ +export default function createLinkTransfer(navigator, transferElement) { + const payloadInput = transferElement.querySelector(CONSTANTS.PAYLOAD_SELECTOR); + if (!(payloadInput)) { + throw new Error('Transfer: payload input not found.'); + } + Array + .from(transferElement.querySelectorAll(CONSTANTS.TRANSFER_BUTTON_SELECTOR)) + .forEach((transferButton) => { + const transferType = transferButton.getAttribute(CONSTANTS.TRANSFER_TYPE_ATTRIBUTE); + createTransferButton( + navigator, + createTransferCallbackForType(navigator, payloadInput, transferType), + transferButton, + payloadInput.value ?? '', + ); + }); +} diff --git a/components/ILIAS/UI/resources/js/Transfer/src/createTransferButton.js b/components/ILIAS/UI/resources/js/Transfer/src/createTransferButton.js new file mode 100644 index 000000000000..7f20daa511d2 --- /dev/null +++ b/components/ILIAS/UI/resources/js/Transfer/src/createTransferButton.js @@ -0,0 +1,116 @@ +/** + * 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 * as CONSTANTS from './constants.js'; +import sleep from '../../Core/src/sleep.js'; + +/** @type {number} defines how long a success/failure status is visible. */ +const STATUS_DURATION_IN_MS = 1_300; + +/** @type {Set} holds buttons which temporarily show status. */ +const busyButtonSet = new Set(); + +/** @param {HTMLElement} element */ +function showElement(element) { + return element.classList.replace(CONSTANTS.HIDDEN_CLASS, CONSTANTS.VISIBLE_CLASS); +} + +/** @param {HTMLElement} element */ +function hideElement(element) { + return element.classList.replace(CONSTANTS.VISIBLE_CLASS, CONSTANTS.HIDDEN_CLASS); +} + +/** + * Hides the default status and shows the given temporary status, then reverses it. + * + * @param {HTMLElement} defaultStatus + * @param {HTMLElement} temporaryStatus + */ +async function showStatusTemporarily(defaultStatus, temporaryStatus) { + hideElement(defaultStatus); + showElement(temporaryStatus); + await sleep(STATUS_DURATION_IN_MS); + hideElement(temporaryStatus); + showElement(defaultStatus); +} + +/** + * Copies the given payload to the computers clipboard and updates the + * temporary status accordingly. + * + * @param {Navigator} navigator + * @param {function(string): Promise} performTransferCallback + * @param {HTMLButtonElement} transferButton + * @param {string} payload + * @param {HTMLElement} defaultStatusElement + * @param {HTMLElement} successStatusElement + * @param {HTMLElement} failureStatusElement + * @returns {Promise} + */ +async function performTransfer( + navigator, + performTransferCallback, + transferButton, + payload, + defaultStatusElement, + successStatusElement, + failureStatusElement, +) { + if (busyButtonSet.has(transferButton)) { + return; + } + try { + busyButtonSet.add(transferButton); + await performTransferCallback(payload); + await showStatusTemporarily(defaultStatusElement, successStatusElement); + } catch (error) { + await showStatusTemporarily(defaultStatusElement, failureStatusElement); + } finally { + busyButtonSet.delete(transferButton); + } +} + +/** + * @param {Navigator} navigator + * @param {function(string): Promise} performTransferCallback + * @param {HTMLButtonElement} transferButton + * @param {string} payload + */ +export default function createTransferButton( + navigator, + performTransferCallback, + transferButton, + payload, +) { + const defaultStatusElement = transferButton.querySelector(CONSTANTS.DEFAULT_STATUS_SELECTOR); + const successStatusElement = transferButton.querySelector(CONSTANTS.SUCCESS_STATUS_SELECTOR); + const failureStatusElement = transferButton.querySelector(CONSTANTS.FAILURE_STATUS_SELECTOR); + if (!defaultStatusElement || !successStatusElement || !failureStatusElement) { + throw new Error('Transfer: one or more status elements not found.'); + } + transferButton.addEventListener('click', () => { + performTransfer( + navigator, + performTransferCallback, + transferButton, + payload, + defaultStatusElement, + successStatusElement, + failureStatusElement, + ); + }); +} diff --git a/components/ILIAS/UI/resources/js/Transfer/src/facade.js b/components/ILIAS/UI/resources/js/Transfer/src/facade.js new file mode 100644 index 000000000000..e9650afe010b --- /dev/null +++ b/components/ILIAS/UI/resources/js/Transfer/src/facade.js @@ -0,0 +1,30 @@ +/** + * 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 il from 'ilias'; +import window from 'window'; +import document from 'document'; +import createLinkTransfer from './createLinkTransfer.js'; + +il.UI = il.UI || {}; + +il.UI.Transfer = { + createLinkTransfer: (id) => createLinkTransfer( + window.navigator, + document.getElementById(id), + ), +}; diff --git a/components/ILIAS/UI/resources/js/Transfer/src/performClipboardTransfer.js b/components/ILIAS/UI/resources/js/Transfer/src/performClipboardTransfer.js new file mode 100644 index 000000000000..348ca19677f3 --- /dev/null +++ b/components/ILIAS/UI/resources/js/Transfer/src/performClipboardTransfer.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 + * + * @author Thibeau Fuhrer + */ + +/** + * @param {string} payload + * @return {Promise} + */ +export default function performClipboardTransfer(navigator, payload) { + return navigator.clipboard.writeText(payload); +} diff --git a/components/ILIAS/UI/resources/js/Transfer/src/performWebShareTransfer.js b/components/ILIAS/UI/resources/js/Transfer/src/performWebShareTransfer.js new file mode 100644 index 000000000000..405d93d8288d --- /dev/null +++ b/components/ILIAS/UI/resources/js/Transfer/src/performWebShareTransfer.js @@ -0,0 +1,28 @@ +/** + * 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 + */ + +/** + * @param {Navigator} navigator + * @param {object} payload + * @returns {Promise} + */ +export default function performWebShareTransfer(navigator, payload) { + if (!navigator.canShare(payload)) { + throw new Error('Transfer: web share api is not available.'); + } + return navigator.share(payload); +} diff --git a/components/ILIAS/UI/src/Component/Transfer/Factory.php b/components/ILIAS/UI/src/Component/Transfer/Factory.php new file mode 100644 index 000000000000..33f9bbc8f3ba --- /dev/null +++ b/components/ILIAS/UI/src/Component/Transfer/Factory.php @@ -0,0 +1,74 @@ + + * The Link Transfer component is used to transfer an URL to another context + * or medium. + * composition: > + * The Link Transfer component consists of: + * - Primary Transfer Mechanism + * - Visual representation of the URL + * - Label describing the URL + * - Optional additional Transfer Mechanisms + * Whereas the Transfer Mechanisms consist of: + * - Clipboard: consists of a Button and a Glyph for its trigger. + * - Web Share: consists of a Button and a Glyph for its trigger. + * - QR Code: consists of a Button and a Glyph for its triggger, and a Modal + * featuring an Image for its transfer. + * effect: > + * When interacted with, the Link Transfer component transfers the URL address + * to another medium or context, according to the used Transfer Mechanism. + * When the Clipboard Transfer Mechanism is operated, the URL address is copied + * into the computers clipboard. + * When the Web Share Transfer Mechanism is operated, the browser opens the Web + * Share API. + * When the QR Code Transfer Mechanism is operated, the Modal is opened which + * shows the QR-code as an Image. + * After a transfer is completed, either successfully or not, an appropriate + * feedback is immediately shown to the user. + * rivals: + * Link: > + * A Link should be used to navigate to an URL instead of transferring it. + * + * background: https://docu.ilias.de/go/wiki/wpage_8762_1357 + * + * context: + * - The permanent-link of the Footer. + * + * rules: + * wording: + * 1: The label SHOULD name the target view or resource. + * ordering: + * 1: The information MUST be presented first. + * 2: Transfer mechanisms SHOULD be ordered by relevance. + * --- + * @param \ILIAS\UI\Component\Transfer\TransferMechanism $primary_transfer_mechanism + * @param \ILIAS\Data\URI $url + * @param string $label + * @return \ILIAS\UI\Component\Transfer\Link + */ + public function link(TransferMechanism $primary_transfer_mechanism, URI $url, string $label): Link; +} diff --git a/components/ILIAS/UI/src/Component/Transfer/HasAdditionalTransferMechanism.php b/components/ILIAS/UI/src/Component/Transfer/HasAdditionalTransferMechanism.php new file mode 100644 index 000000000000..25d0c2d80688 --- /dev/null +++ b/components/ILIAS/UI/src/Component/Transfer/HasAdditionalTransferMechanism.php @@ -0,0 +1,27 @@ + + * Transfer components allow users to transfer information from one context or + * medium to another. Transfer components are always initiated by the user. + * composition: > + * Transfer components consist of the information which should be transfered + * and a primary transfering mechanism (e.g. clipboard). + * effect: > + * Interacting with a Transfer component will transfer information from one + * context or medium to another. + * rivals: + * Button: > + * Button components should be used for actions where the information is acted + * upon, while Transfer components should be used to let users transfer it. + * Link: > + * Link components should be used for navigation and when the information is + * accessed directly, i.e. not transfered. + * + * rules: + * usage: + * 1: Transfer components SHOULD be preferred over isolated copy actions. + * 2: > + * Transfer components MUST NOT be used to trigger protocol-based + * actions such as "mailto:" or "tel:". These are navigational + * interactions and SHOULD be implemented using Link components. + * interaction: + * 1: Feedback MUST be provided after a transfer was initiated/completed. + * accessibility: + * 1: All transfer mechanisms MUST be operable by keyboard only. + * 2: Feedback of interactions MUST be conveyed to assistive technologies. + * --- + * @return \ILIAS\UI\Component\Transfer\Factory + */ + public function transfer(): C\Transfer\Factory; } diff --git a/components/ILIAS/UI/src/Implementation/Component/Button/ButtonRendererFactory.php b/components/ILIAS/UI/src/Implementation/Component/Button/ButtonRendererFactory.php index 27517c12982b..e100dff80d0a 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Button/ButtonRendererFactory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Button/ButtonRendererFactory.php @@ -54,6 +54,7 @@ public function getRendererInContext(Component $component, array $contexts): Com $this->data_factory, $this->help_text_retriever, $this->upload_limit_resolver, + $this->refinery, ); } return parent::getRendererInContext($component, $contexts); diff --git a/components/ILIAS/UI/src/Implementation/Component/Input/Container/Form/FormRendererFactory.php b/components/ILIAS/UI/src/Implementation/Component/Input/Container/Form/FormRendererFactory.php index 28a0ca681817..bb9887a33aba 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Input/Container/Form/FormRendererFactory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Input/Container/Form/FormRendererFactory.php @@ -43,7 +43,8 @@ public function getRendererInContext(Component\Component $component, array $cont $this->image_path_resolver, $this->data_factory, $this->help_text_retriever, - $this->upload_limit_resolver + $this->upload_limit_resolver, + $this->refinery, ); } return new Renderer( @@ -54,7 +55,8 @@ public function getRendererInContext(Component\Component $component, array $cont $this->image_path_resolver, $this->data_factory, $this->help_text_retriever, - $this->upload_limit_resolver + $this->upload_limit_resolver, + $this->refinery, ); } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Input/Field/FieldRendererFactory.php b/components/ILIAS/UI/src/Implementation/Component/Input/Field/FieldRendererFactory.php index 5d3372541d81..622d13546a83 100755 --- a/components/ILIAS/UI/src/Implementation/Component/Input/Field/FieldRendererFactory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Input/Field/FieldRendererFactory.php @@ -38,7 +38,8 @@ public function getRendererInContext(Component\Component $component, array $cont $this->image_path_resolver, $this->data_factory, $this->help_text_retriever, - $this->upload_limit_resolver + $this->upload_limit_resolver, + $this->refinery, ); } if (in_array('StandardFilterContainerInput', $contexts)) { @@ -50,7 +51,8 @@ public function getRendererInContext(Component\Component $component, array $cont $this->image_path_resolver, $this->data_factory, $this->help_text_retriever, - $this->upload_limit_resolver + $this->upload_limit_resolver, + $this->refinery, ); } return new Renderer( @@ -61,7 +63,8 @@ public function getRendererInContext(Component\Component $component, array $cont $this->image_path_resolver, $this->data_factory, $this->help_text_retriever, - $this->upload_limit_resolver + $this->upload_limit_resolver, + $this->refinery, ); } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Menu/MenuRendererFactory.php b/components/ILIAS/UI/src/Implementation/Component/Menu/MenuRendererFactory.php index c72c32a93518..732dbc68f255 100644 --- a/components/ILIAS/UI/src/Implementation/Component/Menu/MenuRendererFactory.php +++ b/components/ILIAS/UI/src/Implementation/Component/Menu/MenuRendererFactory.php @@ -42,6 +42,7 @@ public function getRendererInContext(Component $component, array $contexts): Com $this->data_factory, $this->help_text_retriever, $this->upload_limit_resolver, + $this->refinery, ); } diff --git a/components/ILIAS/UI/src/Implementation/Component/MessageBox/MessageBoxRendererFactory.php b/components/ILIAS/UI/src/Implementation/Component/MessageBox/MessageBoxRendererFactory.php index 07c8a41de2d4..6995e22b39e5 100644 --- a/components/ILIAS/UI/src/Implementation/Component/MessageBox/MessageBoxRendererFactory.php +++ b/components/ILIAS/UI/src/Implementation/Component/MessageBox/MessageBoxRendererFactory.php @@ -36,7 +36,8 @@ public function getRendererInContext(Component\Component $component, array $cont $this->image_path_resolver, $this->data_factory, $this->help_text_retriever, - $this->upload_limit_resolver + $this->upload_limit_resolver, + $this->refinery, ); } return new Renderer( @@ -47,7 +48,8 @@ public function getRendererInContext(Component\Component $component, array $cont $this->image_path_resolver, $this->data_factory, $this->help_text_retriever, - $this->upload_limit_resolver + $this->upload_limit_resolver, + $this->refinery, ); } } diff --git a/components/ILIAS/UI/src/Implementation/Component/Transfer/Factory.php b/components/ILIAS/UI/src/Implementation/Component/Transfer/Factory.php new file mode 100644 index 000000000000..a60d7bc12866 --- /dev/null +++ b/components/ILIAS/UI/src/Implementation/Component/Transfer/Factory.php @@ -0,0 +1,32 @@ + (mechanism-name => mechanism) + */ + protected array $transfer_mechanisms = []; + + /** + * Implements {@see C\Transfer\HasAdditionalTransferMechanism::withAdditionalTransferMechanism()} + */ + public function withAdditionalTransferMechanism(C\Transfer\TransferMechanism ...$transfer_mechanisms): static + { + $clone = clone $this; + foreach ($transfer_mechanisms as $transfer_mechanism) { + $clone->transfer_mechanisms[$transfer_mechanism->name] = $transfer_mechanism; + } + return $clone; + } + + /** + * Sets the transfer mechanisms of this instance. This should be used only + * inside the constructor to initialise the property, as it overwrites the + * property otherwise. + * + * @param C\Transfer\TransferMechanism[] $transfer_mechanisms + */ + public function setTransferMechanisms(array $transfer_mechanisms): void + { + foreach ($transfer_mechanisms as $transfer_mechanism) { + $this->transfer_mechanisms[$transfer_mechanism->name] = $transfer_mechanism; + } + } + + /** @return array */ + public function getTransferMechanisms(): array + { + return $this->transfer_mechanisms; + } +} diff --git a/components/ILIAS/UI/src/Implementation/Component/Transfer/Link.php b/components/ILIAS/UI/src/Implementation/Component/Transfer/Link.php new file mode 100644 index 000000000000..928b4429e2c1 --- /dev/null +++ b/components/ILIAS/UI/src/Implementation/Component/Transfer/Link.php @@ -0,0 +1,51 @@ +setTransferMechanisms([$transfer_mechanism]); + } + + public function getUrl(): URI + { + return $this->url; + } + + public function getLabel(): string + { + return $this->label; + } +} diff --git a/components/ILIAS/UI/src/Implementation/Component/Transfer/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/Transfer/Renderer.php new file mode 100644 index 000000000000..2aee5c45ed2f --- /dev/null +++ b/components/ILIAS/UI/src/Implementation/Component/Transfer/Renderer.php @@ -0,0 +1,179 @@ +register('assets/js/transfer.min.js'); + } + + public function render(Component $component, RendererInterface $default_renderer): string + { + if ($component instanceof Link) { + return $this->renderLink($component, $default_renderer); + } + $this->cannotHandleComponent($component); + } + + protected function renderLink(Link $component, RendererInterface $default_renderer): string + { + $template = $this->getTemplate('tpl.link.html', true, true); + + $template->setVariable('URL', $component->getUrl()); + + if ('' !== $component->getLabel()) { + $template->setVariable('LABEL', $component->getLabel()); + } + + $is_primary_transfer_mechanism = true; + foreach ($component->getTransferMechanisms() as $transfer_mechanism) { + $transfer_mechanism_html = match ($transfer_mechanism) { + TransferMechanism::CLIPBOARD => $this->renderClipboardTransferMechanism($default_renderer, $is_primary_transfer_mechanism), + TransferMechanism::WEB_SHARE => $this->renderWebShareTransferMechanism($default_renderer, $is_primary_transfer_mechanism), + TransferMechanism::QR_CODE => $this->renderQrCodeTransferMechanism( + $default_renderer, + $is_primary_transfer_mechanism, + $this->getUriTransformations()->toSvgQrCode()->transform($component->getUrl()), + $component->getUrl(), + ), + }; + $template->setCurrentBlock('with_transfer_mechanism'); + $template->setVariable('TRANSFER_MECHANISM', $transfer_mechanism_html); + $template->parseCurrentBlock(); + $is_primary_transfer_mechanism = false; + } + + $enriched_component = $component->withAdditionalOnLoadCode(static fn($id) => " + il.UI.Transfer.createLinkTransfer('$id'); + "); + + $id = $this->bindJavaScript($enriched_component); + $template->setVariable('ID', $id); + + return $template->get(); + } + + protected function renderTransferButton( + RendererInterface $default_renderer, + Glyph $default_glyph, + string $transfer_type, + string $default_message, + string $success_message, + string $failure_message, + bool $is_message_visible, + ): string { + $template = $this->getTemplate('tpl.transfer_button.html', true, true); + + $template->setVariable('TRANSFER_TYPE', $transfer_type); + + $template->setVariable('DEFAULT_MESSAGE', $default_message); + $template->setVariable('SUCCESS_MESSAGE', $success_message); + $template->setVariable('FAILURE_MESSAGE', $failure_message); + + if (!$is_message_visible) { + $template->setVariable('MESSAGE_VISIBILITY', 'sr-only'); + } + + $template->setVariable('DEFAULT_GLYPH', $default_renderer->render($default_glyph->withLabel(''))); + $template->setVariable('SUCCESS_GLYPH', $default_renderer->render( + $this->getUIFactory()->symbol()->glyph()->apply()->withLabel(''), + )); + $template->setVariable('FAILURE_GLYPH', $default_renderer->render( + $this->getUIFactory()->symbol()->glyph()->close()->withLabel(''), + )); + + return $template->get(); + } + + protected function renderClipboardTransferMechanism(RendererInterface $default_renderer, bool $is_primary_transfer_mechanism): string + { + return $this->renderTransferButton( + $default_renderer, + $this->getUIFactory()->symbol()->glyph()->copy(), + TransferMechanism::CLIPBOARD->value, + $this->txt('copy_to_clipboard'), + $this->txt('copy_to_clipboard_success'), + $this->txt('copy_to_clipboard_failure'), + $is_primary_transfer_mechanism, + ); + } + + protected function renderWebShareTransferMechanism(RendererInterface $default_renderer, bool $is_primary_transfer_mechanism): string + { + return $this->renderTransferButton( + $default_renderer, + $this->getUIFactory()->symbol()->glyph()->share(), + TransferMechanism::WEB_SHARE->value, + $this->txt('open_web_share_api'), + $this->txt('open_web_share_api_success'), + $this->txt('open_web_share_api_failure'), + $is_primary_transfer_mechanism, + ); + } + + /** + * Note that we only support URI data to be transfered using a QR-code. + * We do not support arbitrary binary data or other unknown formats for + * the foreseeable future. + */ + protected function renderQrCodeTransferMechanism( + RendererInterface $default_renderer, + bool $is_primary_transfer_mechanism, + SVG $qr_code_svg, + URI $qr_code_url, + ): string { + $factory = $this->getUIFactory(); + $svg_data_uri = $this->getUriTransformations()->fromSvg()->transform($qr_code_svg); + $image = $factory->image()->responsive($svg_data_uri, '')->withAction((string) $qr_code_url); + + $modal = $factory + ->modal() + ->roundtrip($this->txt('use_qr_code'), [$image]) + ->withCancelButtonLabel($this->txt('close')) + ->withCloseWithKeyboard(true); + + $button_label = ''; + if ($is_primary_transfer_mechanism) { + $button_label = $this->txt('show_qr_code'); + } + + $modal_button = $factory + ->button() + ->standard($button_label, '') + ->withSymbol($factory->symbol()->glyph()->qrCode()) + ->withOnClick($modal->getShowSignal()); + + return $default_renderer->render([$modal_button, $modal]); + } +} diff --git a/components/ILIAS/UI/src/Implementation/Factory.php b/components/ILIAS/UI/src/Implementation/Factory.php index f3c8d1872c9d..62a1957e8079 100755 --- a/components/ILIAS/UI/src/Implementation/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Factory.php @@ -58,6 +58,7 @@ public function __construct( protected I\Entity\Factory $entity_factory, protected I\Prompt\Factory $prompt_factory, protected I\Navigation\Factory $navigation_factory, + protected I\Transfer\Factory $transfer_factory, ) { } @@ -234,4 +235,8 @@ public function navigation(): I\Navigation\Factory return $this->navigation_factory; } + public function transfer(): I\Transfer\Factory + { + return $this->transfer_factory; + } } diff --git a/components/ILIAS/UI/src/Implementation/FactoryInternal.php b/components/ILIAS/UI/src/Implementation/FactoryInternal.php index 7e3d470a8368..525be2ab338e 100755 --- a/components/ILIAS/UI/src/Implementation/FactoryInternal.php +++ b/components/ILIAS/UI/src/Implementation/FactoryInternal.php @@ -21,6 +21,7 @@ namespace ILIAS\UI\Implementation; use ILIAS\UI\Implementation\Component as I; +use ILIAS\UI\Component as C; interface FactoryInternal extends \ILIAS\UI\Factory { @@ -89,4 +90,6 @@ public function entity(): I\Entity\Factory; public function prompt(): I\Prompt\Factory; public function navigation(): I\Navigation\Factory; + + public function transfer(): I\Transfer\Factory; } diff --git a/components/ILIAS/UI/src/Implementation/Render/AbstractComponentRenderer.php b/components/ILIAS/UI/src/Implementation/Render/AbstractComponentRenderer.php index 08cdeba0c471..49a1971bfd06 100755 --- a/components/ILIAS/UI/src/Implementation/Render/AbstractComponentRenderer.php +++ b/components/ILIAS/UI/src/Implementation/Render/AbstractComponentRenderer.php @@ -53,6 +53,7 @@ final public function __construct( private DataFactory $data_factory, private HelpTextRetriever $help_text_retriever, private UploadLimitResolver $upload_limit_resolver, + private \ILIAS\Refinery\Factory $refinery, ) { } @@ -79,6 +80,11 @@ final protected function getDataFactory(): DataFactory return $this->data_factory; } + final protected function getUriTransformations(): \ILIAS\Refinery\URI\Group + { + return $this->refinery->uri(); + } + final protected function getUploadLimitResolver(): UploadLimitResolver { return $this->upload_limit_resolver; diff --git a/components/ILIAS/UI/src/Implementation/Render/DefaultRendererFactory.php b/components/ILIAS/UI/src/Implementation/Render/DefaultRendererFactory.php index 7c61d196f6f8..44cddc588924 100755 --- a/components/ILIAS/UI/src/Implementation/Render/DefaultRendererFactory.php +++ b/components/ILIAS/UI/src/Implementation/Render/DefaultRendererFactory.php @@ -39,6 +39,7 @@ public function __construct( protected DataFactory $data_factory, protected HelpTextRetriever $help_text_retriever, protected UploadLimitResolver $upload_limit_resolver, + protected \ILIAS\Refinery\Factory $refinery, ) { } @@ -57,6 +58,7 @@ public function getRendererInContext(Component $component, array $contexts): Com $this->data_factory, $this->help_text_retriever, $this->upload_limit_resolver, + $this->refinery, ); } diff --git a/components/ILIAS/UI/src/examples/Transfer/Link/base.php b/components/ILIAS/UI/src/examples/Transfer/Link/base.php new file mode 100644 index 000000000000..c3634b125d80 --- /dev/null +++ b/components/ILIAS/UI/src/examples/Transfer/Link/base.php @@ -0,0 +1,50 @@ + + */ + +declare(strict_types=1); + +namespace ILIAS\UI\examples\Transfer\Link; + +use ILIAS\UI\Component\Transfer\TransferMechanism; + +/** + * --- + * description: > + * ... + * + * expected output: > + * ... + * --- + */ +function base(): string +{ + global $DIC; + + $factory = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + $data_factory = new \ILIAS\Data\Factory(); + + $component = $factory->transfer()->link( + TransferMechanism::CLIPBOARD, + $data_factory->uri("http://ilias.ch"), + "Link to ILIAS", + ); + + return $renderer->render($component); +} diff --git a/components/ILIAS/UI/src/examples/Transfer/Link/with_additional_transfer_mechanism.php b/components/ILIAS/UI/src/examples/Transfer/Link/with_additional_transfer_mechanism.php new file mode 100644 index 000000000000..613606822033 --- /dev/null +++ b/components/ILIAS/UI/src/examples/Transfer/Link/with_additional_transfer_mechanism.php @@ -0,0 +1,51 @@ + + * ... + * + * expected output: > + * ... + * --- + */ +function with_additional_transfer_mechanism(): string +{ + global $DIC; + + $factory = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + $data_factory = new \ILIAS\Data\Factory(); + + $component = $factory->transfer()->link( + TransferMechanism::CLIPBOARD, + $data_factory->uri("http://ilias.ch"), + "Link to ILIAS", + ); + + $component = $component->withAdditionalTransferMechanism(TransferMechanism::WEB_SHARE); + $component = $component->withAdditionalTransferMechanism(TransferMechanism::QR_CODE); + + return $renderer->render($component); +} diff --git a/components/ILIAS/UI/src/templates/default/Transfer/tpl.link.html b/components/ILIAS/UI/src/templates/default/Transfer/tpl.link.html new file mode 100644 index 000000000000..fc59b78fe3c2 --- /dev/null +++ b/components/ILIAS/UI/src/templates/default/Transfer/tpl.link.html @@ -0,0 +1,11 @@ + diff --git a/components/ILIAS/UI/src/templates/default/Transfer/tpl.transfer_button.html b/components/ILIAS/UI/src/templates/default/Transfer/tpl.transfer_button.html new file mode 100644 index 000000000000..4ca4a72281c2 --- /dev/null +++ b/components/ILIAS/UI/src/templates/default/Transfer/tpl.transfer_button.html @@ -0,0 +1,14 @@ + diff --git a/components/ILIAS/UI/tests/Base.php b/components/ILIAS/UI/tests/Base.php index a82b6f9d7ad7..15489893be82 100755 --- a/components/ILIAS/UI/tests/Base.php +++ b/components/ILIAS/UI/tests/Base.php @@ -159,6 +159,9 @@ public function prompt(): I\Prompt\Factory public function navigation(): I\Navigation\Factory { } + public function transfer(): I\Transfer\Factory + { + } } class LoggingRegistry implements ResourceRegistry diff --git a/components/ILIAS/UI/tests/InitUIFramework.php b/components/ILIAS/UI/tests/InitUIFramework.php index 95fae03ffecc..aed6b66c9f05 100755 --- a/components/ILIAS/UI/tests/InitUIFramework.php +++ b/components/ILIAS/UI/tests/InitUIFramework.php @@ -63,6 +63,7 @@ public function init(\ILIAS\DI\Container $c): void $c["ui.factory.entity"], $c["ui.factory.prompt"], $c["ui.factory.navigation"], + new ILIAS\UI\Implementation\Component\Transfer\Factory(), ); }; $c["ui.upload_limit_resolver"] = function ($c) { @@ -327,7 +328,8 @@ public function getRefreshIntervalInMs(): int $c["ui.pathresolver"], $c["ui.data_factory"], $c["help.text_retriever"], - $c["ui.upload_limit_resolver"] + $c["ui.upload_limit_resolver"], + $c["refinery"], ), new ILIAS\UI\Implementation\Component\Button\ButtonRendererFactory( $c["ui.factory"], @@ -337,7 +339,8 @@ public function getRefreshIntervalInMs(): int $c["ui.pathresolver"], $c["ui.data_factory"], $c["help.text_retriever"], - $c["ui.upload_limit_resolver"] + $c["ui.upload_limit_resolver"], + $c["refinery"], ), new ILIAS\UI\Implementation\Component\Input\Field\FieldRendererFactory( $c["ui.factory"], @@ -347,7 +350,8 @@ public function getRefreshIntervalInMs(): int $c["ui.pathresolver"], $c["ui.data_factory"], $c["help.text_retriever"], - $c["ui.upload_limit_resolver"] + $c["ui.upload_limit_resolver"], + $c["refinery"], ), new ILIAS\UI\Implementation\Component\MessageBox\MessageBoxRendererFactory( $c["ui.factory"], @@ -357,7 +361,8 @@ public function getRefreshIntervalInMs(): int $c["ui.pathresolver"], $c["ui.data_factory"], $c["help.text_retriever"], - $c["ui.upload_limit_resolver"] + $c["ui.upload_limit_resolver"], + $c["refinery"], ), new ILIAS\UI\Implementation\Component\Input\Container\Form\FormRendererFactory( $c["ui.factory"], @@ -367,7 +372,8 @@ public function getRefreshIntervalInMs(): int $c["ui.pathresolver"], $c["ui.data_factory"], $c["help.text_retriever"], - $c["ui.upload_limit_resolver"] + $c["ui.upload_limit_resolver"], + $c["refinery"], ), new ILIAS\UI\Implementation\Component\Menu\MenuRendererFactory( $c["ui.factory"], @@ -377,7 +383,8 @@ public function getRefreshIntervalInMs(): int $c["ui.pathresolver"], $c["ui.data_factory"], $c["help.text_retriever"], - $c["ui.upload_limit_resolver"] + $c["ui.upload_limit_resolver"], + $c["refinery"], ), ) ) diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index 7ba9cd4d91c1..1d74aac72ae6 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -17511,6 +17511,9 @@ ui#:#3stars#:#drei von fünf Sternen ui#:#4stars#:#vier von fünf Sternen ui#:#5stars#:#fünf von fünf Sternen ui#:#copy#:#Copy +ui#:#copy_to_clipboard#:#Kopieren +ui#:#copy_to_clipboard_failure#:#Fehler +ui#:#copy_to_clipboard_success#:#Kopiert ui#:#datatable_close_warning#:#OK ui#:#datatable_multiaction_label#:#Sammelaktionen ui#:#datatable_multiactionmodal_actionlabel#:#Aktion für alle Einträge @@ -17538,6 +17541,9 @@ ui#:#label_modeviewcontrol#:#Anzeigemodus ui#:#label_pagination_limit#:#Pagination - Anzahl der Zeilen ui#:#label_pagination_offset#:#Pagination - Start ui#:#label_sortation#:#Sortierung +ui#:#open_web_share_api#:#Teilen +ui#:#open_web_share_api_failure#:#Fehler +ui#:#open_web_share_api_success#:#Geteilt ui#:#order_option_alphabetical_ascending#:#A bis Z ui#:#order_option_alphabetical_descending#:#Z bis A ui#:#order_option_chronological_ascending#:#Älteste zuerst @@ -17553,6 +17559,7 @@ ui#:#rating_average#:#Andere bewerteten mit %s von 5 ui#:#reset_stars#:#neutral ui#:#select_node#:#Knoten %s zur Auswahl hinzufügen ui#:#share#:#Share +ui#:#show_qr_code#:#QR-Code anzeigen ui#:#show_qr_code#:#Show QR-code ui#:#table_posinput_col_title#:#Position ui#:#ui_chars_max#:#Maximum: @@ -17588,6 +17595,7 @@ ui#:#ui_table_no_records#:#Keine Einträge ui#:#ui_table_order#:#Reihenfolge ui#:#ui_transcription#:#Abschrift ui#:#unselect_node#:#Knoten %s von der Auswahl entfernen +ui#:#use_qr_code#:#QR-Code klicken oder scannen ui#:#vc_sort#:#Sortiert nach: ui#:#warning_url_too_long_msg#:#Die Anzahl der ausgewählten Zeilen führt zu einer zu großen Datenmenge, die nicht übertragen werden kann. Wählen Sie weniger Zeilen aus oder führen Sie die Aktion für die gesamte Tabelle durch (Sammelaktionen). user#:#account_not_flagged_for_deletion#:#Das Konto ist nicht zum Löschen markiert. diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index 2727884aa310..b11c6eb246b5 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -17463,6 +17463,9 @@ ui#:#3stars#:#three of five stars ui#:#4stars#:#four of five stars ui#:#5stars#:#five of five stars ui#:#copy#:#Copy +ui#:#copy_to_clipboard#:#Copy +ui#:#copy_to_clipboard_failure#:#Error +ui#:#copy_to_clipboard_success#:#Copied ui#:#datatable_close_warning#:#OK ui#:#datatable_multiaction_label#:#Bulk Actions ui#:#datatable_multiactionmodal_actionlabel#:#Action for All Entries @@ -17490,6 +17493,9 @@ ui#:#label_modeviewcontrol#:#view mode ui#:#label_pagination_limit#:#Pagination Number of Rows ui#:#label_pagination_offset#:#Pagination Offset ui#:#label_sortation#:#Sortation +ui#:#open_web_share_api#:#Share +ui#:#open_web_share_api_failure#:#Error +ui#:#open_web_share_api_success#:#Shared ui#:#order_option_alphabetical_ascending#:#A to Z ui#:#order_option_alphabetical_descending#:#Z to A ui#:#order_option_chronological_ascending#:#Earliest First @@ -17506,6 +17512,7 @@ ui#:#reset_stars#:#neutral ui#:#select_node#:#Add node %s to selection ui#:#share#:#Share ui#:#show_qr_code#:#Show QR-code +ui#:#show_qr_code#:#Show QR-code ui#:#table_posinput_col_title#:#Position ui#:#ui_chars_max#:#Maximum: ui#:#ui_chars_min#:#Minimum: @@ -17540,6 +17547,7 @@ ui#:#ui_table_no_records#:#No records ui#:#ui_table_order#:#Order ui#:#ui_transcription#:#Transcript ui#:#unselect_node#:#Remove node %s from selection +ui#:#use_qr_code#:#Click or scan the QR-code ui#:#vc_sort#:#Sort by: ui#:#warning_url_too_long_msg#:#The amount of selected rows will result in a very large URL; the Server will probably block this request.
Please select less rows or perform the action on all entries. user#:#account_not_flagged_for_deletion#:#The account is not flagged for deletion. diff --git a/templates/default/070-components/UI-framework/Transfer/_ui-component_transfer-link.scss b/templates/default/070-components/UI-framework/Transfer/_ui-component_transfer-link.scss new file mode 100644 index 000000000000..df2683cdda52 --- /dev/null +++ b/templates/default/070-components/UI-framework/Transfer/_ui-component_transfer-link.scss @@ -0,0 +1,29 @@ +@use "../../../010-settings/" as *; +@use "../../../050-layout/basics" as *; + +.c-transfer { + display: flex; + flex-direction: column; + gap: $il-padding-small-vertical; + + &__contents { + display: flex; + align-items: center; + gap: 0; + } + + &__payload { + flex: 0 1 auto; + // align margin with buttons rendered for each transfer mechanism, + // so spacing looks evenly distributed on one line in the end: + margin: 0 ($il-margin-small-horizontal / 2) 0 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + // make modals shrink-wrap to the qr-code image size. + .modal-dialog { + max-width: fit-content; + } +} diff --git a/templates/default/070-components/_index.scss b/templates/default/070-components/_index.scss index fa625b1ab3aa..869499a29d63 100755 --- a/templates/default/070-components/_index.scss +++ b/templates/default/070-components/_index.scss @@ -42,6 +42,7 @@ @use "./UI-framework/Symbol/_ui-component_symbol.scss"; @use "./UI-framework/Table/_ui-component_table.scss"; @use "./UI-framework/Toast/_ui-component_toast.scss"; +@use "./UI-framework/Transfer/_ui-component_transfer-link.scss"; @use "./UI-framework/Tree/_ui-component_tree.scss"; @use "./UI-framework/ViewControl/_ui-component_viewcontrol.scss"; diff --git a/templates/default/delos.css b/templates/default/delos.css index 970a0d644a11..a26454255bac 100644 --- a/templates/default/delos.css +++ b/templates/default/delos.css @@ -12288,6 +12288,27 @@ tr.c-table-data__row.c-table-data__row--drag-settle { flex-direction: column; } +.c-transfer { + display: flex; + flex-direction: column; + gap: 3px; +} +.c-transfer__contents { + display: flex; + align-items: center; + gap: 0; +} +.c-transfer__payload { + flex: 0 1 auto; + margin: 0 3px 0 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.c-transfer .modal-dialog { + max-width: fit-content; +} + .c-tree { list-style-type: none; padding: 0 3px 0 7px; From 4fcf5bae94aeca7fd666635f1535c81d934b9b5f Mon Sep 17 00:00:00 2001 From: Thibeau Fuhrer Date: Wed, 20 May 2026 22:27:58 +0200 Subject: [PATCH 3/4] Revert "Footer: Copy permalink to clipboard (#8634)" This reverts commit 30461e2a488ac7f0a1afc67f6aa24f62902d9f46. --- components/ILIAS/UI/UI.php | 2 - .../js/MainControls/dist/footer.min.js | 15 -- .../js/MainControls/rollup.config.js | 19 --- .../resources/js/MainControls/src/footer.js | 19 --- .../js/MainControls/src/footer/permalink.js | 55 ------ .../Component/MainControls/Renderer.php | 38 +---- .../MainControls/tpl.permanent-link.html | 6 - .../Client/MainControls/permalink.test.js | 160 ------------------ .../Component/MainControls/FooterTest.php | 14 +- lang/ilias_de.lang | 2 - lang/ilias_en.lang | 2 - 11 files changed, 12 insertions(+), 320 deletions(-) delete mode 100644 components/ILIAS/UI/resources/js/MainControls/dist/footer.min.js delete mode 100644 components/ILIAS/UI/resources/js/MainControls/src/footer.js delete mode 100644 components/ILIAS/UI/resources/js/MainControls/src/footer/permalink.js delete mode 100644 components/ILIAS/UI/src/templates/default/MainControls/tpl.permanent-link.html delete mode 100644 components/ILIAS/UI/tests/Client/MainControls/permalink.test.js diff --git a/components/ILIAS/UI/UI.php b/components/ILIAS/UI/UI.php index 9032206b83e8..52ccbc77a533 100644 --- a/components/ILIAS/UI/UI.php +++ b/components/ILIAS/UI/UI.php @@ -641,8 +641,6 @@ public function init( new Component\Resource\NodeModule("chart.js/dist/chart.umd.js"); $contribute[Component\Resource\PublicAsset::class] = fn() => new Component\Resource\ComponentJS($this, "js/Progress/dist/progress.min.js"); - $contribute[Component\Resource\PublicAsset::class] = fn() => - new Component\Resource\ComponentJS($this, "js/MainControls/dist/footer.min.js"); $contribute[Component\Resource\PublicAsset::class] = fn() => new Component\Resource\ComponentJS($this, "js/Input/ViewControl/dist/input.viewcontrols.min.js"); $contribute[Component\Resource\PublicAsset::class] = fn() => diff --git a/components/ILIAS/UI/resources/js/MainControls/dist/footer.min.js b/components/ILIAS/UI/resources/js/MainControls/dist/footer.min.js deleted file mode 100644 index 70f906f9796f..000000000000 --- a/components/ILIAS/UI/resources/js/MainControls/dist/footer.min.js +++ /dev/null @@ -1,15 +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(e){"use strict";var t=Object.freeze({__proto__:null,copyText:e=>{if(window.navigator.clipboard)return window.navigator.clipboard.writeText(e);const t=document.createElement("span"),o=document.createRange(),n=window.getSelection();t.textContent=e,document.body.appendChild(t),o.selectNodeContents(t),n.addRange(o);const r=document.execCommand("copy");return n.removeAllRanges(),t.remove(),r?Promise.resolve():Promise.reject(new Error("Unable to copy text."))},showTooltip:(e,t)=>{const o=(Array.from(document.getElementsByTagName("main")).find((e=>!e.hidden))||document.body).getBoundingClientRect();e.parentNode.classList.add("c-tooltip--visible");const n=e.getBoundingClientRect();o.left>n.left?e.style.transform="translateX(calc("+(o.left-n.left)+"px - 50%))":o.right{e.parentNode.classList.remove("c-tooltip--visible")}),t)}});e.Footer={permalink:t}}(il); diff --git a/components/ILIAS/UI/resources/js/MainControls/rollup.config.js b/components/ILIAS/UI/resources/js/MainControls/rollup.config.js index e00b3ba52758..669a53c90b87 100755 --- a/components/ILIAS/UI/resources/js/MainControls/rollup.config.js +++ b/components/ILIAS/UI/resources/js/MainControls/rollup.config.js @@ -49,23 +49,4 @@ export default [ external: ['il', 'jquery'], }, - { - input: './src/footer.js', - output: { - file: './dist/footer.min.js', - format: 'iife', - banner: copyright, - plugins: [ - terser({ - format: { - comments: preserveCopyright, - }, - }), - ], - globals: { - il: 'il', - }, - }, - external: ['il'], - }, ]; diff --git a/components/ILIAS/UI/resources/js/MainControls/src/footer.js b/components/ILIAS/UI/resources/js/MainControls/src/footer.js deleted file mode 100644 index b3279469352f..000000000000 --- a/components/ILIAS/UI/resources/js/MainControls/src/footer.js +++ /dev/null @@ -1,19 +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 - */ - -import il from 'il'; -import * as permalink from './footer/permalink'; - -il.Footer = { permalink }; diff --git a/components/ILIAS/UI/resources/js/MainControls/src/footer/permalink.js b/components/ILIAS/UI/resources/js/MainControls/src/footer/permalink.js deleted file mode 100644 index bde15be71e8b..000000000000 --- a/components/ILIAS/UI/resources/js/MainControls/src/footer/permalink.js +++ /dev/null @@ -1,55 +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 - */ - -/** - * @param {string} text - * @returns {Promise} - */ -export const copyText = text => { - if (window.navigator.clipboard) { - return window.navigator.clipboard.writeText(text); - } - - const node = document.createElement('span'); - const range = document.createRange(); - const selection = window.getSelection(); - - node.textContent = text; - document.body.appendChild(node); - range.selectNodeContents(node); - selection.addRange(range); - - const success = document.execCommand('copy'); - selection.removeAllRanges(); - node.remove(); - - return success ? Promise.resolve() : Promise.reject(new Error('Unable to copy text.')); -}; - -export const showTooltip = (node, delay) => { - const main = (Array.from(document.getElementsByTagName('main')).find(n => !n.hidden) || document.body).getBoundingClientRect(); - node.parentNode.classList.add('c-tooltip--visible'); - const r = node.getBoundingClientRect(); - - if (main.left > r.left) { - node.style.transform = 'translateX(calc(' + (main.left - r.left) + 'px - 50%))'; - } else if (main.right < r.right) { - node.style.transform = 'translateX(calc(' + (main.right - r.right) + 'px - 50%))'; - } - - setTimeout(() => { - node.parentNode.classList.remove('c-tooltip--visible'); - }, delay); -}; diff --git a/components/ILIAS/UI/src/Implementation/Component/MainControls/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/MainControls/Renderer.php index 9c88d35df7f0..ba013ec63987 100755 --- a/components/ILIAS/UI/src/Implementation/Component/MainControls/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/MainControls/Renderer.php @@ -442,10 +442,15 @@ protected function renderFooter(I\Footer $component, RendererInterface $default_ // maybe render section 1 (permanent link): $permanent_url = $component->getPermanentURL(); if (null !== $permanent_url) { - $template->setCurrentBlock('with_additional_item'); - $template->setVariable('ITEM_CONTENT', $this->permanentLink((string) $permanent_url, $default_renderer)); - $template->parseCurrentBlock(); - $this->parseFooterSection($template, 'permanent-link', $this->txt('footer_permanent_link')); + $this->parseAdditionalFooterSectionItems( + $template, + $default_renderer, + 'permanent-link', + $this->txt('footer_permanent_link'), + [ + [$this->getUIFactory()->link()->standard($this->txt('perma_link'), (string) $permanent_url), null], + ], + ); } // maybe render section 2 (link groups): @@ -596,30 +601,5 @@ public function registerResources(ResourceRegistry $registry): void $registry->register('assets/js/maincontrols.min.js'); $registry->register('assets/js/GS.js'); $registry->register('assets/js/system_info.js'); - $registry->register('assets/js/footer.min.js'); - } - - private function permanentLink(string $permanent_url, RendererInterface $renderer): string - { - $template = $this->getTemplate("tpl.permanent-link.html", true, true); - - $code = function (string $id) use ($permanent_url): string { - $id = $this->jsonEncode($id); - $perm_url = $this->jsonEncode((string) $permanent_url); - - return "document.getElementById($id).addEventListener('click', e => il.Footer.permalink.copyText($perm_url) - .then(() => il.Footer.permalink.showTooltip(e.target.nextElementSibling, 5000)));"; - }; - $button = $this->getUIFactory()->button()->standard($this->txt('copy_perma_link'), '')->withAdditionalOnLoadCode($code); - - $template->setVariable('PERMANENT', $renderer->render($button)); - $template->setVariable('PERMANENT_TOOLTIP', $this->txt('perma_link_copied')); - - return $template->get(); - } - - private function jsonEncode($value): string - { - return json_encode($value, JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_THROW_ON_ERROR); } } diff --git a/components/ILIAS/UI/src/templates/default/MainControls/tpl.permanent-link.html b/components/ILIAS/UI/src/templates/default/MainControls/tpl.permanent-link.html deleted file mode 100644 index bc572c334a17..000000000000 --- a/components/ILIAS/UI/src/templates/default/MainControls/tpl.permanent-link.html +++ /dev/null @@ -1,6 +0,0 @@ -
- {PERMANENT} - -
diff --git a/components/ILIAS/UI/tests/Client/MainControls/permalink.test.js b/components/ILIAS/UI/tests/Client/MainControls/permalink.test.js deleted file mode 100644 index e6f9ee8844e4..000000000000 --- a/components/ILIAS/UI/tests/Client/MainControls/permalink.test.js +++ /dev/null @@ -1,160 +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 - */ - -import { describe, it, beforeEach, afterEach } from 'node:test'; -import { copyText, showTooltip } from '../../../resources/js/MainControls/src/footer/permalink.js'; -import { strict } from 'node:assert/strict'; - -const expectOneCall = () => { - const expected = []; - const called = []; - - return { - callOnce: (proc = () => {}) => { - const f = (...args) => { - if (called.includes(f)) { - throw new Error('Called more than once.'); - } - called.push(f); - return proc(...args); - }; - expected.push(f); - - return f; - }, - finish: () => expected.forEach(proc => { - if (!called.includes(proc)) { - throw new Error('Never called.'); - } - }), - }; -}; - -describe('Test permalink copy to clipboard', () => { - const saved = {}; - beforeEach(() => { - saved.window = globalThis.window; - saved.document = globalThis.document; - }); - afterEach(() => { - globalThis.window = saved.window; - globalThis.document = saved.document; - }); - - it('Clipboard API', () => { - let written = null; - const response = {}; - const writeText = s => { - written = s; - return response; - }; - globalThis.window = { navigator: { clipboard: { writeText } } }; - strict.deepEqual(copyText('foo'), response) - strict.equal(written, 'foo') - }); - - it('Legacy Clipboard API', () => { - const {callOnce, finish} = expectOneCall(); - const node = { remove: callOnce() }; - const range = { - selectNodeContents: callOnce(n => strict.deepEqual(n, node)), - }; - const selection = { - addRange: callOnce(x => strict.equal(x, range)), - removeAllRanges: callOnce(), - }; - - globalThis.window = { - navigator: {}, - getSelection: callOnce(() => selection), - }; - - globalThis.document = { - createRange: callOnce(() => range), - - createElement: callOnce(text => { - strict.equal(text, 'span'); - return node; - }), - - execCommand: callOnce(s => { - strict.equal(s, 'copy'); - return true; - }), - - body: { - appendChild: callOnce(n => { - strict.deepEqual(n, node); - strict.equal(n.textContent, 'foo'); - }), - }, - }; - - return copyText('foo').then(finish); - }); -}); - -describe('Test permanentlink show tooltip', () => { - const saved = {}; - beforeEach(() => { - saved.setTimeout = globalThis.setTimeout; - saved.document = globalThis.document; - }); - afterEach(() => { - globalThis.setTimeout = saved.setTimeout; - globalThis.document = saved.document; - }); - - const testTooltip = (mainRect, nodeRect, expectTransform = null) => () => { - const {callOnce, finish} = expectOneCall(); - let callTimeout = null; - globalThis.document = { - getElementsByTagName: callOnce(tag => { - strict.equal(tag, 'main'); - return [ - {getBoundingClientRect: callOnce(() => mainRect)} - ]; - }), - }; - - globalThis.setTimeout = callOnce((proc, delay) => { - callTimeout = proc; - strict.equal(delay, 4321); - }); - - const isTooltipClass = name => strict.equal(name, 'c-tooltip--visible'); - const node = { - parentNode: { - classList: { - add: callOnce(isTooltipClass), - remove: callOnce(isTooltipClass), - }, - }, - getBoundingClientRect: callOnce(() => nodeRect), - style: {transform: null}, - }; - showTooltip(node, 4321); - - strict.notEqual(callTimeout, null); - strict.deepEqual(node.style.transform, expectTransform); - - callTimeout(); - finish(); - }; - - it('Show tooltip', testTooltip({left: 0, right: 10}, {left: 1, right: 9})); - it('Show tooltip left aligned', testTooltip({left: 5, right: 10}, {left: 3, right: 9}, 'translateX(calc(2px - 50%))')); - it('Show tooltip right aligned', testTooltip({left: 0, right: 7}, {left: 1, right: 9}, 'translateX(calc(-2px - 50%))')); -}); diff --git a/components/ILIAS/UI/tests/Component/MainControls/FooterTest.php b/components/ILIAS/UI/tests/Component/MainControls/FooterTest.php index 4bc7129cbaab..745908547457 100755 --- a/components/ILIAS/UI/tests/Component/MainControls/FooterTest.php +++ b/components/ILIAS/UI/tests/Component/MainControls/FooterTest.php @@ -106,23 +106,15 @@ public function testRenderWithPermanentUrl(): void $footer = $this->getUIFactory()->mainControls()->footer(); $footer = $footer->withPermanentURL($this->uri_mock); - $this->button_factory->expects($this->once())->method('standard')->with('copy_perma_link', ''); - $this->button_mock->expects($this->once())->method('withAdditionalOnLoadCode')->willReturnSelf(); + $this->link_factory->expects($this->once())->method('standard')->with('perma_link', $this->uri_mock); - $renderer = $this->getDefaultRenderer(null, [$this->button_mock]); + $renderer = $this->getDefaultRenderer(null, [$this->link_mock]); $actual_html = $renderer->render($footer); $expected_html = << EOT; diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index 1d74aac72ae6..6013b49ad531 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -3794,7 +3794,6 @@ common#:#copy_all#:#Alle kopieren common#:#copy_n_of_suffix#:#(%1$s) common#:#copy_of#:#Kopie von common#:#copy_of_suffix#:#- Kopie -common#:#copy_perma_link#:#Link in Zwischenablage kopieren common#:#copy_selected_items#:#Kopieren common#:#count#:#Anzahl common#:#counter_novelty#:#Neuigkeiten @@ -5285,7 +5284,6 @@ common#:#pd_items_news#:#Einschließlich Neuigkeiten der ausgewählten Objekte common#:#pdf_export#:#PDF-Export common#:#perm_settings#:#Rechte common#:#perma_link#:#Link zu dieser Seite -common#:#perma_link_copied#:#Link zu dieser Seite wurde in die Zwischenablage kopiert. common#:#permission#:#Recht common#:#permission_denied#:#Kein Zugriffsrecht common#:#permission_settings#:#Rechteeinstellungen diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index b11c6eb246b5..ff7655406f04 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -3795,7 +3795,6 @@ common#:#copy_all#:#Copy all common#:#copy_n_of_suffix#:#(%1$s) common#:#copy_of#:#Copy of common#:#copy_of_suffix#:#- Copy -common#:#copy_perma_link#:#Copy Link to Clipboard common#:#copy_selected_items#:#Copy common#:#count#:#Count common#:#counter_novelty#:#News @@ -5286,7 +5285,6 @@ common#:#pd_items_news#:#Include News of Personal Items common#:#pdf_export#:#PDF Export common#:#perm_settings#:#Permissions common#:#perma_link#:#Permanent Link -common#:#perma_link_copied#:#Link to this page has been copied to the clipboard. common#:#permission#:#Permission common#:#permission_denied#:#Permission Denied common#:#permission_settings#:#Object Permission Settings From 41170a5e9f75ed40a0803cf1ace571d300733d55 Mon Sep 17 00:00:00 2001 From: Thibeau Fuhrer Date: Wed, 20 May 2026 22:45:33 +0200 Subject: [PATCH 4/4] [FEATURE] UI: use `Transfer\Link` for `MainControls\Footer::withPermanentLink()`. * Update `UI\Component\MainControls\Footer::withPermanentLink()`, so it uses a `UI\Component\Transfer\Link` for rendering. --- .../Implementation/Component/MainControls/Renderer.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/components/ILIAS/UI/src/Implementation/Component/MainControls/Renderer.php b/components/ILIAS/UI/src/Implementation/Component/MainControls/Renderer.php index ba013ec63987..3e50f77eac9f 100755 --- a/components/ILIAS/UI/src/Implementation/Component/MainControls/Renderer.php +++ b/components/ILIAS/UI/src/Implementation/Component/MainControls/Renderer.php @@ -442,13 +442,21 @@ protected function renderFooter(I\Footer $component, RendererInterface $default_ // maybe render section 1 (permanent link): $permanent_url = $component->getPermanentURL(); if (null !== $permanent_url) { + $permanent_link_transfer = $this->getUIFactory()->transfer()->link( + Component\Transfer\TransferMechanism::CLIPBOARD, + $permanent_url, + "{$this->txt('perma_link')}", + )->withAdditionalTransferMechanism( + Component\Transfer\TransferMechanism::WEB_SHARE, + Component\Transfer\TransferMechanism::QR_CODE, + ); $this->parseAdditionalFooterSectionItems( $template, $default_renderer, 'permanent-link', $this->txt('footer_permanent_link'), [ - [$this->getUIFactory()->link()->standard($this->txt('perma_link'), (string) $permanent_url), null], + [$permanent_link_transfer, null], ], ); }