MDL-83872 core_courseformat: prepare completion button for reuse

The current completion button has specific templates only for the course
view page. However, from now on this component may be used in other
places and it needs a main template to be rendered in a standard way.
This commit is contained in:
ferran
2025-02-18 18:00:24 +01:00
parent 802920fd4c
commit 6a230a0bca
9 changed files with 151 additions and 46 deletions
@@ -0,0 +1,10 @@
issueNumber: MDL-83872
notes:
core_course:
- message: >-
Now the core_courseformat\local\content\cm\completion output is more
reusable. All the HTML has been moved to its own mustache file,
and the output class has a new set_smallbutton method to decide wether
to rendered it as a small button (like in the course page) or as a
normal one (for other types of pages).
type: improved
+1 -1
View File
@@ -6,6 +6,6 @@ define("core_course/manual_completion_toggle",["exports","core/templates","core/
* @module core_course/manual_completion_toggle
* @copyright 2021 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_templates=_interopRequireDefault(_templates),_notification=_interopRequireDefault(_notification),CourseEvents=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CourseEvents),_pending=_interopRequireDefault(_pending);const SELECTORS_MANUAL_TOGGLE="button[data-action=toggle-manual-completion]",TOGGLE_TYPES_TOGGLE_MARK_DONE="manual:mark-done";let registered=!1;_exports.init=()=>{registered||(document.addEventListener("click",(e=>{const toggleButton=e.target.closest(SELECTORS_MANUAL_TOGGLE);toggleButton&&(e.preventDefault(),toggleManualCompletionState(toggleButton).catch(_notification.default.exception))})),registered=!0)};const toggleManualCompletionState=async toggleButton=>{const pendingPromise=new _pending.default("core_course:toggleManualCompletionState"),originalInnerHtml=toggleButton.innerHTML;toggleButton.setAttribute("disabled","disabled");const toggleType=toggleButton.getAttribute("data-toggletype"),cmid=toggleButton.getAttribute("data-cmid"),activityname=toggleButton.getAttribute("data-activityname"),completed=toggleType===TOGGLE_TYPES_TOGGLE_MARK_DONE;_templates.default.renderForPromise("core/loading",{}).then((loadingHtml=>{_templates.default.replaceNodeContents(toggleButton,loadingHtml,"")})).catch((()=>{}));try{await(0,_repository.toggleManualCompletion)(cmid,completed);const templateContext={cmid:cmid,activityname:activityname,overallcomplete:completed,overallincomplete:!completed,istrackeduser:!0},renderObject=await _templates.default.renderForPromise("core_course/completion_manual",templateContext),newToggleButton=(await _templates.default.replaceNode(toggleButton,renderObject.html,renderObject.js)).pop(),withAvailability=toggleButton.getAttribute("data-withavailability"),toggledEvent=new CustomEvent(CourseEvents.manualCompletionToggled,{bubbles:!0,detail:{cmid:cmid,activityname:activityname,completed:completed,withAvailability:withAvailability}});newToggleButton.dispatchEvent(toggledEvent)}catch(exception){toggleButton.removeAttribute("disabled"),toggleButton.innerHTML=originalInnerHtml,_notification.default.exception(exception)}pendingPromise.resolve()}}));
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_templates=_interopRequireDefault(_templates),_notification=_interopRequireDefault(_notification),CourseEvents=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(CourseEvents),_pending=_interopRequireDefault(_pending);const SELECTORS_MANUAL_TOGGLE="button[data-action=toggle-manual-completion]",TOGGLE_TYPES_TOGGLE_MARK_DONE="manual:mark-done";let registered=!1;_exports.init=()=>{registered||(document.addEventListener("click",(e=>{const toggleButton=e.target.closest(SELECTORS_MANUAL_TOGGLE);toggleButton&&(e.preventDefault(),toggleManualCompletionState(toggleButton).catch(_notification.default.exception))})),registered=!0)};const toggleManualCompletionState=async toggleButton=>{const pendingPromise=new _pending.default("core_course:toggleManualCompletionState"),originalInnerHtml=toggleButton.innerHTML;toggleButton.setAttribute("disabled","disabled");const toggleType=toggleButton.getAttribute("data-toggletype"),cmid=toggleButton.getAttribute("data-cmid"),activityname=toggleButton.getAttribute("data-activityname"),completed=toggleType===TOGGLE_TYPES_TOGGLE_MARK_DONE;_templates.default.renderForPromise("core/loading",{}).then((loadingHtml=>{_templates.default.replaceNodeContents(toggleButton,loadingHtml,"")})).catch((()=>{}));try{await(0,_repository.toggleManualCompletion)(cmid,completed);const templateContext={cmid:cmid,activityname:activityname,overallcomplete:completed,overallincomplete:!completed,istrackeduser:!0,normalbutton:!toggleButton.classList.contains("btn-sm")},renderObject=await _templates.default.renderForPromise("core_course/completion_manual",templateContext),newToggleButton=(await _templates.default.replaceNode(toggleButton,renderObject.html,renderObject.js)).pop(),withAvailability=toggleButton.getAttribute("data-withavailability"),toggledEvent=new CustomEvent(CourseEvents.manualCompletionToggled,{bubbles:!0,detail:{cmid:cmid,activityname:activityname,completed:completed,withAvailability:withAvailability}});newToggleButton.dispatchEvent(toggledEvent)}catch(exception){toggleButton.removeAttribute("disabled"),toggleButton.innerHTML=originalInnerHtml,_notification.default.exception(exception)}pendingPromise.resolve()}}));
//# sourceMappingURL=manual_completion_toggle.min.js.map
File diff suppressed because one or more lines are too long
@@ -110,6 +110,7 @@ const toggleManualCompletionState = async(toggleButton) => {
overallcomplete: completed,
overallincomplete: !completed,
istrackeduser: true, // We know that we're tracking completion for this user given the presence of this button.
normalbutton: !toggleButton.classList.contains('btn-sm'),
};
const renderObject = await Templates.renderForPromise('core_course/completion_manual', templateContext);
@@ -42,8 +42,12 @@ class activity_completion implements renderable, templatable {
* @param cm_completion_details $cmcompletion The course module information.
*/
public function __construct(
/** @var cm_info $cminfo the activity cm_info. */
protected cm_info $cminfo,
protected cm_completion_details $cmcompletion
/** @var cm_completion_details $cmcompletion the activity completion details. */
protected cm_completion_details $cmcompletion,
/** @var bool $smallbutton if the button is rendered small (like in course page). */
protected bool $smallbutton = true,
) {
}
@@ -84,6 +88,8 @@ class activity_completion implements renderable, templatable {
'overrideby' => $overrideby,
'completiondetails' => $this->get_completion_details($overrideby),
'accessibledescription' => $this->get_accessible_description($overrideby, $overallcompletion),
// For backward compatibility, the template uses small button by default when not set normal size.
'normalbutton' => !$this->smallbutton,
];
}
@@ -38,6 +38,9 @@ class completion implements named_templatable, renderable {
use courseformat_named_templatable;
/** @var bool $smallbutton if the button is rendered small (like in course page). */
private bool $smallbutton = true;
/**
* Constructor.
*
@@ -52,6 +55,19 @@ class completion implements named_templatable, renderable {
) {
}
/**
* Set if the button is rendered small.
*
* By default, the button is rendered small like in the course page. However,
* in other pages like the activities overview, the button is rendered like
* a regular button.
*
* @param bool $smallbutton if the button is rendered small (like in course page).
*/
public function set_smallbutton(bool $smallbutton): void {
$this->smallbutton = $smallbutton;
}
/**
* Export this data so it can be used as the context for a mustache template.
*
@@ -76,7 +92,7 @@ class completion implements named_templatable, renderable {
return null;
}
$completion = new activity_completion($this->mod, $completiondetails);
$completion = new activity_completion($this->mod, $completiondetails, $this->smallbutton);
$completiondata = $completion->export_for_template($output);
if ($completiondata->isautomatic || ($completiondata->ismanual && !$completiondata->istrackeduser)) {
@@ -108,6 +124,7 @@ class completion implements named_templatable, renderable {
$buttoncontent = get_string('completionmenuitem', 'completion');
$buttonclass = '';
if ($completioninfo->istrackeduser) {
$buttonclass = 'btn-outline-secondary';
$buttoncontent = get_string('todo', 'completion');
if ($completioninfo->overallcomplete) {
$buttoncontent = $output->pix_icon('i/checked', '') . " " . get_string('completion_manual:done', 'core_course');
@@ -115,9 +132,11 @@ class completion implements named_templatable, renderable {
}
}
$buttonclass .= $this->smallbutton ? ' btn-sm' : '';
$completiondialog = new dropdown_dialog($buttoncontent, $dialogcontent, [
'classes' => 'completion-dropdown',
'buttonclasses' => 'btn btn-sm dropdown-toggle icon-no-margin ' . $buttonclass,
'buttonclasses' => 'btn dropdown-toggle icon-no-margin ' . $buttonclass,
'dropdownposition' => dropdown_dialog::POSITION['end'],
]);
@@ -36,7 +36,7 @@
"isautomatic": true,
"showmanualcompletion": true,
"completiondetails": [
{
{
"statuscomplete": 1,
"description": "Viewed"
},
@@ -48,23 +48,5 @@
}
}}
<div data-region="activity-information" data-activityname="{{activityname}}" class="activity-information">
{{#uservisible}}
<div data-region="completion-info">
{{! Completion dropdown dialog }}
{{#completiondialog}}
<div data-region="completionrequirements" aria-label="{{#str}}completionrequirements, core_course, {{activityname}}{{/str}}">
{{> core/local/dropdown/dialog }}
</div>
{{/completiondialog}}
{{! Manual completion button }}
{{#showmanualcompletion}}
{{#istrackeduser}}
{{$ core_course/completion_manual }}
{{> core_course/completion_manual }}
{{/ core_course/completion_manual }}
{{/istrackeduser}}
{{/showmanualcompletion}}
</div>
{{/uservisible}}
{{> core_courseformat/local/content/cm/completion }}
</div>
@@ -0,0 +1,69 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core_courseformat/local/content/cm/completion
TODO describe template completion
Example context (json):
{
"cmid": 123,
"activityname": "Course announcements",
"hascompletion": true,
"uservisible": true,
"completiondialog": {
"buttoncontent": "To do",
"dialogcontent": "You must view this activity to complete it.",
"buttonclasses": "btn btn-sm btn-outline-secondary dropdown-toggle",
"classes": "completion-dropdown"
},
"isautomatic": true,
"showmanualcompletion": true,
"completiondetails": [
{
"statuscomplete": 1,
"description": "Viewed"
},
{
"statusincomplete": 1,
"description": "Receive a grade"
}
]
}
}}
{{#uservisible}}
<div data-region="completion-info">
{{! Completion dropdown dialog }}
{{#completiondialog}}
<div
data-region="completionrequirements"
aria-label="{{#str}}completionrequirements, core_course, {{activityname}}{{/str}}"
>
{{> core/local/dropdown/dialog }}
</div>
{{/completiondialog}}
{{! Manual completion button }}
{{#showmanualcompletion}}
{{#istrackeduser}}
{{$ core_course/completion_manual }}
{{> core_course/completion_manual }}
{{/ core_course/completion_manual }}
{{/istrackeduser}}
{{/showmanualcompletion}}
</div>
{{/uservisible}}
+39 -21
View File
@@ -23,45 +23,63 @@
{
"cmid": 0,
"overallcomplete": true,
"overallincomplete": false
"overallincomplete": false,
"normalbutton": true
}
}}
{{#istrackeduser}}
{{#overallcomplete}}
<button class="btn btn-success btn-sm text-nowrap" data-action="toggle-manual-completion" data-toggletype="manual:undo" data-cmid="{{cmid}}" data-activityname="{{activityname}}" data-withavailability="{{withavailability}}" {{!
}}{{#accessibledescription}}{{!
}}title="{{.}}" {{!
}}aria-label="{{.}}" {{!
}}{{/accessibledescription}}{{!
}}{{^accessibledescription}}{{!
}}title="{{#str}}completion_manual:aria:done, course, {{activityname}} {{/str}}" {{!
}}aria-label="{{#str}}completion_manual:aria:done, course, {{activityname}} {{/str}}" {{!
}}{{/accessibledescription}}>
<button
class="btn btn-success {{^normalbutton}}btn-sm{{/normalbutton}} text-nowrap"
data-action="toggle-manual-completion"
data-toggletype="manual:undo"
data-cmid="{{cmid}}"
data-activityname="{{activityname}}"
data-withavailability="{{withavailability}}" {{!
}}{{#accessibledescription}}{{!
}}title="{{.}}" {{!
}}aria-label="{{.}}" {{!
}}{{/accessibledescription}}{{!
}}{{^accessibledescription}}{{!
}}title="{{#str}}completion_manual:aria:done, course, {{activityname}} {{/str}}" {{!
}}aria-label="{{#str}}completion_manual:aria:done, course, {{activityname}} {{/str}}" {{!
}}{{/accessibledescription}}
>
<i class="fa fa-check" aria-hidden="true"></i> {{#str}} completion_manual:done, core_course {{/str}}
</button>
{{/overallcomplete}}
{{#overallincomplete}}
<button class="btn btn-outline-secondary btn-sm text-nowrap" data-action="toggle-manual-completion" data-toggletype="manual:mark-done" data-cmid="{{cmid}}" data-activityname="{{activityname}}" data-withavailability="{{withavailability}}" {{!
}}{{#accessibledescription}}{{!
}}title="{{.}}" {{!
}}aria-label="{{.}}" {{!
}}{{/accessibledescription}}{{!
}}{{^accessibledescription}}{{!
}}title="{{#str}}completion_manual:aria:markdone, course, {{activityname}} {{/str}}" {{!
}}aria-label="{{#str}}completion_manual:aria:markdone, course, {{activityname}} {{/str}}" {{!
}}{{/accessibledescription}}>
<button
class="btn btn-outline-secondary {{^normalbutton}}btn-sm{{/normalbutton}} text-nowrap"
data-action="toggle-manual-completion"
data-toggletype="manual:mark-done"
data-cmid="{{cmid}}"
data-activityname="{{activityname}}"
data-withavailability="{{withavailability}}" {{!
}}{{#accessibledescription}}{{!
}}title="{{.}}" {{!
}}aria-label="{{.}}" {{!
}}{{/accessibledescription}}{{!
}}{{^accessibledescription}}{{!
}}title="{{#str}}completion_manual:aria:markdone, course, {{activityname}} {{/str}}" {{!
}}aria-label="{{#str}}completion_manual:aria:markdone, course, {{activityname}} {{/str}}" {{!
}}{{/accessibledescription}}
>
{{#str}} completion_manual:markdone, core_course {{/str}}
</button>
{{/overallincomplete}}
{{/istrackeduser}}
{{^istrackeduser}}
<button class="btn btn-outline-secondary btn-sm text-nowrap" disabled>
<button
class="btn btn-outline-secondary {{^normalbutton}}btn-sm{{/normalbutton}} text-nowrap"
disabled
>
{{#str}} completion_manual:markdone, core_course {{/str}}
</button>
{{/istrackeduser}}
{{#js}}
require(['core_course/manual_completion_toggle'], toggle => {
toggle.init()
toggle.init();
});
{{/js}}