diff --git a/components/ILIAS/UI/src/Component/Progress/Bar.php b/components/ILIAS/UI/src/Component/Progress/Bar.php new file mode 100644 index 000000000000..6ef09e1c94fe --- /dev/null +++ b/components/ILIAS/UI/src/Component/Progress/Bar.php @@ -0,0 +1,39 @@ + + */ +interface Bar extends Component, Triggerable, Triggerer +{ + /** + * Get a Signal which can be used to update the current Progress Bar. + */ + public function getUpdateSignal(): Signal; + + /** + * Get a Signal which can be used to reset the current Progress Bar. + */ + public function getResetSignal(): Signal; +} diff --git a/components/ILIAS/UI/src/Component/Progress/Factory.php b/components/ILIAS/UI/src/Component/Progress/Factory.php new file mode 100644 index 000000000000..1d494b913619 --- /dev/null +++ b/components/ILIAS/UI/src/Component/Progress/Factory.php @@ -0,0 +1,92 @@ + + */ +interface Factory +{ + /** + * --- + * description: + * purpose: > + * The Progress Bar is designed to represent the state of a single or bundled task + * or process, which can be processed in a single step and takes a while to finish. + * composition: > + * The Progress Bar is composed out of one horizontal track, the area of which is + * filled according to the current progress (value). It is also accompanied by a label, + * describing the process/task at hand, and a Glyph to indicate a finished status + * (success or failure). An optional message can be displayed, to inform about a + * concrete status. + * effect: > + * When the Progress Bar value is updated, the filled area of the track changes + * accordingly. + * When the Progress Bar is finished, the Glyph changes to one indicating success or + * failure, and an according message will be shown. + * rivals: + * ProgressMeter: use a ProgressMeter if the quality of the progress is evaluated + * and/or the progress is compared. + * Workflow: use a Workflow component if the underlying process/task is completed + * in multiple steps. + * + * background: + * - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress + * - https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/progressbar_role + * + * rules: + * usage: + * 1: > + * The Progress Bar (value) SHOULD NOT be decreased. It should only be reset to 0, if + * the underlying process/task is restarted. + * 2: > + * The Progress Bar SHOULD NOT be used, if the underlying process/task can only be + * 0% or 100% processed. A loading animation and/or Glyph COULD be used instead. + * --- + * @param \ILIAS\Data\URI $async_url + * @param string $label + * @return \ILIAS\UI\Component\Progress\Bar + */ + public function bar(string $label, ?URI $async_url = null): Bar; + + /** + * --- + * description: + * purpose: > + * Instructions are used to communicate with out client during asynchronous requests. They + * are a way to convey information in a manner that is understood by our clientside + * components, and instructs them to perform a desierd change. We have been referring to + * this concept as "HTML over the wire" in the past, and are now implementing it for certain + * components in iterations. + * composition: > + * Instructions consist of HTML structures, typically other (or the same) UI component(s). + * effect: > + * When an Instruction is rendered it will be used by the clientside component to update the + * existing HTML structure according to the Instruction at hand. + * + * rules: + * usage: + * 1: You MUST NOT use Instructions outside of asynchronous requests. + * 2: You MUST use @see Renderer::renderAsync() to render Instructions. + * --- + * @return \ILIAS\UI\Component\Progress\Instruction\Factory + */ + public function instruction(): Instruction\Factory; +} diff --git a/components/ILIAS/UI/src/Component/Progress/Instruction/Bar/Factory.php b/components/ILIAS/UI/src/Component/Progress/Instruction/Bar/Factory.php new file mode 100644 index 000000000000..676c2af0f30c --- /dev/null +++ b/components/ILIAS/UI/src/Component/Progress/Instruction/Bar/Factory.php @@ -0,0 +1,130 @@ + + */ +interface Factory +{ + /** + * --- + * description: + * purpose: > + * Factors a Progress Bar Instruction which orders the clientside Progress Bar + * to change its status to "indeterminate". This state indicates that progress + * is being made, but no exact progress can (yet) be calculated. This is + * typically used in order to start the Progress Bar. + * composition: > + * The Instruction consists of an optional message. + * effect: > + * The Progress Bar will change to "indeterminate". + * The Progress Bar shows the given message. + * rivals: + * Determinate: use a determinate Instruction if the progress can be calculated. + * + * rules: + * usage: + * 1: You SHOULD NOT provide a message, if the progress can be calculated soon. + * 2: The provided message MUST BE concise and short. E.g. "determining progress". + * --- + * @param string|null $message + * @return Instruction + */ + public function indeterminate(?string $message = null): Instruction; + + /** + * --- + * description: + * purpose: > + * Factors a Progress Bar Instruction which orders the clientside Progress Bar + * to change its status to "determinate". This state shows the exact amount of + * progress being made. + * composition: > + * The Instruction consists of a percentage (0-1), representing the exact progress, + * and an optional message. + * effect: > + * The Progress Bar will change to "determinate" and show the progress. + * The Progress Bar shows the given message. + * rivals: + * Indeterminate: use an indeterminate Instruction if the progress cannot be calculated. + * Success: use a success Instruction if the progress is 1 (100%). + * Failure: use a failure Instruction if the underlying task failed. + * + * rules: + * usage: + * 1: The progress percentage MUST BE a floating point number between (0 and 1). + * 2: You MUST NOT use this Instruction, if the percentage is 1. + * 3: The provided message MUST BE concise and short. E.g. "Processing task XY". + * --- + * @param float $progress_percentage + * @param string|null $message + * @return Instruction + */ + public function determinate(float $progress_percentage, ?string $message = null): Instruction; + + /** + * --- + * description: + * purpose: > + * Factors a Progress Bar Instruction which orders the clientside Progress Bar to + * finish with success. + * composition: > + * The Instruction consists of a message. + * effect: > + * The Progress Bar fills up to 100% and shows a success Glyph and the provided + * message. + * rivals: + * Determinate: use a determinate Instruction if the progress is below 100%. + * Failure: use a failure Instruction if the underlying task failed. + * + * rules: + * usage: + * 1: The provided message MUST BE concise and short. E.g. "Task XY done". + * --- + * @param string $message + * @return Instruction + */ + public function success(string $message): Instruction; + + /** + * --- + * description: + * purpose: > + * Factors a Progress Bar Instruction which orders the clientside Progress Bar to + * finish with failure. + * composition: > + * The Instruction consists of a message. + * effect: > + * The Progress Bar fills up to 100% and shows a failure Glyph and the provided + * message. + * rivals: + * Determinate: use a determinate Instruction if the progress is below 100%. + * Success: use a success Instruction if the underlying task was successful. + * + * rules: + * usage: + * 1: The provided message MUST BE concise and short. E.g. "Task XY failed." + * --- + * @param string $message + * @return Instruction + */ + public function failure(string $message): Instruction; +} diff --git a/components/ILIAS/UI/src/Component/Progress/Instruction/Factory.php b/components/ILIAS/UI/src/Component/Progress/Instruction/Factory.php new file mode 100644 index 000000000000..dc3dc618546f --- /dev/null +++ b/components/ILIAS/UI/src/Component/Progress/Instruction/Factory.php @@ -0,0 +1,48 @@ + + */ +interface Factory +{ + /** + * --- + * description: + * purpose: > + * Progress Bar Instructions are used to order a clientside Progress Bar to perform a desired + * update, when pulled asynchronously from a source. + * composition: > + * Progress Bar Instructions cary information about the Progress Bar status and progress (value), + * and optionally provide a message for the user. + * + * context: + * - Progress Bar Instruction's are used by Progress Bar's which pull updates asynchrnously + * from a source. + * + * rules: + * usage: + * 1: > + * Progress Bar Instructions MUST NOT be used for anything other than updating a + * Progress Bar asynchronously. + * --- + * @return Bar\Factory + */ + public function bar(): Bar\Factory; +} diff --git a/components/ILIAS/UI/src/Component/Progress/Instruction/Instruction.php b/components/ILIAS/UI/src/Component/Progress/Instruction/Instruction.php new file mode 100644 index 000000000000..8aeac66e21e6 --- /dev/null +++ b/components/ILIAS/UI/src/Component/Progress/Instruction/Instruction.php @@ -0,0 +1,27 @@ + + */ +interface Instruction extends Component +{ +} diff --git a/components/ILIAS/UI/src/Factory.php b/components/ILIAS/UI/src/Factory.php index 754ca5205eee..9a70b31a13c5 100755 --- a/components/ILIAS/UI/src/Factory.php +++ b/components/ILIAS/UI/src/Factory.php @@ -678,6 +678,36 @@ public function modal(): C\Modal\Factory; */ public function popover(): C\Popover\Factory; + /** + * --- + * description: + * purpose: > + * A Progress component is designed to represent the users advancement within + * a certain process or task. They are of informative nature and provide feedback + * about the current state and said advancement of the process/task. Their goal + * is to guide users by communicating how much of a process/task is completed, + * what remains, and what is ahead of them. + * composition: > + * Progress components are composed out of appropriate scales and status indicators, + * which clearly convey the state of the users advancement. + * + * rules: + * usage: + * 1: A Progress component SHOULD be used whenever a process/task is time-consuming. + * 2: > + * A progress component MUST NOT be used to convey any other type of information + * than the users advancement. + * interaction: + * 1: Progress components are passive and MUST NOT be operable in any way for the user. + * accessibility: + * 1: > + * A progress component MUST be fully accessible using a screen-reader. Any colouring + * or other visual indicators MUST provide according alternative-texts. + * --- + * @return \ILIAS\UI\Component\Progress\Factory + */ + public function progress(): C\Progress\Factory; + /** * --- * description: diff --git a/components/ILIAS/UI/src/Implementation/Factory.php b/components/ILIAS/UI/src/Implementation/Factory.php index 92ed8d7eefe7..10c5dc394b1b 100755 --- a/components/ILIAS/UI/src/Implementation/Factory.php +++ b/components/ILIAS/UI/src/Implementation/Factory.php @@ -25,6 +25,7 @@ // TODO: This might cache the created factories. use ILIAS\UI\Implementation\Component\SignalGenerator; +use ILIAS\UI\NotImplementedException; class Factory implements \ILIAS\UI\Factory { diff --git a/components/ILIAS/UI/src/examples/Progress/Bar/client.php b/components/ILIAS/UI/src/examples/Progress/Bar/client.php new file mode 100644 index 000000000000..823edf607832 --- /dev/null +++ b/components/ILIAS/UI/src/examples/Progress/Bar/client.php @@ -0,0 +1,48 @@ + + * This example shows how a Progress Bar can be rendered and used on the client. + * The trigger button is supplied with the according JavaScript code, which uses + * the clientside facility of a Progress Bar. + * + * expected output: > + * ILIAS shows the rendered Progress Bar and Standard Button. The Progress Bar is + * initially empty (no progress), and cannot be operated in any way. When the + * Stadnard Button is clicked, the Progress Bar value us increased by 10% each time. + * After the 10th click, the Progress Bar is finished showing a successful state. + * --- + */ +function client(): string +{ + global $DIC; + $factory = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + + $progress_bar = $factory->progress()->bar('clicking the button 10 times'); + + $trigger = $factory->button()->standard('make some progress', '#'); + $trigger = $trigger->withAdditionalOnLoadCode( + static fn(string $id) => " + let count = 1; + document.getElementById('$id')?.addEventListener('click', ({ target }) => { + if (10 === count) { + il.UI.Progress.Bar.success('{$progress_bar->getUpdateSignal()->getId()}', 'all done!'); + target.disabled = true; + return; + } + + let how_much_work_done = 10 / count; + il.UI.Progress.Bar.determinate('{$progress_bar->getUpdateSignal()->getId()}', how_much_work_done); + count += 1; + }); + " + ); + + return $renderer->render([$progress_bar, $trigger]); +} diff --git a/components/ILIAS/UI/src/examples/Progress/Bar/server.php b/components/ILIAS/UI/src/examples/Progress/Bar/server.php new file mode 100644 index 000000000000..b0b4c9d8c7ff --- /dev/null +++ b/components/ILIAS/UI/src/examples/Progress/Bar/server.php @@ -0,0 +1,117 @@ + + * This example shows how a Progress Bar can be rendered and updated by the server. + * The artificial endpoint uses Progres Bar Instructions to order the clientside + * Progress Bar to perform a desired update. A Standard Button can be used to start + * this process. + * + * expected output: > + * ILIAS shows the rendered Progress Bar and Standard Button. The Progress Bar is + * initially empty (no progress), and cannot be operated in any way. When the + * Stadnard Button is clicked, the Progress Bar value us increased by 10% ~every + * second. After the ~10 seconds, the Progress Bar will be finished showing a + * successful state. + * --- + */ +function server(): string +{ + global $DIC; + $http = $DIC->http(); + $uri = $http->request()->getUri(); + $request = $http->wrapper()->query(); + $factory = $DIC->ui()->factory(); + $renderer = $DIC->ui()->renderer(); + $data_factory = new \ILIAS\Data\Factory(); + + $endpoint_flag = 'progress_bar_example_endpoint'; + $endpoint_url = $uri . "$endpoint_flag=1"; + $endpoint_url = $data_factory->uri($endpoint_url); + + $progress_bar = $factory->progress()->bar('waiting about 10 seconds', $endpoint_url); + + $trigger = $factory->button()->standard('start making progress', '#'); + $trigger = $trigger->withAdditionalOnLoadCode( + static fn(string $id) => " + document.getElementById('$id')?.addEventListener('click', ({ target }) => { + il.UI.Progress.Bar.startAsync('{$progress_bar->getUpdateSignal()->getId()}', 'Estimating'); + }); + " + ); + + if ($request->has($endpoint_flag)) { + callArtificialTaskEndpoint($http, $factory, $renderer); + } + + return $renderer->render([$progress_bar, $trigger]); +} + +function callArtificialTaskEndpoint(GlobalHttpState $http, UI\Factory $factory, UI\Renderer $renderer): void +{ + initialiseArtificialTaskOnce(); + + $task_progress = getTaskProgress(); + + $instruction = match ($task_progress) { + 1 => $instruction = $factory->progress()->instruction()->bar()->determinate(0.10), + 2 => $instruction = $factory->progress()->instruction()->bar()->determinate(0.20), + 3 => $instruction = $factory->progress()->instruction()->bar()->determinate(0.30), + 4 => $instruction = $factory->progress()->instruction()->bar()->determinate(0.40), + 5 => $instruction = $factory->progress()->instruction()->bar()->determinate(0.50, 'Still processing.'), + 6 => $instruction = $factory->progress()->instruction()->bar()->determinate(0.60), + 7 => $instruction = $factory->progress()->instruction()->bar()->determinate(0.70), + 8 => $instruction = $factory->progress()->instruction()->bar()->determinate(0.80), + 9 => $instruction = $factory->progress()->instruction()->bar()->determinate(0.90), + 10 => $instruction = $factory->progress()->instruction()->bar()->success("All done!"), + default => $instruction = $factory->progress()->instruction()->bar()->failure("An error ocurred."), + }; + + if (10 > $task_progress) { + incrementTaskProgress(); + } else { + resetTask(); + } + + $html = $renderer->renderAsync($instruction); + + $http->saveResponse( + $http->response() + ->withHeader('Content-Type', 'text/html; charset=utf-8') + ->withBody(Streams::ofString($html)) + ); + $http->sendResponse(); + $http->close(); +} + +function initialiseArtificialTaskOnce(): void +{ + if (!\ilSession::has(__NAMESPACE__ . '_example_task_progress')) { + \ilSession::set(__NAMESPACE__ . '_example_task_progress', 1); + } +} + +function incrementTaskProgress(): void +{ + $previous_value = \ilSession::get(__NAMESPACE__ . '_example_task_progress'); + \ilSession::set(__NAMESPACE__ . '_example_task_progress', (int) $previous_value + 1); +} + +function getTaskProgress(): int +{ + return \ilSession::get(__NAMESPACE__ . '_example_task_progress') ?? 1; +} + +function resetTask(): void +{ + \ilSession::clear(__NAMESPACE__ . '_example_task_progress'); +}