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,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);
}
};