From 46318e53d070d30f681c6fd7db9234dacfc5da56 Mon Sep 17 00:00:00 2001 From: ferran Date: Fri, 20 Dec 2024 11:54:32 +0100 Subject: [PATCH 1/2] MDL-83869 output: new collapsable section component --- .upgradenotes/MDL-83869-2025011609294854.yml | 9 + .../local/collapsable_section/controls.min.js | 19 ++ .../collapsable_section/controls.min.js.map | 1 + .../local/collapsable_section/events.min.js | 20 ++ .../collapsable_section/events.min.js.map | 1 + .../src/local/collapsable_section/controls.js | 138 ++++++++++++ .../src/local/collapsable_section/events.js | 86 ++++++++ .../output/local/collapsable_section.php | 147 +++++++++++++ .../local/collapsable_section.mustache | 147 +++++++++++++ .../behat/collapsable_section_output.feature | 66 ++++++ .../collapsable_section_output_testpage.php | 204 ++++++++++++++++++ 11 files changed, 838 insertions(+) create mode 100644 .upgradenotes/MDL-83869-2025011609294854.yml create mode 100644 lib/amd/build/local/collapsable_section/controls.min.js create mode 100644 lib/amd/build/local/collapsable_section/controls.min.js.map create mode 100644 lib/amd/build/local/collapsable_section/events.min.js create mode 100644 lib/amd/build/local/collapsable_section/events.min.js.map create mode 100644 lib/amd/src/local/collapsable_section/controls.js create mode 100644 lib/amd/src/local/collapsable_section/events.js create mode 100644 lib/classes/output/local/collapsable_section.php create mode 100644 lib/templates/local/collapsable_section.mustache create mode 100644 lib/tests/behat/collapsable_section_output.feature create mode 100644 lib/tests/behat/fixtures/collapsable_section_output_testpage.php diff --git a/.upgradenotes/MDL-83869-2025011609294854.yml b/.upgradenotes/MDL-83869-2025011609294854.yml new file mode 100644 index 0000000000000..a704c3768a913 --- /dev/null +++ b/.upgradenotes/MDL-83869-2025011609294854.yml @@ -0,0 +1,9 @@ +issueNumber: MDL-83869 +notes: + core: + - message: > + New generic collapsable section output added. Use + core\output\local\collapsable_section or include the + core/local/collapsable_section template to use it. See the full + documentation in the component library. + type: improved diff --git a/lib/amd/build/local/collapsable_section/controls.min.js b/lib/amd/build/local/collapsable_section/controls.min.js new file mode 100644 index 0000000000000..61f7fa94ead01 --- /dev/null +++ b/lib/amd/build/local/collapsable_section/controls.min.js @@ -0,0 +1,19 @@ +define("core/local/collapsable_section/controls",["exports","core/local/collapsable_section/events","jquery"],(function(_exports,_events,_jquery){var obj; +/** + * The collapsable sections controls. + * + * @module core/local/collapsable_section/controls + * @copyright 2024 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @example Example of controlling a collapsable section. + * + * import CollapsableSection from 'core/local/collapsable_section/controls'; + * + * const section = CollapsableSection.instanceFromSelector('#MyCollapsableSection'); + * + * // Use hide, show and toggle methods to control the section. + * section.hide(); + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};let initialized=!1;return _exports.default=class{static instanceFromSelector(selector){const elements=document.querySelector(selector);if(!elements)throw new Error("No elements found with the selector: "+selector);return new this(elements)}static init(){initialized||(initialized=!0,(0,_jquery.default)(document).on(_events.eventTypes.hiddenBsCollapse,(event=>{this.isCollapsableComponent(event.target)&&(0,_events.notifyCollapsableSectionHidden)(event.target)})),(0,_jquery.default)(document).on(_events.eventTypes.shownBsCollapse,(event=>{this.isCollapsableComponent(event.target)&&(0,_events.notifyCollapsableSectionShown)(event.target)})))}static isCollapsableComponent(element){return element.hasAttribute("data-mdl-component")&&"core/local/collapsable_section"===element.getAttribute("data-mdl-component")}constructor(element){this.element=element}hide(){(0,_jquery.default)(this.element).collapse("hide")}show(){(0,_jquery.default)(this.element).collapse("show")}toggle(){(0,_jquery.default)(this.element).collapse("toggle")}isVisible(){return this.element.classList.contains("show")}},_exports.default})); + +//# sourceMappingURL=controls.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/collapsable_section/controls.min.js.map b/lib/amd/build/local/collapsable_section/controls.min.js.map new file mode 100644 index 0000000000000..2ec15aec9aa2e --- /dev/null +++ b/lib/amd/build/local/collapsable_section/controls.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"controls.min.js","sources":["../../../src/local/collapsable_section/controls.js"],"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 * The collapsable sections controls.\n *\n * @module core/local/collapsable_section/controls\n * @copyright 2024 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * @example Example of controlling a collapsable section.\n *\n * import CollapsableSection from 'core/local/collapsable_section/controls';\n *\n * const section = CollapsableSection.instanceFromSelector('#MyCollapsableSection');\n *\n * // Use hide, show and toggle methods to control the section.\n * section.hide();\n */\n\nimport {\n eventTypes,\n notifyCollapsableSectionHidden,\n notifyCollapsableSectionShown\n} from 'core/local/collapsable_section/events';\n\n// The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated.\nimport jQuery from 'jquery';\n\nlet initialized = false;\n\nexport default class {\n /**\n * Create a new instance from a query selector.\n *\n * @param {String} selector The selector of the collapsable section.\n * @return {CollapsableSection} The collapsable section controls.\n * @throws {Error} If no elements are found with the selector.\n */\n static instanceFromSelector(selector) {\n const elements = document.querySelector(selector);\n if (!elements) {\n throw new Error('No elements found with the selector: ' + selector);\n }\n return new this(elements);\n }\n\n /**\n * Initialize the collapsable section controls.\n */\n static init() {\n if (initialized) {\n return;\n }\n initialized = true;\n\n // We want to add extra events to the standard bootstrap collapsable events.\n // TODO: change all jquery events to custom events once MDL-71979 is integrated.\n jQuery(document).on(eventTypes.hiddenBsCollapse, event => {\n if (!this.isCollapsableComponent(event.target)) {\n return;\n }\n notifyCollapsableSectionHidden(event.target);\n });\n jQuery(document).on(eventTypes.shownBsCollapse, event => {\n if (!this.isCollapsableComponent(event.target)) {\n return;\n }\n notifyCollapsableSectionShown(event.target);\n });\n }\n\n /**\n * Check if the element is a collapsable section.\n *\n * @private\n * @param {HTMLElement} element The element to check.\n * @return {boolean} True if the element is a collapsable section.\n */\n static isCollapsableComponent(element) {\n return element.hasAttribute('data-mdl-component')\n && element.getAttribute('data-mdl-component') === 'core/local/collapsable_section';\n }\n\n /**\n * Creates an instance of the controls for a collapsable section.\n *\n * @param {HTMLElement} element - The DOM element that this control will manage.\n */\n constructor(element) {\n this.element = element;\n }\n\n /**\n * Hides the collapsible section element.\n */\n hide() {\n // TODO: change all jquery once MDL-71979 is integrated.\n jQuery(this.element).collapse('hide');\n }\n\n /**\n * Shows the collapsible section element.\n */\n show() {\n // TODO: change all jquery once MDL-71979 is integrated.\n jQuery(this.element).collapse('show');\n }\n\n /**\n * Toggle the collapsible section element.\n */\n toggle() {\n // TODO: change all jquery once MDL-71979 is integrated.\n jQuery(this.element).collapse('toggle');\n }\n\n /**\n * Check if the collapsable section is visible.\n *\n * @return {boolean} True if the collapsable section is visible.\n */\n isVisible() {\n return this.element.classList.contains('show');\n }\n}\n"],"names":["initialized","selector","elements","document","querySelector","Error","this","on","eventTypes","hiddenBsCollapse","event","isCollapsableComponent","target","shownBsCollapse","element","hasAttribute","getAttribute","constructor","hide","collapse","show","toggle","isVisible","classList","contains"],"mappings":";;;;;;;;;;;;;;;;iJAyCIA,aAAc,4DAUcC,gBAClBC,SAAWC,SAASC,cAAcH,cACnCC,eACK,IAAIG,MAAM,wCAA0CJ,iBAEvD,IAAIK,KAAKJ,wBAOZF,cAGJA,aAAc,sBAIPG,UAAUI,GAAGC,mBAAWC,kBAAkBC,QACxCJ,KAAKK,uBAAuBD,MAAME,oDAGRF,MAAME,+BAElCT,UAAUI,GAAGC,mBAAWK,iBAAiBH,QACvCJ,KAAKK,uBAAuBD,MAAME,mDAGTF,MAAME,0CAWdE,gBACnBA,QAAQC,aAAa,uBAC0B,mCAA/CD,QAAQE,aAAa,sBAQhCC,YAAYH,cACHA,QAAUA,QAMnBI,2BAEWZ,KAAKQ,SAASK,SAAS,QAMlCC,2BAEWd,KAAKQ,SAASK,SAAS,QAMlCE,6BAEWf,KAAKQ,SAASK,SAAS,UAQlCG,mBACWhB,KAAKQ,QAAQS,UAAUC,SAAS"} \ No newline at end of file diff --git a/lib/amd/build/local/collapsable_section/events.min.js b/lib/amd/build/local/collapsable_section/events.min.js new file mode 100644 index 0000000000000..a6cda37c345a8 --- /dev/null +++ b/lib/amd/build/local/collapsable_section/events.min.js @@ -0,0 +1,20 @@ +define("core/local/collapsable_section/events",["exports","core/event_dispatcher"],(function(_exports,_event_dispatcher){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.notifyCollapsableSectionShown=_exports.notifyCollapsableSectionHidden=_exports.eventTypes=void 0; +/** + * The collapsable section events. + * + * This module wraps the standard bootstrap collapsable events, but for collapsable sections. + * + * @module core/local/collapsable_section/events + * @copyright 2024 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @example Example of listening to a collapsable section events. + * import {eventTypes as collapsableSectionEventTypes} from 'core/local/collapsable_section/events'; + * + * document.addEventListener(collapsableSectionEventTypes.shown, event => { + * window.console.log(event.target); // The HTMLElement relating to the block whose content was updated. + * }); + */ +const eventTypes={shown:"core_collapsable_section_shown",hidden:"core_collapsable_section_hidden",hideBsCollapse:"hide.bs.collapse",hiddenBsCollapse:"hidden.bs.collapse",showBsCollapse:"show.bs.collapse",shownBsCollapse:"shown.bs.collapse"};_exports.eventTypes=eventTypes;_exports.notifyCollapsableSectionShown=element=>(0,_event_dispatcher.dispatchEvent)(eventTypes.shown,{},element);_exports.notifyCollapsableSectionHidden=element=>(0,_event_dispatcher.dispatchEvent)(eventTypes.hidden,{},element)})); + +//# sourceMappingURL=events.min.js.map \ No newline at end of file diff --git a/lib/amd/build/local/collapsable_section/events.min.js.map b/lib/amd/build/local/collapsable_section/events.min.js.map new file mode 100644 index 0000000000000..1004111815052 --- /dev/null +++ b/lib/amd/build/local/collapsable_section/events.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"events.min.js","sources":["../../../src/local/collapsable_section/events.js"],"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 * The collapsable section events.\n *\n * This module wraps the standard bootstrap collapsable events, but for collapsable sections.\n *\n * @module core/local/collapsable_section/events\n * @copyright 2024 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * @example Example of listening to a collapsable section events.\n * import {eventTypes as collapsableSectionEventTypes} from 'core/local/collapsable_section/events';\n *\n * document.addEventListener(collapsableSectionEventTypes.shown, event => {\n * window.console.log(event.target); // The HTMLElement relating to the block whose content was updated.\n * });\n */\n\nimport {dispatchEvent} from 'core/event_dispatcher';\n\n/**\n * Events for `core_block`.\n *\n * @constant\n * @property {String} blockContentUpdated See {@link event:blockContentUpdated}\n */\nexport const eventTypes = {\n /**\n * An event triggered when the content of a block has changed.\n *\n * @event blockContentUpdated\n * @type {CustomEvent}\n * @property {HTMLElement} target The block element that was updated\n * @property {object} detail\n * @property {number} detail.instanceId The block instance id\n */\n shown: 'core_collapsable_section_shown',\n hidden: 'core_collapsable_section_hidden',\n // All Bootstrap 4 jQuery events are wrapped while MDL-71979 is not integrated.\n hideBsCollapse: 'hide.bs.collapse',\n hiddenBsCollapse: 'hidden.bs.collapse',\n showBsCollapse: 'show.bs.collapse',\n shownBsCollapse: 'shown.bs.collapse',\n};\n\n/**\n * Trigger an event to indicate that the content of a block was updated.\n *\n * @method notifyBlockContentUpdated\n * @param {HTMLElement} element The HTMLElement containing the updated block.\n * @returns {CustomEvent}\n * @fires blockContentUpdated\n */\nexport const notifyCollapsableSectionShown = element => dispatchEvent(\n eventTypes.shown,\n {},\n element\n);\n\n/**\n * Trigger an event to indicate that the content of a block was updated.\n *\n * @method notifyBlockContentUpdated\n * @param {HTMLElement} element The HTMLElement containing the updated block.\n * @returns {CustomEvent}\n * @fires blockContentUpdated\n */\nexport const notifyCollapsableSectionHidden = element => dispatchEvent(\n eventTypes.hidden,\n {},\n element\n);\n"],"names":["eventTypes","shown","hidden","hideBsCollapse","hiddenBsCollapse","showBsCollapse","shownBsCollapse","element"],"mappings":";;;;;;;;;;;;;;;;;MAwCaA,WAAa,CAUtBC,MAAO,iCACPC,OAAQ,kCAERC,eAAgB,mBAChBC,iBAAkB,qBAClBC,eAAgB,mBAChBC,gBAAiB,2FAWwBC,UAAW,mCACpDP,WAAWC,MACX,GACAM,iDAW0CA,UAAW,mCACrDP,WAAWE,OACX,GACAK"} \ No newline at end of file diff --git a/lib/amd/src/local/collapsable_section/controls.js b/lib/amd/src/local/collapsable_section/controls.js new file mode 100644 index 0000000000000..49ead59af3a25 --- /dev/null +++ b/lib/amd/src/local/collapsable_section/controls.js @@ -0,0 +1,138 @@ +// 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 . + +/** + * The collapsable sections controls. + * + * @module core/local/collapsable_section/controls + * @copyright 2024 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @example Example of controlling a collapsable section. + * + * import CollapsableSection from 'core/local/collapsable_section/controls'; + * + * const section = CollapsableSection.instanceFromSelector('#MyCollapsableSection'); + * + * // Use hide, show and toggle methods to control the section. + * section.hide(); + */ + +import { + eventTypes, + notifyCollapsableSectionHidden, + notifyCollapsableSectionShown +} from 'core/local/collapsable_section/events'; + +// The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated. +import jQuery from 'jquery'; + +let initialized = false; + +export default class { + /** + * Create a new instance from a query selector. + * + * @param {String} selector The selector of the collapsable section. + * @return {CollapsableSection} The collapsable section controls. + * @throws {Error} If no elements are found with the selector. + */ + static instanceFromSelector(selector) { + const elements = document.querySelector(selector); + if (!elements) { + throw new Error('No elements found with the selector: ' + selector); + } + return new this(elements); + } + + /** + * Initialize the collapsable section controls. + */ + static init() { + if (initialized) { + return; + } + initialized = true; + + // We want to add extra events to the standard bootstrap collapsable events. + // TODO: change all jquery events to custom events once MDL-71979 is integrated. + jQuery(document).on(eventTypes.hiddenBsCollapse, event => { + if (!this.isCollapsableComponent(event.target)) { + return; + } + notifyCollapsableSectionHidden(event.target); + }); + jQuery(document).on(eventTypes.shownBsCollapse, event => { + if (!this.isCollapsableComponent(event.target)) { + return; + } + notifyCollapsableSectionShown(event.target); + }); + } + + /** + * Check if the element is a collapsable section. + * + * @private + * @param {HTMLElement} element The element to check. + * @return {boolean} True if the element is a collapsable section. + */ + static isCollapsableComponent(element) { + return element.hasAttribute('data-mdl-component') + && element.getAttribute('data-mdl-component') === 'core/local/collapsable_section'; + } + + /** + * Creates an instance of the controls for a collapsable section. + * + * @param {HTMLElement} element - The DOM element that this control will manage. + */ + constructor(element) { + this.element = element; + } + + /** + * Hides the collapsible section element. + */ + hide() { + // TODO: change all jquery once MDL-71979 is integrated. + jQuery(this.element).collapse('hide'); + } + + /** + * Shows the collapsible section element. + */ + show() { + // TODO: change all jquery once MDL-71979 is integrated. + jQuery(this.element).collapse('show'); + } + + /** + * Toggle the collapsible section element. + */ + toggle() { + // TODO: change all jquery once MDL-71979 is integrated. + jQuery(this.element).collapse('toggle'); + } + + /** + * Check if the collapsable section is visible. + * + * @return {boolean} True if the collapsable section is visible. + */ + isVisible() { + return this.element.classList.contains('show'); + } +} diff --git a/lib/amd/src/local/collapsable_section/events.js b/lib/amd/src/local/collapsable_section/events.js new file mode 100644 index 0000000000000..0d32cf093cc25 --- /dev/null +++ b/lib/amd/src/local/collapsable_section/events.js @@ -0,0 +1,86 @@ +// 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 . + +/** + * The collapsable section events. + * + * This module wraps the standard bootstrap collapsable events, but for collapsable sections. + * + * @module core/local/collapsable_section/events + * @copyright 2024 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * + * @example Example of listening to a collapsable section events. + * import {eventTypes as collapsableSectionEventTypes} from 'core/local/collapsable_section/events'; + * + * document.addEventListener(collapsableSectionEventTypes.shown, event => { + * window.console.log(event.target); // The HTMLElement relating to the block whose content was updated. + * }); + */ + +import {dispatchEvent} from 'core/event_dispatcher'; + +/** + * Events for `core_block`. + * + * @constant + * @property {String} blockContentUpdated See {@link event:blockContentUpdated} + */ +export const eventTypes = { + /** + * An event triggered when the content of a block has changed. + * + * @event blockContentUpdated + * @type {CustomEvent} + * @property {HTMLElement} target The block element that was updated + * @property {object} detail + * @property {number} detail.instanceId The block instance id + */ + shown: 'core_collapsable_section_shown', + hidden: 'core_collapsable_section_hidden', + // All Bootstrap 4 jQuery events are wrapped while MDL-71979 is not integrated. + hideBsCollapse: 'hide.bs.collapse', + hiddenBsCollapse: 'hidden.bs.collapse', + showBsCollapse: 'show.bs.collapse', + shownBsCollapse: 'shown.bs.collapse', +}; + +/** + * Trigger an event to indicate that the content of a block was updated. + * + * @method notifyBlockContentUpdated + * @param {HTMLElement} element The HTMLElement containing the updated block. + * @returns {CustomEvent} + * @fires blockContentUpdated + */ +export const notifyCollapsableSectionShown = element => dispatchEvent( + eventTypes.shown, + {}, + element +); + +/** + * Trigger an event to indicate that the content of a block was updated. + * + * @method notifyBlockContentUpdated + * @param {HTMLElement} element The HTMLElement containing the updated block. + * @returns {CustomEvent} + * @fires blockContentUpdated + */ +export const notifyCollapsableSectionHidden = element => dispatchEvent( + eventTypes.hidden, + {}, + element +); diff --git a/lib/classes/output/local/collapsable_section.php b/lib/classes/output/local/collapsable_section.php new file mode 100644 index 0000000000000..de7e9ed890b44 --- /dev/null +++ b/lib/classes/output/local/collapsable_section.php @@ -0,0 +1,147 @@ +. + +namespace core\output\local; + +use core\output\named_templatable; +use core\output\renderable; + +/** + * Collapsable section output. + * + * @package core + * @copyright 2024 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class collapsable_section implements named_templatable, renderable { + /** + * Constructor. + * + * @param string $titlecontent The content to be displayed inside the button. + * @param string $sectioncontent The content to be displayed inside the dialog. + * @param string $classes Additional CSS classes to be applied to the section. + * @param array $extras An attribute => value array to be added to the element. + * @param bool $open If the section is opened by default. + * @param string|null $expandlabel The label for the expand button. + * @param string|null $collapselabel The label for the collapse button. + */ + public function __construct( + /** @var string $titlecontent The content to be displayed inside the button. */ + protected string $titlecontent, + /** @var string $sectioncontent The content to be displayed inside the dialog. */ + protected string $sectioncontent, + /** @var string $classes Additional CSS classes to be applied to the section. */ + protected string $classes = '', + /** @var array $extras A attribute => value array to be added to the element. */ + protected array $extras = [], + /** @var bool $open if the section is opened by default. */ + protected bool $open = false, + /** @var string|null $expandlabel The label for the expand button. */ + protected string|null $expandlabel = null, + /** @var string|null $collapselabel The label for the collapse button. */ + protected string|null $collapselabel = null, + ) { + } + + /** + * Set the title content. + * + * @param string $titlecontent + */ + public function set_title_content(string $titlecontent) { + $this->titlecontent = $titlecontent; + } + + /** + * Sets the content for the collapsable section. + * + * @param string $sectioncontent The content to be set for the section. + */ + public function set_section_content(string $sectioncontent) { + $this->sectioncontent = $sectioncontent; + } + + /** + * Sets the CSS classes for the collapsable section. + * + * @param string $classes The CSS classes to be applied to the collapsable section. + */ + public function set_classes(string $classes) { + $this->classes = $classes; + } + + /** + * Merges the provided extras array with the existing extras array. + * + * @param array $extras The array of extra attributes => extra value. + */ + public function add_extra_attributes(array $extras) { + $this->extras = array_merge($this->extras, $extras); + } + + /** + * Sets the default open state of the collapsible section. + * + * @param bool $open + */ + public function set_open(bool $open) { + $this->open = $open; + } + + #[\Override] + public function export_for_template(\renderer_base $output): array { + $elementid = $this->extras['id'] ?? \html_writer::random_id('collapsableSection_'); + + $data = [ + 'titlecontent' => $this->titlecontent, + 'sectioncontent' => $this->sectioncontent, + 'classes' => $this->classes, + 'extras' => $this->export_extras(), + 'elementid' => $elementid, + ]; + if ($this->open) { + $data['open'] = 'true'; + } + if ($this->expandlabel) { + $data['expandlabel'] = $this->expandlabel; + } + if ($this->collapselabel) { + $data['collapselabel'] = $this->collapselabel; + } + return $data; + } + + /** + * Exports the extras as an array of attribute-value pairs. + * + * @return array An array of associative arrays, each containing 'attribute' and 'value' keys. + */ + private function export_extras(): array { + $extras = []; + foreach ($this->extras as $attribute => $value) { + $extras[] = [ + 'attribute' => $attribute, + 'value' => $value, + ]; + } + return $extras; + } + + #[\Override] + public function get_template_name(\renderer_base $renderer): string { + return 'core/local/collapsable_section'; + } +} diff --git a/lib/templates/local/collapsable_section.mustache b/lib/templates/local/collapsable_section.mustache new file mode 100644 index 0000000000000..6b79e58e72587 --- /dev/null +++ b/lib/templates/local/collapsable_section.mustache @@ -0,0 +1,147 @@ +{{! + 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/local/collapsable_section + + Standard collapsible section. + + Optional blocks: + * extraclasses - additional classes. + * elementid - optional element id. + * titlecontent - the collpasible title content. + * sectioncontent - the collapsible content. + * extras - custom HTML attributes for the component. + * expandlabel - the label for the expand icon. + * collapselabel - the label for the collapse icon. + + Example context (json): + { + "titlecontent": "New content", + "sectioncontent": "New content", + "classes": "someclass", + "extras": [ + { + "attribute": "data-example", + "value": "something" + } + ], + "open": true, + "expandlabel": "Expand", + "collapselabel": "Collapse", + "elementid": "someuniqueid" + } +}} + +{{#js}} +require(['core/local/collapsable_section/controls'], function(Controls) { + Controls.init(); +}); +{{/js}} diff --git a/lib/tests/behat/collapsable_section_output.feature b/lib/tests/behat/collapsable_section_output.feature new file mode 100644 index 0000000000000..975fc425d51a7 --- /dev/null +++ b/lib/tests/behat/collapsable_section_output.feature @@ -0,0 +1,66 @@ +@core @javascript +Feature: Test collapsable section output module + In order to show extra information to the user + As a user + I need to interact with the collapsable section output + + Background: + # Get to the fixture page. + Given I log in as "admin" + And I am on fixture page "/lib/tests/behat/fixtures/collapsable_section_output_testpage.php" + + Scenario: Collapsable sections can be opened and closed + Given I should not see "Dialog content" + And I should not see "This is the closed section content." in the "closedsection" "region" + And I should see "This is the open section content." in the "opensection" "region" + When I click on "Expand" "button" in the "closedsection" "region" + And I click on "Collapse" "button" in the "opensection" "region" + Then I should see "This is the closed section content." in the "closedsection" "region" + And I should not see "This is the open section content." in the "opensection" "region" + + Scenario: Collapsable sections content can have rich content inside + When I click on "Expand" "button" in the "closedsection" "region" + Then I should see "This is the closed section content." in the "closedsection" "region" + And "Link" "link" should exist in the "closedsection" "region" + And "Eye icon" "icon" should exist in the "closedsection" "region" + + Scenario: Collapsable sections HTML attributtes can be overriden + When I click on "Expand" "button" in the "extraclasses" "region" + And I click on "Expand" "button" in the "extraattributes" "region" + Then ".extraclass" "css_element" should exist in the "extraclasses" "region" + And "[data-foo='bar']" "css_element" should exist in the "extraattributes" "region" + And "#myid" "css_element" should exist in the "extraattributes" "region" + + Scenario: Collapsable sections can have custom labels for expand and collapse + When I click on "Custom expand" "button" in the "customlabels" "region" + Then I should see "This is the custom labels content." in the "customlabels" "region" + And I click on "Custom collapse" "button" in the "customlabels" "region" + And I should not see "This is the custom labels content." in the "customlabels" "region" + + Scenario: Collapsable sections can be controlled via javascript + # Toggle. + Given I should not see "This is the javascript controls content." in the "jscontrols" "region" + When I click on "Toggle" "button" in the "jscontrols" "region" + Then I should see "This is the javascript controls content." in the "jscontrols" "region" + And I click on "Toggle" "button" in the "jscontrols" "region" + And I should not see "This is the javascript controls content." in the "jscontrols" "region" + # Show and Hide. + And I click on "Show" "button" in the "jscontrols" "region" + And I should see "This is the javascript controls content." in the "jscontrols" "region" + And I click on "Show" "button" in the "jscontrols" "region" + And I should see "This is the javascript controls content." in the "jscontrols" "region" + And I click on "Hide" "button" in the "jscontrols" "region" + And I should not see "This is the javascript controls content." in the "jscontrols" "region" + And I click on "Hide" "button" in the "jscontrols" "region" + And I should not see "This is the javascript controls content." in the "jscontrols" "region" + # Test state. + And I click on "Test state" "button" in the "jscontrols" "region" + And I should see "hidden" in the "state" "region" + And I click on "Show" "button" in the "jscontrols" "region" + And I click on "Test state" "button" in the "jscontrols" "region" + And I should see "visible" in the "state" "region" + # Events. + And I click on "Show" "button" in the "jscontrols" "region" + And I should see "Last event: Section shown" in the "jscontrols" "region" + And I click on "Hide" "button" in the "jscontrols" "region" + And I should see "Last event: Section hidden" in the "jscontrols" "region" diff --git a/lib/tests/behat/fixtures/collapsable_section_output_testpage.php b/lib/tests/behat/fixtures/collapsable_section_output_testpage.php new file mode 100644 index 0000000000000..3f29e6814d57b --- /dev/null +++ b/lib/tests/behat/fixtures/collapsable_section_output_testpage.php @@ -0,0 +1,204 @@ +. + +/** + * Test page for the collapsable section output component. + * + * @copyright 2024 Ferran Recio + * @package core + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../../config.php'); + +use core\output\local\collapsable_section; + +defined('BEHAT_SITE_RUNNING') || die(); + +/** + * Generate the title content. + * + * @param string $content The content to be displayed inside the button. + * @return string + */ +function title_content(string $content): string { + return ucfirst($content) . ' title'; +} + +/** + * Generate the section content. + * + * @param string $content The content to be displayed inside the dialog. + * @return string + */ +function section_content(string $content): string { + global $OUTPUT; + $icon = $OUTPUT->pix_icon('t/hide', 'Eye icon'); + return ' +

This is the ' . $content . ' content.

+

Some rich content Link ' . $icon . '.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, + quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, + quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, + quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat.

+ '; +} + +global $CFG, $PAGE, $OUTPUT; +$PAGE->set_url('/lib/tests/behat/fixtures/collapsable_section_output_testpage.php'); +$PAGE->add_body_class('limitedwidth'); +require_login(); +$PAGE->set_context(core\context\system::instance()); +$PAGE->set_title('Collapsable section test page'); + +echo $OUTPUT->header(); + +echo "

Collapsable section test page

"; +echo $OUTPUT->paragraph('This page is used to test the collapsable section output component.'); + +$sample = 'closed section'; +echo '
'; +echo $OUTPUT->paragraph($sample . ' example'); +$collapsable = new collapsable_section( + titlecontent: title_content($sample), + sectioncontent: section_content($sample), +); +echo $OUTPUT->render($collapsable); +echo '
'; + +$sample = 'open section'; +echo '
'; +echo $OUTPUT->paragraph($sample . ' example'); +$collapsable = new collapsable_section( + titlecontent: title_content($sample), + sectioncontent: section_content($sample), + open: true, +); +echo $OUTPUT->render($collapsable); +echo '
'; + +$sample = 'extra classes'; +echo '
'; +echo $OUTPUT->paragraph($sample . ' example'); +$collapsable = new collapsable_section( + titlecontent: title_content($sample), + sectioncontent: section_content($sample), + classes: 'bg-dark text-white p-3 rounded-3 extraclass', +); +echo $OUTPUT->render($collapsable); +echo '
'; + +$sample = 'extra attributes'; +echo '
'; +echo $OUTPUT->paragraph($sample . ' example'); +$collapsable = new collapsable_section( + titlecontent: title_content($sample), + sectioncontent: section_content($sample), + extras: ['data-foo' => 'bar', 'id' => 'myid'], +); +echo $OUTPUT->render($collapsable); +echo '
'; + +$sample = 'custom labels'; +echo '
'; +echo $OUTPUT->paragraph($sample . ' example'); +$collapsable = new collapsable_section( + titlecontent: title_content($sample), + sectioncontent: section_content($sample), + expandlabel: 'Custom expand', + collapselabel: 'Custom collapse', +); +echo $OUTPUT->render($collapsable); +echo '
'; + +$sample = 'javascript controls'; +echo '
'; +echo $OUTPUT->paragraph($sample . ' example'); +echo ' +
+ + + + +
+ Current state:
?
+
+
'; +echo '
+ Last event:
?
+
'; +$collapsable = new collapsable_section( + titlecontent: title_content($sample), + sectioncontent: section_content($sample), + extras: ['id' => 'jsCollapsable'], +); +echo $OUTPUT->render($collapsable); +echo '
'; + +$inlinejs = <<requires->js_amd_inline($inlinejs); + +echo ''; +echo $OUTPUT->footer(); From 53886acba67417b7812be2f1a1918d4ed56a8d0f Mon Sep 17 00:00:00 2001 From: ferran Date: Fri, 20 Dec 2024 11:55:03 +0100 Subject: [PATCH 2/2] MDL-83869 tool_componentlibrary: collapsable section page --- .../moodle/components/collapsable-sections.md | 158 ++++++++++++++++++ .../includesection.mustache | 35 ++++ 2 files changed, 193 insertions(+) create mode 100644 admin/tool/componentlibrary/content/moodle/components/collapsable-sections.md create mode 100644 admin/tool/componentlibrary/templates/examples/collapsablesections/includesection.mustache diff --git a/admin/tool/componentlibrary/content/moodle/components/collapsable-sections.md b/admin/tool/componentlibrary/content/moodle/components/collapsable-sections.md new file mode 100644 index 0000000000000..655d034177a4a --- /dev/null +++ b/admin/tool/componentlibrary/content/moodle/components/collapsable-sections.md @@ -0,0 +1,158 @@ +--- +layout: docs +title: "Collapsable sections" +description: "A reusable collapsable section component" +date: 2024-12-20T10:10:00+08:00 +draft: false +tags: +- MDL-83869 +- "5.0" +--- + +## How it works + +The collapsable section component in Moodle allows you to create sections of content that can be expanded or collapsed by the user. This is useful for organizing content in a way that doesn't overwhelm the user with too much information at once. The component is built using a combination of PHP, Mustache templates, and JavaScript. + +## Source files + +- `lib/templates/local/collapsable_section.mustache`: The Mustache template for rendering the collapsable section. +- `lib/classes/output/local/collapsable_section.php`: The output class for the collapsable section component. +- `lib/amd/src/local/collapsable_section/events.js`: JavaScript module for handling events related to the collapsable section. +- `lib/amd/src/local/collapsable_section/controls.js`: JavaScript module for controlling the collapsable section. + +## Usage + +To use the collapsable section component, you need to create an instance of the `collapsable_section` class and render it using Moodle's output renderer. You can customize the title content, section content, CSS classes, and additional HTML attributes. + +Example: + +{{< php >}} +use core\output\local\collapsable_section; + +// Create an instance of the collapsable section. +$section = new collapsable_section( + titlecontent: 'Section Title', + sectioncontent: 'This is the content of the section.', +); + +echo $OUTPUT->render($section); +{{< / php >}} + +{{< mustache template="core/local/collapsable_section" >}} + { + "titlecontent": "Section Title", + "sectioncontent": "This is the content of the section." + } +{{< /mustache >}} + +You can also add CSS classes, extra HTML attributes, and customize the expand and collapse labels of the collapsable section: + +{{< php >}} +$section = new collapsable_section( + titlecontent: 'Section Title', + sectioncontent: 'This is the content of the section.', + open: true, // Optional parameter to set the section as open by default. + classes: 'p-3 rounded bg-dark text-white', // Optional parameter to add custom CSS classes. + extras: ['id' => 'MyCollapsableSection', 'data-foo' => 'bar'], // Optional HTML attributes. + expandlabel: 'Show more', // Optional label for the expand button. + collapselabel: 'Show less', // Optional label for the collapse button. +); + +echo $OUTPUT->render($section); +{{< / php >}} + +{{< mustache template="core/local/collapsable_section" >}} + { + "titlecontent": "Section Title", + "sectioncontent": "This is the content of the section.", + "open": true, + "classes": "p-3 rounded bg-dark text-white", + "elementid": "someuniqueid", + "extras": [ + { + "attribute": "id", + "value": "MyCollapsableSection" + }, + { + "attribute": "data-foo", + "value": "bar" + } + ], + "expandlabel": "Show more", + "collapselabel": "Show less" + } +{{< /mustache >}} + +## Include a collapsable section from a mustache template + +Collapsable sections can also be included from a Mustache template using the `core/local/collapsable_section` template. This template allows you to define the title content and section content within the template. + +{{< mustache template="tool_componentlibrary/examples/collapsablesections/includesection" >}} + { + } +{{< /mustache >}} + +## JavaScript + +### Control a section + +The collapsable sections component includes a JavaScript module for controlling the sections. This module provides methods to hide, show, and toggle the visibility of the sections. + +To use the JavaScript controls, you need to import the `CollapsableSection` module and create an instance from a selector: + +```javascript +import CollapsableSection from 'core/local/collapsable_section/controls'; + +const section = CollapsableSection.instanceFromSelector('#MyCollapsableSection'); + +// Use hide, show, and toggle methods to control the section. +section.hide(); +section.show(); +section.toggle(); +``` + +### Get the state of a section + +You can also check the state of a section using the `isHidden` method: + +```javascript +import CollapsableSection from 'core/local/collapsable_section/controls'; + +const section = CollapsableSection.instanceFromSelector('#MyCollapsableSection'); + +if (section.isVisible()) { + console.log('The section is hidden.'); +} else { + console.log('The section is visible.'); +} +``` + +### Events + +The collapsable sections component also includes a JavaScript module for handling events. This module wraps the standard Bootstrap collapsable events and provides custom event types for collapsable sections. + +The component triggers two main events: + +- `core_collapsable_section_shown`: when the collapsed content is shown. +- `core_collapsable_section_hidden`: when the collapsed content is hidden. + +For convenience, the `core/local/collapsable_section/events` also list the original Bootstrap events. They should not be needed in most cases, but they are available if you need them: + +- `show.bs.collapse`: when the collapse is starting to show. +- `shown.bs.collapse`: when the collapse has been shown. +- `hide.bs.collapse`: when the collapse is starting to hide. +- `hidden.bs.collapse`: when the collapse has been hidden. + +To listen for events related to the collapsable sections, you need to import the `eventTypes` from the `events` module and add event listeners: + +```javascript +import {eventTypes as collapsableSectionEventTypes} from 'core/local/collapsable_section/events'; + +document.addEventListener(collapsableSectionEventTypes.shown, event => { + console.log(event.target); // The HTMLElement relating to the section that was shown. +}); + +document.addEventListener(collapsableSectionEventTypes.hidden, event => { + console.log(event.target); // The HTMLElement relating to the section that was hidden. +}); +``` diff --git a/admin/tool/componentlibrary/templates/examples/collapsablesections/includesection.mustache b/admin/tool/componentlibrary/templates/examples/collapsablesections/includesection.mustache new file mode 100644 index 0000000000000..5f6da08b58225 --- /dev/null +++ b/admin/tool/componentlibrary/templates/examples/collapsablesections/includesection.mustache @@ -0,0 +1,35 @@ +{{! + 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 tool_componentlibrary/examples/collapsablesections/includesection + + TODO describe template includesection + + Example context (json): + { + } +}} +{{