first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,10 @@
/**
* A javascript module to retrieve calendar events from the server.
*
* @module block_timeline/calendar_events_repository
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define("block_timeline/calendar_events_repository",["jquery","core/ajax","core/notification"],(function($,Ajax,Notification){return{queryByTime:function(args){args.hasOwnProperty("limit")||(args.limit=20),args.limitnum=args.limit,delete args.limit,args.hasOwnProperty("starttime")&&(args.timesortfrom=args.starttime,delete args.starttime),args.hasOwnProperty("endtime")&&(args.timesortto=args.endtime,delete args.endtime),args.limittononsuspendedevents=!0;var request={methodname:"core_calendar_get_action_events_by_timesort",args:args},promise=Ajax.call([request])[0];return promise.fail(Notification.exception),promise},queryByCourse:function(args){args.hasOwnProperty("limit")||(args.limit=20),args.limitnum=args.limit,delete args.limit,args.hasOwnProperty("starttime")&&(args.timesortfrom=args.starttime,delete args.starttime),args.hasOwnProperty("endtime")&&(args.timesortto=args.endtime,delete args.endtime);var request={methodname:"core_calendar_get_action_events_by_course",args:args},promise=Ajax.call([request])[0];return promise.fail(Notification.exception),promise},queryByCourses:function(args){args.hasOwnProperty("limit")||(args.limit=10),args.limitnum=args.limit,delete args.limit,args.hasOwnProperty("starttime")&&(args.timesortfrom=args.starttime,delete args.starttime),args.hasOwnProperty("endtime")&&(args.timesortto=args.endtime,delete args.endtime);var request={methodname:"core_calendar_get_action_events_by_courses",args:args},promise=Ajax.call([request])[0];return promise.fail(Notification.exception),promise}}}));
//# sourceMappingURL=calendar_events_repository.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
+9
View File
@@ -0,0 +1,9 @@
/**
* Javascript to initialise the timeline block.
*
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define("block_timeline/main",["jquery","block_timeline/view_nav","block_timeline/view"],(function($,ViewNav,View){var SELECTORS_TIMELINE_VIEW='[data-region="timeline-view"]';return{init:function(root){var viewRoot=(root=$(root)).find(SELECTORS_TIMELINE_VIEW);ViewNav.init(root,viewRoot),View.init(viewRoot)}}}));
//# sourceMappingURL=main.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"main.min.js","sources":["../src/main.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Javascript to initialise the timeline block.\n *\n * @copyright 2018 Ryan Wyllie <ryan@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(\n[\n 'jquery',\n 'block_timeline/view_nav',\n 'block_timeline/view'\n],\nfunction(\n $,\n ViewNav,\n View\n) {\n\n var SELECTORS = {\n TIMELINE_VIEW: '[data-region=\"timeline-view\"]'\n };\n\n /**\n * Initialise all of the modules for the timeline block.\n *\n * @param {object} root The root element for the timeline block.\n */\n var init = function(root) {\n root = $(root);\n var viewRoot = root.find(SELECTORS.TIMELINE_VIEW);\n\n // Initialise the timeline navigation elements.\n ViewNav.init(root, viewRoot);\n // Initialise the timeline view modules.\n View.init(viewRoot);\n };\n\n return {\n init: init\n };\n});\n"],"names":["define","$","ViewNav","View","SELECTORS","init","root","viewRoot","find"],"mappings":";;;;;;AAsBAA,6BACA,CACI,SACA,0BACA,wBAEJ,SACIC,EACAC,QACAC,UAGIC,wBACe,sCAkBZ,CACHC,KAXO,SAASC,UAEZC,UADJD,KAAOL,EAAEK,OACWE,KAAKJ,yBAGzBF,QAAQG,KAAKC,KAAMC,UAEnBJ,KAAKE,KAAKE"}
+9
View File
@@ -0,0 +1,9 @@
/**
* Manage the timeline view for the timeline block.
*
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define("block_timeline/view",["jquery","block_timeline/view_dates","block_timeline/view_courses"],(function($,ViewDates,ViewCourses){var SELECTORS_TIMELINE_DATES_VIEW='[data-region="view-dates"]',SELECTORS_TIMELINE_COURSES_VIEW='[data-region="view-courses"]';return{init:function(root){var datesViewRoot=(root=$(root)).find(SELECTORS_TIMELINE_DATES_VIEW),coursesViewRoot=root.find(SELECTORS_TIMELINE_COURSES_VIEW);ViewDates.init(datesViewRoot),ViewCourses.init(coursesViewRoot)},reset:function(root){var datesViewRoot=root.find(SELECTORS_TIMELINE_DATES_VIEW),coursesViewRoot=root.find(SELECTORS_TIMELINE_COURSES_VIEW);ViewDates.reset(datesViewRoot),ViewCourses.reset(coursesViewRoot)},shown:function(root){var datesViewRoot=root.find(SELECTORS_TIMELINE_DATES_VIEW),coursesViewRoot=root.find(SELECTORS_TIMELINE_COURSES_VIEW);datesViewRoot.hasClass("active")?ViewDates.shown(datesViewRoot):ViewCourses.shown(coursesViewRoot)}}}));
//# sourceMappingURL=view.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"view.min.js","sources":["../src/view.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 <http://www.gnu.org/licenses/>.\n\n/**\n * Manage the timeline view for the timeline block.\n *\n * @copyright 2018 Ryan Wyllie <ryan@moodle.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(\n[\n 'jquery',\n 'block_timeline/view_dates',\n 'block_timeline/view_courses',\n],\nfunction(\n $,\n ViewDates,\n ViewCourses\n) {\n\n var SELECTORS = {\n TIMELINE_DATES_VIEW: '[data-region=\"view-dates\"]',\n TIMELINE_COURSES_VIEW: '[data-region=\"view-courses\"]',\n };\n\n /**\n * Intialise the timeline dates and courses views on page load.\n * This function should only be called once per page load because\n * it can cause event listeners to be added to the page.\n *\n * @param {object} root The root element for the timeline view.\n */\n var init = function(root) {\n root = $(root);\n var datesViewRoot = root.find(SELECTORS.TIMELINE_DATES_VIEW);\n var coursesViewRoot = root.find(SELECTORS.TIMELINE_COURSES_VIEW);\n\n ViewDates.init(datesViewRoot);\n ViewCourses.init(coursesViewRoot);\n };\n\n /**\n * Reset the timeline dates and courses views to their original\n * state on first page load.\n *\n * This is called when configuration has changed for the event lists\n * to cause them to reload their data.\n *\n * @param {object} root The root element for the timeline view.\n */\n var reset = function(root) {\n var datesViewRoot = root.find(SELECTORS.TIMELINE_DATES_VIEW);\n var coursesViewRoot = root.find(SELECTORS.TIMELINE_COURSES_VIEW);\n ViewDates.reset(datesViewRoot);\n ViewCourses.reset(coursesViewRoot);\n };\n\n /**\n * Tell the timeline dates or courses view that it has been displayed.\n *\n * This is called each time one of the views is displayed and is used to\n * lazy load the data within it on first load.\n *\n * @param {object} root The root element for the timeline view.\n */\n var shown = function(root) {\n var datesViewRoot = root.find(SELECTORS.TIMELINE_DATES_VIEW);\n var coursesViewRoot = root.find(SELECTORS.TIMELINE_COURSES_VIEW);\n\n if (datesViewRoot.hasClass('active')) {\n ViewDates.shown(datesViewRoot);\n } else {\n ViewCourses.shown(coursesViewRoot);\n }\n };\n\n return {\n init: init,\n reset: reset,\n shown: shown,\n };\n});\n"],"names":["define","$","ViewDates","ViewCourses","SELECTORS","init","root","datesViewRoot","find","coursesViewRoot","reset","shown","hasClass"],"mappings":";;;;;;AAsBAA,6BACA,CACI,SACA,4BACA,gCAEJ,SACIC,EACAC,UACAC,iBAGIC,8BACqB,6BADrBA,gCAEuB,qCAsDpB,CACHC,KA7CO,SAASC,UAEZC,eADJD,KAAOL,EAAEK,OACgBE,KAAKJ,+BAC1BK,gBAAkBH,KAAKE,KAAKJ,iCAEhCF,UAAUG,KAAKE,eACfJ,YAAYE,KAAKI,kBAwCjBC,MA5BQ,SAASJ,UACbC,cAAgBD,KAAKE,KAAKJ,+BAC1BK,gBAAkBH,KAAKE,KAAKJ,iCAChCF,UAAUQ,MAAMH,eAChBJ,YAAYO,MAAMD,kBAyBlBE,MAdQ,SAASL,UACbC,cAAgBD,KAAKE,KAAKJ,+BAC1BK,gBAAkBH,KAAKE,KAAKJ,iCAE5BG,cAAcK,SAAS,UACvBV,UAAUS,MAAMJ,eAEhBJ,YAAYQ,MAAMF"}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
/**
* Manage the timeline dates view for the timeline block.
*
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define("block_timeline/view_dates",["jquery","block_timeline/event_list","core/pubsub","core/paged_content_events"],(function($,EventList,PubSub,PagedContentEvents){var SELECTORS_EVENT_LIST_CONTAINER='[data-region="event-list-container"]',SELECTORS_NO_COURSES_EMPTY_MESSAGE='[data-region="no-courses-empty-message"]',load=function(root){if(!root.find(SELECTORS_NO_COURSES_EMPTY_MESSAGE).length){var eventListContainer=root.find(SELECTORS_EVENT_LIST_CONTAINER),namespace=$(eventListContainer).attr("id")+"user_block_timeline"+Math.random();!function(root,namespace){var event=namespace+PagedContentEvents.SET_ITEMS_PER_PAGE_LIMIT;PubSub.subscribe(event,(function(limit){$(root).data("limit",limit)}))}(root,namespace);var config={persistentLimitKey:"block_timeline_user_limit_preference",eventNamespace:namespace};EventList.init(eventListContainer,config)}};return{init:function(root){(root=$(root)).hasClass("active")&&!root.find(SELECTORS_NO_COURSES_EMPTY_MESSAGE).length&&(load(root),root.attr("data-seen",!0))},reset:function(root){root.removeAttr("data-seen"),root.hasClass("active")&&(load(root),root.attr("data-seen",!0))},shown:function(root){root.attr("data-seen")||(load(root),root.attr("data-seen",!0))}}}));
//# sourceMappingURL=view_dates.min.js.map
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
define("block_timeline/view_nav",["exports","jquery","core/custom_interaction_events","block_timeline/view","core/notification","core/utils","core_user/repository"],(function(_exports,_jquery,CustomEvents,View,Notification,Utils,UserRepository){var obj;
/**
* Manage the timeline view navigation for the timeline block.
*
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/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)}function _interopRequireWildcard(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]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj},CustomEvents=_interopRequireWildcard(CustomEvents),View=_interopRequireWildcard(View),Notification=_interopRequireWildcard(Notification),Utils=_interopRequireWildcard(Utils),UserRepository=_interopRequireWildcard(UserRepository);const SELECTORS_TIMELINE_DAY_FILTER='[data-region="day-filter"]',SELECTORS_TIMELINE_DAY_FILTER_OPTION="[data-from]",SELECTORS_TIMELINE_VIEW_SELECTOR='[data-region="view-selector"]',SELECTORS_DATA_DAYS_OFFSET="[data-days-offset]",SELECTORS_TIMELINE_SEARCH_INPUT='[data-action="search"]',SELECTORS_TIMELINE_SEARCH_CLEAR_ICON='[data-action="clearsearch"]',SELECTORS_NO_COURSES_EMPTY_MESSAGE='[data-region="no-courses-empty-message"]',activeSearchState=(clearSearchIcon,timelineViewRoot)=>{clearSearchIcon.removeClass("d-none"),View.reset(timelineViewRoot)},clearSearchState=(clearSearchIcon,timelineViewRoot)=>{clearSearchIcon.addClass("d-none"),View.reset(timelineViewRoot)};_exports.init=function(root,timelineViewRoot){(function(root,timelineViewRoot){const viewSelector=root.find(SELECTORS_TIMELINE_VIEW_SELECTOR);viewSelector.on("shown shown.bs.tab",(function(e){View.shown(timelineViewRoot),(0,_jquery.default)(e.target).removeClass("active")})),CustomEvents.define(viewSelector,[CustomEvents.events.activate]),viewSelector.on(CustomEvents.events.activate,"[data-toggle='tab']",(function(e){var filtername=(0,_jquery.default)(e.currentTarget).data("filtername");UserRepository.setUserPreference("block_timeline_user_sort_preference",filtername).catch(Notification.exception)}))})(root=(0,_jquery.default)(root),timelineViewRoot),root.find(SELECTORS_NO_COURSES_EMPTY_MESSAGE).length||(function(root,timelineViewRoot){const timelineDaySelectorContainer=root.find(SELECTORS_TIMELINE_DAY_FILTER);CustomEvents.define(timelineDaySelectorContainer,[CustomEvents.events.activate]),timelineDaySelectorContainer.on(CustomEvents.events.activate,SELECTORS_TIMELINE_DAY_FILTER_OPTION,(function(e,data){var filtername=(0,_jquery.default)(e.currentTarget).data("filtername");UserRepository.setUserPreference("block_timeline_user_filter_preference",filtername).catch(Notification.exception);var option=(0,_jquery.default)(e.target).closest(SELECTORS_TIMELINE_DAY_FILTER_OPTION);if("true"!=option.attr("aria-current")){var daysOffset=option.attr("data-from"),daysLimit=option.attr("data-to"),elementsWithDaysOffset=root.find(SELECTORS_DATA_DAYS_OFFSET);elementsWithDaysOffset.attr("data-days-offset",daysOffset),null!=daysLimit?elementsWithDaysOffset.attr("data-days-limit",daysLimit):elementsWithDaysOffset.removeAttr("data-days-limit"),"overdue"===option.attr("data-filtername")?elementsWithDaysOffset.attr("data-filter-overdue",!0):elementsWithDaysOffset.removeAttr("data-filter-overdue"),View.reset(timelineViewRoot),data.originalEvent.preventDefault()}}))}(root,timelineViewRoot),((root,timelineViewRoot)=>{const searchInput=root.find(SELECTORS_TIMELINE_SEARCH_INPUT),clearSearchIcon=root.find(SELECTORS_TIMELINE_SEARCH_CLEAR_ICON);searchInput.on("input",Utils.debounce((()=>{""!==searchInput.val()?activeSearchState(clearSearchIcon,timelineViewRoot):clearSearchState(clearSearchIcon,timelineViewRoot)}),1e3)),clearSearchIcon.on("click",(()=>{searchInput.val(""),clearSearchState(clearSearchIcon,timelineViewRoot),searchInput.focus()}))})(root,timelineViewRoot))}}));
//# sourceMappingURL=view_nav.min.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,168 @@
// 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/>.
/**
* A javascript module to retrieve calendar events from the server.
*
* @module block_timeline/calendar_events_repository
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notification) {
var DEFAULT_LIMIT = 20;
/**
* Retrieve a list of calendar events for the logged in user for the
* given course.
*
* Valid args are:
* int courseid Only get events for this course
* int starttime Only get events after this time
* int endtime Only get events before this time
* int limit Limit the number of results returned
* int aftereventid Offset the result set from the given id
*
* @method queryByCourse
* @param {object} args The request arguments
* @return {promise} Resolved with an array of the calendar events
*/
var queryByCourse = function(args) {
if (!args.hasOwnProperty('limit')) {
args.limit = DEFAULT_LIMIT;
}
args.limitnum = args.limit;
delete args.limit;
if (args.hasOwnProperty('starttime')) {
args.timesortfrom = args.starttime;
delete args.starttime;
}
if (args.hasOwnProperty('endtime')) {
args.timesortto = args.endtime;
delete args.endtime;
}
var request = {
methodname: 'core_calendar_get_action_events_by_course',
args: args
};
var promise = Ajax.call([request])[0];
promise.fail(Notification.exception);
return promise;
};
/**
* Retrieve a list of calendar events for the given courses for the
* logged in user.
*
* Valid args are:
* array courseids Get events for these courses
* int starttime Only get events after this time
* int endtime Only get events before this time
* int limit Limit the number of results returned
*
* @method queryByCourses
* @param {object} args The request arguments
* @return {promise} Resolved with an array of the calendar events
*/
var queryByCourses = function(args) {
if (!args.hasOwnProperty('limit')) {
// This is intentionally smaller than the default limit.
args.limit = 10;
}
args.limitnum = args.limit;
delete args.limit;
if (args.hasOwnProperty('starttime')) {
args.timesortfrom = args.starttime;
delete args.starttime;
}
if (args.hasOwnProperty('endtime')) {
args.timesortto = args.endtime;
delete args.endtime;
}
var request = {
methodname: 'core_calendar_get_action_events_by_courses',
args: args
};
var promise = Ajax.call([request])[0];
promise.fail(Notification.exception);
return promise;
};
/**
* Retrieve a list of calendar events for the logged in user after the given
* time.
*
* Valid args are:
* int starttime Only get events after this time
* int endtime Only get events before this time
* int limit Limit the number of results returned
* int aftereventid Offset the result set from the given id
*
* @method queryByTime
* @param {object} args The request arguments
* @return {promise} Resolved with an array of the calendar events
*/
var queryByTime = function(args) {
if (!args.hasOwnProperty('limit')) {
args.limit = DEFAULT_LIMIT;
}
args.limitnum = args.limit;
delete args.limit;
if (args.hasOwnProperty('starttime')) {
args.timesortfrom = args.starttime;
delete args.starttime;
}
if (args.hasOwnProperty('endtime')) {
args.timesortto = args.endtime;
delete args.endtime;
}
// Don't show events related to courses that the user is suspended in.
args.limittononsuspendedevents = true;
var request = {
methodname: 'core_calendar_get_action_events_by_timesort',
args: args
};
var promise = Ajax.call([request])[0];
promise.fail(Notification.exception);
return promise;
};
return {
queryByTime: queryByTime,
queryByCourse: queryByCourse,
queryByCourses: queryByCourses,
};
});
+544
View File
@@ -0,0 +1,544 @@
// 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/>.
/**
* Javascript to load and render the list of calendar events for a
* given day range.
*
* @module block_timeline/event_list
* @copyright 2016 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core/notification',
'core/templates',
'core/str',
'core/user_date',
'block_timeline/calendar_events_repository',
'core/pending'
],
function(
$,
Notification,
Templates,
Str,
UserDate,
CalendarEventsRepository,
Pending
) {
var SECONDS_IN_DAY = 60 * 60 * 24;
var courseview = false;
var SELECTORS = {
EMPTY_MESSAGE: '[data-region="no-events-empty-message"]',
ROOT: '[data-region="event-list-container"]',
EVENT_LIST_CONTENT: '[data-region="event-list-content"]',
EVENT_LIST_WRAPPER: '[data-region="event-list-wrapper"]',
EVENT_LIST_LOADING_PLACEHOLDER: '[data-region="event-list-loading-placeholder"]',
TIMELINE_BLOCK: '[data-region="timeline"]',
TIMELINE_SEARCH: '[data-action="search"]',
MORE_ACTIVITIES_BUTTON: '[data-action="more-events"]',
MORE_ACTIVITIES_BUTTON_CONTAINER: '[data-region="more-events-button-container"]'
};
var TEMPLATES = {
EVENT_LIST_CONTENT: 'block_timeline/event-list-content',
MORE_ACTIVITIES_BUTTON: 'block_timeline/event-list-loadmore',
LOADING_ICON: 'core/loading'
};
/** @property {number} The total items will be shown on the first load. */
const DEFAULT_LAZY_LOADING_ITEMS_FIRST_LOAD = 5;
/** @property {number} The total items will be shown when click on the Show more activities button. */
const DEFAULT_LAZY_LOADING_ITEMS_OTHER_LOAD = 10;
/**
* Hide the content area and display the empty content message.
*
* @param {object} root The container element
*/
var hideContent = function(root) {
root.find(SELECTORS.EVENT_LIST_CONTENT).addClass('hidden');
root.find(SELECTORS.EMPTY_MESSAGE).removeClass('hidden');
};
/**
* Show the content area and hide the empty content message.
*
* @param {object} root The container element
*/
var showContent = function(root) {
root.find(SELECTORS.EVENT_LIST_CONTENT).removeClass('hidden');
root.find(SELECTORS.EMPTY_MESSAGE).addClass('hidden');
};
/**
* Empty the content area.
*
* @param {object} root The container element
*/
var emptyContent = function(root) {
root.find(SELECTORS.EVENT_LIST_CONTENT).empty();
};
/**
* Construct the template context from a list of calendar events. The events
* are grouped by which day they are on. The day is calculated from the user's
* midnight timestamp to ensure that the calculation is timezone agnostic.
*
* The return data structure will look like:
* {
* eventsbyday: [
* {
* dayTimestamp: 1533744000,
* events: [
* { ...event 1 data... },
* { ...event 2 data... }
* ]
* },
* {
* dayTimestamp: 1533830400,
* events: [
* { ...event 3 data... },
* { ...event 4 data... }
* ]
* }
* ]
* }
*
* Each day timestamp is the day's midnight in the user's timezone.
*
* @param {array} calendarEvents List of calendar events
* @return {object}
*/
var buildTemplateContext = function(calendarEvents) {
var eventsByDay = {};
var templateContext = {
courseview,
eventsbyday: []
};
calendarEvents.forEach(function(calendarEvent) {
var dayTimestamp = calendarEvent.timeusermidnight;
if (eventsByDay[dayTimestamp]) {
eventsByDay[dayTimestamp].push(calendarEvent);
} else {
eventsByDay[dayTimestamp] = [calendarEvent];
}
});
Object.keys(eventsByDay).forEach(function(dayTimestamp) {
var events = eventsByDay[dayTimestamp];
templateContext.eventsbyday.push({
dayTimestamp: dayTimestamp,
events: events
});
});
return templateContext;
};
/**
* Render the HTML for the given calendar events.
*
* @param {array} calendarEvents A list of calendar events
* @return {promise} Resolved with HTML and JS strings.
*/
var render = function(calendarEvents) {
var templateContext = buildTemplateContext(calendarEvents);
var templateName = TEMPLATES.EVENT_LIST_CONTENT;
return Templates.render(templateName, templateContext);
};
/**
* Retrieve a list of calendar events from the server for the given
* constraints.
*
* @param {Number} midnight The user's midnight time in unix timestamp.
* @param {Number} limit Limit the result set to this number of items
* @param {Number} daysOffset How many days (from midnight) to offset the results from
* @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to
* @param {int|false} lastId The ID of the last seen event (if any)
* @param {int|undefined} courseId Course ID to restrict events to
* @param {string|undefined} searchValue Search value
* @return {Promise} A jquery promise
*/
var load = function(midnight, limit, daysOffset, daysLimit, lastId, courseId, searchValue) {
var startTime = midnight + (daysOffset * SECONDS_IN_DAY);
var endTime = daysLimit != undefined ? midnight + (daysLimit * SECONDS_IN_DAY) : false;
var args = {
starttime: startTime,
limit: limit,
};
if (lastId) {
args.aftereventid = lastId;
}
if (endTime) {
args.endtime = endTime;
}
if (searchValue) {
args.searchvalue = searchValue;
}
if (courseId) {
// If we have a course id then we only want events from that course.
args.courseid = courseId;
return CalendarEventsRepository.queryByCourse(args);
} else {
// Otherwise we want events from any course.
return CalendarEventsRepository.queryByTime(args);
}
};
/**
* Create a lazy-loading region for the calendar events in the given root element.
*
* @param {object} root The event list container element.
* @param {object} additionalConfig Additional config options to pass to pagedContentFactory.
*/
var init = function(root, additionalConfig = {}) {
const pendingPromise = new Pending('block/timeline:event-init');
root = $(root);
courseview = !!additionalConfig.courseview;
// Create a promise that will be resolved once the first set of page
// data has been loaded. This ensures that the loading placeholder isn't
// hidden until we have all of the data back to prevent the page elements
// jumping around.
var firstLoad = $.Deferred();
var eventListContent = root.find(SELECTORS.EVENT_LIST_CONTENT);
var loadingPlaceholder = root.find(SELECTORS.EVENT_LIST_LOADING_PLACEHOLDER);
var courseId = root.attr('data-course-id');
var daysOffset = parseInt(root.attr('data-days-offset'), 10);
var daysLimit = root.attr('data-days-limit');
var midnight = parseInt(root.attr('data-midnight'), 10);
const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val();
// Make sure the content area and loading placeholder is visible.
// This is because the init function can be called to re-initialise
// an existing event list area.
emptyContent(root);
showContent(root);
loadingPlaceholder.removeClass('hidden');
// Days limit isn't mandatory.
if (daysLimit != undefined) {
daysLimit = parseInt(daysLimit, 10);
}
// Create the lazy loading content element.
return createLazyLoadingContent(root, firstLoad,
DEFAULT_LAZY_LOADING_ITEMS_FIRST_LOAD, midnight, 0, courseId, daysOffset, daysLimit, searchValue)
.then(function(html, js) {
firstLoad.then(function(data) {
if (!data.hasContent) {
loadingPlaceholder.addClass('hidden');
// If we didn't get any data then show the empty data message.
return hideContent(root);
}
html = $(html);
// Hide the content for now.
html.addClass('hidden');
// Replace existing elements with the newly created lazy-loading region.
Templates.replaceNodeContents(eventListContent, html, js);
// Prevent changing page elements too much by only showing the content
// once we've loaded some data for the first time. This allows our
// fancy loading placeholder to shine.
html.removeClass('hidden');
loadingPlaceholder.addClass('hidden');
if (!data.loadedAll) {
Templates.render(TEMPLATES.MORE_ACTIVITIES_BUTTON, {courseview}).then(function(html) {
eventListContent.append(html);
setLastTimestamp(root, data.lastTimeStamp);
// Init the event handler.
initEventListener(root);
return html;
}).catch(function() {
return false;
});
}
return data;
})
.catch(function() {
return false;
});
return html;
}).then(() => {
return pendingPromise.resolve();
})
.catch(Notification.exception);
};
/**
* Create a lazy-loading content element for showing the event list for the initial load.
*
* @param {object} root The event list container element.
* @param {object} firstLoad A jQuery promise to be resolved after the first set of data is loaded.
* @param {int} itemLimit Limit the number of items.
* @param {Number} midnight The user's midnight time in unix timestamp.
* @param {int} lastId The last event ID for each loaded page. Page number is key, id is value.
* @param {int|undefined} courseId Course ID to restrict events to.
* @param {Number} daysOffset How many days (from midnight) to offset the results from.
* @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to.
* @param {string|undefined} searchValue Search value.
* @return {object} jQuery promise resolved with calendar events.
*/
const createLazyLoadingContent = (root, firstLoad, itemLimit, midnight, lastId,
courseId, daysOffset, daysLimit, searchValue) => {
return loadEventsForLazyLoading(
root,
itemLimit,
midnight,
lastId,
courseId,
daysOffset,
daysLimit,
searchValue
).then(data => {
if (data.calendarEvents.length) {
const lastEventId = data.calendarEvents.at(-1).id;
const lastTimeStamp = data.calendarEvents.at(-1).timeusermidnight;
firstLoad.resolve({
hasContent: true,
lastId: lastEventId,
lastTimeStamp: lastTimeStamp,
loadedAll: data.loadedAll
});
return render(data.calendarEvents, midnight);
} else {
firstLoad.resolve({
hasContent: false,
lastId: 0,
lastTimeStamp: 0,
loadedAll: true
});
return data.calendarEvents;
}
}).catch(Notification.exception);
};
/**
* Handle the request from the lazy-loading region.
* Uses the given data like course id, offset... to request the events from the server.
*
* @param {object} root The event list container element.
* @param {int} itemLimit Limit the number of items.
* @param {Number} midnight The user's midnight time in unix timestamp.
* @param {int} lastId The last event ID for each loaded page.
* @param {int|undefined} courseId Course ID to restrict events to.
* @param {Number} daysOffset How many days (from midnight) to offset the results from.
* @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to.
* @param {string|undefined} searchValue Search value.
* @return {object} jQuery promise resolved with calendar events.
*/
const loadEventsForLazyLoading = (root, itemLimit, midnight, lastId, courseId, daysOffset, daysLimit, searchValue) => {
// Load one more than the given limit so that we can tell if there
// is more content to load after this.
const eventsPromise = load(midnight, itemLimit + 1, daysOffset, daysLimit, lastId, courseId, searchValue);
let calendarEvents = [];
let loadedAll = true;
return eventsPromise.then(result => {
if (!result.events.length) {
return {calendarEvents, loadedAll};
}
// Determine if the overdue filter is applied.
const overdueFilter = document.querySelector("[data-filtername='overdue']");
const filterByOverdue = (overdueFilter && overdueFilter.getAttribute('aria-current'));
calendarEvents = result.events.filter(event => {
if (event.eventtype == 'open' || event.eventtype == 'opensubmission') {
const dayTimestamp = UserDate.getUserMidnightForTimestamp(event.timesort, midnight);
return dayTimestamp > midnight;
}
// When filtering by overdue, we fetch all events due today, in case any have elapsed already and are overdue.
// This means if filtering by overdue, some events fetched might not be required (eg if due later today).
return (!filterByOverdue || event.overdue);
});
loadedAll = calendarEvents.length <= itemLimit;
if (!loadedAll) {
// Remove the last element from the array because it isn't
// needed in this result set.
calendarEvents.pop();
}
if (calendarEvents.length) {
const lastEventId = calendarEvents.at(-1).id;
setOffset(root, lastEventId);
}
return {calendarEvents, loadedAll};
});
};
/**
* Load new events and append to current list.
*
* @param {object} root The event list container element.
*/
const loadMoreEvents = root => {
const midnight = parseInt(root.attr('data-midnight'), 10);
const courseId = root.attr('data-course-id');
const daysOffset = parseInt(root.attr('data-days-offset'), 10);
const daysLimit = root.attr('data-days-limit');
const lastId = getOffset(root);
const eventListWrapper = root.find(SELECTORS.EVENT_LIST_WRAPPER);
const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val();
const eventsPromise = loadEventsForLazyLoading(
root,
DEFAULT_LAZY_LOADING_ITEMS_OTHER_LOAD,
midnight,
lastId,
courseId,
daysOffset,
daysLimit,
searchValue
);
eventsPromise.then(data => {
if (data.calendarEvents.length) {
const renderPromise = render(data.calendarEvents);
const lastTimestamp = getLastTimestamp(root);
renderPromise.then((html, js) => {
html = $(html);
// Remove the date heading if it has the same value as the previous one.
html.find(`[data-timestamp="${lastTimestamp}"]`).remove();
Templates.appendNodeContents(eventListWrapper, html.html(), js);
if (!data.loadedAll) {
Templates.render(TEMPLATES.MORE_ACTIVITIES_BUTTON, {}).then(html => {
eventListWrapper.append(html);
setLastTimestamp(root, data.calendarEvents.at(-1).timeusermidnight);
// Init the event handler.
initEventListener(root);
return html;
}).catch(() => {
return false;
});
}
return html;
}).catch(Notification.exception);
}
return data;
}).then(() => {
return disableMoreActivitiesButtonLoading(root);
}).catch(Notification.exception);
};
/**
* Return the offset value for lazy loading fetching.
*
* @param {object} element The event list container element.
* @return {Number} Offset value.
*/
const getOffset = element => {
return parseInt(element.attr('data-lazyload-offset'), 10);
};
/**
* Set the offset value for lazy loading fetching.
*
* @param {object} element The event list container element.
* @param {Number} offset Offset value.
*/
const setOffset = (element, offset) => {
element.attr('data-lazyload-offset', offset);
};
/**
* Return the timestamp value for lazy loading fetching.
*
* @param {object} element The event list container element.
* @return {Number} Timestamp value.
*/
const getLastTimestamp = element => {
return parseInt(element.attr('data-timestamp'), 10);
};
/**
* Set the timestamp value for lazy loading fetching.
*
* @param {object} element The event list container element.
* @param {Number} timestamp Timestamp value.
*/
const setLastTimestamp = (element, timestamp) => {
element.attr('data-timestamp', timestamp);
};
/**
* Add the "Show more activities" button and remove and loading spinner.
*
* @param {object} root The event list container element.
*/
const enableMoreActivitiesButtonLoading = root => {
const loadMoreButton = root.find(SELECTORS.MORE_ACTIVITIES_BUTTON);
loadMoreButton.prop('disabled', true);
Templates.render(TEMPLATES.LOADING_ICON, {}).then(html => {
loadMoreButton.append(html);
return html;
}).catch(() => {
// It's not important if this false so just do so silently.
return false;
});
};
/**
* Remove the "Show more activities" button and remove and loading spinner.
*
* @param {object} root The event list container element.
*/
const disableMoreActivitiesButtonLoading = root => {
const loadMoreButtonContainer = root.find(SELECTORS.MORE_ACTIVITIES_BUTTON_CONTAINER);
loadMoreButtonContainer.remove();
};
/**
* Event initialise.
*
* @param {object} root The event list container element.
*/
const initEventListener = root => {
const loadMoreButton = root.find(SELECTORS.MORE_ACTIVITIES_BUTTON);
loadMoreButton.on('click', () => {
enableMoreActivitiesButtonLoading(root);
loadMoreEvents(root);
});
};
return {
init: init,
rootSelector: SELECTORS.ROOT,
};
});
+57
View File
@@ -0,0 +1,57 @@
// 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/>.
/**
* Javascript to initialise the timeline block.
*
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'block_timeline/view_nav',
'block_timeline/view'
],
function(
$,
ViewNav,
View
) {
var SELECTORS = {
TIMELINE_VIEW: '[data-region="timeline-view"]'
};
/**
* Initialise all of the modules for the timeline block.
*
* @param {object} root The root element for the timeline block.
*/
var init = function(root) {
root = $(root);
var viewRoot = root.find(SELECTORS.TIMELINE_VIEW);
// Initialise the timeline navigation elements.
ViewNav.init(root, viewRoot);
// Initialise the timeline view modules.
View.init(viewRoot);
};
return {
init: init
};
});
+96
View File
@@ -0,0 +1,96 @@
// 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/>.
/**
* Manage the timeline view for the timeline block.
*
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'block_timeline/view_dates',
'block_timeline/view_courses',
],
function(
$,
ViewDates,
ViewCourses
) {
var SELECTORS = {
TIMELINE_DATES_VIEW: '[data-region="view-dates"]',
TIMELINE_COURSES_VIEW: '[data-region="view-courses"]',
};
/**
* Intialise the timeline dates and courses views on page load.
* This function should only be called once per page load because
* it can cause event listeners to be added to the page.
*
* @param {object} root The root element for the timeline view.
*/
var init = function(root) {
root = $(root);
var datesViewRoot = root.find(SELECTORS.TIMELINE_DATES_VIEW);
var coursesViewRoot = root.find(SELECTORS.TIMELINE_COURSES_VIEW);
ViewDates.init(datesViewRoot);
ViewCourses.init(coursesViewRoot);
};
/**
* Reset the timeline dates and courses views to their original
* state on first page load.
*
* This is called when configuration has changed for the event lists
* to cause them to reload their data.
*
* @param {object} root The root element for the timeline view.
*/
var reset = function(root) {
var datesViewRoot = root.find(SELECTORS.TIMELINE_DATES_VIEW);
var coursesViewRoot = root.find(SELECTORS.TIMELINE_COURSES_VIEW);
ViewDates.reset(datesViewRoot);
ViewCourses.reset(coursesViewRoot);
};
/**
* Tell the timeline dates or courses view that it has been displayed.
*
* This is called each time one of the views is displayed and is used to
* lazy load the data within it on first load.
*
* @param {object} root The root element for the timeline view.
*/
var shown = function(root) {
var datesViewRoot = root.find(SELECTORS.TIMELINE_DATES_VIEW);
var coursesViewRoot = root.find(SELECTORS.TIMELINE_COURSES_VIEW);
if (datesViewRoot.hasClass('active')) {
ViewDates.shown(datesViewRoot);
} else {
ViewCourses.shown(coursesViewRoot);
}
};
return {
init: init,
reset: reset,
shown: shown,
};
});
+582
View File
@@ -0,0 +1,582 @@
// 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/>.
/**
* Manage the timeline courses view for the timeline block.
*
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core/notification',
'core/custom_interaction_events',
'core/templates',
'block_timeline/event_list',
'core_course/repository',
'block_timeline/calendar_events_repository',
'core/pending'
],
function(
$,
Notification,
CustomEvents,
Templates,
EventList,
CourseRepository,
EventsRepository,
Pending
) {
var SELECTORS = {
MORE_COURSES_BUTTON: '[data-action="more-courses"]',
MORE_COURSES_BUTTON_CONTAINER: '[data-region="more-courses-button-container"]',
NO_COURSES_EMPTY_MESSAGE: '[data-region="no-courses-empty-message"]',
NO_COURSES_WITH_EVENTS_MESSAGE: '[data-region="no-events-empty-message"]',
COURSES_LIST: '[data-region="courses-list"]',
COURSE_ITEMS_LOADING_PLACEHOLDER: '[data-region="course-items-loading-placeholder"]',
COURSE_EVENTS_CONTAINER: '[data-region="course-events-container"]',
COURSE_NAME: '[data-region="course-name"]',
LOADING_ICON: '.loading-icon',
TIMELINE_BLOCK: '[data-region="timeline"]',
TIMELINE_SEARCH: '[data-action="search"]'
};
var TEMPLATES = {
COURSE_ITEMS: 'block_timeline/course-items',
LOADING_ICON: 'core/loading'
};
var COURSE_CLASSIFICATION = 'all';
var COURSE_SORT = 'fullname asc';
var COURSE_EVENT_LIMIT = 5;
var COURSE_LIMIT = 2;
var SECONDS_IN_DAY = 60 * 60 * 24;
const additionalConfig = {courseview: true};
/**
* Hide the loading placeholder elements.
*
* @param {object} root The rool element.
*/
var hideLoadingPlaceholder = function(root) {
root.find(SELECTORS.COURSE_ITEMS_LOADING_PLACEHOLDER).addClass('hidden');
};
/**
* Show the loading placeholder elements.
*
* @param {object} root The rool element.
*/
const showLoadingPlaceholder = function(root) {
root.find(SELECTORS.COURSE_ITEMS_LOADING_PLACEHOLDER).removeClass('hidden');
};
/**
* Hide the "more courses" button.
*
* @param {object} root The rool element.
*/
var hideMoreCoursesButton = function(root) {
root.find(SELECTORS.MORE_COURSES_BUTTON_CONTAINER).addClass('hidden');
};
/**
* Show the "more courses" button.
*
* @param {object} root The rool element.
*/
var showMoreCoursesButton = function(root) {
root.find(SELECTORS.MORE_COURSES_BUTTON_CONTAINER).removeClass('hidden');
};
/**
* Disable the "more courses" button and show the loading spinner.
*
* @param {object} root The rool element.
*/
var enableMoreCoursesButtonLoading = function(root) {
var button = root.find(SELECTORS.MORE_COURSES_BUTTON);
button.prop('disabled', true);
Templates.render(TEMPLATES.LOADING_ICON, {})
.then(function(html) {
button.append(html);
return html;
})
.catch(function() {
// It's not important if this false so just do so silently.
return false;
});
};
/**
* Enable the "more courses" button and remove the loading spinner.
*
* @param {object} root The rool element.
*/
var disableMoreCoursesButtonLoading = function(root) {
var button = root.find(SELECTORS.MORE_COURSES_BUTTON);
button.prop('disabled', false);
button.find(SELECTORS.LOADING_ICON).remove();
};
/**
* Display the message for when courses have no events available (within the current filtering).
*
* @param {object} root The rool element.
*/
const showNoCoursesWithEventsMessage = function(root) {
// Remove any course list contents, since we will display the no events message.
const container = root.find(SELECTORS.COURSES_LIST);
Templates.replaceNodeContents(container, '', '');
root.find(SELECTORS.NO_COURSES_WITH_EVENTS_MESSAGE).removeClass('hidden');
};
/**
* Hide the message for when courses have no events available (within the current filtering).
*
* @param {object} root The rool element.
*/
const hideNoCoursesWithEventsMessage = function(root) {
root.find(SELECTORS.NO_COURSES_WITH_EVENTS_MESSAGE).addClass('hidden');
};
/**
* Render the course items HTML to the page.
*
* @param {object} root The rool element.
* @param {string} html The course items HTML to render.
* @param {boolean} append Whether the HTML should be appended (eg pressed "show more courses").
* Defaults to false - replaces the existing content (eg when modifying filter values).
*/
var renderCourseItemsHTML = function(root, html, append = false) {
var container = root.find(SELECTORS.COURSES_LIST);
if (append) {
Templates.appendNodeContents(container, html, '');
} else {
Templates.replaceNodeContents(container, html, '');
}
};
/**
* Return the offset value for fetching courses.
*
* @param {object} root The rool element.
* @return {Number}
*/
var getOffset = function(root) {
return parseInt(root.attr('data-offset'), 10);
};
/**
* Set the offset value for fetching courses.
*
* @param {object} root The rool element.
* @param {Number} offset Offset value.
*/
var setOffset = function(root, offset) {
root.attr('data-offset', offset);
};
/**
* Return the limit value for fetching courses.
*
* @param {object} root The rool element.
* @return {Number}
*/
var getLimit = function(root) {
return parseInt(root.attr('data-limit'), 10);
};
/**
* Return the days offset value for fetching events.
*
* @param {object} root The rool element.
* @return {Number}
*/
var getDaysOffset = function(root) {
return parseInt(root.attr('data-days-offset'), 10);
};
/**
* Return the days limit value for fetching events. The days
* limit is optional so undefined will be returned if it isn't
* set.
*
* @param {object} root The rool element.
* @return {int|undefined}
*/
var getDaysLimit = function(root) {
var daysLimit = root.attr('data-days-limit');
return daysLimit != undefined ? parseInt(daysLimit, 10) : undefined;
};
/**
* Return the timestamp for the user's midnight.
*
* @param {object} root The rool element.
* @return {Number}
*/
var getMidnight = function(root) {
return parseInt(root.attr('data-midnight'), 10);
};
/**
* Return the start time for fetching events. This is calculated
* based on the user's midnight value so that timezones are
* preserved.
*
* @param {object} root The rool element.
* @return {Number}
*/
var getStartTime = function(root) {
var midnight = getMidnight(root);
var daysOffset = getDaysOffset(root);
return midnight + (daysOffset * SECONDS_IN_DAY);
};
/**
* Return the end time for fetching events. This is calculated
* based on the user's midnight value so that timezones are
* preserved, unless filtering by overdue, where the current UNIX timestamp is used.
*
* @param {object} root The rool element.
* @return {Number}
*/
var getEndTime = function(root) {
let endTime = null;
if (root.attr('data-filter-overdue')) {
// If filtering by overdue, end time will be the current timestamp in seconds.
endTime = Math.floor(Date.now() / 1000);
} else {
const midnight = getMidnight(root);
const daysLimit = getDaysLimit(root);
if (daysLimit != undefined) {
endTime = midnight + (daysLimit * SECONDS_IN_DAY);
}
}
return endTime;
};
/**
* Get a list of events for the given course ids. Returns a promise that will
* be resolved with the events.
*
* @param {array} courseIds The list of course ids to fetch events for.
* @param {Number} startTime Timestamp to fetch events from.
* @param {Number} limit Limit to the number of events (this applies per course, not total)
* @param {Number} endTime Timestamp to fetch events to.
* @param {string|undefined} searchValue Search value
* @return {object} jQuery promise.
*/
var getEventsForCourseIds = function(courseIds, startTime, limit, endTime, searchValue) {
var args = {
courseids: courseIds,
starttime: startTime,
limit: limit
};
if (endTime) {
args.endtime = endTime;
}
if (searchValue) {
args.searchvalue = searchValue;
}
return EventsRepository.queryByCourses(args);
};
/**
* Get the last time the events were reloaded.
*
* @param {object} root The rool element.
* @return {Number}
*/
var getEventReloadTime = function(root) {
return root.data('last-event-load-time');
};
/**
* Set the last time the events were reloaded.
*
* @param {object} root The rool element.
* @param {Number} time Timestamp in milliseconds.
*/
var setEventReloadTime = function(root, time) {
root.data('last-event-load-time', time);
};
/**
* Check if events have begun reloading since the given
* time.
*
* @param {object} root The rool element.
* @param {Number} time Timestamp in milliseconds.
* @return {bool}
*/
var hasReloadedEventsSince = function(root, time) {
return getEventReloadTime(root) > time;
};
/**
* Send a request to the server to load the events for the courses.
*
* @param {array} courses List of course objects.
* @param {Number} startTime Timestamp to load events after.
* @param {int|undefined} endTime Timestamp to load events up until.
* @param {string|undefined} searchValue Search value
* @return {object} jQuery promise resolved with the events.
*/
var loadEventsForCourses = function(courses, startTime, endTime, searchValue) {
var courseIds = courses.map(function(course) {
return course.id;
});
return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime, searchValue);
};
/**
* Render the courses in the DOM once the server has returned the courses.
*
* @param {array} courses List of course objects.
* @param {object} root The root element
* @param {Number} midnight The midnight timestamp in the user's timezone.
* @param {Number} daysOffset Number of days from today to offset the events.
* @param {Number} daysLimit Number of days from today to limit the events to.
* @param {boolean} append Whether new content should be appended instead of replaced (eg "show more courses").
* @return {object} jQuery promise resolved after rendering is complete.
*/
var updateDisplayFromCourses = function(courses, root, midnight, daysOffset, daysLimit, append) {
// Render the courses template.
return Templates.render(TEMPLATES.COURSE_ITEMS, {
courses: courses,
midnight: midnight,
hasdaysoffset: true,
hasdayslimit: daysLimit != undefined,
daysoffset: daysOffset,
dayslimit: daysLimit,
nodayslimit: daysLimit == undefined,
courseview: true,
hascourses: true
}).then(function(html) {
hideLoadingPlaceholder(root);
if (html) {
// Template rendering is complete and we have the HTML so we can
// add it to the DOM.
renderCourseItemsHTML(root, html, append);
}
return html;
})
.then(function(html) {
if (courses.length < COURSE_LIMIT) {
// We know there aren't any more courses because we got back less
// than we asked for so hide the button to request more.
hideMoreCoursesButton(root);
} else {
// Make sure the button is visible if there are more courses to load.
showMoreCoursesButton(root);
}
return html;
})
.catch(function() {
hideLoadingPlaceholder(root);
});
};
/**
* Find all of the visible course blocks and initialise the event
* list module to being loading the events for the course block.
*
* @param {object} root The root element for the timeline courses view.
* @param {boolean} append Whether content should be appended instead of replaced (eg "show more courses"). False by default.
* @return {object} jQuery promise resolved with courses and events.
*/
var loadMoreCourses = function(root, append = false) {
const pendingPromise = new Pending('block/timeline:load-more-courses');
var offset = getOffset(root);
var limit = getLimit(root);
const startTime = getStartTime(root);
const endTime = getEndTime(root);
const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val();
// Start loading the next set of courses.
// Fetch up to limit number of courses with at least one action event in the time filtering specified.
// Courses without events will also be fetched, but hidden in case they have events in other timespans.
return CourseRepository.getEnrolledCoursesWithEventsByTimelineClassification(
COURSE_CLASSIFICATION,
limit,
offset,
COURSE_SORT,
searchValue,
startTime,
endTime
).then(function(result) {
var startEventLoadingTime = Date.now();
var courses = result.courses;
var nextOffset = result.nextoffset;
var daysOffset = getDaysOffset(root);
var daysLimit = getDaysLimit(root);
var midnight = getMidnight(root);
const moreCoursesAvailable = result.morecoursesavailable;
// Record the next offset if we want to request more courses.
setOffset(root, nextOffset);
// Load the events for these courses.
var eventsPromise = loadEventsForCourses(courses, startTime, endTime, searchValue);
// Render the courses in the DOM.
var renderPromise = updateDisplayFromCourses(courses, root, midnight, daysOffset, daysLimit, append);
return $.when(eventsPromise, renderPromise)
.then(function(eventsByCourse) {
if (hasReloadedEventsSince(root, startEventLoadingTime)) {
// All of the events are being reloaded so ignore our results.
return eventsByCourse;
}
if (courses.length > 0) {
// Render the events in the correct course event list.
courses.forEach(function(course) {
const courseId = course.id;
const containerSelector = '[data-region="course-events-container"][data-course-id="' + courseId + '"]';
const courseEventsContainer = root.find(containerSelector);
const eventListRoot = courseEventsContainer.find(EventList.rootSelector);
EventList.init(eventListRoot, additionalConfig);
});
if (!moreCoursesAvailable) {
// If no more courses with events matching the current filtering exist, hide the more courses button.
hideMoreCoursesButton(root);
} else {
// If more courses exist with events matching the current filtering, show the more courses button.
showMoreCoursesButton(root);
}
} else {
// No more courses to load, hide the more courses button.
hideMoreCoursesButton(root);
// A zero offset means this was not loading "more courses", so we need to display the no results message.
if (offset == 0) {
showNoCoursesWithEventsMessage(root);
}
}
return eventsByCourse;
});
}).then(() => {
return pendingPromise.resolve();
}).catch(Notification.exception);
};
/**
* Add event listeners to load more courses for the courses view.
*
* @param {object} root The root element for the timeline courses view.
*/
var registerEventListeners = function(root) {
CustomEvents.define(root, [CustomEvents.events.activate]);
// Show more courses and load their events when the user clicks the "more courses" button.
root.on(CustomEvents.events.activate, SELECTORS.MORE_COURSES_BUTTON, function(e, data) {
enableMoreCoursesButtonLoading(root);
loadMoreCourses(root, true)
.then(function() {
disableMoreCoursesButtonLoading(root);
return;
})
.catch(function() {
disableMoreCoursesButtonLoading(root);
});
if (data) {
data.originalEvent.preventDefault();
data.originalEvent.stopPropagation();
}
e.stopPropagation();
});
};
/**
* Initialise the timeline courses view. Begin loading the events
* if this view is active. Add the relevant event listeners.
*
* This function should only be called once per page load because it
* is adding event listeners to the page.
*
* @param {object} root The root element for the timeline courses view.
*/
var init = function(root) {
root = $(root);
// Only need to handle course loading if the user is actively enrolled in a course.
if (!root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).length) {
setEventReloadTime(root, Date.now());
if (root.hasClass('active')) {
// Only load if this is active otherwise it will be lazy loaded later.
loadMoreCourses(root);
root.attr('data-seen', true);
}
registerEventListeners(root);
}
};
/**
* Reset the element back to it's initial state. Begin loading the events again
* if this view is active.
*
* @param {object} root The root element for the timeline courses view.
*/
var reset = function(root) {
setOffset(root, 0);
showLoadingPlaceholder(root);
hideNoCoursesWithEventsMessage(root);
root.removeAttr('data-seen');
if (root.hasClass('active')) {
shown(root);
}
};
/**
* Begin loading the events unless we know there are no actively enrolled courses.
*
* @param {object} root The root element for the timeline courses view.
*/
var shown = function(root) {
if (!root.attr('data-seen') && !root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).length) {
loadMoreCourses(root);
root.attr('data-seen', true);
}
};
return {
init: init,
reset: reset,
shown: shown
};
});
+122
View File
@@ -0,0 +1,122 @@
// 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/>.
/**
* Manage the timeline dates view for the timeline block.
*
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'block_timeline/event_list',
'core/pubsub',
'core/paged_content_events'
],
function(
$,
EventList,
PubSub,
PagedContentEvents
) {
var SELECTORS = {
EVENT_LIST_CONTAINER: '[data-region="event-list-container"]',
NO_COURSES_EMPTY_MESSAGE: '[data-region="no-courses-empty-message"]',
};
/**
* Setup the listeners for the timeline block
*
* @param {string} root view dates container
* @param {string} namespace The namespace for the paged content
*/
var registerEventListeners = function(root, namespace) {
var event = namespace + PagedContentEvents.SET_ITEMS_PER_PAGE_LIMIT;
PubSub.subscribe(event, function(limit) {
$(root).data('limit', limit);
});
};
/**
* Initialise the event list and being loading the events.
*
* @param {object} root The root element for the timeline dates view.
*/
var load = function(root) {
if (!root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).length) {
var eventListContainer = root.find(SELECTORS.EVENT_LIST_CONTAINER);
var namespace = $(eventListContainer).attr('id') + "user_block_timeline" + Math.random();
registerEventListeners(root, namespace);
var config = {
persistentLimitKey: "block_timeline_user_limit_preference",
eventNamespace: namespace
};
EventList.init(eventListContainer, config);
}
};
/**
* Initialise the timeline dates view. Begin loading the events
* if this view is active.
*
* @param {object} root The root element for the timeline courses view.
*/
var init = function(root) {
root = $(root);
// Only need to handle events loading if the user is actively enrolled in a course and this view is active.
if (root.hasClass('active') && !root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).length) {
load(root);
root.attr('data-seen', true);
}
};
/**
* Reset the view back to it's initial state. If this view is active then
* beging loading the events.
*
* @param {object} root The root element for the timeline courses view.
*/
var reset = function(root) {
root.removeAttr('data-seen');
if (root.hasClass('active')) {
load(root);
root.attr('data-seen', true);
}
};
/**
* Load the events if this is the first time the view is displayed.
*
* @param {object} root The root element for the timeline courses view.
*/
var shown = function(root) {
if (!root.attr('data-seen')) {
load(root);
root.attr('data-seen', true);
}
};
return {
init: init,
reset: reset,
shown: shown
};
});
+189
View File
@@ -0,0 +1,189 @@
// 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/>.
/**
* Manage the timeline view navigation for the timeline block.
*
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import $ from 'jquery';
import * as CustomEvents from 'core/custom_interaction_events';
import * as View from 'block_timeline/view';
import * as Notification from 'core/notification';
import * as Utils from 'core/utils';
import * as UserRepository from 'core_user/repository';
const SELECTORS = {
TIMELINE_DAY_FILTER: '[data-region="day-filter"]',
TIMELINE_DAY_FILTER_OPTION: '[data-from]',
TIMELINE_VIEW_SELECTOR: '[data-region="view-selector"]',
DATA_DAYS_OFFSET: '[data-days-offset]',
DATA_DAYS_LIMIT: '[data-days-limit]',
TIMELINE_SEARCH_INPUT: '[data-action="search"]',
TIMELINE_SEARCH_CLEAR_ICON: '[data-action="clearsearch"]',
NO_COURSES_EMPTY_MESSAGE: '[data-region="no-courses-empty-message"]',
};
/**
* Event listener for the day selector ("Next 7 days", "Next 30 days", etc).
*
* @param {object} root The root element for the timeline block
* @param {object} timelineViewRoot The root element for the timeline view
*/
const registerTimelineDaySelector = function(root, timelineViewRoot) {
const timelineDaySelectorContainer = root.find(SELECTORS.TIMELINE_DAY_FILTER);
CustomEvents.define(timelineDaySelectorContainer, [CustomEvents.events.activate]);
timelineDaySelectorContainer.on(
CustomEvents.events.activate,
SELECTORS.TIMELINE_DAY_FILTER_OPTION,
function(e, data) {
// Update the user preference
var filtername = $(e.currentTarget).data('filtername');
var type = 'block_timeline_user_filter_preference';
UserRepository.setUserPreference(type, filtername)
.catch(Notification.exception);
var option = $(e.target).closest(SELECTORS.TIMELINE_DAY_FILTER_OPTION);
if (option.attr('aria-current') == 'true') {
// If it's already active then we don't need to do anything.
return;
}
var daysOffset = option.attr('data-from');
var daysLimit = option.attr('data-to');
var elementsWithDaysOffset = root.find(SELECTORS.DATA_DAYS_OFFSET);
elementsWithDaysOffset.attr('data-days-offset', daysOffset);
if (daysLimit != undefined) {
elementsWithDaysOffset.attr('data-days-limit', daysLimit);
} else {
elementsWithDaysOffset.removeAttr('data-days-limit');
}
if (option.attr('data-filtername') === 'overdue') {
elementsWithDaysOffset.attr('data-filter-overdue', true);
} else {
elementsWithDaysOffset.removeAttr('data-filter-overdue');
}
// Reset the views to reinitialise the event lists now that we've
// updated the day limits.
View.reset(timelineViewRoot);
data.originalEvent.preventDefault();
}
);
};
/**
* Event listener for the "sort" button in the timeline navigation that allows for
* changing between the timeline dates and courses views.
*
* On a view change we tell the timeline view module that the view has been shown
* so that it can handle how to display the appropriate view.
*
* @param {object} root The root element for the timeline block
* @param {object} timelineViewRoot The root element for the timeline view
*/
const registerViewSelector = function(root, timelineViewRoot) {
const viewSelector = root.find(SELECTORS.TIMELINE_VIEW_SELECTOR);
// Listen for when the user changes tab so that we can show the first set of courses
// and load their events when they request the sort by courses view for the first time.
viewSelector.on('shown shown.bs.tab', function(e) {
View.shown(timelineViewRoot);
$(e.target).removeClass('active');
});
// Event selector for user_sort
CustomEvents.define(viewSelector, [CustomEvents.events.activate]);
viewSelector.on(CustomEvents.events.activate, "[data-toggle='tab']", function(e) {
var filtername = $(e.currentTarget).data('filtername');
var type = 'block_timeline_user_sort_preference';
UserRepository.setUserPreference(type, filtername)
.catch(Notification.exception);
});
};
/**
* Event listener for the "search" input field in the timeline navigation that allows for
* searching the activity name, course name and activity type.
*
* @param {object} root The root element for the timeline block
* @param {object} timelineViewRoot The root element for the timeline view
*/
const registerSearch = (root, timelineViewRoot) => {
const searchInput = root.find(SELECTORS.TIMELINE_SEARCH_INPUT);
const clearSearchIcon = root.find(SELECTORS.TIMELINE_SEARCH_CLEAR_ICON);
searchInput.on('input', Utils.debounce(() => {
if (searchInput.val() !== '') {
activeSearchState(clearSearchIcon, timelineViewRoot);
} else {
clearSearchState(clearSearchIcon, timelineViewRoot);
}
}, 1000));
clearSearchIcon.on('click', () => {
searchInput.val('');
clearSearchState(clearSearchIcon, timelineViewRoot);
searchInput.focus();
});
};
/**
* Show the clear search icon.
*
* @param {object} clearSearchIcon Clear search icon element.
* @param {object} timelineViewRoot The root element for the timeline view
*/
const activeSearchState = (clearSearchIcon, timelineViewRoot) => {
clearSearchIcon.removeClass('d-none');
View.reset(timelineViewRoot);
};
/**
* Hide the clear search icon.
*
* @param {object} clearSearchIcon Clear search icon element.
* @param {object} timelineViewRoot The root element for the timeline view
*/
const clearSearchState = (clearSearchIcon, timelineViewRoot) => {
clearSearchIcon.addClass('d-none');
View.reset(timelineViewRoot);
};
/**
* Initialise the timeline view navigation by adding event listeners to
* the navigation elements.
*
* @param {jQuery|HTMLElement} root The root element for the timeline block
* @param {object} timelineViewRoot The root element for the timeline view
*/
export const init = function(root, timelineViewRoot) {
root = $(root);
registerViewSelector(root, timelineViewRoot);
// Only need to handle filtering if the user is actively enrolled in a course.
if (!root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).length) {
registerTimelineDaySelector(root, timelineViewRoot);
registerSearch(root, timelineViewRoot);
}
};