diff --git a/lang/en/error.php b/lang/en/error.php index f13a4ea308b..038f56b074c 100644 --- a/lang/en/error.php +++ b/lang/en/error.php @@ -451,6 +451,7 @@ $string['nonmeaningfulcontent'] = 'Non meaningful content'; $string['noparticipants'] = 'No participants found for this course'; $string['noparticipatorycms'] = 'Sorry, but you have no participatory course modules to report on'; $string['nopermissions'] = 'Sorry, but you do not currently have permissions to do that ({$a}).'; +$string['nopermissiontoaccesspage'] = 'You don\'t have permission to access this page.'; $string['nopermissiontocomment'] = 'You can\'t add comments'; $string['nopermissiontodelentry'] = 'You can\'t delete this comment!'; $string['nopermissiontoeditcomment'] = 'You can\'t edit other people\'s comments!'; diff --git a/lib/amd/build/dynamic_tabs.min.js b/lib/amd/build/dynamic_tabs.min.js new file mode 100644 index 00000000000..9f33266c5be --- /dev/null +++ b/lib/amd/build/dynamic_tabs.min.js @@ -0,0 +1,2 @@ +define ("core/dynamic_tabs",["exports","jquery","core/templates","core/notification","core/pending","core/local/repository/dynamic_tabs"],function(a,b,c,d,e,f){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.init=void 0;b=g(b);c=g(c);d=g(d);e=g(e);function g(a){return a&&a.__esModule?a:{default:a}}function h(a,b){var c=Object.keys(a);if(Object.getOwnPropertySymbols){var d=Object.getOwnPropertySymbols(a);if(b)d=d.filter(function(b){return Object.getOwnPropertyDescriptor(a,b).enumerable});c.push.apply(c,d)}return c}function i(a){for(var b=1,c;b.\n\n/**\n * Dynamic Tabs UI element with AJAX loading of tabs content\n *\n * @module core/dynamic_tabs\n * @copyright 2021 David Matamoros based on code from Marina Glancy\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport Templates from 'core/templates';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport {getContent} from 'core/local/repository/dynamic_tabs';\n\nconst SELECTORS = {\n dynamicTabs: '.dynamictabs',\n activeTab: '.dynamictabs .nav-link.active',\n allActiveTabs: '.dynamictabs .nav-link[data-toggle=\"tab\"]:not(.disabled)',\n tabContent: '.dynamictabs .tab-pane [data-tab-content]',\n tabToggle: 'a[data-toggle=\"tab\"]',\n tabPane: '.dynamictabs .tab-pane',\n};\n\nSELECTORS.forTabName = tabName => `.dynamictabs [data-tab-content=\"${tabName}\"]`;\nSELECTORS.forTabId = tabName => `.dynamictabs [data-toggle=\"tab\"][href=\"#${tabName}\"]`;\n\n/**\n * Initialises the tabs view on the page (only one tabs view per page is supported)\n */\nexport const init = () => {\n // This code listens to Bootstrap event 'shown.bs.tab' which is triggered using JQuery and\n // can not be converted yet to native events.\n $(SELECTORS.tabToggle).on('shown.bs.tab', function() {\n const tab = $($(this).attr('href'));\n if (tab.length !== 1) {\n return;\n }\n loadTab(tab.attr('id'));\n });\n\n if (!openTabFromHash()) {\n const tabs = document.querySelector(SELECTORS.allActiveTabs);\n if (tabs) {\n openTab(tabs.getAttribute('aria-controls'));\n } else {\n // We may hide tabs if there is only one available, just load the contents of the first tab.\n const tabPane = document.querySelector(SELECTORS.tabPane);\n if (tabPane) {\n tabPane.classList.add('active', 'show');\n loadTab(tabPane.getAttribute('id'));\n }\n }\n }\n};\n\n/**\n * Show \"loading\" template instead of a node\n *\n * @param {HTMLElement} node\n * @return {Promise}\n */\nconst indicateNodeIsLoading = (node) => {\n return Templates.render('core/loading', {})\n .then((html, js) => {\n return Templates.replaceNodeContents(node, html, js);\n }).catch(Notification.exception);\n};\n\n/**\n * Returns id/name of the currently active tab\n *\n * @return {String|null}\n */\nconst getActiveTabName = () => {\n const element = document.querySelector(SELECTORS.activeTab);\n return element?.getAttribute('aria-controls') || null;\n};\n\n/**\n * Returns the id/name of the first tab\n *\n * @return {String|null}\n */\nconst getFirstTabName = () => {\n const element = document.querySelector(SELECTORS.tabContent);\n return element?.dataset.tabContent || null;\n};\n\n/**\n * Loads contents of a tab using an AJAX request\n *\n * @param {String} tabName\n * @param {Object} additionalData additional data to pass to WS\n */\nconst loadTab = (tabName, additionalData = {}) => {\n // If tabName is not specified find the active tab, or if is not defined, the first available tab.\n tabName = tabName ?? getActiveTabName() ?? getFirstTabName();\n\n const tab = document.querySelector(SELECTORS.forTabName(tabName));\n if (!tab) {\n return;\n }\n\n const pendingPromise = new Pending('core/dynamic_tabs:loadTab:' + tabName);\n const tabdata = tab.closest(SELECTORS.dynamicTabs);\n const wsData = {\n 'reportid': tabdata.dataset.reportid,\n 'id': tabdata.dataset.id,\n ...additionalData\n };\n let tabjs = '';\n tab.textContent = '';\n\n indicateNodeIsLoading(tab)\n .then(() => {\n return getContent(tab.dataset.tabClass, JSON.stringify(wsData));\n })\n .then((data) => {\n tabjs = data.javascript;\n return Templates.render(data.template, JSON.parse(data.content));\n })\n .then((html, js) => {\n return Templates.replaceNodeContents(tab, html, js + tabjs);\n })\n .then(() => {\n pendingPromise.resolve();\n return null;\n })\n .catch(Notification.exception);\n};\n\n/**\n * Return the tab given the tab name\n *\n * @param {String} tabName\n * @return {HTMLElement}\n */\nconst getTab = (tabName) => {\n return document.querySelector(SELECTORS.forTabId(tabName));\n};\n\n/**\n * Return the tab pane given the tab name\n *\n * @param {String} tabName\n * @return {HTMLElement}\n */\nconst getTabPane = (tabName) => {\n return document.getElementById(tabName);\n};\n\n/**\n * Open the tab on page load. If this script loads before theme_boost/tab we need to open tab ourselves\n *\n * @param {String} tabName\n * @return {Boolean}\n */\nconst openTab = (tabName) => {\n const tab = getTab(tabName);\n if (!tab) {\n return false;\n }\n\n loadTab(tabName);\n tab.classList.add('active');\n getTabPane(tabName).classList.add('active', 'show');\n return true;\n};\n\n/**\n * If there is a location hash that is the same as the tab name - open this tab.\n *\n * @return {Boolean}\n */\nconst openTabFromHash = () => {\n const hash = document.location.hash;\n if (hash.match(/^#\\w+$/g)) {\n return openTab(hash.replace(/^#/g, ''));\n }\n\n return false;\n};\n"],"file":"dynamic_tabs.min.js"} \ No newline at end of file diff --git a/lib/amd/build/local/repository/dynamic_tabs.min.js b/lib/amd/build/local/repository/dynamic_tabs.min.js new file mode 100644 index 00000000000..773fc7e8a5b --- /dev/null +++ b/lib/amd/build/local/repository/dynamic_tabs.min.js @@ -0,0 +1,2 @@ +define ("core/local/repository/dynamic_tabs",["exports","core/ajax"],function(a,b){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.getContent=void 0;b=function(a){return a&&a.__esModule?a:{default:a}}(b);var c=function(a,c){return b.default.call([{methodname:"core_dynamic_tabs_get_content",args:{tab:a,jsondata:c}}])[0]};a.getContent=c}); +//# sourceMappingURL=dynamic_tabs.min.js.map diff --git a/lib/amd/build/local/repository/dynamic_tabs.min.js.map b/lib/amd/build/local/repository/dynamic_tabs.min.js.map new file mode 100644 index 00000000000..09d0b64c01b --- /dev/null +++ b/lib/amd/build/local/repository/dynamic_tabs.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../../src/local/repository/dynamic_tabs.js"],"names":["getContent","tab","jsondata","Ajax","call","methodname","args"],"mappings":"qKAuBA,uDASO,GAAMA,CAAAA,CAAU,CAAG,SAACC,CAAD,CAAMC,CAAN,CAAmB,CAMzC,MAAOC,WAAKC,IAAL,CAAU,CALD,CACZC,UAAU,CAAE,+BADA,CAEZC,IAAI,CAAE,CAACL,GAAG,CAAEA,CAAN,CAAWC,QAAQ,CAAEA,CAArB,CAFM,CAKC,CAAV,EAAqB,CAArB,CACV,CAPM,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n* Module to handle dynamic tabs AJAX requests\n*\n* @module core/local/repository/dynamic_tabs\n* @copyright 2021 David Matamoros \n* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n*/\n\nimport Ajax from 'core/ajax';\n\n/**\n* Return tab content\n*\n* @param {String} tab\n* @param {String} jsondata\n* @return {Promise}\n*/\nexport const getContent = (tab, jsondata) => {\n const request = {\n methodname: 'core_dynamic_tabs_get_content',\n args: {tab: tab, jsondata: jsondata}\n };\n\n return Ajax.call([request])[0];\n};\n"],"file":"dynamic_tabs.min.js"} \ No newline at end of file diff --git a/lib/amd/src/dynamic_tabs.js b/lib/amd/src/dynamic_tabs.js new file mode 100644 index 00000000000..c89f621d280 --- /dev/null +++ b/lib/amd/src/dynamic_tabs.js @@ -0,0 +1,197 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Dynamic Tabs UI element with AJAX loading of tabs content + * + * @module core/dynamic_tabs + * @copyright 2021 David Matamoros based on code from Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import $ from 'jquery'; +import Templates from 'core/templates'; +import Notification from 'core/notification'; +import Pending from 'core/pending'; +import {getContent} from 'core/local/repository/dynamic_tabs'; + +const SELECTORS = { + dynamicTabs: '.dynamictabs', + activeTab: '.dynamictabs .nav-link.active', + allActiveTabs: '.dynamictabs .nav-link[data-toggle="tab"]:not(.disabled)', + tabContent: '.dynamictabs .tab-pane [data-tab-content]', + tabToggle: 'a[data-toggle="tab"]', + tabPane: '.dynamictabs .tab-pane', +}; + +SELECTORS.forTabName = tabName => `.dynamictabs [data-tab-content="${tabName}"]`; +SELECTORS.forTabId = tabName => `.dynamictabs [data-toggle="tab"][href="#${tabName}"]`; + +/** + * Initialises the tabs view on the page (only one tabs view per page is supported) + */ +export const init = () => { + // This code listens to Bootstrap event 'shown.bs.tab' which is triggered using JQuery and + // can not be converted yet to native events. + $(SELECTORS.tabToggle).on('shown.bs.tab', function() { + const tab = $($(this).attr('href')); + if (tab.length !== 1) { + return; + } + loadTab(tab.attr('id')); + }); + + if (!openTabFromHash()) { + const tabs = document.querySelector(SELECTORS.allActiveTabs); + if (tabs) { + openTab(tabs.getAttribute('aria-controls')); + } else { + // We may hide tabs if there is only one available, just load the contents of the first tab. + const tabPane = document.querySelector(SELECTORS.tabPane); + if (tabPane) { + tabPane.classList.add('active', 'show'); + loadTab(tabPane.getAttribute('id')); + } + } + } +}; + +/** + * Show "loading" template instead of a node + * + * @param {HTMLElement} node + * @return {Promise} + */ +const indicateNodeIsLoading = (node) => { + return Templates.render('core/loading', {}) + .then((html, js) => { + return Templates.replaceNodeContents(node, html, js); + }).catch(Notification.exception); +}; + +/** + * Returns id/name of the currently active tab + * + * @return {String|null} + */ +const getActiveTabName = () => { + const element = document.querySelector(SELECTORS.activeTab); + return element?.getAttribute('aria-controls') || null; +}; + +/** + * Returns the id/name of the first tab + * + * @return {String|null} + */ +const getFirstTabName = () => { + const element = document.querySelector(SELECTORS.tabContent); + return element?.dataset.tabContent || null; +}; + +/** + * Loads contents of a tab using an AJAX request + * + * @param {String} tabName + * @param {Object} additionalData additional data to pass to WS + */ +const loadTab = (tabName, additionalData = {}) => { + // If tabName is not specified find the active tab, or if is not defined, the first available tab. + tabName = tabName ?? getActiveTabName() ?? getFirstTabName(); + + const tab = document.querySelector(SELECTORS.forTabName(tabName)); + if (!tab) { + return; + } + + const pendingPromise = new Pending('core/dynamic_tabs:loadTab:' + tabName); + const tabdata = tab.closest(SELECTORS.dynamicTabs); + const wsData = { + 'reportid': tabdata.dataset.reportid, + 'id': tabdata.dataset.id, + ...additionalData + }; + let tabjs = ''; + tab.textContent = ''; + + indicateNodeIsLoading(tab) + .then(() => { + return getContent(tab.dataset.tabClass, JSON.stringify(wsData)); + }) + .then((data) => { + tabjs = data.javascript; + return Templates.render(data.template, JSON.parse(data.content)); + }) + .then((html, js) => { + return Templates.replaceNodeContents(tab, html, js + tabjs); + }) + .then(() => { + pendingPromise.resolve(); + return null; + }) + .catch(Notification.exception); +}; + +/** + * Return the tab given the tab name + * + * @param {String} tabName + * @return {HTMLElement} + */ +const getTab = (tabName) => { + return document.querySelector(SELECTORS.forTabId(tabName)); +}; + +/** + * Return the tab pane given the tab name + * + * @param {String} tabName + * @return {HTMLElement} + */ +const getTabPane = (tabName) => { + return document.getElementById(tabName); +}; + +/** + * Open the tab on page load. If this script loads before theme_boost/tab we need to open tab ourselves + * + * @param {String} tabName + * @return {Boolean} + */ +const openTab = (tabName) => { + const tab = getTab(tabName); + if (!tab) { + return false; + } + + loadTab(tabName); + tab.classList.add('active'); + getTabPane(tabName).classList.add('active', 'show'); + return true; +}; + +/** + * If there is a location hash that is the same as the tab name - open this tab. + * + * @return {Boolean} + */ +const openTabFromHash = () => { + const hash = document.location.hash; + if (hash.match(/^#\w+$/g)) { + return openTab(hash.replace(/^#/g, '')); + } + + return false; +}; diff --git a/lib/amd/src/local/repository/dynamic_tabs.js b/lib/amd/src/local/repository/dynamic_tabs.js new file mode 100644 index 00000000000..17d2b6c47cf --- /dev/null +++ b/lib/amd/src/local/repository/dynamic_tabs.js @@ -0,0 +1,40 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** +* Module to handle dynamic tabs AJAX requests +* +* @module core/local/repository/dynamic_tabs +* @copyright 2021 David Matamoros +* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later +*/ + +import Ajax from 'core/ajax'; + +/** +* Return tab content +* +* @param {String} tab +* @param {String} jsondata +* @return {Promise} +*/ +export const getContent = (tab, jsondata) => { + const request = { + methodname: 'core_dynamic_tabs_get_content', + args: {tab: tab, jsondata: jsondata} + }; + + return Ajax.call([request])[0]; +}; diff --git a/lib/classes/external/dynamic_tabs_get_content.php b/lib/classes/external/dynamic_tabs_get_content.php new file mode 100644 index 00000000000..cc78e882678 --- /dev/null +++ b/lib/classes/external/dynamic_tabs_get_content.php @@ -0,0 +1,113 @@ +. + +declare(strict_types=1); + +namespace core\external; + +use coding_exception; +use context_system; +use core\output\dynamic_tabs\base; +use external_api; +use external_function_parameters; +use external_single_structure; +use external_value; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once("$CFG->libdir/externallib.php"); + +/** + * External method for getting tab contents + * + * @package core + * @copyright 2021 David Matamoros based on code from Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class dynamic_tabs_get_content extends external_api { + + /** + * External method parameters + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters([ + 'tab' => new external_value(PARAM_RAW_TRIMMED, 'Tab class', VALUE_REQUIRED), + 'jsondata' => new external_value(PARAM_RAW, 'Json-encoded data', VALUE_REQUIRED), + ]); + } + + /** + * Tab content + * + * @param string $tabclass class of the tab + * @param string $jsondata + * @return array + */ + public static function execute(string $tabclass, string $jsondata): array { + global $PAGE, $OUTPUT; + + [ + 'tab' => $tabclass, + 'jsondata' => $jsondata, + ] = self::validate_parameters(self::execute_parameters(), [ + 'tab' => $tabclass, + 'jsondata' => $jsondata, + ]); + + $data = @json_decode($jsondata, true); + + $context = context_system::instance(); + self::validate_context($context); + + // This call is needed to avoid debug messages on webserver log. + $PAGE->set_url('/'); + // This call is needed to initiate moodle page. + $OUTPUT->header(); + + if (!class_exists($tabclass) || !is_subclass_of($tabclass, base::class)) { + throw new coding_exception('unknown dynamic tab class', $tabclass); + } + + /** @var base $tab */ + $tab = new $tabclass($data); + $tab->require_access(); + $PAGE->start_collecting_javascript_requirements(); + + $content = $tab->export_for_template($PAGE->get_renderer('core')); + $jsfooter = $PAGE->requires->get_end_code(); + return [ + 'template' => $tab->get_template(), + 'content' => json_encode($content), + 'javascript' => $jsfooter, + ]; + } + + /** + * External method return value + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'template' => new external_value(PARAM_PATH, 'Template name'), + 'content' => new external_value(PARAM_RAW, 'JSON-encoded data for template'), + 'javascript' => new external_value(PARAM_RAW, 'JavaScript fragment'), + ]); + } +} diff --git a/lib/classes/output/dynamic_tabs.php b/lib/classes/output/dynamic_tabs.php new file mode 100644 index 00000000000..3a14323fc16 --- /dev/null +++ b/lib/classes/output/dynamic_tabs.php @@ -0,0 +1,90 @@ +. + +declare(strict_types=1); + +namespace core\output; + +use core\output\dynamic_tabs\base; +use renderer_base; +use templatable; + +/** + * Class dynamic tabs + * + * @package core + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class dynamic_tabs implements templatable { + + /** @var array */ + protected $attributes; + /** @var base[] */ + protected $tabs = []; + + /** + * tabs constructor. + * + * @param array $attributes additional attributes that will be passed to the webservice + * @param base[] $tabs array of tab + */ + public function __construct(array $attributes = [], array $tabs = []) { + $this->attributes = $attributes; + foreach ($tabs as $tab) { + $this->add_tab($tab); + } + } + + /** + * Add a tab + * + * @param base $tab + */ + public function add_tab(base $tab): void { + $this->tabs[] = $tab; + } + + /** + * Implementation of exporter from templatable interface + * + * @param renderer_base $output + * @return array + */ + public function export_for_template(renderer_base $output): array { + $data = [ + 'dataattributes' => [], + 'tabs' => [] + ]; + + foreach ($this->attributes as $name => $value) { + $data['dataattributes'][] = ['name' => $name, 'value' => $value]; + } + + foreach ($this->tabs as $tab) { + $data['tabs'][] = [ + 'shortname' => $tab->get_tab_id(), + 'displayname' => $tab->get_tab_label(), + 'enabled' => $tab->is_available(), + 'tabclass' => get_class($tab) + ]; + } + + $data['showtabsnavigation'] = (count($data['tabs']) > 1) ? 1 : 0; + + return $data; + } +} diff --git a/lib/classes/output/dynamic_tabs/base.php b/lib/classes/output/dynamic_tabs/base.php new file mode 100644 index 00000000000..987ecd1ffab --- /dev/null +++ b/lib/classes/output/dynamic_tabs/base.php @@ -0,0 +1,86 @@ +. + +declare(strict_types=1); + +namespace core\output\dynamic_tabs; + +use moodle_exception; +use templatable; + +/** + * Class tab_base + * + * @package core + * @copyright 2018 Marina Glancy + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class base implements templatable { + + /** @var array */ + protected $data; + + /** + * tab constructor. + * + * @param array $data + */ + final public function __construct(array $data) { + $this->data = $data; + } + + /** + * HTML "id" attribute that should be used for this tab, by default the last part of class name + * + * @return string + */ + public function get_tab_id(): string { + $parts = preg_split('/\\\\/', static::class); + return array_pop($parts); + } + + /** + * The label to be displayed on the tab + * + * @return string + */ + abstract public function get_tab_label(): string; + + /** + * Check permission of the current user to access this tab + * + * @return bool + */ + abstract public function is_available(): bool; + + /** + * Check that tab is accessible, throw exception otherwise - used from WS requesting tab contents + * + * @throws moodle_exception + */ + final public function require_access() { + if (!$this->is_available()) { + throw new moodle_exception('nopermissiontoaccesspage', 'error'); + } + } + + /** + * Template to use to display tab contents + * + * @return string + */ + abstract public function get_template(): string; +} diff --git a/lib/db/services.php b/lib/db/services.php index 89b5fe07e61..e0da03974a8 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -2779,6 +2779,13 @@ $functions = array( 'type' => 'write', 'ajax' => true, ], + 'core_dynamic_tabs_get_content' => [ + 'classname' => 'core\external\dynamic_tabs_get_content', + 'description' => 'Returns the content for a dynamic tab', + 'type' => 'read', + 'ajax' => true, + ], + ); $services = array( diff --git a/lib/templates/dynamic_tabs.mustache b/lib/templates/dynamic_tabs.mustache new file mode 100644 index 00000000000..34d49e9ebe2 --- /dev/null +++ b/lib/templates/dynamic_tabs.mustache @@ -0,0 +1,75 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core/dynamic_tabs + + Template for showing dynamic tabs + + Example context (json): + { + "dataattributes": [ {"name": "programid", "value": "13"} ], + "showtabsnavigation": "1", + "tabs": [ + { + "shortname": "tab1", + "displayname": "Tab 1", + "content": "Content of tab 1", + "enabled": "1" + }, + { + "shortname": "tab2", + "displayname": "Tab 2", + "content": "Content of tab 2", + "enabled": "1" + } + ] + } +}} +{{! We must not use the JS helper otherwise this gets executed too late. Tell behat to wait. }} + + +
+ {{#showtabsnavigation}} + + {{/showtabsnavigation}} +
+ {{#tabs}} +
+
+ {{{content}}} +
+
+ {{/tabs}} +
+
+ +{{#js}} + require(['core/dynamic_tabs'], function(Tabs) { + Tabs.init(); + M.util.js_complete('core_dynamic_tabs_init'); + }); +{{/js}} diff --git a/lib/tests/behat/behat_general.php b/lib/tests/behat/behat_general.php index 237d653a8cf..494612582a6 100644 --- a/lib/tests/behat/behat_general.php +++ b/lib/tests/behat/behat_general.php @@ -2084,4 +2084,17 @@ EOF; public function i_mark_this_test_as_long_running(int $factor = 2): void { $this->set_test_timeout_factor($factor); } + + /** + * Click on a dynamic tab to load its content + * + * @Given /^I click on the "(?P(?:[^"]|\\")*)" dynamic tab$/ + * + * @param string $tabname + */ + public function i_click_on_the_dynamic_tab(string $tabname): void { + $xpath = "//*[@id='dynamictabs-tabs'][descendant::a[contains(text(), '" . $this->escape($tabname) . "')]]"; + $this->execute('behat_general::i_click_on_in_the', + [$tabname, 'link', $xpath, 'xpath_element']); + } } diff --git a/lib/tests/external/dynamic_tabs_get_content_test.php b/lib/tests/external/dynamic_tabs_get_content_test.php new file mode 100644 index 00000000000..ce0e779453d --- /dev/null +++ b/lib/tests/external/dynamic_tabs_get_content_test.php @@ -0,0 +1,53 @@ +. + +declare(strict_types=1); + +namespace core\external; + +use external_api; + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); +require_once($CFG->dirroot . '/lib/tests/fixtures/testeable_dynamic_tab.php'); + +/** + * Unit tests external dynamic tabs get content + * + * @package core + * @covers core\external\dynamic_tabs_get_content + * @copyright 2021 David Matamoros + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class dynamic_tabs_get_content_testcase extends \externallib_advanced_testcase { + + /** + * Text execute method + */ + public function test_execute(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + + $result = dynamic_tabs_get_content::execute(testeable_dynamic_tab::class, json_encode([])); + $result = external_api::clean_returnvalue(dynamic_tabs_get_content::execute_returns(), $result); + $this->assertEquals('templates/tabs/mytab', $result['template']); + $this->assertEquals(json_encode(['content' => get_string('content')]), $result['content']); + $this->assertNotEmpty($result['javascript']); + $this->assertStringStartsWith('