MDL-81644 calendar: The behaviour of event dates in the block calendar

There is a difference in the behaviour of the calendar block when it is on the calendar page and when it is not.
On the calendar page, when the user clicks on the date or link next/previous month or day name in the calendar block,
it will have the effect of changing the URL, which should not happen.

The patch also includes hiding popover after the user clicks the day number. When the user clicks on the date or
is focused on the date and then presses enter on the keyboard, the popover does not automatically close.
To fix this, I added an event type, "click", to be attached to the hidePopover function and added conditioning to
the target element when there was a "click" event.

An additional step was added to the Behat calendar for the click event to ease the testing process.

For the popover, I set the "animation" to false to avoid the random Behat failure caused by the slow animation.
This commit is contained in:
Meirza 2024-06-07 13:44:03 +07:00 committed by meirzamoodle
parent 70c8bfa038
commit 821f2a390c
17 changed files with 136 additions and 35 deletions

View File

@ -56,6 +56,9 @@ class block_calendar_month extends block_base {
$calendar = \calendar_information::create(time(), $courseid, $categoryid);
list($data, $template) = calendar_get_view($calendar, 'monthblock', isloggedin());
// Add a flag that this is coming from calendar block.
$data->iscalendarblock = true;
$renderer = $this->page->get_renderer('core_calendar');
$this->content->text .= $renderer->render_from_template($template, $data);

View File

@ -90,3 +90,22 @@ Feature: Enable the calendar block in a course and test it's functionality
Then I should see "Group Event"
And I am on the "Course 1" course page logged in as student2
And I should not see "Group Event"
@javascript
Scenario: Click on today's course event on the calendar view page's calendar block
Given I log in as "admin"
And I create a calendar event with form data:
| id_eventtype | Site |
| id_name | Site Event |
And I am on site homepage
And I turn editing mode on
And I add the "Calendar" block
And I configure the "Calendar" block
And I set the following fields to these values:
| Page contexts | Display throughout the entire site |
And I press "Save changes"
When I am on "Course 1" course homepage
And I follow "Course calendar"
And I click on today in the mini-calendar block
Then I should see "Site Event" in the "Calendar" "block"
And ".popover" "css_element" should not exist

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,6 @@
* @copyright 2017 Simey Lameze <simey@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define("core_calendar/calendar_view",["jquery","core/notification","core_calendar/selectors","core_calendar/events","core_calendar/view_manager","core_calendar/crud"],(function($,Notification,CalendarSelectors,CalendarEvents,CalendarViewManager,CalendarCrud){return{init:function(root,type){root=$(root),CalendarViewManager.init(root,type),function(root,type){var body=$("body");CalendarCrud.registerRemove(root);var reloadFunction="reloadCurrent"+type.charAt(0).toUpperCase()+type.slice(1);body.on(CalendarEvents.created,(function(){CalendarViewManager[reloadFunction](root)})),body.on(CalendarEvents.deleted,(function(){CalendarViewManager[reloadFunction](root)})),body.on(CalendarEvents.updated,(function(){CalendarViewManager[reloadFunction](root)})),root.on("change",CalendarSelectors.courseSelector,(function(){var selectElement=$(this),courseId=selectElement.val();const courseName=$("option:selected",selectElement).text();CalendarViewManager[reloadFunction](root,courseId,null).then((function(){return root.find(CalendarSelectors.courseSelector).val(courseId)})).then((function(){CalendarViewManager.updateUrl("?view="+type+"&course="+courseId),CalendarViewManager.handleCourseChange(Number(courseId),courseName)})).fail(Notification.exception)})),body.on(CalendarEvents.filterChanged,(function(e,data){var daysWithEvent=root.find(CalendarSelectors.eventType[data.type]);1==data.hidden?daysWithEvent.addClass("hidden"):daysWithEvent.removeClass("hidden"),CalendarViewManager.foldDayEvents(root)}));var eventFormPromise=CalendarCrud.registerEventFormModal(root);CalendarCrud.registerEditListeners(root,eventFormPromise)}(root,type)}}}));
define("core_calendar/calendar_view",["jquery","core/notification","core_calendar/selectors","core_calendar/events","core_calendar/view_manager","core_calendar/crud"],(function($,Notification,CalendarSelectors,CalendarEvents,CalendarViewManager,CalendarCrud){var registerEventListeners=function(root,type){var body=$("body");CalendarCrud.registerRemove(root);var reloadFunction="reloadCurrent"+type.charAt(0).toUpperCase()+type.slice(1);body.on(CalendarEvents.created,(function(){CalendarViewManager[reloadFunction](root)})),body.on(CalendarEvents.deleted,(function(){CalendarViewManager[reloadFunction](root)})),body.on(CalendarEvents.updated,(function(){CalendarViewManager[reloadFunction](root)})),root.on("change",CalendarSelectors.courseSelector,(function(){var selectElement=$(this),courseId=selectElement.val();const courseName=$("option:selected",selectElement).text();CalendarViewManager[reloadFunction](root,courseId,null).then((function(){return root.find(CalendarSelectors.courseSelector).val(courseId)})).then((function(){CalendarViewManager.updateUrl("?view="+type+"&course="+courseId),CalendarViewManager.handleCourseChange(Number(courseId),courseName)})).fail(Notification.exception)})),body.on(CalendarEvents.filterChanged,(function(e,data){var daysWithEvent=root.find(CalendarSelectors.eventType[data.type]);1==data.hidden?daysWithEvent.addClass("hidden"):daysWithEvent.removeClass("hidden"),CalendarViewManager.foldDayEvents(root)}));var eventFormPromise=CalendarCrud.registerEventFormModal(root);CalendarCrud.registerEditListeners(root,eventFormPromise)};return{init:function(root,type){let isCalendarBlock=arguments.length>2&&void 0!==arguments[2]&&arguments[2];root=$(root),CalendarViewManager.init(root,type,isCalendarBlock),registerEventListeners(root,type)}}}));
//# sourceMappingURL=calendar_view.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -6,6 +6,6 @@ define("core_calendar/popover",["theme_boost/popover","jquery","core_calendar/se
* @copyright 2021 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 4.0
*/function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj},CalendarSelectors=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}(CalendarSelectors);const isPopoverConfigured=new Map,showPopover=target=>{const dateContainer=target.closest(CalendarSelectors.elements.dateContainer);if(!isPopoverConfigured.has(dateContainer)){(0,_jquery.default)(target).popover({trigger:"manual",placement:"top",html:!0,title:dateContainer.dataset.title,content:()=>{const source=(0,_jquery.default)(dateContainer).find(CalendarSelectors.elements.dateContent),content=(0,_jquery.default)("<div>");if(source.length){const temptContent=source.find(".hidden").clone(!1);content.html(temptContent.html())}return content.html()}}),isPopoverConfigured.set(dateContainer,!0)}(dateContainer=>"none"===window.getComputedStyle(dateContainer.querySelector(CalendarSelectors.elements.dateContent)).display)(dateContainer)&&((0,_jquery.default)(target).popover("show"),target.addEventListener("mouseleave",hidePopover),target.addEventListener("focusout",hidePopover))},hidePopover=e=>{const target=e.target,dateContainer=e.target.closest(CalendarSelectors.elements.dateContainer);if(dateContainer&&isPopoverConfigured.has(dateContainer)){const isTargetActive=target.contains(document.activeElement),isTargetHover=target.matches(":hover");isTargetActive||isTargetHover||((0,_jquery.default)(target).popover("hide"),target.removeEventListener("mouseleave",hidePopover),target.removeEventListener("focusout",hidePopover))}};let listenersRegistered=!1;listenersRegistered||((()=>{const showPopoverHandler=e=>{const dayLink=e.target.closest(CalendarSelectors.links.dayLink);dayLink&&(e.preventDefault(),showPopover(dayLink))};document.addEventListener("mouseover",showPopoverHandler),document.addEventListener("focusin",showPopoverHandler)})(),listenersRegistered=!0)}));
*/function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj},CalendarSelectors=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}(CalendarSelectors);const isPopoverConfigured=new Map,showPopover=target=>{const dateContainer=target.closest(CalendarSelectors.elements.dateContainer);if(!isPopoverConfigured.has(dateContainer)){(0,_jquery.default)(target).popover({trigger:"manual",placement:"top",html:!0,title:dateContainer.dataset.title,content:()=>{const source=(0,_jquery.default)(dateContainer).find(CalendarSelectors.elements.dateContent),content=(0,_jquery.default)("<div>");if(source.length){const temptContent=source.find(".hidden").clone(!1);content.html(temptContent.html())}return content.html()},animation:!1}),isPopoverConfigured.set(dateContainer,!0)}(dateContainer=>"none"===window.getComputedStyle(dateContainer.querySelector(CalendarSelectors.elements.dateContent)).display)(dateContainer)&&((0,_jquery.default)(target).popover("show"),target.addEventListener("mouseleave",hidePopover),target.addEventListener("focusout",hidePopover),target.addEventListener("click",hidePopover))},hidePopover=e=>{const target=e.target,dateContainer=e.target.closest(CalendarSelectors.elements.dateContainer);if(dateContainer&&isPopoverConfigured.has(dateContainer)){const isTargetActive=target.contains(document.activeElement),isTargetHover=target.matches(":hover"),isTargetClicked=document.activeElement.contains(target);let removeListener=!0;isTargetActive||isTargetHover?isTargetClicked?(0,_jquery.default)(document.activeElement).popover("hide"):removeListener=!1:(0,_jquery.default)(target).popover("hide"),removeListener&&(target.removeEventListener("mouseleave",hidePopover),target.removeEventListener("focusout",hidePopover),target.removeEventListener("click",hidePopover))}};let listenersRegistered=!1;listenersRegistered||((()=>{const showPopoverHandler=e=>{const dayLink=e.target.closest(CalendarSelectors.links.dayLink);dayLink&&(e.preventDefault(),showPopover(dayLink))};document.addEventListener("mouseover",showPopoverHandler),document.addEventListener("focusin",showPopoverHandler)})(),listenersRegistered=!0)}));
//# sourceMappingURL=popover.min.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -33,7 +33,6 @@ define([
'core_calendar/view_manager',
'core_calendar/crud',
'core_calendar/selectors',
'core/config',
'core/url',
'core/str',
],
@ -46,7 +45,6 @@ function(
CalendarViewManager,
CalendarCrud,
CalendarSelectors,
Config,
Url,
Str,
) {
@ -171,8 +169,9 @@ function(
* Register event listeners for the module.
*
* @param {object} root The calendar root element
* @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block.
*/
var registerEventListeners = function(root) {
var registerEventListeners = function(root, isCalendarBlock) {
const viewingFullCalendar = document.getElementById(CalendarSelectors.fullCalendarView);
// Listen the click on the day link to render the day view.
root.on('click', SELECTORS.VIEW_DAY_LINK, function(e) {
@ -193,9 +192,13 @@ function(
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
CalendarViewManager.refreshDayContent(root, year, month, day, courseId, categoryId, root,
'core_calendar/calendar_day').then(function() {
'core_calendar/calendar_day', isCalendarBlock).then(function() {
e.preventDefault();
return CalendarViewManager.updateUrl(urlParamString);
// Update the URL if it's not calendar block.
if (!isCalendarBlock) {
CalendarViewManager.updateUrl('?' + urlParamString);
}
return;
}).catch(Notification.exception);
} else {
window.location.assign(Url.relativeUrl('calendar/view.php', urlParams));
@ -266,10 +269,16 @@ function(
};
return {
init: function(root) {
/**
* Initializes the calendar view manager and registers event listeners.
*
* @param {HTMLElement} root - The root element where the calendar view manager and event listeners will be attached.
* @param {boolean} [isCalendarBlock=false] - A flag indicating whether this is a calendar block.
*/
init: function(root, isCalendarBlock = false) {
root = $(root);
CalendarViewManager.init(root);
registerEventListeners(root);
CalendarViewManager.init(root, 'month', isCalendarBlock);
registerEventListeners(root, isCalendarBlock);
}
};
});

View File

@ -86,11 +86,11 @@ function(
};
return {
init: function(root, type) {
init: function(root, type, isCalendarBlock = false) {
root = $(root);
CalendarViewManager.init(root, type);
registerEventListeners(root, type);
CalendarViewManager.init(root, type, isCalendarBlock);
registerEventListeners(root, type, isCalendarBlock);
}
};
});

View File

@ -54,7 +54,8 @@ const showPopover = target => {
content.html(temptContent.html());
}
return content.html();
}
},
'animation': false,
});
isPopoverConfigured.set(dateContainer, true);
@ -64,6 +65,8 @@ const showPopover = target => {
jQuery(target).popover('show');
target.addEventListener('mouseleave', hidePopover);
target.addEventListener('focusout', hidePopover);
// Set up the hide function to the click event type.
target.addEventListener('click', hidePopover);
}
};
@ -76,10 +79,23 @@ const hidePopover = e => {
if (isPopoverConfigured.has(dateContainer)) {
const isTargetActive = target.contains(document.activeElement);
const isTargetHover = target.matches(':hover');
// Checks if a target element is clicked or pressed.
const isTargetClicked = document.activeElement.contains(target);
let removeListener = true;
if (!isTargetActive && !isTargetHover) {
jQuery(target).popover('hide');
} else if (isTargetClicked) {
jQuery(document.activeElement).popover('hide');
} else {
removeListener = false;
}
if (removeListener) {
target.removeEventListener('mouseleave', hidePopover);
target.removeEventListener('focusout', hidePopover);
target.removeEventListener('click', hidePopover);
}
}
};

View File

@ -145,8 +145,9 @@ export const registerEventListenersForMonthDetailed = (pendingId) => {
* Register event listeners for the module.
*
* @param {object} root The root element.
* @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block.
*/
const registerEventListeners = (root) => {
const registerEventListeners = (root, isCalendarBlock) => {
root = $(root);
// Bind click events to event links.
@ -192,10 +193,12 @@ const registerEventListeners = (root) => {
const link = e.currentTarget;
if (view === 'month' || view === 'monthblock') {
changeMonth(root, link.href, link.dataset.year, link.dataset.month, courseId, categoryId, link.dataset.day);
changeMonth(root, link.href, link.dataset.year, link.dataset.month,
courseId, categoryId, link.dataset.day, isCalendarBlock);
e.preventDefault();
} else if (view === 'day') {
changeDay(root, link.href, link.dataset.year, link.dataset.month, link.dataset.day, courseId, categoryId);
changeDay(root, link.href, link.dataset.year, link.dataset.month, link.dataset.day,
courseId, categoryId, isCalendarBlock);
e.preventDefault();
}
});
@ -292,18 +295,19 @@ export const refreshMonthContent = (root, year, month, courseId, categoryId, tar
* @param {number} courseId The id of the course whose events are shown
* @param {number} categoryId The id of the category whose events are shown
* @param {number} day Day (optional)
* @param {boolean} [isCalendarBlock=false] - A flag indicating whether this is a calendar block.
* @return {promise}
*/
export const changeMonth = (root, url, year, month, courseId, categoryId, day = 1) => {
export const changeMonth = (root, url, year, month, courseId, categoryId, day = 1, isCalendarBlock = false) => {
return refreshMonthContent(root, year, month, courseId, categoryId, null, '', day)
.then((...args) => {
if (url.length && url !== '#') {
if (url.length && url !== '#' && !isCalendarBlock) {
updateUrl(url);
}
return args;
})
.then((...args) => {
$('body').trigger(CalendarEvents.monthChanged, [year, month, courseId, categoryId]);
$('body').trigger(CalendarEvents.monthChanged, [year, month, courseId, categoryId, day, isCalendarBlock]);
return args;
});
};
@ -343,10 +347,12 @@ export const reloadCurrentMonth = (root, courseId = 0, categoryId = 0) => {
* @param {number} categoryId The id of the category whose events are shown
* @param {object} target The element being replaced. If not specified, the calendarwrapper is used.
* @param {string} template The template to be rendered.
* @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block.
*
* @return {promise}
*/
export const refreshDayContent = (root, year, month, day, courseId, categoryId, target = null, template = '') => {
export const refreshDayContent = (root, year, month, day, courseId, categoryId,
target = null, template = '', isCalendarBlock = false) => {
startLoading(root);
if (!target || target.length == 0){
@ -359,6 +365,7 @@ export const refreshDayContent = (root, year, month, day, courseId, categoryId,
.then((context) => {
context.viewingday = true;
context.showviewselector = true;
context.iscalendarblock = isCalendarBlock;
return Templates.render(template, context);
})
.then((html, js) => {
@ -405,18 +412,19 @@ export const reloadCurrentDay = (root, courseId = 0, categoryId = 0) => {
* @param {Number} day Day
* @param {Number} courseId The id of the course whose events are shown
* @param {Number} categoryId The id of the category whose events are shown
* @param {boolean} [isCalendarBlock=false] - A flag indicating whether this is a calendar block.
* @return {promise}
*/
export const changeDay = (root, url, year, month, day, courseId, categoryId) => {
return refreshDayContent(root, year, month, day, courseId, categoryId)
export const changeDay = (root, url, year, month, day, courseId, categoryId, isCalendarBlock = false) => {
return refreshDayContent(root, year, month, day, courseId, categoryId, null, '', isCalendarBlock)
.then((...args) => {
if (url.length && url !== '#') {
if (url.length && url !== '#' && !isCalendarBlock) {
updateUrl(url);
}
return args;
})
.then((...args) => {
$('body').trigger(CalendarEvents.dayChanged, [year, month, courseId, categoryId]);
$('body').trigger(CalendarEvents.dayChanged, [year, month, courseId, categoryId, isCalendarBlock]);
return args;
});
};
@ -562,10 +570,18 @@ const renderEventSummaryModal = (eventId) => {
.catch(Notification.exception);
};
export const init = (root, view) => {
/**
* Initializes the calendar component by prefetching strings, folding day events,
* and registering event listeners.
*
* @param {HTMLElement} root - The root element where the calendar view manager and event listeners will be attached.
* @param {string} view - A flag indicating whether this is a calendar block.
* @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block.
*/
export const init = (root, view, isCalendarBlock) => {
prefetchStrings('calendar', ['moreevents']);
foldDayEvents();
registerEventListeners(root, view);
registerEventListeners(root, isCalendarBlock);
const calendarTable = root.find(CalendarSelectors.elements.monthDetailed);
if (calendarTable.length) {
const pendingId = `month-detailed-${calendarTable.id}-filterChanged`;

View File

@ -29,6 +29,7 @@
Example context (json):
{
"iscalendarblock": false
}
}}
<div id="calendar-day-{{uniqid}}" data-template="core_calendar/day_detailed">
@ -37,6 +38,7 @@
</div>
{{#js}}
require(['jquery', 'core_calendar/calendar_view'], function($, CalendarView) {
CalendarView.init($("#calendar-day-{{uniqid}}"), 'day');
const isCalendarBlock = {{#iscalendarblock}}true{{/iscalendarblock}}{{^iscalendarblock}}false{{/iscalendarblock}};
CalendarView.init($("#calendar-day-{{uniqid}}"), 'day', isCalendarBlock);
});
{{/js}}

View File

@ -29,6 +29,7 @@
Example context (json):
{
"iscalendarblock": false
}
}}
<div id="calendar-month-{{uniqid}}-{{calendarinstanceid}}" data-template="core_calendar/month_detailed">
@ -37,6 +38,7 @@
</div>
{{#js}}
require(['jquery', 'core_calendar/calendar', 'core_calendar/popover'], function($, Calendar, calendarPopover) {
Calendar.init($("#calendar-month-{{uniqid}}-{{calendarinstanceid}}"));
const isCalendarBlock = {{#iscalendarblock}}true{{/iscalendarblock}}{{^iscalendarblock}}false{{/iscalendarblock}};
Calendar.init($("#calendar-month-{{uniqid}}-{{calendarinstanceid}}"), isCalendarBlock);
});
{{/js}}

View File

@ -132,6 +132,26 @@ class behat_calendar extends behat_base {
);
}
/**
* Click on a specific day in the mini-calendar.
*
* @Given /^I click on day "(?P<dayofmonth>\d+)" of this month in the mini-calendar block(?P<responsive> responsive view|)$/
*
* @param int $day The day of the current month.
* @param string $responsive If not null, find the responsive version of the link.
*/
public function i_click_on_day_of_this_month_in_mini_calendar_block(int $day, string $responsive = ''): void {
$this->execute(
contextapi: "behat_general::i_click_on_in_the",
params: [
$day,
empty($responsive) ? 'core_calendar > calendar day' : 'core_calendar > responsive calendar day',
'',
'core_calendar > mini calendar block',
],
);
}
/**
* Hover over today in the mini-calendar.
*
@ -144,6 +164,20 @@ class behat_calendar extends behat_base {
$this->i_hover_over_day_of_this_month_in_mini_calendar_block($todaysday, $responsive);
}
/**
* Click on today in the mini-calendar.
*
* @Given /^I click on today in the mini-calendar block( responsive view|)$/
*
* @param string $responsive If not empty, use the responsive calendar link.
*/
public function i_click_on_today_in_mini_calendar_block(string $responsive = ''): void {
$this->i_click_on_day_of_this_month_in_mini_calendar_block(
day: date('j'),
responsive: $responsive,
);
}
/**
* Navigate to a specific month in the calendar.
*