diff --git a/blocks/timeline/amd/build/event_list.min.js b/blocks/timeline/amd/build/event_list.min.js index 3fbed128f91..076f5bf9470 100644 --- a/blocks/timeline/amd/build/event_list.min.js +++ b/blocks/timeline/amd/build/event_list.min.js @@ -1,2 +1,2 @@ -define ("block_timeline/event_list",["jquery","core/notification","core/templates","core/paged_content_factory","core/str","core/user_date","block_timeline/calendar_events_repository"],function(a,b,c,d,e,f,g){var h=!1,i={EMPTY_MESSAGE:"[data-region=\"empty-message\"]",ROOT:"[data-region=\"event-list-container\"]",EVENT_LIST_CONTENT:"[data-region=\"event-list-content\"]",EVENT_LIST_LOADING_PLACEHOLDER:"[data-region=\"event-list-loading-placeholder\"]"},j={EVENT_LIST_CONTENT:"block_timeline/event-list-content"},k={ignoreControlWhileLoading:!0,controlPlacementBottom:!0,ariaLabels:{itemsperpagecomponents:"ariaeventlistpagelimit, block_timeline"}},l=function(a){a.find(i.EVENT_LIST_CONTENT).addClass("hidden");a.find(i.EMPTY_MESSAGE).removeClass("hidden")},m=function(a){a.find(i.EVENT_LIST_CONTENT).removeClass("hidden");a.find(i.EMPTY_MESSAGE).addClass("hidden")},n=function(a){a.find(i.EVENT_LIST_CONTENT).empty()},o=function(a,b){var c={},d={courseview:h,eventsbyday:[]};a.forEach(function(a){var b=a.timeusermidnight;if(c[b]){c[b].push(a)}else{c[b]=[a]}});Object.keys(c).forEach(function(a){var e=c[a];d.eventsbyday.push({past:ac}return!0}),e=d.length<=k;if(e){b.allItemsLoaded(j)}else{d.pop()}return d})},s=function(c,f,g,h,i,j,l,m,n){var o={1:0},q=!1,s=a.extend({},k,n);return e.get_string("ariaeventlistpagelimit","block_timeline",a.isArray(c)?c[0].value:c).then(function(a){s.ariaLabels.itemsperpage=a;s.ariaLabels.paginationnav=m;return a}).then(function(){return d.createWithLimit(c,function(c,d){var e=[];c.forEach(function(a){var c=a.pageNumber,h=r(a,d,g,o,f,i,j,l).then(function(a){if(a.length){q=!0;var b=a[a.length-1].id;o[c+1]=b;return p(a,g)}else{return a}}).catch(b.exception);e.push(h)});a.when.apply(a,e).then(function(){h.resolve(q)}).catch(function(){h.resolve(q)});return e},s)})};return{init:function init(d,e,f,g){var j=4c}return!0}),e=d.length<=l;if(e){b.allItemsLoaded(k)}else{d.pop()}return d})},s=function(c,f,g,h,i,j,l,m,n,o){var q={1:0},s=!1,t=a.extend({},k,n);return e.get_string("ariaeventlistpagelimit","block_timeline",a.isArray(c)?c[0].value:c).then(function(a){t.ariaLabels.itemsperpage=a;t.ariaLabels.paginationnav=m;return a}).then(function(){return d.createWithLimit(c,function(c,d){var e=[];c.forEach(function(a){var c=a.pageNumber,h=r(a,d,g,q,f,i,j,l,o).then(function(a){if(a.length){s=!0;var b=a[a.length-1].id;q[c+1]=b;return p(a,g)}else{return a}}).catch(b.exception);e.push(h)});a.when.apply(a,e).then(function(){h.resolve(s)}).catch(function(){h.resolve(s)});return e},t)})};return{init:function init(d,e,f,g){var j=4.\n\n/**\n * Javascript to load and render the list of calendar events for a\n * given day range.\n *\n * @module block_timeline/event_list\n * @copyright 2016 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(\n[\n 'jquery',\n 'core/notification',\n 'core/templates',\n 'core/paged_content_factory',\n 'core/str',\n 'core/user_date',\n 'block_timeline/calendar_events_repository'\n],\nfunction(\n $,\n Notification,\n Templates,\n PagedContentFactory,\n Str,\n UserDate,\n CalendarEventsRepository\n) {\n\n var SECONDS_IN_DAY = 60 * 60 * 24;\n var courseview = false;\n\n var SELECTORS = {\n EMPTY_MESSAGE: '[data-region=\"empty-message\"]',\n ROOT: '[data-region=\"event-list-container\"]',\n EVENT_LIST_CONTENT: '[data-region=\"event-list-content\"]',\n EVENT_LIST_LOADING_PLACEHOLDER: '[data-region=\"event-list-loading-placeholder\"]',\n };\n\n var TEMPLATES = {\n EVENT_LIST_CONTENT: 'block_timeline/event-list-content'\n };\n\n // We want the paged content controls below the paged content area\n // and the controls should be ignored while data is loading.\n var DEFAULT_PAGED_CONTENT_CONFIG = {\n ignoreControlWhileLoading: true,\n controlPlacementBottom: true,\n ariaLabels: {\n itemsperpagecomponents: 'ariaeventlistpagelimit, block_timeline',\n }\n };\n\n /**\n * Hide the content area and display the empty content message.\n *\n * @param {object} root The container element\n */\n var hideContent = function(root) {\n root.find(SELECTORS.EVENT_LIST_CONTENT).addClass('hidden');\n root.find(SELECTORS.EMPTY_MESSAGE).removeClass('hidden');\n };\n\n /**\n * Show the content area and hide the empty content message.\n *\n * @param {object} root The container element\n */\n var showContent = function(root) {\n root.find(SELECTORS.EVENT_LIST_CONTENT).removeClass('hidden');\n root.find(SELECTORS.EMPTY_MESSAGE).addClass('hidden');\n };\n\n /**\n * Empty the content area.\n *\n * @param {object} root The container element\n */\n var emptyContent = function(root) {\n root.find(SELECTORS.EVENT_LIST_CONTENT).empty();\n };\n\n /**\n * Construct the template context from a list of calendar events. The events\n * are grouped by which day they are on. The day is calculated from the user's\n * midnight timestamp to ensure that the calculation is timezone agnostic.\n *\n * The return data structure will look like:\n * {\n * eventsbyday: [\n * {\n * dayTimestamp: 1533744000,\n * events: [\n * { ...event 1 data... },\n * { ...event 2 data... }\n * ]\n * },\n * {\n * dayTimestamp: 1533830400,\n * events: [\n * { ...event 3 data... },\n * { ...event 4 data... }\n * ]\n * }\n * ]\n * }\n *\n * Each day timestamp is the day's midnight in the user's timezone.\n *\n * @param {array} calendarEvents List of calendar events\n * @param {Number} midnight A timestamp representing midnight in the user's timezone\n * @return {object}\n */\n var buildTemplateContext = function(calendarEvents, midnight) {\n var eventsByDay = {};\n var templateContext = {\n courseview,\n eventsbyday: []\n };\n\n calendarEvents.forEach(function(calendarEvent) {\n var dayTimestamp = calendarEvent.timeusermidnight;\n if (eventsByDay[dayTimestamp]) {\n eventsByDay[dayTimestamp].push(calendarEvent);\n } else {\n eventsByDay[dayTimestamp] = [calendarEvent];\n }\n });\n\n Object.keys(eventsByDay).forEach(function(dayTimestamp) {\n var events = eventsByDay[dayTimestamp];\n templateContext.eventsbyday.push({\n past: dayTimestamp < midnight,\n dayTimestamp: dayTimestamp,\n events: events\n });\n });\n\n return templateContext;\n };\n\n /**\n * Render the HTML for the given calendar events.\n *\n * @param {array} calendarEvents A list of calendar events\n * @param {Number} midnight A timestamp representing midnight for the user\n * @return {promise} Resolved with HTML and JS strings.\n */\n var render = function(calendarEvents, midnight) {\n var templateContext = buildTemplateContext(calendarEvents, midnight);\n var templateName = TEMPLATES.EVENT_LIST_CONTENT;\n\n return Templates.render(templateName, templateContext);\n };\n\n /**\n * Retrieve a list of calendar events from the server for the given\n * constraints.\n *\n * @param {Number} midnight The user's midnight time in unix timestamp.\n * @param {Number} limit Limit the result set to this number of items\n * @param {Number} daysOffset How many days (from midnight) to offset the results from\n * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to\n * @param {int|false} lastId The ID of the last seen event (if any)\n * @param {int|undefined} courseId Course ID to restrict events to\n * @return {Promise} A jquery promise\n */\n var load = function(midnight, limit, daysOffset, daysLimit, lastId, courseId) {\n var startTime = midnight + (daysOffset * SECONDS_IN_DAY);\n var endTime = daysLimit != undefined ? midnight + (daysLimit * SECONDS_IN_DAY) : false;\n\n var args = {\n starttime: startTime,\n limit: limit,\n };\n\n if (lastId) {\n args.aftereventid = lastId;\n }\n\n if (endTime) {\n args.endtime = endTime;\n }\n\n if (courseId) {\n // If we have a course id then we only want events from that course.\n args.courseid = courseId;\n return CalendarEventsRepository.queryByCourse(args);\n } else {\n // Otherwise we want events from any course.\n return CalendarEventsRepository.queryByTime(args);\n }\n };\n\n /**\n * Handle a single page request from the paged content. Uses the given page data to request\n * the events from the server.\n *\n * Checks the given preloadedPages before sending a request to the server to make sure we\n * don't load data unnecessarily.\n *\n * @param {object} pageData A single page data (see core/paged_content_pages for more info).\n * @param {object} actions Paged content actions (see core/paged_content_pages for more info).\n * @param {Number} midnight The user's midnight time in unix timestamp.\n * @param {object} lastIds The last event ID for each loaded page. Page number is key, id is value.\n * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.\n * @param {int|undefined} courseId Course ID to restrict events to\n * @param {Number} daysOffset How many days (from midnight) to offset the results from\n * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to\n * @return {object} jQuery promise resolved with calendar events.\n */\n var loadEventsFromPageData = function(\n pageData,\n actions,\n midnight,\n lastIds,\n preloadedPages,\n courseId,\n daysOffset,\n daysLimit\n ) {\n var pageNumber = pageData.pageNumber;\n var limit = pageData.limit;\n var lastPageNumber = pageNumber;\n\n // This is here to protect us if, for some reason, the pages\n // are loaded out of order somehow and we don't have a reference\n // to the previous page. In that case, scan back to find the most\n // recent page we've seen.\n while (!lastIds.hasOwnProperty(lastPageNumber)) {\n lastPageNumber--;\n }\n // Use the last id of the most recent page.\n var lastId = lastIds[lastPageNumber];\n var eventsPromise = null;\n\n if (preloadedPages && preloadedPages.hasOwnProperty(pageNumber)) {\n // This page has been preloaded so use that rather than load the values\n // again.\n eventsPromise = preloadedPages[pageNumber];\n } else {\n // Load one more than the given limit so that we can tell if there\n // is more content to load after this.\n eventsPromise = load(midnight, limit + 1, daysOffset, daysLimit, lastId, courseId);\n }\n\n return eventsPromise.then(function(result) {\n if (!result.events.length) {\n // If we didn't get any events back then tell the paged content\n // that we're done loading.\n actions.allItemsLoaded(pageNumber);\n return [];\n }\n\n var calendarEvents = result.events.filter(function(event) {\n if (event.eventtype == \"open\" || event.eventtype == \"opensubmission\") {\n var dayTimestamp = UserDate.getUserMidnightForTimestamp(event.timesort, midnight);\n return dayTimestamp > midnight;\n }\n return true;\n });\n // We expect to receive limit + 1 events back from the server.\n // Any less means there are no more events to load.\n var loadedAll = calendarEvents.length <= limit;\n\n if (loadedAll) {\n // Tell the pagination that everything is loaded.\n actions.allItemsLoaded(pageNumber);\n } else {\n // Remove the last element from the array because it isn't\n // needed in this result set.\n calendarEvents.pop();\n }\n\n return calendarEvents;\n });\n };\n\n /**\n * Use the paged content factory to create a paged content element for showing\n * the event list. We only provide a page limit to the factory because we don't\n * know exactly how many pages we'll need. This creates a paging bar with just\n * next/previous buttons.\n *\n * This function specifies the callback for loading the event data that the user\n * is requesting.\n *\n * @param {int|array} pageLimit A single limit or list of limits as options for the paged content\n * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.\n * @param {Number} midnight The user's midnight time in unix timestamp.\n * @param {object} firstLoad A jQuery promise to be resolved after the first set of data is loaded.\n * @param {int|undefined} courseId Course ID to restrict events to\n * @param {Number} daysOffset How many days (from midnight) to offset the results from\n * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to\n * @param {string} paginationAriaLabel String to set as the aria label for the pagination bar.\n * @param {object} additionalConfig Additional config options to pass to pagedContentFactory\n * @return {object} jQuery promise.\n */\n var createPagedContent = function(\n pageLimit,\n preloadedPages,\n midnight,\n firstLoad,\n courseId,\n daysOffset,\n daysLimit,\n paginationAriaLabel,\n additionalConfig\n ) {\n // Remember the last event id we loaded on each page because we can't\n // use the offset value since the backend can skip events if the user doesn't\n // have the capability to see them. Instead we load the next page of events\n // based on the last seen event id.\n var lastIds = {'1': 0};\n var hasContent = false;\n var config = $.extend({}, DEFAULT_PAGED_CONTENT_CONFIG, additionalConfig);\n\n return Str.get_string(\n 'ariaeventlistpagelimit',\n 'block_timeline',\n $.isArray(pageLimit) ? pageLimit[0].value : pageLimit\n )\n .then(function(string) {\n config.ariaLabels.itemsperpage = string;\n config.ariaLabels.paginationnav = paginationAriaLabel;\n return string;\n })\n .then(function() {\n return PagedContentFactory.createWithLimit(\n pageLimit,\n function(pagesData, actions) {\n var promises = [];\n\n pagesData.forEach(function(pageData) {\n var pageNumber = pageData.pageNumber;\n // Load the page data.\n var pagePromise = loadEventsFromPageData(\n pageData,\n actions,\n midnight,\n lastIds,\n preloadedPages,\n courseId,\n daysOffset,\n daysLimit\n ).then(function(calendarEvents) {\n if (calendarEvents.length) {\n // Remember that we've loaded content.\n hasContent = true;\n // Remember the last id we've seen.\n var lastEventId = calendarEvents[calendarEvents.length - 1].id;\n // Record the id that the next page will need to start from.\n lastIds[pageNumber + 1] = lastEventId;\n // Get the HTML and JS for these calendar events.\n return render(calendarEvents, midnight);\n } else {\n return calendarEvents;\n }\n })\n .catch(Notification.exception);\n\n promises.push(pagePromise);\n });\n\n $.when.apply($, promises).then(function() {\n // Tell the calling code that the first page has been loaded\n // and whether it contains any content.\n firstLoad.resolve(hasContent);\n return;\n })\n .catch(function() {\n firstLoad.resolve(hasContent);\n });\n\n return promises;\n },\n config\n );\n });\n };\n\n /**\n * Create a paged content region for the calendar events in the given root element.\n * The content of the root element are replaced with a new paged content section\n * each time this function is called.\n *\n * This function will be called each time the offset or limit values are changed to\n * reload the event list region.\n *\n * @param {object} root The event list container element\n * @param {int|array} pageLimit A single limit or list of limits as options for the paged content\n * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.\n * @param {string} paginationAriaLabel String to set as the aria label for the pagination bar.\n * @param {object} additionalConfig Additional config options to pass to pagedContentFactory\n */\n var init = function(root, pageLimit, preloadedPages, paginationAriaLabel, additionalConfig = {}) {\n root = $(root);\n\n courseview = !!additionalConfig.courseview;\n\n // Create a promise that will be resolved once the first set of page\n // data has been loaded. This ensures that the loading placeholder isn't\n // hidden until we have all of the data back to prevent the page elements\n // jumping around.\n var firstLoad = $.Deferred();\n var eventListContent = root.find(SELECTORS.EVENT_LIST_CONTENT);\n var loadingPlaceholder = root.find(SELECTORS.EVENT_LIST_LOADING_PLACEHOLDER);\n var courseId = root.attr('data-course-id');\n var daysOffset = parseInt(root.attr('data-days-offset'), 10);\n var daysLimit = root.attr('data-days-limit');\n var midnight = parseInt(root.attr('data-midnight'), 10);\n\n // Make sure the content area and loading placeholder is visible.\n // This is because the init function can be called to re-initialise\n // an existing event list area.\n emptyContent(root);\n showContent(root);\n loadingPlaceholder.removeClass('hidden');\n\n // Days limit isn't mandatory.\n if (daysLimit != undefined) {\n daysLimit = parseInt(daysLimit, 10);\n }\n\n // Created the paged content element.\n return createPagedContent(pageLimit, preloadedPages, midnight, firstLoad, courseId, daysOffset, daysLimit,\n paginationAriaLabel, additionalConfig)\n .then(function(html, js) {\n html = $(html);\n // Hide the content for now.\n html.addClass('hidden');\n // Replace existing elements with the newly created paged content.\n // If we're reinitialising an existing event list this will replace\n // the old event list (including removing any event handlers).\n Templates.replaceNodeContents(eventListContent, html, js);\n\n firstLoad.then(function(hasContent) {\n // Prevent changing page elements too much by only showing the content\n // once we've loaded some data for the first time. This allows our\n // fancy loading placeholder to shine.\n html.removeClass('hidden');\n loadingPlaceholder.addClass('hidden');\n\n if (!hasContent) {\n // If we didn't get any data then show the empty data message.\n hideContent(root);\n }\n\n return hasContent;\n })\n .catch(function() {\n return false;\n });\n\n return html;\n })\n .catch(Notification.exception);\n };\n\n return {\n init: init,\n rootSelector: SELECTORS.ROOT,\n };\n});\n"],"file":"event_list.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/event_list.js"],"names":["define","$","Notification","Templates","PagedContentFactory","Str","UserDate","CalendarEventsRepository","courseview","SELECTORS","EMPTY_MESSAGE","ROOT","EVENT_LIST_CONTENT","EVENT_LIST_LOADING_PLACEHOLDER","TIMELINE_BLOCK","TIMELINE_SEARCH","TEMPLATES","DEFAULT_PAGED_CONTENT_CONFIG","ignoreControlWhileLoading","controlPlacementBottom","ariaLabels","itemsperpagecomponents","hideContent","root","find","addClass","removeClass","showContent","emptyContent","empty","buildTemplateContext","calendarEvents","midnight","eventsByDay","templateContext","eventsbyday","forEach","calendarEvent","dayTimestamp","timeusermidnight","push","Object","keys","events","past","render","templateName","load","limit","daysOffset","daysLimit","lastId","courseId","searchValue","endTime","args","starttime","aftereventid","endtime","searchvalue","courseid","queryByCourse","queryByTime","loadEventsFromPageData","pageData","actions","lastIds","preloadedPages","pageNumber","lastPageNumber","hasOwnProperty","eventsPromise","then","result","length","allItemsLoaded","filter","event","eventtype","getUserMidnightForTimestamp","timesort","loadedAll","pop","createPagedContent","pageLimit","firstLoad","paginationAriaLabel","additionalConfig","hasContent","config","extend","get_string","isArray","value","string","itemsperpage","paginationnav","createWithLimit","pagesData","promises","pagePromise","lastEventId","id","catch","exception","when","apply","resolve","init","Deferred","eventListContent","loadingPlaceholder","attr","parseInt","closest","val","html","js","replaceNodeContents","rootSelector"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript to load and render the list of calendar events for a\n * given day range.\n *\n * @module block_timeline/event_list\n * @copyright 2016 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(\n[\n 'jquery',\n 'core/notification',\n 'core/templates',\n 'core/paged_content_factory',\n 'core/str',\n 'core/user_date',\n 'block_timeline/calendar_events_repository'\n],\nfunction(\n $,\n Notification,\n Templates,\n PagedContentFactory,\n Str,\n UserDate,\n CalendarEventsRepository\n) {\n\n var SECONDS_IN_DAY = 60 * 60 * 24;\n var courseview = false;\n\n var SELECTORS = {\n EMPTY_MESSAGE: '[data-region=\"empty-message\"]',\n ROOT: '[data-region=\"event-list-container\"]',\n EVENT_LIST_CONTENT: '[data-region=\"event-list-content\"]',\n EVENT_LIST_LOADING_PLACEHOLDER: '[data-region=\"event-list-loading-placeholder\"]',\n TIMELINE_BLOCK: '[data-region=\"timeline\"]',\n TIMELINE_SEARCH: '[data-region=\"search-input\"]'\n };\n\n var TEMPLATES = {\n EVENT_LIST_CONTENT: 'block_timeline/event-list-content'\n };\n\n // We want the paged content controls below the paged content area\n // and the controls should be ignored while data is loading.\n var DEFAULT_PAGED_CONTENT_CONFIG = {\n ignoreControlWhileLoading: true,\n controlPlacementBottom: true,\n ariaLabels: {\n itemsperpagecomponents: 'ariaeventlistpagelimit, block_timeline',\n }\n };\n\n /**\n * Hide the content area and display the empty content message.\n *\n * @param {object} root The container element\n */\n var hideContent = function(root) {\n root.find(SELECTORS.EVENT_LIST_CONTENT).addClass('hidden');\n root.find(SELECTORS.EMPTY_MESSAGE).removeClass('hidden');\n };\n\n /**\n * Show the content area and hide the empty content message.\n *\n * @param {object} root The container element\n */\n var showContent = function(root) {\n root.find(SELECTORS.EVENT_LIST_CONTENT).removeClass('hidden');\n root.find(SELECTORS.EMPTY_MESSAGE).addClass('hidden');\n };\n\n /**\n * Empty the content area.\n *\n * @param {object} root The container element\n */\n var emptyContent = function(root) {\n root.find(SELECTORS.EVENT_LIST_CONTENT).empty();\n };\n\n /**\n * Construct the template context from a list of calendar events. The events\n * are grouped by which day they are on. The day is calculated from the user's\n * midnight timestamp to ensure that the calculation is timezone agnostic.\n *\n * The return data structure will look like:\n * {\n * eventsbyday: [\n * {\n * dayTimestamp: 1533744000,\n * events: [\n * { ...event 1 data... },\n * { ...event 2 data... }\n * ]\n * },\n * {\n * dayTimestamp: 1533830400,\n * events: [\n * { ...event 3 data... },\n * { ...event 4 data... }\n * ]\n * }\n * ]\n * }\n *\n * Each day timestamp is the day's midnight in the user's timezone.\n *\n * @param {array} calendarEvents List of calendar events\n * @param {Number} midnight A timestamp representing midnight in the user's timezone\n * @return {object}\n */\n var buildTemplateContext = function(calendarEvents, midnight) {\n var eventsByDay = {};\n var templateContext = {\n courseview,\n eventsbyday: []\n };\n\n calendarEvents.forEach(function(calendarEvent) {\n var dayTimestamp = calendarEvent.timeusermidnight;\n if (eventsByDay[dayTimestamp]) {\n eventsByDay[dayTimestamp].push(calendarEvent);\n } else {\n eventsByDay[dayTimestamp] = [calendarEvent];\n }\n });\n\n Object.keys(eventsByDay).forEach(function(dayTimestamp) {\n var events = eventsByDay[dayTimestamp];\n templateContext.eventsbyday.push({\n past: dayTimestamp < midnight,\n dayTimestamp: dayTimestamp,\n events: events\n });\n });\n\n return templateContext;\n };\n\n /**\n * Render the HTML for the given calendar events.\n *\n * @param {array} calendarEvents A list of calendar events\n * @param {Number} midnight A timestamp representing midnight for the user\n * @return {promise} Resolved with HTML and JS strings.\n */\n var render = function(calendarEvents, midnight) {\n var templateContext = buildTemplateContext(calendarEvents, midnight);\n var templateName = TEMPLATES.EVENT_LIST_CONTENT;\n\n return Templates.render(templateName, templateContext);\n };\n\n /**\n * Retrieve a list of calendar events from the server for the given\n * constraints.\n *\n * @param {Number} midnight The user's midnight time in unix timestamp.\n * @param {Number} limit Limit the result set to this number of items\n * @param {Number} daysOffset How many days (from midnight) to offset the results from\n * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to\n * @param {int|false} lastId The ID of the last seen event (if any)\n * @param {int|undefined} courseId Course ID to restrict events to\n * @param {string|undefined} searchValue Search value\n * @return {Promise} A jquery promise\n */\n var load = function(midnight, limit, daysOffset, daysLimit, lastId, courseId, searchValue) {\n var startTime = midnight + (daysOffset * SECONDS_IN_DAY);\n var endTime = daysLimit != undefined ? midnight + (daysLimit * SECONDS_IN_DAY) : false;\n\n var args = {\n starttime: startTime,\n limit: limit,\n };\n\n if (lastId) {\n args.aftereventid = lastId;\n }\n\n if (endTime) {\n args.endtime = endTime;\n }\n\n if (searchValue) {\n args.searchvalue = searchValue;\n }\n\n if (courseId) {\n // If we have a course id then we only want events from that course.\n args.courseid = courseId;\n return CalendarEventsRepository.queryByCourse(args);\n } else {\n // Otherwise we want events from any course.\n return CalendarEventsRepository.queryByTime(args);\n }\n };\n\n /**\n * Handle a single page request from the paged content. Uses the given page data to request\n * the events from the server.\n *\n * Checks the given preloadedPages before sending a request to the server to make sure we\n * don't load data unnecessarily.\n *\n * @param {object} pageData A single page data (see core/paged_content_pages for more info).\n * @param {object} actions Paged content actions (see core/paged_content_pages for more info).\n * @param {Number} midnight The user's midnight time in unix timestamp.\n * @param {object} lastIds The last event ID for each loaded page. Page number is key, id is value.\n * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.\n * @param {int|undefined} courseId Course ID to restrict events to\n * @param {Number} daysOffset How many days (from midnight) to offset the results from\n * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to\n * @param {string|undefined} searchValue Search value\n * @return {object} jQuery promise resolved with calendar events.\n */\n var loadEventsFromPageData = function(\n pageData,\n actions,\n midnight,\n lastIds,\n preloadedPages,\n courseId,\n daysOffset,\n daysLimit,\n searchValue\n ) {\n var pageNumber = pageData.pageNumber;\n var limit = pageData.limit;\n var lastPageNumber = pageNumber;\n\n // This is here to protect us if, for some reason, the pages\n // are loaded out of order somehow and we don't have a reference\n // to the previous page. In that case, scan back to find the most\n // recent page we've seen.\n while (!lastIds.hasOwnProperty(lastPageNumber)) {\n lastPageNumber--;\n }\n // Use the last id of the most recent page.\n var lastId = lastIds[lastPageNumber];\n var eventsPromise = null;\n\n if (preloadedPages && preloadedPages.hasOwnProperty(pageNumber)) {\n // This page has been preloaded so use that rather than load the values\n // again.\n eventsPromise = preloadedPages[pageNumber];\n } else {\n // Load one more than the given limit so that we can tell if there\n // is more content to load after this.\n eventsPromise = load(midnight, limit + 1, daysOffset, daysLimit, lastId, courseId, searchValue);\n }\n\n return eventsPromise.then(function(result) {\n if (!result.events.length) {\n // If we didn't get any events back then tell the paged content\n // that we're done loading.\n actions.allItemsLoaded(pageNumber);\n return [];\n }\n\n var calendarEvents = result.events.filter(function(event) {\n if (event.eventtype == \"open\" || event.eventtype == \"opensubmission\") {\n var dayTimestamp = UserDate.getUserMidnightForTimestamp(event.timesort, midnight);\n return dayTimestamp > midnight;\n }\n return true;\n });\n // We expect to receive limit + 1 events back from the server.\n // Any less means there are no more events to load.\n var loadedAll = calendarEvents.length <= limit;\n\n if (loadedAll) {\n // Tell the pagination that everything is loaded.\n actions.allItemsLoaded(pageNumber);\n } else {\n // Remove the last element from the array because it isn't\n // needed in this result set.\n calendarEvents.pop();\n }\n\n return calendarEvents;\n });\n };\n\n /**\n * Use the paged content factory to create a paged content element for showing\n * the event list. We only provide a page limit to the factory because we don't\n * know exactly how many pages we'll need. This creates a paging bar with just\n * next/previous buttons.\n *\n * This function specifies the callback for loading the event data that the user\n * is requesting.\n *\n * @param {int|array} pageLimit A single limit or list of limits as options for the paged content\n * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.\n * @param {Number} midnight The user's midnight time in unix timestamp.\n * @param {object} firstLoad A jQuery promise to be resolved after the first set of data is loaded.\n * @param {int|undefined} courseId Course ID to restrict events to\n * @param {Number} daysOffset How many days (from midnight) to offset the results from\n * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to\n * @param {string} paginationAriaLabel String to set as the aria label for the pagination bar.\n * @param {object} additionalConfig Additional config options to pass to pagedContentFactory\n * @param {string|undefined} searchValue Search value\n * @return {object} jQuery promise.\n */\n var createPagedContent = function(\n pageLimit,\n preloadedPages,\n midnight,\n firstLoad,\n courseId,\n daysOffset,\n daysLimit,\n paginationAriaLabel,\n additionalConfig,\n searchValue\n ) {\n // Remember the last event id we loaded on each page because we can't\n // use the offset value since the backend can skip events if the user doesn't\n // have the capability to see them. Instead we load the next page of events\n // based on the last seen event id.\n var lastIds = {'1': 0};\n var hasContent = false;\n var config = $.extend({}, DEFAULT_PAGED_CONTENT_CONFIG, additionalConfig);\n\n return Str.get_string(\n 'ariaeventlistpagelimit',\n 'block_timeline',\n $.isArray(pageLimit) ? pageLimit[0].value : pageLimit\n )\n .then(function(string) {\n config.ariaLabels.itemsperpage = string;\n config.ariaLabels.paginationnav = paginationAriaLabel;\n return string;\n })\n .then(function() {\n return PagedContentFactory.createWithLimit(\n pageLimit,\n function(pagesData, actions) {\n var promises = [];\n\n pagesData.forEach(function(pageData) {\n var pageNumber = pageData.pageNumber;\n // Load the page data.\n var pagePromise = loadEventsFromPageData(\n pageData,\n actions,\n midnight,\n lastIds,\n preloadedPages,\n courseId,\n daysOffset,\n daysLimit,\n searchValue\n ).then(function(calendarEvents) {\n if (calendarEvents.length) {\n // Remember that we've loaded content.\n hasContent = true;\n // Remember the last id we've seen.\n var lastEventId = calendarEvents[calendarEvents.length - 1].id;\n // Record the id that the next page will need to start from.\n lastIds[pageNumber + 1] = lastEventId;\n // Get the HTML and JS for these calendar events.\n return render(calendarEvents, midnight);\n } else {\n return calendarEvents;\n }\n })\n .catch(Notification.exception);\n\n promises.push(pagePromise);\n });\n\n $.when.apply($, promises).then(function() {\n // Tell the calling code that the first page has been loaded\n // and whether it contains any content.\n firstLoad.resolve(hasContent);\n return;\n })\n .catch(function() {\n firstLoad.resolve(hasContent);\n });\n\n return promises;\n },\n config\n );\n });\n };\n\n /**\n * Create a paged content region for the calendar events in the given root element.\n * The content of the root element are replaced with a new paged content section\n * each time this function is called.\n *\n * This function will be called each time the offset or limit values are changed to\n * reload the event list region.\n *\n * @param {object} root The event list container element\n * @param {int|array} pageLimit A single limit or list of limits as options for the paged content\n * @param {object} preloadedPages An object of preloaded page data. Page number as key, data promise as value.\n * @param {string} paginationAriaLabel String to set as the aria label for the pagination bar.\n * @param {object} additionalConfig Additional config options to pass to pagedContentFactory\n */\n var init = function(root, pageLimit, preloadedPages, paginationAriaLabel, additionalConfig = {}) {\n root = $(root);\n\n courseview = !!additionalConfig.courseview;\n\n // Create a promise that will be resolved once the first set of page\n // data has been loaded. This ensures that the loading placeholder isn't\n // hidden until we have all of the data back to prevent the page elements\n // jumping around.\n var firstLoad = $.Deferred();\n var eventListContent = root.find(SELECTORS.EVENT_LIST_CONTENT);\n var loadingPlaceholder = root.find(SELECTORS.EVENT_LIST_LOADING_PLACEHOLDER);\n var courseId = root.attr('data-course-id');\n var daysOffset = parseInt(root.attr('data-days-offset'), 10);\n var daysLimit = root.attr('data-days-limit');\n var midnight = parseInt(root.attr('data-midnight'), 10);\n const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val();\n\n // Make sure the content area and loading placeholder is visible.\n // This is because the init function can be called to re-initialise\n // an existing event list area.\n emptyContent(root);\n showContent(root);\n loadingPlaceholder.removeClass('hidden');\n\n // Days limit isn't mandatory.\n if (daysLimit != undefined) {\n daysLimit = parseInt(daysLimit, 10);\n }\n\n // Created the paged content element.\n return createPagedContent(pageLimit, preloadedPages, midnight, firstLoad, courseId, daysOffset, daysLimit,\n paginationAriaLabel, additionalConfig, searchValue)\n .then(function(html, js) {\n html = $(html);\n // Hide the content for now.\n html.addClass('hidden');\n // Replace existing elements with the newly created paged content.\n // If we're reinitialising an existing event list this will replace\n // the old event list (including removing any event handlers).\n Templates.replaceNodeContents(eventListContent, html, js);\n\n firstLoad.then(function(hasContent) {\n // Prevent changing page elements too much by only showing the content\n // once we've loaded some data for the first time. This allows our\n // fancy loading placeholder to shine.\n html.removeClass('hidden');\n loadingPlaceholder.addClass('hidden');\n\n if (!hasContent) {\n // If we didn't get any data then show the empty data message.\n hideContent(root);\n }\n\n return hasContent;\n })\n .catch(function() {\n return false;\n });\n\n return html;\n })\n .catch(Notification.exception);\n };\n\n return {\n init: init,\n rootSelector: SELECTORS.ROOT,\n };\n});\n"],"file":"event_list.min.js"} \ No newline at end of file diff --git a/blocks/timeline/amd/build/view_courses.min.js b/blocks/timeline/amd/build/view_courses.min.js index 0e8c4092b47..0b50026ad14 100644 --- a/blocks/timeline/amd/build/view_courses.min.js +++ b/blocks/timeline/amd/build/view_courses.min.js @@ -1,2 +1,2 @@ -define ("block_timeline/view_courses",["jquery","core/notification","core/custom_interaction_events","core/str","core/templates","block_timeline/event_list","core_course/repository","block_timeline/calendar_events_repository"],function(a,b,c,d,e,f,g,h){var i={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\"]",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"},j={COURSE_ITEMS:"block_timeline/course-items",LOADING_ICON:"core/loading"},k=5,l=86400,m=function(a){a.find(i.COURSE_ITEMS_LOADING_PLACEHOLDER).addClass("hidden")},n=function(a){a.find(i.MORE_COURSES_BUTTON_CONTAINER).addClass("hidden")},o=function(a){a.find(i.MORE_COURSES_BUTTON_CONTAINER).removeClass("hidden")},p=function(a){var b=a.find(i.MORE_COURSES_BUTTON);b.prop("disabled",!0);e.render(j.LOADING_ICON,{}).then(function(a){b.append(a);return a}).catch(function(){return!1})},q=function(a){var b=a.find(i.MORE_COURSES_BUTTON);b.prop("disabled",!1);b.find(i.LOADING_ICON).remove()},r=function(a){a.find(i.NO_COURSES_EMPTY_MESSAGE).removeClass("hidden")},s=function(a,b){var c=a.find(i.COURSES_LIST);e.appendNodeContents(c,b,"")},t=function(a){return 0b},G=function(a,b,c){var d=a.map(function(a){return a.id});return C(d,b,k+1,c)},H=function(a,b,c,d,f){return e.render(j.COURSE_ITEMS,{courses:a,midnight:c,hasdaysoffset:!0,hasdayslimit:f!=void 0,daysoffset:d,dayslimit:f,nodayslimit:f==void 0}).then(function(a){m(b);if(a){s(b,a)}else{if(!t(b)){r(b)}}return a}).then(function(c){if(a.length<2){n(b)}else{o(b)}return c}).catch(function(){m(b)})},I=function(c){var e=u(c),h=w(c);return g.getEnrolledCoursesByTimelineClassification("inprogress",h,e,"fullname asc").then(function(b){var e=Date.now(),g=b.courses,h=b.nextoffset,i=x(c),j=y(c),l=z(c),m=A(c),n=B(c);v(c,h);var o=G(g,m,n),p=H(g,c,l,i,j);return a.when(o,p).then(function(b){if(F(c,e)){return b}g.forEach(function(e){var g=e.id,h=[],i=c.find("[data-region=\"course-events-container\"][data-course-id=\""+g+"\"]"),j=i.find(f.rootSelector),l=b.groupedbycourse.filter(function(a){return a.courseid==g});if(l.length){h=l[0].events}var m=a.Deferred().resolve({events:h}).promise();d.get_string("ariaeventlistpaginationnavcourses","block_timeline",e.fullnamedisplay).then(function(a){f.init(j,k,{1:m},a);return a}).catch(function(){f.init(j,k,{1:m})})});return b})}).catch(b.exception)},J=function(c){var e=Date.now(),g=A(c),h=B(c),j=c.find(i.COURSE_EVENTS_CONTAINER),l=j.map(function(){return a(this).attr("data-course-id")}).get();E(c,e);return C(l,g,k+1,h).then(function(b){if(F(c,e)){return b}j.each(function(c,e){e=a(e);var g=e.attr("data-course-id"),h=e.find(i.COURSE_NAME).text(),j=e.find(f.rootSelector),l=a.Deferred(),m=[],n=b.groupedbycourse.filter(function(a){return a.courseid==g});if(n.length){m=n[0].events}l.resolve({events:m});var o={courseview:!0};d.get_string("ariaeventlistpaginationnavcourses","block_timeline",h).then(function(a){f.init(j,k,{1:l.promise()},a,o);return a}).catch(function(){f.init(j,k,{1:l.promise()},void 0,o)})});return b}).catch(b.exception)},K=function(a){c.define(a,[c.events.activate]);a.on(c.events.activate,i.MORE_COURSES_BUTTON,function(b,c){p(a);I(a).then(function(){q(a)}).catch(function(){q(a)});if(c){c.originalEvent.preventDefault();c.originalEvent.stopPropagation()}b.stopPropagation()})},L=function(a){if(!a.attr("data-seen")){if(t(a)){J(a)}else{I(a)}a.attr("data-seen",!0)}};return{init:function init(b){b=a(b);E(b,Date.now());if(b.hasClass("active")){I(b);b.attr("data-seen",!0)}K(b)},reset:function reset(a){a.removeAttr("data-seen");if(a.hasClass("active")){L(a)}},shown:L}}); +define ("block_timeline/view_courses",["jquery","core/notification","core/custom_interaction_events","core/str","core/templates","block_timeline/event_list","core_course/repository","block_timeline/calendar_events_repository"],function(a,b,c,d,e,f,g,h){var i={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\"]",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-region=\"search-input\"]"},j={COURSE_ITEMS:"block_timeline/course-items",LOADING_ICON:"core/loading"},k=5,l=86400,m=function(a){a.find(i.COURSE_ITEMS_LOADING_PLACEHOLDER).addClass("hidden")},n=function(a){a.find(i.MORE_COURSES_BUTTON_CONTAINER).addClass("hidden")},o=function(a){a.find(i.MORE_COURSES_BUTTON_CONTAINER).removeClass("hidden")},p=function(a){var b=a.find(i.MORE_COURSES_BUTTON);b.prop("disabled",!0);e.render(j.LOADING_ICON,{}).then(function(a){b.append(a);return a}).catch(function(){return!1})},q=function(a){var b=a.find(i.MORE_COURSES_BUTTON);b.prop("disabled",!1);b.find(i.LOADING_ICON).remove()},r=function(a){a.find(i.NO_COURSES_EMPTY_MESSAGE).removeClass("hidden")},s=function(a,b){var c=a.find(i.COURSES_LIST);e.appendNodeContents(c,b,"")},t=function(a){return 0b},G=function(a,b,c,d){var e=a.map(function(a){return a.id});return C(e,b,k+1,c,d)},H=function(a,b,c,d,f){return e.render(j.COURSE_ITEMS,{courses:a,midnight:c,hasdaysoffset:!0,hasdayslimit:f!=void 0,daysoffset:d,dayslimit:f,nodayslimit:f==void 0}).then(function(a){m(b);if(a){s(b,a)}else{if(!t(b)){r(b)}}return a}).then(function(c){if(a.length<2){n(b)}else{o(b)}return c}).catch(function(){m(b)})},I=function(c){var e=u(c),h=w(c);return g.getEnrolledCoursesByTimelineClassification("inprogress",h,e,"fullname asc").then(function(b){var e=Date.now(),g=b.courses,h=b.nextoffset,j=x(c),l=y(c),m=z(c),n=A(c),o=B(c),p=c.closest(i.TIMELINE_BLOCK).find(i.TIMELINE_SEARCH).val();v(c,h);var q=G(g,n,o,p),r=H(g,c,m,j,l);return a.when(q,r).then(function(b){if(F(c,e)){return b}g.forEach(function(e){var g=e.id,h=[],i=c.find("[data-region=\"course-events-container\"][data-course-id=\""+g+"\"]"),j=i.find(f.rootSelector),l=b.groupedbycourse.filter(function(a){return a.courseid==g});if(l.length){h=l[0].events}var m=a.Deferred().resolve({events:h}).promise();d.get_string("ariaeventlistpaginationnavcourses","block_timeline",e.fullnamedisplay).then(function(a){f.init(j,k,{1:m},a);return a}).catch(function(){f.init(j,k,{1:m})})});return b})}).catch(b.exception)},J=function(c){var e=Date.now(),g=A(c),h=B(c),j=c.find(i.COURSE_EVENTS_CONTAINER),l=j.map(function(){return a(this).attr("data-course-id")}).get(),m=c.closest(i.TIMELINE_BLOCK).find(i.TIMELINE_SEARCH).val();E(c,e);return C(l,g,k+1,h,m).then(function(b){if(F(c,e)){return b}j.each(function(c,e){e=a(e);var g=e.attr("data-course-id"),h=e.find(i.COURSE_NAME).text(),j=e.find(f.rootSelector),l=a.Deferred(),m=[],n=b.groupedbycourse.filter(function(a){return a.courseid==g});if(n.length){m=n[0].events}l.resolve({events:m});var o={courseview:!0};d.get_string("ariaeventlistpaginationnavcourses","block_timeline",h).then(function(a){f.init(j,k,{1:l.promise()},a,o);return a}).catch(function(){f.init(j,k,{1:l.promise()},void 0,o)})});return b}).catch(b.exception)},K=function(a){c.define(a,[c.events.activate]);a.on(c.events.activate,i.MORE_COURSES_BUTTON,function(b,c){p(a);I(a).then(function(){q(a)}).catch(function(){q(a)});if(c){c.originalEvent.preventDefault();c.originalEvent.stopPropagation()}b.stopPropagation()})},L=function(a){if(!a.attr("data-seen")){if(t(a)){J(a)}else{I(a)}a.attr("data-seen",!0)}};return{init:function init(b){b=a(b);E(b,Date.now());if(b.hasClass("active")){I(b);b.attr("data-seen",!0)}K(b)},reset:function reset(a){a.removeAttr("data-seen");if(a.hasClass("active")){L(a)}},shown:L}}); //# sourceMappingURL=view_courses.min.js.map diff --git a/blocks/timeline/amd/build/view_courses.min.js.map b/blocks/timeline/amd/build/view_courses.min.js.map index 959b6f0de97..7f6dc904c6c 100644 --- a/blocks/timeline/amd/build/view_courses.min.js.map +++ b/blocks/timeline/amd/build/view_courses.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/view_courses.js"],"names":["define","$","Notification","CustomEvents","Str","Templates","EventList","CourseRepository","EventsRepository","SELECTORS","MORE_COURSES_BUTTON","MORE_COURSES_BUTTON_CONTAINER","NO_COURSES_EMPTY_MESSAGE","COURSES_LIST","COURSE_ITEMS_LOADING_PLACEHOLDER","COURSE_EVENTS_CONTAINER","COURSE_NAME","LOADING_ICON","TEMPLATES","COURSE_ITEMS","COURSE_EVENT_LIMIT","SECONDS_IN_DAY","hideLoadingPlaceholder","root","find","addClass","hideMoreCoursesButton","showMoreCoursesButton","removeClass","enableMoreCoursesButtonLoading","button","prop","render","then","html","append","catch","disableMoreCoursesButtonLoading","remove","showNoCoursesEmptyMessage","renderCourseItemsHTML","container","appendNodeContents","hasLoadedCourses","length","getOffset","parseInt","attr","setOffset","offset","getLimit","getDaysOffset","getDaysLimit","daysLimit","getMidnight","getStartTime","midnight","daysOffset","getEndTime","getEventsForCourseIds","courseIds","startTime","limit","endTime","args","courseids","starttime","endtime","queryByCourses","getEventReloadTime","data","setEventReloadTime","time","hasReloadedEventsSince","loadEventsForCourses","courses","map","course","id","updateDisplayFromCourses","hasdaysoffset","hasdayslimit","daysoffset","dayslimit","nodayslimit","loadMoreCourses","getEnrolledCoursesByTimelineClassification","result","startEventLoadingTime","Date","now","nextOffset","nextoffset","eventsPromise","renderPromise","when","eventsByCourse","forEach","courseId","events","courseEventsContainer","eventListRoot","rootSelector","courseGroups","groupedbycourse","filter","group","courseid","pageOnePreload","Deferred","resolve","promise","get_string","fullnamedisplay","string","init","exception","reloadCourseEvents","startReloadTime","courseEventsContainers","get","each","index","courseName","text","eventListContainer","pageDeferred","additionalConfig","courseview","registerEventListeners","activate","on","e","originalEvent","preventDefault","stopPropagation","shown","hasClass","reset","removeAttr"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Manage the timeline courses view for the timeline block.\n *\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(\n[\n 'jquery',\n 'core/notification',\n 'core/custom_interaction_events',\n 'core/str',\n 'core/templates',\n 'block_timeline/event_list',\n 'core_course/repository',\n 'block_timeline/calendar_events_repository'\n],\nfunction(\n $,\n Notification,\n CustomEvents,\n Str,\n Templates,\n EventList,\n CourseRepository,\n EventsRepository\n) {\n\n var SELECTORS = {\n MORE_COURSES_BUTTON: '[data-action=\"more-courses\"]',\n MORE_COURSES_BUTTON_CONTAINER: '[data-region=\"more-courses-button-container\"]',\n NO_COURSES_EMPTY_MESSAGE: '[data-region=\"no-courses-empty-message\"]',\n COURSES_LIST: '[data-region=\"courses-list\"]',\n COURSE_ITEMS_LOADING_PLACEHOLDER: '[data-region=\"course-items-loading-placeholder\"]',\n COURSE_EVENTS_CONTAINER: '[data-region=\"course-events-container\"]',\n COURSE_NAME: '[data-region=\"course-name\"]',\n LOADING_ICON: '.loading-icon'\n };\n\n var TEMPLATES = {\n COURSE_ITEMS: 'block_timeline/course-items',\n LOADING_ICON: 'core/loading'\n };\n\n var COURSE_CLASSIFICATION = 'inprogress';\n var COURSE_SORT = 'fullname asc';\n var COURSE_EVENT_LIMIT = 5;\n var COURSE_LIMIT = 2;\n var SECONDS_IN_DAY = 60 * 60 * 24;\n\n /**\n * Hide the loading placeholder elements.\n *\n * @param {object} root The rool element.\n */\n var hideLoadingPlaceholder = function(root) {\n root.find(SELECTORS.COURSE_ITEMS_LOADING_PLACEHOLDER).addClass('hidden');\n };\n\n /**\n * Hide the \"more courses\" button.\n *\n * @param {object} root The rool element.\n */\n var hideMoreCoursesButton = function(root) {\n root.find(SELECTORS.MORE_COURSES_BUTTON_CONTAINER).addClass('hidden');\n };\n\n /**\n * Show the \"more courses\" button.\n *\n * @param {object} root The rool element.\n */\n var showMoreCoursesButton = function(root) {\n root.find(SELECTORS.MORE_COURSES_BUTTON_CONTAINER).removeClass('hidden');\n };\n\n /**\n * Disable the \"more courses\" button and show the loading spinner.\n *\n * @param {object} root The rool element.\n */\n var enableMoreCoursesButtonLoading = function(root) {\n var button = root.find(SELECTORS.MORE_COURSES_BUTTON);\n button.prop('disabled', true);\n Templates.render(TEMPLATES.LOADING_ICON, {})\n .then(function(html) {\n button.append(html);\n return html;\n })\n .catch(function() {\n // It's not important if this false so just do so silently.\n return false;\n });\n };\n\n /**\n * Enable the \"more courses\" button and remove the loading spinner.\n *\n * @param {object} root The rool element.\n */\n var disableMoreCoursesButtonLoading = function(root) {\n var button = root.find(SELECTORS.MORE_COURSES_BUTTON);\n button.prop('disabled', false);\n button.find(SELECTORS.LOADING_ICON).remove();\n };\n\n /**\n * Display the message for when there are no courses available.\n *\n * @param {object} root The rool element.\n */\n var showNoCoursesEmptyMessage = function(root) {\n root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).removeClass('hidden');\n };\n\n /**\n * Render the course items HTML to the page.\n *\n * @param {object} root The rool element.\n * @param {string} html The course items HTML to render.\n */\n var renderCourseItemsHTML = function(root, html) {\n var container = root.find(SELECTORS.COURSES_LIST);\n Templates.appendNodeContents(container, html, '');\n };\n\n /**\n * Check if any courses have been loaded.\n *\n * @param {object} root The rool element.\n * @return {bool}\n */\n var hasLoadedCourses = function(root) {\n return root.find(SELECTORS.COURSE_EVENTS_CONTAINER).length > 0;\n };\n\n /**\n * Return the offset value for fetching courses.\n *\n * @param {object} root The rool element.\n * @return {Number}\n */\n var getOffset = function(root) {\n return parseInt(root.attr('data-offset'), 10);\n };\n\n /**\n * Set the offset value for fetching courses.\n *\n * @param {object} root The rool element.\n * @param {Number} offset Offset value.\n */\n var setOffset = function(root, offset) {\n root.attr('data-offset', offset);\n };\n\n /**\n * Return the limit value for fetching courses.\n *\n * @param {object} root The rool element.\n * @return {Number}\n */\n var getLimit = function(root) {\n return parseInt(root.attr('data-limit'), 10);\n };\n\n /**\n * Return the days offset value for fetching events.\n *\n * @param {object} root The rool element.\n * @return {Number}\n */\n var getDaysOffset = function(root) {\n return parseInt(root.attr('data-days-offset'), 10);\n };\n\n /**\n * Return the days limit value for fetching events. The days\n * limit is optional so undefined will be returned if it isn't\n * set.\n *\n * @param {object} root The rool element.\n * @return {int|undefined}\n */\n var getDaysLimit = function(root) {\n var daysLimit = root.attr('data-days-limit');\n return daysLimit != undefined ? parseInt(daysLimit, 10) : undefined;\n };\n\n /**\n * Return the timestamp for the user's midnight.\n *\n * @param {object} root The rool element.\n * @return {Number}\n */\n var getMidnight = function(root) {\n return parseInt(root.attr('data-midnight'), 10);\n };\n\n /**\n * Return the start time for fetching events. This is calculated\n * based on the user's midnight value so that timezones are\n * preserved.\n *\n * @param {object} root The rool element.\n * @return {Number}\n */\n var getStartTime = function(root) {\n var midnight = getMidnight(root);\n var daysOffset = getDaysOffset(root);\n return midnight + (daysOffset * SECONDS_IN_DAY);\n };\n\n /**\n * Return the end time for fetching events. This is calculated\n * based on the user's midnight value so that timezones are\n * preserved.\n *\n * @param {object} root The rool element.\n * @return {Number}\n */\n var getEndTime = function(root) {\n var midnight = getMidnight(root);\n var daysLimit = getDaysLimit(root);\n return daysLimit != undefined ? midnight + (daysLimit * SECONDS_IN_DAY) : false;\n };\n\n /**\n * Get a list of events for the given course ids. Returns a promise that will\n * be resolved with the events.\n *\n * @param {array} courseIds The list of course ids to fetch events for.\n * @param {Number} startTime Timestamp to fetch events from.\n * @param {Number} limit Limit to the number of events (this applies per course, not total)\n * @param {Number} endTime Timestamp to fetch events to.\n * @return {object} jQuery promise.\n */\n var getEventsForCourseIds = function(courseIds, startTime, limit, endTime) {\n var args = {\n courseids: courseIds,\n starttime: startTime,\n limit: limit\n };\n\n if (endTime) {\n args.endtime = endTime;\n }\n\n return EventsRepository.queryByCourses(args);\n };\n\n /**\n * Get the last time the events were reloaded.\n *\n * @param {object} root The rool element.\n * @return {Number}\n */\n var getEventReloadTime = function(root) {\n return root.data('last-event-load-time');\n };\n\n /**\n * Set the last time the events were reloaded.\n *\n * @param {object} root The rool element.\n * @param {Number} time Timestamp in milliseconds.\n */\n var setEventReloadTime = function(root, time) {\n root.data('last-event-load-time', time);\n };\n\n /**\n * Check if events have begun reloading since the given\n * time.\n *\n * @param {object} root The rool element.\n * @param {Number} time Timestamp in milliseconds.\n * @return {bool}\n */\n var hasReloadedEventsSince = function(root, time) {\n return getEventReloadTime(root) > time;\n };\n\n /**\n * Send a request to the server to load the events for the courses.\n *\n * @param {array} courses List of course objects.\n * @param {Number} startTime Timestamp to load events after.\n * @param {int|undefined} endTime Timestamp to load events up until.\n * @return {object} jQuery promise resolved with the events.\n */\n var loadEventsForCourses = function(courses, startTime, endTime) {\n var courseIds = courses.map(function(course) {\n return course.id;\n });\n\n return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime);\n };\n\n /**\n * Render the courses in the DOM once the server has returned the courses.\n *\n * @param {array} courses List of course objects.\n * @param {object} root The root element\n * @param {Number} midnight The midnight timestamp in the user's timezone.\n * @param {Number} daysOffset Number of days from today to offset the events.\n * @param {Number} daysLimit Number of days from today to limit the events to.\n * @return {object} jQuery promise resolved after rendering is complete.\n */\n var updateDisplayFromCourses = function(courses, root, midnight, daysOffset, daysLimit) {\n // Render the courses template.\n return Templates.render(TEMPLATES.COURSE_ITEMS, {\n courses: courses,\n midnight: midnight,\n hasdaysoffset: true,\n hasdayslimit: daysLimit != undefined,\n daysoffset: daysOffset,\n dayslimit: daysLimit,\n nodayslimit: daysLimit == undefined,\n }).then(function(html) {\n hideLoadingPlaceholder(root);\n\n if (html) {\n // Template rendering is complete and we have the HTML so we can\n // add it to the DOM.\n renderCourseItemsHTML(root, html);\n } else {\n if (!hasLoadedCourses(root)) {\n // There were no courses to render so show the empty placeholder\n // message for the user to tell them.\n showNoCoursesEmptyMessage(root);\n }\n }\n\n return html;\n })\n .then(function(html) {\n if (courses.length < COURSE_LIMIT) {\n // We know there aren't any more courses because we got back less\n // than we asked for so hide the button to request more.\n hideMoreCoursesButton(root);\n } else {\n // Make sure the button is visible if there are more courses to load.\n showMoreCoursesButton(root);\n }\n\n return html;\n })\n .catch(function() {\n hideLoadingPlaceholder(root);\n });\n };\n\n /**\n * Find all of the visible course blocks and initialise the event\n * list module to being loading the events for the course block.\n *\n * @param {object} root The root element for the timeline courses view.\n * @return {object} jQuery promise resolved with courses and events.\n */\n var loadMoreCourses = function(root) {\n var offset = getOffset(root);\n var limit = getLimit(root);\n\n // Start loading the next set of courses.\n return CourseRepository.getEnrolledCoursesByTimelineClassification(\n COURSE_CLASSIFICATION,\n limit,\n offset,\n COURSE_SORT\n ).then(function(result) {\n var startEventLoadingTime = Date.now();\n var courses = result.courses;\n var nextOffset = result.nextoffset;\n var daysOffset = getDaysOffset(root);\n var daysLimit = getDaysLimit(root);\n var midnight = getMidnight(root);\n var startTime = getStartTime(root);\n var endTime = getEndTime(root);\n\n // Record the next offset if we want to request more courses.\n setOffset(root, nextOffset);\n // Load the events for these courses.\n var eventsPromise = loadEventsForCourses(courses, startTime, endTime);\n // Render the courses in the DOM.\n var renderPromise = updateDisplayFromCourses(courses, root, midnight, daysOffset, daysLimit);\n\n return $.when(eventsPromise, renderPromise)\n .then(function(eventsByCourse) {\n if (hasReloadedEventsSince(root, startEventLoadingTime)) {\n // All of the events are being reloaded so ignore our results.\n return eventsByCourse;\n }\n\n // When we've got all of the courses and events we can render the events in the\n // correct course event list.\n courses.forEach(function(course) {\n var courseId = course.id;\n var events = [];\n var containerSelector = '[data-region=\"course-events-container\"][data-course-id=\"' + courseId + '\"]';\n var courseEventsContainer = root.find(containerSelector);\n var eventListRoot = courseEventsContainer.find(EventList.rootSelector);\n var courseGroups = eventsByCourse.groupedbycourse.filter(function(group) {\n return group.courseid == courseId;\n });\n\n if (courseGroups.length) {\n // Get the events for this course.\n events = courseGroups[0].events;\n }\n\n // Create a preloaded page to pass to the event list because we've already\n // loaded the first page of events.\n var pageOnePreload = $.Deferred().resolve({events: events}).promise();\n // Initialise the event list pagination area for this course.\n Str.get_string('ariaeventlistpaginationnavcourses', 'block_timeline', course.fullnamedisplay)\n .then(function(string) {\n EventList.init(eventListRoot, COURSE_EVENT_LIMIT, {'1': pageOnePreload}, string);\n return string;\n })\n .catch(function() {\n // An error is ok, just render with the default string.\n EventList.init(eventListRoot, COURSE_EVENT_LIMIT, {'1': pageOnePreload});\n });\n });\n\n return eventsByCourse;\n });\n }).catch(Notification.exception);\n };\n\n /**\n * Reload the events for all of the visible courses. These events will be loaded\n * in a single request to the server.\n *\n * @param {object} root The root element.\n * @return {object} jQuery promise resolved with courses and events.\n */\n var reloadCourseEvents = function(root) {\n var startReloadTime = Date.now();\n var startTime = getStartTime(root);\n var endTime = getEndTime(root);\n var courseEventsContainers = root.find(SELECTORS.COURSE_EVENTS_CONTAINER);\n var courseIds = courseEventsContainers.map(function() {\n return $(this).attr('data-course-id');\n }).get();\n\n // Record when we started our request.\n setEventReloadTime(root, startReloadTime);\n\n // Load all of the events for the given courses.\n return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime)\n .then(function(eventsByCourse) {\n if (hasReloadedEventsSince(root, startReloadTime)) {\n // A new reload has begun so ignore our results.\n return eventsByCourse;\n }\n\n courseEventsContainers.each(function(index, container) {\n container = $(container);\n var courseId = container.attr('data-course-id');\n var courseName = container.find(SELECTORS.COURSE_NAME).text();\n var eventListContainer = container.find(EventList.rootSelector);\n var pageDeferred = $.Deferred();\n var events = [];\n var courseGroups = eventsByCourse.groupedbycourse.filter(function(group) {\n return group.courseid == courseId;\n });\n\n if (courseGroups.length) {\n // Get the events just for this course.\n events = courseGroups[0].events;\n }\n\n pageDeferred.resolve({events: events});\n\n var additionalConfig = {courseview: true};\n // Re-initialise the events list with the preloaded events we just got from\n // the server.\n Str.get_string('ariaeventlistpaginationnavcourses', 'block_timeline', courseName)\n .then(function(string) {\n EventList.init(eventListContainer, COURSE_EVENT_LIMIT, {'1': pageDeferred.promise()},\n string, additionalConfig);\n return string;\n })\n .catch(function() {\n // Ignore a failure to load the string. Just render with the default string.\n EventList.init(eventListContainer, COURSE_EVENT_LIMIT, {'1': pageDeferred.promise()},\n undefined, additionalConfig);\n });\n });\n\n return eventsByCourse;\n }).catch(Notification.exception);\n };\n\n /**\n * Add event listeners to load more courses for the courses view.\n *\n * @param {object} root The root element for the timeline courses view.\n */\n var registerEventListeners = function(root) {\n CustomEvents.define(root, [CustomEvents.events.activate]);\n // Show more courses and load their events when the user clicks the \"more courses\"\n // button.\n root.on(CustomEvents.events.activate, SELECTORS.MORE_COURSES_BUTTON, function(e, data) {\n enableMoreCoursesButtonLoading(root);\n loadMoreCourses(root)\n .then(function() {\n disableMoreCoursesButtonLoading(root);\n return;\n })\n .catch(function() {\n disableMoreCoursesButtonLoading(root);\n });\n\n if (data) {\n data.originalEvent.preventDefault();\n data.originalEvent.stopPropagation();\n }\n e.stopPropagation();\n });\n };\n\n /**\n * Initialise the timeline courses view. Begin loading the events\n * if this view is active. Add the relevant event listeners.\n *\n * This function should only be called once per page load because it\n * is adding event listeners to the page.\n *\n * @param {object} root The root element for the timeline courses view.\n */\n var init = function(root) {\n root = $(root);\n\n setEventReloadTime(root, Date.now());\n\n if (root.hasClass('active')) {\n // Only load if this is active otherwise it will be lazy loaded later.\n loadMoreCourses(root);\n root.attr('data-seen', true);\n }\n\n registerEventListeners(root);\n };\n\n /**\n * Reset the element back to it's initial state. Begin loading the events again\n * if this view is active.\n *\n * @param {object} root The root element for the timeline courses view.\n */\n var reset = function(root) {\n root.removeAttr('data-seen');\n if (root.hasClass('active')) {\n shown(root);\n }\n };\n\n /**\n * If this is the first time this view has been displayed then begin loading\n * the events.\n *\n * @param {object} root The root element for the timeline courses view.\n */\n var shown = function(root) {\n if (!root.attr('data-seen')) {\n if (hasLoadedCourses(root)) {\n // This isn't the first time this view is shown so just reload the\n // events for the courses we've already loaded.\n reloadCourseEvents(root);\n } else {\n // We haven't loaded any courses yet so do that now.\n loadMoreCourses(root);\n }\n\n root.attr('data-seen', true);\n }\n };\n\n return {\n init: init,\n reset: reset,\n shown: shown\n };\n});\n"],"file":"view_courses.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/view_courses.js"],"names":["define","$","Notification","CustomEvents","Str","Templates","EventList","CourseRepository","EventsRepository","SELECTORS","MORE_COURSES_BUTTON","MORE_COURSES_BUTTON_CONTAINER","NO_COURSES_EMPTY_MESSAGE","COURSES_LIST","COURSE_ITEMS_LOADING_PLACEHOLDER","COURSE_EVENTS_CONTAINER","COURSE_NAME","LOADING_ICON","TIMELINE_BLOCK","TIMELINE_SEARCH","TEMPLATES","COURSE_ITEMS","COURSE_EVENT_LIMIT","SECONDS_IN_DAY","hideLoadingPlaceholder","root","find","addClass","hideMoreCoursesButton","showMoreCoursesButton","removeClass","enableMoreCoursesButtonLoading","button","prop","render","then","html","append","catch","disableMoreCoursesButtonLoading","remove","showNoCoursesEmptyMessage","renderCourseItemsHTML","container","appendNodeContents","hasLoadedCourses","length","getOffset","parseInt","attr","setOffset","offset","getLimit","getDaysOffset","getDaysLimit","daysLimit","getMidnight","getStartTime","midnight","daysOffset","getEndTime","getEventsForCourseIds","courseIds","startTime","limit","endTime","searchValue","args","courseids","starttime","endtime","searchvalue","queryByCourses","getEventReloadTime","data","setEventReloadTime","time","hasReloadedEventsSince","loadEventsForCourses","courses","map","course","id","updateDisplayFromCourses","hasdaysoffset","hasdayslimit","daysoffset","dayslimit","nodayslimit","loadMoreCourses","getEnrolledCoursesByTimelineClassification","result","startEventLoadingTime","Date","now","nextOffset","nextoffset","closest","val","eventsPromise","renderPromise","when","eventsByCourse","forEach","courseId","events","courseEventsContainer","eventListRoot","rootSelector","courseGroups","groupedbycourse","filter","group","courseid","pageOnePreload","Deferred","resolve","promise","get_string","fullnamedisplay","string","init","exception","reloadCourseEvents","startReloadTime","courseEventsContainers","get","each","index","courseName","text","eventListContainer","pageDeferred","additionalConfig","courseview","registerEventListeners","activate","on","e","originalEvent","preventDefault","stopPropagation","shown","hasClass","reset","removeAttr"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Manage the timeline courses view for the timeline block.\n *\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(\n[\n 'jquery',\n 'core/notification',\n 'core/custom_interaction_events',\n 'core/str',\n 'core/templates',\n 'block_timeline/event_list',\n 'core_course/repository',\n 'block_timeline/calendar_events_repository'\n],\nfunction(\n $,\n Notification,\n CustomEvents,\n Str,\n Templates,\n EventList,\n CourseRepository,\n EventsRepository\n) {\n\n var SELECTORS = {\n MORE_COURSES_BUTTON: '[data-action=\"more-courses\"]',\n MORE_COURSES_BUTTON_CONTAINER: '[data-region=\"more-courses-button-container\"]',\n NO_COURSES_EMPTY_MESSAGE: '[data-region=\"no-courses-empty-message\"]',\n COURSES_LIST: '[data-region=\"courses-list\"]',\n COURSE_ITEMS_LOADING_PLACEHOLDER: '[data-region=\"course-items-loading-placeholder\"]',\n COURSE_EVENTS_CONTAINER: '[data-region=\"course-events-container\"]',\n COURSE_NAME: '[data-region=\"course-name\"]',\n LOADING_ICON: '.loading-icon',\n TIMELINE_BLOCK: '[data-region=\"timeline\"]',\n TIMELINE_SEARCH: '[data-region=\"search-input\"]'\n };\n\n var TEMPLATES = {\n COURSE_ITEMS: 'block_timeline/course-items',\n LOADING_ICON: 'core/loading'\n };\n\n var COURSE_CLASSIFICATION = 'inprogress';\n var COURSE_SORT = 'fullname asc';\n var COURSE_EVENT_LIMIT = 5;\n var COURSE_LIMIT = 2;\n var SECONDS_IN_DAY = 60 * 60 * 24;\n\n /**\n * Hide the loading placeholder elements.\n *\n * @param {object} root The rool element.\n */\n var hideLoadingPlaceholder = function(root) {\n root.find(SELECTORS.COURSE_ITEMS_LOADING_PLACEHOLDER).addClass('hidden');\n };\n\n /**\n * Hide the \"more courses\" button.\n *\n * @param {object} root The rool element.\n */\n var hideMoreCoursesButton = function(root) {\n root.find(SELECTORS.MORE_COURSES_BUTTON_CONTAINER).addClass('hidden');\n };\n\n /**\n * Show the \"more courses\" button.\n *\n * @param {object} root The rool element.\n */\n var showMoreCoursesButton = function(root) {\n root.find(SELECTORS.MORE_COURSES_BUTTON_CONTAINER).removeClass('hidden');\n };\n\n /**\n * Disable the \"more courses\" button and show the loading spinner.\n *\n * @param {object} root The rool element.\n */\n var enableMoreCoursesButtonLoading = function(root) {\n var button = root.find(SELECTORS.MORE_COURSES_BUTTON);\n button.prop('disabled', true);\n Templates.render(TEMPLATES.LOADING_ICON, {})\n .then(function(html) {\n button.append(html);\n return html;\n })\n .catch(function() {\n // It's not important if this false so just do so silently.\n return false;\n });\n };\n\n /**\n * Enable the \"more courses\" button and remove the loading spinner.\n *\n * @param {object} root The rool element.\n */\n var disableMoreCoursesButtonLoading = function(root) {\n var button = root.find(SELECTORS.MORE_COURSES_BUTTON);\n button.prop('disabled', false);\n button.find(SELECTORS.LOADING_ICON).remove();\n };\n\n /**\n * Display the message for when there are no courses available.\n *\n * @param {object} root The rool element.\n */\n var showNoCoursesEmptyMessage = function(root) {\n root.find(SELECTORS.NO_COURSES_EMPTY_MESSAGE).removeClass('hidden');\n };\n\n /**\n * Render the course items HTML to the page.\n *\n * @param {object} root The rool element.\n * @param {string} html The course items HTML to render.\n */\n var renderCourseItemsHTML = function(root, html) {\n var container = root.find(SELECTORS.COURSES_LIST);\n Templates.appendNodeContents(container, html, '');\n };\n\n /**\n * Check if any courses have been loaded.\n *\n * @param {object} root The rool element.\n * @return {bool}\n */\n var hasLoadedCourses = function(root) {\n return root.find(SELECTORS.COURSE_EVENTS_CONTAINER).length > 0;\n };\n\n /**\n * Return the offset value for fetching courses.\n *\n * @param {object} root The rool element.\n * @return {Number}\n */\n var getOffset = function(root) {\n return parseInt(root.attr('data-offset'), 10);\n };\n\n /**\n * Set the offset value for fetching courses.\n *\n * @param {object} root The rool element.\n * @param {Number} offset Offset value.\n */\n var setOffset = function(root, offset) {\n root.attr('data-offset', offset);\n };\n\n /**\n * Return the limit value for fetching courses.\n *\n * @param {object} root The rool element.\n * @return {Number}\n */\n var getLimit = function(root) {\n return parseInt(root.attr('data-limit'), 10);\n };\n\n /**\n * Return the days offset value for fetching events.\n *\n * @param {object} root The rool element.\n * @return {Number}\n */\n var getDaysOffset = function(root) {\n return parseInt(root.attr('data-days-offset'), 10);\n };\n\n /**\n * Return the days limit value for fetching events. The days\n * limit is optional so undefined will be returned if it isn't\n * set.\n *\n * @param {object} root The rool element.\n * @return {int|undefined}\n */\n var getDaysLimit = function(root) {\n var daysLimit = root.attr('data-days-limit');\n return daysLimit != undefined ? parseInt(daysLimit, 10) : undefined;\n };\n\n /**\n * Return the timestamp for the user's midnight.\n *\n * @param {object} root The rool element.\n * @return {Number}\n */\n var getMidnight = function(root) {\n return parseInt(root.attr('data-midnight'), 10);\n };\n\n /**\n * Return the start time for fetching events. This is calculated\n * based on the user's midnight value so that timezones are\n * preserved.\n *\n * @param {object} root The rool element.\n * @return {Number}\n */\n var getStartTime = function(root) {\n var midnight = getMidnight(root);\n var daysOffset = getDaysOffset(root);\n return midnight + (daysOffset * SECONDS_IN_DAY);\n };\n\n /**\n * Return the end time for fetching events. This is calculated\n * based on the user's midnight value so that timezones are\n * preserved.\n *\n * @param {object} root The rool element.\n * @return {Number}\n */\n var getEndTime = function(root) {\n var midnight = getMidnight(root);\n var daysLimit = getDaysLimit(root);\n return daysLimit != undefined ? midnight + (daysLimit * SECONDS_IN_DAY) : false;\n };\n\n /**\n * Get a list of events for the given course ids. Returns a promise that will\n * be resolved with the events.\n *\n * @param {array} courseIds The list of course ids to fetch events for.\n * @param {Number} startTime Timestamp to fetch events from.\n * @param {Number} limit Limit to the number of events (this applies per course, not total)\n * @param {Number} endTime Timestamp to fetch events to.\n * @param {string|undefined} searchValue Search value\n * @return {object} jQuery promise.\n */\n var getEventsForCourseIds = function(courseIds, startTime, limit, endTime, searchValue) {\n var args = {\n courseids: courseIds,\n starttime: startTime,\n limit: limit\n };\n\n if (endTime) {\n args.endtime = endTime;\n }\n\n if (searchValue) {\n args.searchvalue = searchValue;\n }\n\n return EventsRepository.queryByCourses(args);\n };\n\n /**\n * Get the last time the events were reloaded.\n *\n * @param {object} root The rool element.\n * @return {Number}\n */\n var getEventReloadTime = function(root) {\n return root.data('last-event-load-time');\n };\n\n /**\n * Set the last time the events were reloaded.\n *\n * @param {object} root The rool element.\n * @param {Number} time Timestamp in milliseconds.\n */\n var setEventReloadTime = function(root, time) {\n root.data('last-event-load-time', time);\n };\n\n /**\n * Check if events have begun reloading since the given\n * time.\n *\n * @param {object} root The rool element.\n * @param {Number} time Timestamp in milliseconds.\n * @return {bool}\n */\n var hasReloadedEventsSince = function(root, time) {\n return getEventReloadTime(root) > time;\n };\n\n /**\n * Send a request to the server to load the events for the courses.\n *\n * @param {array} courses List of course objects.\n * @param {Number} startTime Timestamp to load events after.\n * @param {int|undefined} endTime Timestamp to load events up until.\n * @param {string|undefined} searchValue Search value\n * @return {object} jQuery promise resolved with the events.\n */\n var loadEventsForCourses = function(courses, startTime, endTime, searchValue) {\n var courseIds = courses.map(function(course) {\n return course.id;\n });\n\n return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime, searchValue);\n };\n\n /**\n * Render the courses in the DOM once the server has returned the courses.\n *\n * @param {array} courses List of course objects.\n * @param {object} root The root element\n * @param {Number} midnight The midnight timestamp in the user's timezone.\n * @param {Number} daysOffset Number of days from today to offset the events.\n * @param {Number} daysLimit Number of days from today to limit the events to.\n * @return {object} jQuery promise resolved after rendering is complete.\n */\n var updateDisplayFromCourses = function(courses, root, midnight, daysOffset, daysLimit) {\n // Render the courses template.\n return Templates.render(TEMPLATES.COURSE_ITEMS, {\n courses: courses,\n midnight: midnight,\n hasdaysoffset: true,\n hasdayslimit: daysLimit != undefined,\n daysoffset: daysOffset,\n dayslimit: daysLimit,\n nodayslimit: daysLimit == undefined,\n }).then(function(html) {\n hideLoadingPlaceholder(root);\n\n if (html) {\n // Template rendering is complete and we have the HTML so we can\n // add it to the DOM.\n renderCourseItemsHTML(root, html);\n } else {\n if (!hasLoadedCourses(root)) {\n // There were no courses to render so show the empty placeholder\n // message for the user to tell them.\n showNoCoursesEmptyMessage(root);\n }\n }\n\n return html;\n })\n .then(function(html) {\n if (courses.length < COURSE_LIMIT) {\n // We know there aren't any more courses because we got back less\n // than we asked for so hide the button to request more.\n hideMoreCoursesButton(root);\n } else {\n // Make sure the button is visible if there are more courses to load.\n showMoreCoursesButton(root);\n }\n\n return html;\n })\n .catch(function() {\n hideLoadingPlaceholder(root);\n });\n };\n\n /**\n * Find all of the visible course blocks and initialise the event\n * list module to being loading the events for the course block.\n *\n * @param {object} root The root element for the timeline courses view.\n * @return {object} jQuery promise resolved with courses and events.\n */\n var loadMoreCourses = function(root) {\n var offset = getOffset(root);\n var limit = getLimit(root);\n\n // Start loading the next set of courses.\n return CourseRepository.getEnrolledCoursesByTimelineClassification(\n COURSE_CLASSIFICATION,\n limit,\n offset,\n COURSE_SORT\n ).then(function(result) {\n var startEventLoadingTime = Date.now();\n var courses = result.courses;\n var nextOffset = result.nextoffset;\n var daysOffset = getDaysOffset(root);\n var daysLimit = getDaysLimit(root);\n var midnight = getMidnight(root);\n var startTime = getStartTime(root);\n var endTime = getEndTime(root);\n const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val();\n\n // Record the next offset if we want to request more courses.\n setOffset(root, nextOffset);\n // Load the events for these courses.\n var eventsPromise = loadEventsForCourses(courses, startTime, endTime, searchValue);\n // Render the courses in the DOM.\n var renderPromise = updateDisplayFromCourses(courses, root, midnight, daysOffset, daysLimit);\n\n return $.when(eventsPromise, renderPromise)\n .then(function(eventsByCourse) {\n if (hasReloadedEventsSince(root, startEventLoadingTime)) {\n // All of the events are being reloaded so ignore our results.\n return eventsByCourse;\n }\n\n // When we've got all of the courses and events we can render the events in the\n // correct course event list.\n courses.forEach(function(course) {\n var courseId = course.id;\n var events = [];\n var containerSelector = '[data-region=\"course-events-container\"][data-course-id=\"' + courseId + '\"]';\n var courseEventsContainer = root.find(containerSelector);\n var eventListRoot = courseEventsContainer.find(EventList.rootSelector);\n var courseGroups = eventsByCourse.groupedbycourse.filter(function(group) {\n return group.courseid == courseId;\n });\n\n if (courseGroups.length) {\n // Get the events for this course.\n events = courseGroups[0].events;\n }\n\n // Create a preloaded page to pass to the event list because we've already\n // loaded the first page of events.\n var pageOnePreload = $.Deferred().resolve({events: events}).promise();\n // Initialise the event list pagination area for this course.\n Str.get_string('ariaeventlistpaginationnavcourses', 'block_timeline', course.fullnamedisplay)\n .then(function(string) {\n EventList.init(eventListRoot, COURSE_EVENT_LIMIT, {'1': pageOnePreload}, string);\n return string;\n })\n .catch(function() {\n // An error is ok, just render with the default string.\n EventList.init(eventListRoot, COURSE_EVENT_LIMIT, {'1': pageOnePreload});\n });\n });\n\n return eventsByCourse;\n });\n }).catch(Notification.exception);\n };\n\n /**\n * Reload the events for all of the visible courses. These events will be loaded\n * in a single request to the server.\n *\n * @param {object} root The root element.\n * @return {object} jQuery promise resolved with courses and events.\n */\n var reloadCourseEvents = function(root) {\n var startReloadTime = Date.now();\n var startTime = getStartTime(root);\n var endTime = getEndTime(root);\n var courseEventsContainers = root.find(SELECTORS.COURSE_EVENTS_CONTAINER);\n var courseIds = courseEventsContainers.map(function() {\n return $(this).attr('data-course-id');\n }).get();\n const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val();\n\n // Record when we started our request.\n setEventReloadTime(root, startReloadTime);\n\n // Load all of the events for the given courses.\n return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime, searchValue)\n .then(function(eventsByCourse) {\n if (hasReloadedEventsSince(root, startReloadTime)) {\n // A new reload has begun so ignore our results.\n return eventsByCourse;\n }\n\n courseEventsContainers.each(function(index, container) {\n container = $(container);\n var courseId = container.attr('data-course-id');\n var courseName = container.find(SELECTORS.COURSE_NAME).text();\n var eventListContainer = container.find(EventList.rootSelector);\n var pageDeferred = $.Deferred();\n var events = [];\n var courseGroups = eventsByCourse.groupedbycourse.filter(function(group) {\n return group.courseid == courseId;\n });\n\n if (courseGroups.length) {\n // Get the events just for this course.\n events = courseGroups[0].events;\n }\n\n pageDeferred.resolve({events: events});\n\n var additionalConfig = {courseview: true};\n // Re-initialise the events list with the preloaded events we just got from\n // the server.\n Str.get_string('ariaeventlistpaginationnavcourses', 'block_timeline', courseName)\n .then(function(string) {\n EventList.init(eventListContainer, COURSE_EVENT_LIMIT, {'1': pageDeferred.promise()},\n string, additionalConfig);\n return string;\n })\n .catch(function() {\n // Ignore a failure to load the string. Just render with the default string.\n EventList.init(eventListContainer, COURSE_EVENT_LIMIT, {'1': pageDeferred.promise()},\n undefined, additionalConfig);\n });\n });\n\n return eventsByCourse;\n }).catch(Notification.exception);\n };\n\n /**\n * Add event listeners to load more courses for the courses view.\n *\n * @param {object} root The root element for the timeline courses view.\n */\n var registerEventListeners = function(root) {\n CustomEvents.define(root, [CustomEvents.events.activate]);\n // Show more courses and load their events when the user clicks the \"more courses\"\n // button.\n root.on(CustomEvents.events.activate, SELECTORS.MORE_COURSES_BUTTON, function(e, data) {\n enableMoreCoursesButtonLoading(root);\n loadMoreCourses(root)\n .then(function() {\n disableMoreCoursesButtonLoading(root);\n return;\n })\n .catch(function() {\n disableMoreCoursesButtonLoading(root);\n });\n\n if (data) {\n data.originalEvent.preventDefault();\n data.originalEvent.stopPropagation();\n }\n e.stopPropagation();\n });\n };\n\n /**\n * Initialise the timeline courses view. Begin loading the events\n * if this view is active. Add the relevant event listeners.\n *\n * This function should only be called once per page load because it\n * is adding event listeners to the page.\n *\n * @param {object} root The root element for the timeline courses view.\n */\n var init = function(root) {\n root = $(root);\n\n setEventReloadTime(root, Date.now());\n\n if (root.hasClass('active')) {\n // Only load if this is active otherwise it will be lazy loaded later.\n loadMoreCourses(root);\n root.attr('data-seen', true);\n }\n\n registerEventListeners(root);\n };\n\n /**\n * Reset the element back to it's initial state. Begin loading the events again\n * if this view is active.\n *\n * @param {object} root The root element for the timeline courses view.\n */\n var reset = function(root) {\n root.removeAttr('data-seen');\n if (root.hasClass('active')) {\n shown(root);\n }\n };\n\n /**\n * If this is the first time this view has been displayed then begin loading\n * the events.\n *\n * @param {object} root The root element for the timeline courses view.\n */\n var shown = function(root) {\n if (!root.attr('data-seen')) {\n if (hasLoadedCourses(root)) {\n // This isn't the first time this view is shown so just reload the\n // events for the courses we've already loaded.\n reloadCourseEvents(root);\n } else {\n // We haven't loaded any courses yet so do that now.\n loadMoreCourses(root);\n }\n\n root.attr('data-seen', true);\n }\n };\n\n return {\n init: init,\n reset: reset,\n shown: shown\n };\n});\n"],"file":"view_courses.min.js"} \ No newline at end of file diff --git a/blocks/timeline/amd/build/view_nav.min.js b/blocks/timeline/amd/build/view_nav.min.js index 0ed7161ac8c..64f3d1f5f49 100644 --- a/blocks/timeline/amd/build/view_nav.min.js +++ b/blocks/timeline/amd/build/view_nav.min.js @@ -1,2 +1,2 @@ -define ("block_timeline/view_nav",["jquery","core/custom_interaction_events","block_timeline/view","core/ajax","core/notification"],function(a,b,c,d,e){var f={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]"},g=function(a,b){d.call([{methodname:"core_user_update_user_preferences",args:{preferences:[{type:a,value:b}]}}])[0].fail(e.exception)},h=function(d,h){var i=d.find(f.TIMELINE_DAY_FILTER);b.define(i,[b.events.activate]);i.on(b.events.activate,f.TIMELINE_DAY_FILTER_OPTION,function(b,e){var i=a(b.currentTarget).data("filtername");g("block_timeline_user_filter_preference",i);var j=a(b.target).closest(f.TIMELINE_DAY_FILTER_OPTION);if("true"==j.attr("aria-current")){return}var k=j.attr("data-from"),l=j.attr("data-to"),m=d.find(f.DATA_DAYS_OFFSET);m.attr("data-days-offset",k);if(l!=void 0){m.attr("data-days-limit",l)}else{m.removeAttr("data-days-limit")}c.reset(h);e.originalEvent.preventDefault()})},i=function(d,h){var i=d.find(f.TIMELINE_VIEW_SELECTOR);i.on("shown shown.bs.tab",function(b){c.shown(h);a(b.target).removeClass("active")});b.define(i,[b.events.activate]);i.on(b.events.activate,"[data-toggle='tab']",function(b){var c=a(b.currentTarget).data("filtername");g("block_timeline_user_sort_preference",c)})};return{init:function init(b,c){b=a(b);h(b,c);i(b,c)}}}); +define ("block_timeline/view_nav",["jquery","core/custom_interaction_events","block_timeline/view","core/ajax","core/notification","core/utils"],function(a,b,c,d,e,f){var g={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-region=\"search-input\"]",TIMELINE_SEARCH_CLEAR_ICON:"[data-region=\"clear-icon\"]",TIMELINE_SEARCH_SEARCH_ICON:"[data-region=\"search-icon\"]"},h=function(a,b){d.call([{methodname:"core_user_update_user_preferences",args:{preferences:[{type:a,value:b}]}}])[0].fail(e.exception)},i=function(d,f){var i=d.find(g.TIMELINE_DAY_FILTER);b.define(i,[b.events.activate]);i.on(b.events.activate,g.TIMELINE_DAY_FILTER_OPTION,function(b,e){var i=a(b.currentTarget).data("filtername");h("block_timeline_user_filter_preference",i);var j=a(b.target).closest(g.TIMELINE_DAY_FILTER_OPTION);if("true"==j.attr("aria-current")){return}var k=j.attr("data-from"),l=j.attr("data-to"),m=d.find(g.DATA_DAYS_OFFSET);m.attr("data-days-offset",k);if(l!=void 0){m.attr("data-days-limit",l)}else{m.removeAttr("data-days-limit")}c.reset(f);e.originalEvent.preventDefault()})},j=function(d,f){var i=d.find(g.TIMELINE_VIEW_SELECTOR);i.on("shown shown.bs.tab",function(b){c.shown(f);a(b.target).removeClass("active")});b.define(i,[b.events.activate]);i.on(b.events.activate,"[data-toggle='tab']",function(b){var c=a(b.currentTarget).data("filtername");h("block_timeline_user_sort_preference",c)})},k=function(a,b){var c=a.find(g.TIMELINE_SEARCH_INPUT),d=a.find(g.TIMELINE_SEARCH_SEARCH_ICON),e=a.find(g.TIMELINE_SEARCH_CLEAR_ICON);c.on("input",f.debounce(function(){if(""!==c.val()){l(d,e,b)}else{m(d,e,b)}},300));e.on("click",function(){c.val("");m(d,e,b)})},l=function(a,b,d){a.addClass("d-none");b.parent().removeClass("d-none");c.reset(d)},m=function(a,b,d){a.removeClass("d-none");b.parent().addClass("d-none");c.reset(d)};return{init:function init(b,c){b=a(b);i(b,c);j(b,c);k(b,c)}}}); //# sourceMappingURL=view_nav.min.js.map diff --git a/blocks/timeline/amd/build/view_nav.min.js.map b/blocks/timeline/amd/build/view_nav.min.js.map index 1ccedd50ff2..7732db28bb3 100644 --- a/blocks/timeline/amd/build/view_nav.min.js.map +++ b/blocks/timeline/amd/build/view_nav.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/view_nav.js"],"names":["define","$","CustomEvents","View","Ajax","Notification","SELECTORS","TIMELINE_DAY_FILTER","TIMELINE_DAY_FILTER_OPTION","TIMELINE_VIEW_SELECTOR","DATA_DAYS_OFFSET","DATA_DAYS_LIMIT","updateUserPreferences","type","value","call","methodname","args","preferences","fail","exception","registerTimelineDaySelector","root","timelineViewRoot","timelineDaySelectorContainer","find","events","activate","on","e","data","filtername","currentTarget","option","target","closest","attr","daysOffset","daysLimit","elementsWithDaysOffset","removeAttr","reset","originalEvent","preventDefault","registerViewSelector","viewSelector","shown","removeClass","init"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Manage the timeline view navigation for the timeline block.\n *\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(\n[\n 'jquery',\n 'core/custom_interaction_events',\n 'block_timeline/view',\n 'core/ajax',\n 'core/notification'\n],\nfunction(\n $,\n CustomEvents,\n View,\n Ajax,\n Notification\n) {\n\n var SELECTORS = {\n TIMELINE_DAY_FILTER: '[data-region=\"day-filter\"]',\n TIMELINE_DAY_FILTER_OPTION: '[data-from]',\n TIMELINE_VIEW_SELECTOR: '[data-region=\"view-selector\"]',\n DATA_DAYS_OFFSET: '[data-days-offset]',\n DATA_DAYS_LIMIT: '[data-days-limit]',\n };\n\n /**\n * Generic handler to persist user preferences\n *\n * @param {string} type The name of the attribute you're updating\n * @param {string} value The value of the attribute you're updating\n */\n var updateUserPreferences = function(type, value) {\n var request = {\n methodname: 'core_user_update_user_preferences',\n args: {\n preferences: [\n {\n type: type,\n value: value\n }\n ]\n }\n };\n\n Ajax.call([request])[0]\n .fail(Notification.exception);\n };\n\n /**\n * Event listener for the day selector (\"Next 7 days\", \"Next 30 days\", etc).\n *\n * @param {object} root The root element for the timeline block\n * @param {object} timelineViewRoot The root element for the timeline view\n */\n var registerTimelineDaySelector = function(root, timelineViewRoot) {\n var timelineDaySelectorContainer = root.find(SELECTORS.TIMELINE_DAY_FILTER);\n\n CustomEvents.define(timelineDaySelectorContainer, [CustomEvents.events.activate]);\n timelineDaySelectorContainer.on(\n CustomEvents.events.activate,\n SELECTORS.TIMELINE_DAY_FILTER_OPTION,\n function(e, data) {\n // Update the user preference\n var filtername = $(e.currentTarget).data('filtername');\n var type = 'block_timeline_user_filter_preference';\n updateUserPreferences(type, filtername);\n\n var option = $(e.target).closest(SELECTORS.TIMELINE_DAY_FILTER_OPTION);\n\n if (option.attr('aria-current') == 'true') {\n // If it's already active then we don't need to do anything.\n return;\n }\n\n var daysOffset = option.attr('data-from');\n var daysLimit = option.attr('data-to');\n var elementsWithDaysOffset = root.find(SELECTORS.DATA_DAYS_OFFSET);\n\n elementsWithDaysOffset.attr('data-days-offset', daysOffset);\n\n if (daysLimit != undefined) {\n elementsWithDaysOffset.attr('data-days-limit', daysLimit);\n } else {\n elementsWithDaysOffset.removeAttr('data-days-limit');\n }\n\n // Reset the views to reinitialise the event lists now that we've\n // updated the day limits.\n View.reset(timelineViewRoot);\n\n data.originalEvent.preventDefault();\n }\n );\n };\n\n /**\n * Event listener for the \"sort\" button in the timeline navigation that allows for\n * changing between the timeline dates and courses views.\n *\n * On a view change we tell the timeline view module that the view has been shown\n * so that it can handle how to display the appropriate view.\n *\n * @param {object} root The root element for the timeline block\n * @param {object} timelineViewRoot The root element for the timeline view\n */\n var registerViewSelector = function(root, timelineViewRoot) {\n var viewSelector = root.find(SELECTORS.TIMELINE_VIEW_SELECTOR);\n\n // Listen for when the user changes tab so that we can show the first set of courses\n // and load their events when they request the sort by courses view for the first time.\n viewSelector.on('shown shown.bs.tab', function(e) {\n View.shown(timelineViewRoot);\n $(e.target).removeClass('active');\n });\n\n\n // Event selector for user_sort\n CustomEvents.define(viewSelector, [CustomEvents.events.activate]);\n viewSelector.on(CustomEvents.events.activate, \"[data-toggle='tab']\", function(e) {\n var filtername = $(e.currentTarget).data('filtername');\n var type = 'block_timeline_user_sort_preference';\n updateUserPreferences(type, filtername);\n });\n };\n\n /**\n * Initialise the timeline view navigation by adding event listeners to\n * the navigation elements.\n *\n * @param {object} root The root element for the timeline block\n * @param {object} timelineViewRoot The root element for the timeline view\n */\n var init = function(root, timelineViewRoot) {\n root = $(root);\n registerTimelineDaySelector(root, timelineViewRoot);\n registerViewSelector(root, timelineViewRoot);\n };\n\n return {\n init: init\n };\n});\n"],"file":"view_nav.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/view_nav.js"],"names":["define","$","CustomEvents","View","Ajax","Notification","Utils","SELECTORS","TIMELINE_DAY_FILTER","TIMELINE_DAY_FILTER_OPTION","TIMELINE_VIEW_SELECTOR","DATA_DAYS_OFFSET","DATA_DAYS_LIMIT","TIMELINE_SEARCH_INPUT","TIMELINE_SEARCH_CLEAR_ICON","TIMELINE_SEARCH_SEARCH_ICON","updateUserPreferences","type","value","call","methodname","args","preferences","fail","exception","registerTimelineDaySelector","root","timelineViewRoot","timelineDaySelectorContainer","find","events","activate","on","e","data","filtername","currentTarget","option","target","closest","attr","daysOffset","daysLimit","elementsWithDaysOffset","removeAttr","reset","originalEvent","preventDefault","registerViewSelector","viewSelector","shown","removeClass","registerSearch","searchInput","searchIcon","clearSearchIcon","debounce","val","activeSearchState","clearSearchState","addClass","parent","init"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Manage the timeline view navigation for the timeline block.\n *\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(\n[\n 'jquery',\n 'core/custom_interaction_events',\n 'block_timeline/view',\n 'core/ajax',\n 'core/notification',\n 'core/utils'\n],\nfunction(\n $,\n CustomEvents,\n View,\n Ajax,\n Notification,\n Utils\n) {\n\n var SELECTORS = {\n TIMELINE_DAY_FILTER: '[data-region=\"day-filter\"]',\n TIMELINE_DAY_FILTER_OPTION: '[data-from]',\n TIMELINE_VIEW_SELECTOR: '[data-region=\"view-selector\"]',\n DATA_DAYS_OFFSET: '[data-days-offset]',\n DATA_DAYS_LIMIT: '[data-days-limit]',\n TIMELINE_SEARCH_INPUT: '[data-region=\"search-input\"]',\n TIMELINE_SEARCH_CLEAR_ICON: '[data-region=\"clear-icon\"]',\n TIMELINE_SEARCH_SEARCH_ICON: '[data-region=\"search-icon\"]'\n };\n\n /**\n * Generic handler to persist user preferences\n *\n * @param {string} type The name of the attribute you're updating\n * @param {string} value The value of the attribute you're updating\n */\n var updateUserPreferences = function(type, value) {\n var request = {\n methodname: 'core_user_update_user_preferences',\n args: {\n preferences: [\n {\n type: type,\n value: value\n }\n ]\n }\n };\n\n Ajax.call([request])[0]\n .fail(Notification.exception);\n };\n\n /**\n * Event listener for the day selector (\"Next 7 days\", \"Next 30 days\", etc).\n *\n * @param {object} root The root element for the timeline block\n * @param {object} timelineViewRoot The root element for the timeline view\n */\n var registerTimelineDaySelector = function(root, timelineViewRoot) {\n var timelineDaySelectorContainer = root.find(SELECTORS.TIMELINE_DAY_FILTER);\n\n CustomEvents.define(timelineDaySelectorContainer, [CustomEvents.events.activate]);\n timelineDaySelectorContainer.on(\n CustomEvents.events.activate,\n SELECTORS.TIMELINE_DAY_FILTER_OPTION,\n function(e, data) {\n // Update the user preference\n var filtername = $(e.currentTarget).data('filtername');\n var type = 'block_timeline_user_filter_preference';\n updateUserPreferences(type, filtername);\n\n var option = $(e.target).closest(SELECTORS.TIMELINE_DAY_FILTER_OPTION);\n\n if (option.attr('aria-current') == 'true') {\n // If it's already active then we don't need to do anything.\n return;\n }\n\n var daysOffset = option.attr('data-from');\n var daysLimit = option.attr('data-to');\n var elementsWithDaysOffset = root.find(SELECTORS.DATA_DAYS_OFFSET);\n\n elementsWithDaysOffset.attr('data-days-offset', daysOffset);\n\n if (daysLimit != undefined) {\n elementsWithDaysOffset.attr('data-days-limit', daysLimit);\n } else {\n elementsWithDaysOffset.removeAttr('data-days-limit');\n }\n\n // Reset the views to reinitialise the event lists now that we've\n // updated the day limits.\n View.reset(timelineViewRoot);\n\n data.originalEvent.preventDefault();\n }\n );\n };\n\n /**\n * Event listener for the \"sort\" button in the timeline navigation that allows for\n * changing between the timeline dates and courses views.\n *\n * On a view change we tell the timeline view module that the view has been shown\n * so that it can handle how to display the appropriate view.\n *\n * @param {object} root The root element for the timeline block\n * @param {object} timelineViewRoot The root element for the timeline view\n */\n var registerViewSelector = function(root, timelineViewRoot) {\n var viewSelector = root.find(SELECTORS.TIMELINE_VIEW_SELECTOR);\n\n // Listen for when the user changes tab so that we can show the first set of courses\n // and load their events when they request the sort by courses view for the first time.\n viewSelector.on('shown shown.bs.tab', function(e) {\n View.shown(timelineViewRoot);\n $(e.target).removeClass('active');\n });\n\n\n // Event selector for user_sort\n CustomEvents.define(viewSelector, [CustomEvents.events.activate]);\n viewSelector.on(CustomEvents.events.activate, \"[data-toggle='tab']\", function(e) {\n var filtername = $(e.currentTarget).data('filtername');\n var type = 'block_timeline_user_sort_preference';\n updateUserPreferences(type, filtername);\n });\n };\n\n /**\n * Event listener for the \"search\" input field in the timeline navigation that allows for\n * searching the activity name, course name and activity type.\n *\n * @param {object} root The root element for the timeline block\n * @param {object} timelineViewRoot The root element for the timeline view\n */\n const registerSearch = (root, timelineViewRoot) => {\n const searchInput = root.find(SELECTORS.TIMELINE_SEARCH_INPUT);\n const searchIcon = root.find(SELECTORS.TIMELINE_SEARCH_SEARCH_ICON);\n const clearSearchIcon = root.find(SELECTORS.TIMELINE_SEARCH_CLEAR_ICON);\n searchInput.on('input', Utils.debounce(() => {\n if (searchInput.val() !== '') {\n activeSearchState(searchIcon, clearSearchIcon, timelineViewRoot);\n } else {\n clearSearchState(searchIcon, clearSearchIcon, timelineViewRoot);\n }\n }, 300));\n clearSearchIcon.on('click', () => {\n searchInput.val('');\n clearSearchState(searchIcon, clearSearchIcon, timelineViewRoot);\n });\n };\n\n /**\n * Change the search icon to clear icon.\n *\n * @param {object} searchIcon Search icon element.\n * @param {object} clearSearchIcon Clear search icon element.\n * @param {object} timelineViewRoot The root element for the timeline view\n */\n const activeSearchState = (searchIcon, clearSearchIcon, timelineViewRoot) => {\n searchIcon.addClass('d-none');\n clearSearchIcon.parent().removeClass('d-none');\n View.reset(timelineViewRoot);\n };\n\n /**\n * Change the clear search icon to search icon.\n *\n * @param {object} searchIcon Search icon element.\n * @param {object} clearSearchIcon Clear search icon element.\n * @param {object} timelineViewRoot The root element for the timeline view\n */\n const clearSearchState = (searchIcon, clearSearchIcon, timelineViewRoot) => {\n searchIcon.removeClass('d-none');\n clearSearchIcon.parent().addClass('d-none');\n View.reset(timelineViewRoot);\n };\n\n /**\n * Initialise the timeline view navigation by adding event listeners to\n * the navigation elements.\n *\n * @param {object} root The root element for the timeline block\n * @param {object} timelineViewRoot The root element for the timeline view\n */\n var init = function(root, timelineViewRoot) {\n root = $(root);\n registerTimelineDaySelector(root, timelineViewRoot);\n registerViewSelector(root, timelineViewRoot);\n registerSearch(root, timelineViewRoot);\n };\n\n return {\n init: init\n };\n});\n"],"file":"view_nav.min.js"} \ No newline at end of file diff --git a/blocks/timeline/amd/src/event_list.js b/blocks/timeline/amd/src/event_list.js index 30f9368a248..df38f5bfd35 100644 --- a/blocks/timeline/amd/src/event_list.js +++ b/blocks/timeline/amd/src/event_list.js @@ -49,6 +49,8 @@ function( ROOT: '[data-region="event-list-container"]', EVENT_LIST_CONTENT: '[data-region="event-list-content"]', EVENT_LIST_LOADING_PLACEHOLDER: '[data-region="event-list-loading-placeholder"]', + TIMELINE_BLOCK: '[data-region="timeline"]', + TIMELINE_SEARCH: '[data-region="search-input"]' }; var TEMPLATES = { @@ -177,9 +179,10 @@ function( * @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) { + 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; @@ -196,6 +199,10 @@ function( 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; @@ -221,6 +228,7 @@ function( * @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. */ var loadEventsFromPageData = function( @@ -231,7 +239,8 @@ function( preloadedPages, courseId, daysOffset, - daysLimit + daysLimit, + searchValue ) { var pageNumber = pageData.pageNumber; var limit = pageData.limit; @@ -255,7 +264,7 @@ function( } else { // Load one more than the given limit so that we can tell if there // is more content to load after this. - eventsPromise = load(midnight, limit + 1, daysOffset, daysLimit, lastId, courseId); + eventsPromise = load(midnight, limit + 1, daysOffset, daysLimit, lastId, courseId, searchValue); } return eventsPromise.then(function(result) { @@ -308,6 +317,7 @@ function( * @param {int|undefined} daysLimit How many dates (from midnight) to limit the result to * @param {string} paginationAriaLabel String to set as the aria label for the pagination bar. * @param {object} additionalConfig Additional config options to pass to pagedContentFactory + * @param {string|undefined} searchValue Search value * @return {object} jQuery promise. */ var createPagedContent = function( @@ -319,7 +329,8 @@ function( daysOffset, daysLimit, paginationAriaLabel, - additionalConfig + additionalConfig, + searchValue ) { // Remember the last event id we loaded on each page because we can't // use the offset value since the backend can skip events if the user doesn't @@ -356,7 +367,8 @@ function( preloadedPages, courseId, daysOffset, - daysLimit + daysLimit, + searchValue ).then(function(calendarEvents) { if (calendarEvents.length) { // Remember that we've loaded content. @@ -423,6 +435,7 @@ function( 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 @@ -438,7 +451,7 @@ function( // Created the paged content element. return createPagedContent(pageLimit, preloadedPages, midnight, firstLoad, courseId, daysOffset, daysLimit, - paginationAriaLabel, additionalConfig) + paginationAriaLabel, additionalConfig, searchValue) .then(function(html, js) { html = $(html); // Hide the content for now. diff --git a/blocks/timeline/amd/src/view_courses.js b/blocks/timeline/amd/src/view_courses.js index 221c4e8a5bf..22c24842937 100644 --- a/blocks/timeline/amd/src/view_courses.js +++ b/blocks/timeline/amd/src/view_courses.js @@ -50,7 +50,9 @@ function( 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' + LOADING_ICON: '.loading-icon', + TIMELINE_BLOCK: '[data-region="timeline"]', + TIMELINE_SEARCH: '[data-region="search-input"]' }; var TEMPLATES = { @@ -250,9 +252,10 @@ function( * @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) { + var getEventsForCourseIds = function(courseIds, startTime, limit, endTime, searchValue) { var args = { courseids: courseIds, starttime: startTime, @@ -263,6 +266,10 @@ function( args.endtime = endTime; } + if (searchValue) { + args.searchvalue = searchValue; + } + return EventsRepository.queryByCourses(args); }; @@ -304,14 +311,15 @@ function( * @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) { + 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); + return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime, searchValue); }; /** @@ -394,11 +402,12 @@ function( var midnight = getMidnight(root); var startTime = getStartTime(root); var endTime = getEndTime(root); + const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val(); // 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); + var eventsPromise = loadEventsForCourses(courses, startTime, endTime, searchValue); // Render the courses in the DOM. var renderPromise = updateDisplayFromCourses(courses, root, midnight, daysOffset, daysLimit); @@ -461,12 +470,13 @@ function( var courseIds = courseEventsContainers.map(function() { return $(this).attr('data-course-id'); }).get(); + const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val(); // Record when we started our request. setEventReloadTime(root, startReloadTime); // Load all of the events for the given courses. - return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime) + return getEventsForCourseIds(courseIds, startTime, COURSE_EVENT_LIMIT + 1, endTime, searchValue) .then(function(eventsByCourse) { if (hasReloadedEventsSince(root, startReloadTime)) { // A new reload has begun so ignore our results. diff --git a/blocks/timeline/amd/src/view_nav.js b/blocks/timeline/amd/src/view_nav.js index 180ff9dba3b..4ae4277a56a 100644 --- a/blocks/timeline/amd/src/view_nav.js +++ b/blocks/timeline/amd/src/view_nav.js @@ -26,14 +26,16 @@ define( 'core/custom_interaction_events', 'block_timeline/view', 'core/ajax', - 'core/notification' + 'core/notification', + 'core/utils' ], function( $, CustomEvents, View, Ajax, - Notification + Notification, + Utils ) { var SELECTORS = { @@ -42,6 +44,9 @@ function( TIMELINE_VIEW_SELECTOR: '[data-region="view-selector"]', DATA_DAYS_OFFSET: '[data-days-offset]', DATA_DAYS_LIMIT: '[data-days-limit]', + TIMELINE_SEARCH_INPUT: '[data-region="search-input"]', + TIMELINE_SEARCH_CLEAR_ICON: '[data-region="clear-icon"]', + TIMELINE_SEARCH_SEARCH_ICON: '[data-region="search-icon"]' }; /** @@ -144,6 +149,56 @@ function( }); }; + /** + * 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 searchIcon = root.find(SELECTORS.TIMELINE_SEARCH_SEARCH_ICON); + const clearSearchIcon = root.find(SELECTORS.TIMELINE_SEARCH_CLEAR_ICON); + searchInput.on('input', Utils.debounce(() => { + if (searchInput.val() !== '') { + activeSearchState(searchIcon, clearSearchIcon, timelineViewRoot); + } else { + clearSearchState(searchIcon, clearSearchIcon, timelineViewRoot); + } + }, 300)); + clearSearchIcon.on('click', () => { + searchInput.val(''); + clearSearchState(searchIcon, clearSearchIcon, timelineViewRoot); + }); + }; + + /** + * Change the search icon to clear icon. + * + * @param {object} searchIcon Search icon element. + * @param {object} clearSearchIcon Clear search icon element. + * @param {object} timelineViewRoot The root element for the timeline view + */ + const activeSearchState = (searchIcon, clearSearchIcon, timelineViewRoot) => { + searchIcon.addClass('d-none'); + clearSearchIcon.parent().removeClass('d-none'); + View.reset(timelineViewRoot); + }; + + /** + * Change the clear search icon to search icon. + * + * @param {object} searchIcon Search icon element. + * @param {object} clearSearchIcon Clear search icon element. + * @param {object} timelineViewRoot The root element for the timeline view + */ + const clearSearchState = (searchIcon, clearSearchIcon, timelineViewRoot) => { + searchIcon.removeClass('d-none'); + clearSearchIcon.parent().addClass('d-none'); + View.reset(timelineViewRoot); + }; + /** * Initialise the timeline view navigation by adding event listeners to * the navigation elements. @@ -155,6 +210,7 @@ function( root = $(root); registerTimelineDaySelector(root, timelineViewRoot); registerViewSelector(root, timelineViewRoot); + registerSearch(root, timelineViewRoot); }; return { diff --git a/blocks/timeline/lang/en/block_timeline.php b/blocks/timeline/lang/en/block_timeline.php index f2fc7dc2e86..9bbef4d835b 100644 --- a/blocks/timeline/lang/en/block_timeline.php +++ b/blocks/timeline/lang/en/block_timeline.php @@ -30,6 +30,7 @@ $string['ariaeventlistpaginationnavdates'] = 'Timeline activities pagination'; $string['ariaeventlistpaginationnavcourses'] = 'Timeline activities for course {$a} pagination'; $string['ariaviewselector'] = 'Sort timeline items'; $string['ariaviewselectoroption'] = '{$a} sort option'; +$string['clearsearch'] = 'Clear search'; $string['duedate'] = 'Due date'; $string['morecourses'] = 'More courses'; $string['timeline:myaddinstance'] = 'Add a new timeline block to Dashboard'; @@ -42,6 +43,7 @@ $string['next6months'] = 'Next 6 months'; $string['overdue'] = 'Overdue'; $string['all'] = 'All'; $string['pluginname'] = 'Timeline'; +$string['searchevents'] = 'Search by activity type or name'; $string['sortbycourses'] = 'Sort by courses'; $string['sortbydates'] = 'Sort by dates'; $string['timeline'] = 'Timeline'; diff --git a/blocks/timeline/templates/main.mustache b/blocks/timeline/templates/main.mustache index 5a0b44fae5b..83bde9c2655 100644 --- a/blocks/timeline/templates/main.mustache +++ b/blocks/timeline/templates/main.mustache @@ -26,12 +26,15 @@
-
+
{{> block_timeline/nav-day-filter }}
-
+
{{> block_timeline/nav-view-selector }}
+
+ {{> block_timeline/nav-search }} +
diff --git a/blocks/timeline/templates/nav-search.mustache b/blocks/timeline/templates/nav-search.mustache new file mode 100644 index 00000000000..4909e314dc8 --- /dev/null +++ b/blocks/timeline/templates/nav-search.mustache @@ -0,0 +1,47 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_timeline/nav-search + + This template renders the main content area for the timeline block. + + Example context (json): + {} +}} + diff --git a/blocks/timeline/tests/behat/block_timeline_search.feature b/blocks/timeline/tests/behat/block_timeline_search.feature new file mode 100644 index 00000000000..0ef5fdd43d6 --- /dev/null +++ b/blocks/timeline/tests/behat/block_timeline_search.feature @@ -0,0 +1,95 @@ +@block @block_timeline @javascript +Feature: The timeline block allows users to search for upcoming activities + As a student + I can search for the upcoming activities in the timeline block + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | student1 | Student | 1 | student1@example.com | S1 | + And the following "courses" exist: + | fullname | shortname | category | startdate | enddate | + | Course 1 | C1 | 0 | ##1 month ago## | ##15 days ago## | + | Course 2 | C2 | 0 | ##yesterday## | ##tomorrow## | + | Course 3 | C3 | 0 | ##first day of next month## | ##last day of next month## | + And the following "activities" exist: + | activity | course | idnumber | name | intro | timeopen | timeclose | + | choice | C2 | choice1 | Test choice 1 | Test choice description | ##yesterday## | ##tomorrow## | + | choice | C1 | choice2 | Test choice 2 | Test choice description | ##first day of +5 months## | ##last day of +5 months## | + | choice | C3 | choice3 | Test choice 3 | Test choice description | ##first day of +5 months## | ##last day of +10 months## | + | choice | C2 | choice4 | Test choice 4 | Test choice description | ##first day of +5 months## | ##last day of +15 months## | + | choice | C1 | choice5 | Test choice 5 | Test choice description | ##first day of +5 months## | ##last day of +20 months## | + | choice | C3 | choice6 | Test choice 6 | Test choice description | ##first day of +5 months## | ##last day of +25 months## | + | feedback | C2 | feedback1 | Test feedback 1 | Test feedback description | ##yesterday## | ##tomorrow## | + | feedback | C1 | feedback2 | Test feedback 2 | Test feedback description | ##first day of +5 months## | ##last day of +5 months## | + | feedback | C3 | feedback3 | Test feedback 3 | Test feedback description | ##first day of +5 months## | ##last day of +10 months## | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | student1 | C2 | student | + | student1 | C3 | student | + + Scenario: The search should return no events if I enter the wrong value + Given I log in as "student1" + And I click on "Filter timeline by date" "button" in the "Timeline" "block" + And I click on "All" "link" in the "Timeline" "block" + When I set the field "Search by activity type or name" to "Fake example" + Then I should see "No activities require action" in the "Timeline" "block" + + Scenario: Search for Course name + Given I log in as "student1" + And I click on "Filter timeline by date" "button" in the "Timeline" "block" + And I click on "All" "link" in the "Timeline" "block" + When I set the field "Search by activity type or name" to "Course 1" + Then I should see "Test choice 2" in the "Timeline" "block" + And I should see "Test choice 5" in the "Timeline" "block" + And I should see "Test feedback 2" in the "Timeline" "block" + And I should not see "Test choice 1" in the "Timeline" "block" + And I should not see "Test choice 3" in the "Timeline" "block" + And I should not see "Test choice 4" in the "Timeline" "block" + And I should not see "Test feedback 1" in the "Timeline" "block" + And I should not see "Test feedback 3" in the "Timeline" "block" + + Scenario: Search for Activity name + Given I log in as "student1" + And I click on "Filter timeline by date" "button" in the "Timeline" "block" + And I click on "All" "link" in the "Timeline" "block" + When I set the field "Search by activity type or name" to "Test choice 1" + And I wait until "Test choice 2" "text" does not exist + Then I should see "Test choice 1" in the "Timeline" "block" + And I should not see "Test choice 2" in the "Timeline" "block" + And I should not see "Test choice 3" in the "Timeline" "block" + And I should not see "Test choice 4" in the "Timeline" "block" + And I should not see "Test choice 5" in the "Timeline" "block" + And I should not see "Test choice 6" in the "Timeline" "block" + And I should not see "Test feedback 1" in the "Timeline" "block" + And I should not see "Test feedback 2" in the "Timeline" "block" + And I should not see "Test feedback 3" in the "Timeline" "block" + + Scenario: Search for Activity type + Given I log in as "student1" + And I click on "Filter timeline by date" "button" in the "Timeline" "block" + And I click on "All" "link" in the "Timeline" "block" + When I set the field "Search by activity type or name" to "feedback" + Then I should see "Test feedback 1" in the "Timeline" "block" + And I should see "Test feedback 2" in the "Timeline" "block" + And I should see "Test feedback 3" in the "Timeline" "block" + And I should not see "Test choice 1" in the "Timeline" "block" + And I should not see "Test choice 2" in the "Timeline" "block" + And I should not see "Test choice 3" in the "Timeline" "block" + And I should not see "Test choice 4" in the "Timeline" "block" + And I should not see "Test choice 5" in the "Timeline" "block" + + Scenario: Timeline paginated search + Given I log in as "student1" + And I click on "Filter timeline by date" "button" in the "Timeline" "block" + And I click on "All" "link" in the "Timeline" "block" + When I set the field "Search by activity type or name" to "choice" + Then I should see "Test choice 1" in the "Timeline" "block" + And I should see "Test choice 2" in the "Timeline" "block" + And I should see "Test choice 3" in the "Timeline" "block" + And I should see "Test choice 4" in the "Timeline" "block" + And I should see "Test choice 5" in the "Timeline" "block" + And I should not see "Test choice 6" in the "Timeline" "block" + And I click on "[data-control='next']" "css_element" in the "Timeline" "block" + And I should see "Test choice 6" in the "Timeline" "block" diff --git a/calendar/classes/local/api.php b/calendar/classes/local/api.php index 82566ff075b..e59f34e95ec 100644 --- a/calendar/classes/local/api.php +++ b/calendar/classes/local/api.php @@ -120,6 +120,7 @@ class api { * @param int $limitnum Limit results to this amount (between 1 and 50) * @param bool $lmittononsuspendedevents Limit course events to courses the user is active in (not suspended). * @param \stdClass|null $user The user id or false for $USER + * @param string|null $searchvalue The value a user wishes to search against * @return array A list of action_event_interface objects * @throws \moodle_exception */ @@ -129,7 +130,8 @@ class api { $aftereventid = null, $limitnum = 20, $limittononsuspendedevents = false, - ?\stdClass $user = null + ?\stdClass $user = null, + ?string $searchvalue = null ) { global $USER; @@ -154,7 +156,7 @@ class api { } return $vault->get_action_events_by_timesort($user, $timesortfrom, $timesortto, $afterevent, $limitnum, - $limittononsuspendedevents); + $limittononsuspendedevents, $searchvalue); } /** @@ -166,6 +168,7 @@ class api { * @param int|null $timesortto The end timesort value (inclusive) * @param int|null $aftereventid Only return events after this one * @param int $limitnum Limit results to this amount (between 1 and 50) + * @param string|null $searchvalue The value a user wishes to search against * @return array A list of action_event_interface objects * @throws limit_invalid_parameter_exception */ @@ -174,7 +177,8 @@ class api { $timesortfrom = null, $timesortto = null, $aftereventid = null, - $limitnum = 20 + $limitnum = 20, + ?string $searchvalue = null ) { global $USER; @@ -191,7 +195,7 @@ class api { } return $vault->get_action_events_by_course( - $USER, $course, $timesortfrom, $timesortto, $afterevent, $limitnum); + $USER, $course, $timesortfrom, $timesortto, $afterevent, $limitnum, $searchvalue); } /** @@ -206,13 +210,15 @@ class api { * @param int|null $timesortfrom The start timesort value (inclusive) * @param int|null $timesortto The end timesort value (inclusive) * @param int $limitnum Limit results per course to this amount (between 1 and 50) + * @param string|null $searchvalue The value a user wishes to search against * @return array A list of action_event_interface objects indexed by course id */ public static function get_action_events_by_courses( $courses = [], $timesortfrom = null, $timesortto = null, - $limitnum = 20 + $limitnum = 20, + ?string $searchvalue = null ) { $return = []; @@ -222,7 +228,8 @@ class api { $timesortfrom, $timesortto, null, - $limitnum + $limitnum, + $searchvalue ); } diff --git a/calendar/classes/local/event/data_access/event_vault.php b/calendar/classes/local/event/data_access/event_vault.php index 7bda7bfb807..0dde57f9b23 100644 --- a/calendar/classes/local/event/data_access/event_vault.php +++ b/calendar/classes/local/event/data_access/event_vault.php @@ -98,7 +98,8 @@ class event_vault implements event_vault_interface { array $categoriesfilter = null, $withduration = true, $ignorehidden = true, - callable $filter = null + callable $filter = null, + ?string $searchvalue = null ) { $fromquery = function($field, $timefrom, $lastseenmethod, $afterevent, $withduration) { @@ -137,12 +138,15 @@ class event_vault implements event_vault_interface { return []; } + $searchquery = $this->generate_search_subquery($searchvalue); + $params = array_merge( $type ? ['type' => $type] : [], $timesortfromquery ? $timesortfromquery['params'] : [], $timesorttoquery ? $timesorttoquery['params'] : [], $timestartfromquery ? $timestartfromquery['params'] : [], - $timestarttoquery ? $timestarttoquery['params'] : [] + $timestarttoquery ? $timestarttoquery['params'] : [], + $searchquery ? $searchquery['params'] : [], ); $where = array_merge( @@ -150,7 +154,8 @@ class event_vault implements event_vault_interface { $timesortfromquery ? $timesortfromquery['where'] : [], $timesorttoquery ? $timesorttoquery['where'] : [], $timestartfromquery ? $timestartfromquery['where'] : [], - $timestarttoquery ? $timestarttoquery['where'] : [] + $timestarttoquery ? $timestarttoquery['where'] : [], + $searchquery ? [$searchquery['where']] : [], ); $offset = 0; @@ -199,7 +204,8 @@ class event_vault implements event_vault_interface { $timesortto = null, event_interface $afterevent = null, $limitnum = 20, - $limittononsuspendedevents = false + $limittononsuspendedevents = false, + ?string $searchvalue = null ) { $courseids = array_map(function($course) { return $course->id; @@ -231,7 +237,8 @@ class event_vault implements event_vault_interface { true, function ($event) { return $event instanceof action_event_interface; - } + }, + $searchvalue ); } @@ -241,7 +248,8 @@ class event_vault implements event_vault_interface { $timesortfrom = null, $timesortto = null, event_interface $afterevent = null, - $limitnum = 20 + $limitnum = 20, + ?string $searchvalue = null ) { $groupings = groups_get_user_groups($course->id, $user->id); return array_values( @@ -262,7 +270,8 @@ class event_vault implements event_vault_interface { true, function ($event) use ($course) { return $event instanceof action_event_interface && $event->get_course()->get('id') == $course->id; - } + }, + $searchvalue ) ); } @@ -393,4 +402,33 @@ class event_vault implements event_vault_interface { ) ); } + + /** + * Generates SQL subquery and parameters for event searching. + * + * @param string|null $searchvalue Search value. + * @return array|null + */ + protected function generate_search_subquery(?string $searchvalue): ?array { + global $DB; + if (!$searchvalue) { + return null; + } + + // Course searching. + $whereconditions[] = $DB->sql_like('c.fullname', ':cfullname', false); + $params['cfullname'] = '%' . $DB->sql_like_escape($searchvalue) . '%'; + + // Activity name searching. + $whereconditions[] = $DB->sql_like('e.name', ':eventname', false); + $params['eventname'] = '%' . $DB->sql_like_escape($searchvalue) . '%'; + + // Activity type searching. + $whereconditions[] = $DB->sql_like('e.modulename', ':modulename', false); + $params['modulename'] = '%' . $DB->sql_like_escape($searchvalue) . '%'; + + $whereclause = '(' . implode(' OR ', $whereconditions) . ')'; + + return ['where' => $whereclause, 'params' => $params]; + } } diff --git a/calendar/classes/local/event/data_access/event_vault_interface.php b/calendar/classes/local/event/data_access/event_vault_interface.php index 28b98152c65..78dbf91fabf 100644 --- a/calendar/classes/local/event/data_access/event_vault_interface.php +++ b/calendar/classes/local/event/data_access/event_vault_interface.php @@ -62,6 +62,7 @@ interface event_vault_interface { * @param bool $ignorehidden If true don't return hidden events. * @param callable|null $filter Additional logic to filter out unwanted events. * Must return true to keep the event, false to discard it. + * @param string|null $searchvalue The value a user wishes to search against * @return event_interface[] Array of event_interfaces. */ public function get_events( @@ -79,7 +80,8 @@ interface event_vault_interface { array $categoriesfilter = null, $withduration = true, $ignorehidden = true, - callable $filter = null + callable $filter = null, + ?string $searchvalue = null ); /** @@ -94,6 +96,7 @@ interface event_vault_interface { * @param event_interface $afterevent Only return events after this one * @param int $limitnum Return at most this number of events * @param bool $lmittononsuspendedevents Limit course events to courses the user is active in (not suspended). + * @param string|null $searchvalue The value a user wishes to search against * @return event_interface */ public function get_action_events_by_timesort( @@ -102,7 +105,8 @@ interface event_vault_interface { $timesortto, event_interface $afterevent, $limitnum, - $limittononsuspendedevents + $limittononsuspendedevents, + ?string $searchvalue = null ); /** @@ -118,6 +122,7 @@ interface event_vault_interface { * @param int $timesortto Events with timesort until this value (inclusive) * @param event_interface $afterevent Only return events after this one * @param int $limitnum Return at most this number of events + * @param string|null $searchvalue The value a user wishes to search against * @return action_event_interface */ public function get_action_events_by_course( @@ -126,6 +131,7 @@ interface event_vault_interface { $timesortfrom, $timesortto, event_interface $afterevent, - $limitnum + $limitnum, + ?string $searchvalue = null ); } diff --git a/calendar/classes/local/event/strategies/raw_event_retrieval_strategy.php b/calendar/classes/local/event/strategies/raw_event_retrieval_strategy.php index 46b42d7899c..8e632ff4ce7 100644 --- a/calendar/classes/local/event/strategies/raw_event_retrieval_strategy.php +++ b/calendar/classes/local/event/strategies/raw_event_retrieval_strategy.php @@ -300,7 +300,7 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter GROUP BY ev.modulename, ev.instance, ev.eventtype"; // Build the main query. - $sql = "SELECT e.* + $sql = "SELECT e.*, c.fullname AS coursefullname, c.shortname AS courseshortname FROM {event} e INNER JOIN ($subquery) fe ON e.modulename = fe.modulename @@ -309,6 +309,8 @@ class raw_event_retrieval_strategy implements raw_event_retrieval_strategy_inter AND (e.priority = fe.priority OR (e.priority IS NULL AND fe.priority IS NULL)) LEFT JOIN {modules} m ON e.modulename = m.name + LEFT JOIN {course} c + ON c.id = e.courseid WHERE (m.visible = 1 OR m.visible IS NULL) AND $whereclause ORDER BY " . ($ordersql ? $ordersql : "e.timestart"); diff --git a/calendar/externallib.php b/calendar/externallib.php index 8c52e713556..2fd1fdf4c4e 100644 --- a/calendar/externallib.php +++ b/calendar/externallib.php @@ -408,6 +408,7 @@ class core_calendar_external extends external_api { 'limittononsuspendedevents' => new external_value(PARAM_BOOL, 'Limit the events to courses the user is not suspended in', VALUE_DEFAULT, false), 'userid' => new external_value(PARAM_INT, 'The user id', VALUE_DEFAULT, null), + 'searchvalue' => new external_value(PARAM_TEXT, 'The value a user wishes to search against', VALUE_DEFAULT, null) ) ); } @@ -421,11 +422,12 @@ class core_calendar_external extends external_api { * @param null|int $aftereventid Get events with ids greater than this one * @param int $limitnum Limit the number of results to this value * @param null|int $userid The user id + * @param string|null $searchvalue The value a user wishes to search against * @return array */ public static function get_calendar_action_events_by_timesort($timesortfrom = 0, $timesortto = null, $aftereventid = 0, $limitnum = 20, $limittononsuspendedevents = false, - $userid = null) { + $userid = null, ?string $searchvalue = null) { global $PAGE, $USER; $params = self::validate_parameters( @@ -437,6 +439,7 @@ class core_calendar_external extends external_api { 'limitnum' => $limitnum, 'limittononsuspendedevents' => $limittononsuspendedevents, 'userid' => $userid, + 'searchvalue' => $searchvalue ] ); if ($params['userid']) { @@ -459,7 +462,8 @@ class core_calendar_external extends external_api { $params['aftereventid'], $params['limitnum'], $params['limittononsuspendedevents'], - $user + $user, + $params['searchvalue'] ); $exportercache = new events_related_objects_cache($events); @@ -574,7 +578,8 @@ class core_calendar_external extends external_api { ), 'timesortfrom' => new external_value(PARAM_INT, 'Time sort from', VALUE_DEFAULT, null), 'timesortto' => new external_value(PARAM_INT, 'Time sort to', VALUE_DEFAULT, null), - 'limitnum' => new external_value(PARAM_INT, 'Limit number', VALUE_DEFAULT, 10) + 'limitnum' => new external_value(PARAM_INT, 'Limit number', VALUE_DEFAULT, 10), + 'searchvalue' => new external_value(PARAM_TEXT, 'The value a user wishes to search against', VALUE_DEFAULT, null) ) ); } @@ -587,10 +592,11 @@ class core_calendar_external extends external_api { * @param null|int $timesortfrom Events after this time (inclusive) * @param null|int $timesortto Events before this time (inclusive) * @param int $limitnum Limit the number of results per course to this value + * @param string|null $searchvalue The value a user wishes to search against * @return array */ public static function get_calendar_action_events_by_courses( - array $courseids, $timesortfrom = null, $timesortto = null, $limitnum = 10) { + array $courseids, $timesortfrom = null, $timesortto = null, $limitnum = 10, ?string $searchvalue = null) { global $PAGE, $USER; @@ -602,6 +608,7 @@ class core_calendar_external extends external_api { 'timesortfrom' => $timesortfrom, 'timesortto' => $timesortto, 'limitnum' => $limitnum, + 'searchvalue' => $searchvalue ] ); $context = \context_user::instance($USER->id); @@ -623,7 +630,8 @@ class core_calendar_external extends external_api { $courses, $params['timesortfrom'], $params['timesortto'], - $params['limitnum'] + $params['limitnum'], + $params['searchvalue'] ); if (empty($events)) { diff --git a/calendar/tests/externallib_test.php b/calendar/tests/externallib_test.php index bc33c032785..f8e61c9fd72 100644 --- a/calendar/tests/externallib_test.php +++ b/calendar/tests/externallib_test.php @@ -1438,6 +1438,112 @@ class core_calendar_externallib_testcase extends externallib_advanced_testcase { $this->assertEquals('Event 3', $groupedbycourse[$course2->id][0]['name']); } + /** + * Test get_action_events_by_courses with search feature + */ + public function test_get_action_events_by_courses_with_search() { + // Generate data. + $user = $this->getDataGenerator()->create_user(); + $course1 = $this->getDataGenerator()->create_course(); + $course2 = $this->getDataGenerator()->create_course(); + $course3 = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $instance1 = $generator->create_instance(['course' => $course1->id]); + $instance2 = $generator->create_instance(['course' => $course2->id]); + $instance3 = $generator->create_instance(['course' => $course3->id]); + + $this->getDataGenerator()->enrol_user($user->id, $course1->id); + $this->getDataGenerator()->enrol_user($user->id, $course2->id); + $this->resetAfterTest(true); + $this->setUser($user); + + $mapresult = function($result) { + $groupedbycourse = []; + foreach ($result['groupedbycourse'] as $group) { + $events = $group['events']; + $courseid = $group['courseid']; + $groupedbycourse[$courseid] = $events; + } + + return $groupedbycourse; + }; + + for ($i = 1; $i < 10; $i++) { + if ($i < 3) { + $courseid = $course1->id; + $instance = $instance1->id; + } else if ($i < 6) { + $courseid = $course2->id; + $instance = $instance2->id; + } else { + $courseid = $course3->id; + $instance = $instance3->id; + } + + $records[] = $this->create_calendar_event( + sprintf('Event %d', $i), + $user->id, + 'user', + 0, + 1, + [ + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $courseid, + 'timesort' => $i, + 'modulename' => 'assign', + 'instance' => $instance, + ] + ); + } + + // No result found for fake search. + $result = core_calendar_external::get_calendar_action_events_by_courses([$course1->id, $course2->id, $course3->id], + 1, null, 20, 'Fake search'); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_courses_returns(), + $result + ); + $groupedbycourse = $mapresult($result); + + $this->assertEmpty($groupedbycourse[$course1->id]); + $this->assertEmpty($groupedbycourse[$course2->id]); + $this->assertArrayNotHasKey($course3->id, $groupedbycourse); + + // Search for event name called 'Event 1'. + $result = core_calendar_external::get_calendar_action_events_by_courses([$course1->id, $course2->id, $course3->id], + 1, null, 20, 'Event 1'); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_courses_returns(), + $result + ); + $groupedbycourse = $mapresult($result); + + $this->assertArrayNotHasKey($course3->id, $groupedbycourse); + $this->assertCount(2, $groupedbycourse); + $this->assertCount(1, $groupedbycourse[$course1->id]); + $this->assertCount(0, $groupedbycourse[$course2->id]); + $this->assertEquals('Event 1', $groupedbycourse[$course1->id][0]['name']); + + // Search for activity type called 'assign'. + $result = core_calendar_external::get_calendar_action_events_by_courses([$course1->id, $course2->id, $course3->id], + 1, null, 20, 'assign'); + $result = external_api::clean_returnvalue( + core_calendar_external::get_calendar_action_events_by_courses_returns(), + $result + ); + $groupedbycourse = $mapresult($result); + + $this->assertArrayNotHasKey($course3->id, $groupedbycourse); + $this->assertCount(2, $groupedbycourse); + $this->assertCount(2, $groupedbycourse[$course1->id]); + $this->assertCount(3, $groupedbycourse[$course2->id]); + $this->assertEquals('Event 1', $groupedbycourse[$course1->id][0]['name']); + $this->assertEquals('Event 2', $groupedbycourse[$course1->id][1]['name']); + $this->assertEquals('Event 3', $groupedbycourse[$course2->id][0]['name']); + $this->assertEquals('Event 4', $groupedbycourse[$course2->id][1]['name']); + $this->assertEquals('Event 5', $groupedbycourse[$course2->id][2]['name']); + } + /** * Test for deleting module events. */ diff --git a/calendar/tests/local_api_test.php b/calendar/tests/local_api_test.php index 987de7b3915..9057fa68493 100644 --- a/calendar/tests/local_api_test.php +++ b/calendar/tests/local_api_test.php @@ -290,6 +290,64 @@ class core_calendar_local_api_testcase extends advanced_testcase { $this->assertEmpty($result); } + /** + * Test get_calendar_action_events_by_timesort with search feature. + */ + public function test_get_calendar_action_events_by_timesort_with_search() { + // Generate data. + $user = $this->getDataGenerator()->create_user(); + $course = $this->getDataGenerator()->create_course(); + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $moduleinstance = $generator->create_instance(['course' => $course->id]); + + $this->getDataGenerator()->enrol_user($user->id, $course->id); + $this->resetAfterTest(true); + $this->setAdminUser(); + + $params = [ + 'type' => CALENDAR_EVENT_TYPE_ACTION, + 'courseid' => $course->id, + 'modulename' => 'assign', + 'instance' => $moduleinstance->id, + 'userid' => 1, + 'eventtype' => 'user', + 'repeats' => 0, + 'timestart' => 1, + ]; + + $event1 = create_event(array_merge($params, ['name' => 'Event 1', 'timesort' => 1])); + $event2 = create_event(array_merge($params, ['name' => 'Event 2', 'timesort' => 2])); + $event3 = create_event(array_merge($params, ['name' => 'Event 3', 'timesort' => 3])); + $event4 = create_event(array_merge($params, ['name' => 'Event 4', 'timesort' => 4])); + $event5 = create_event(array_merge($params, ['name' => 'Event 5', 'timesort' => 5])); + $event6 = create_event(array_merge($params, ['name' => 'Event 6', 'timesort' => 6])); + $event7 = create_event(array_merge($params, ['name' => 'Event 7', 'timesort' => 7])); + $event8 = create_event(array_merge($params, ['name' => 'Event 8', 'timesort' => 8])); + + $this->setUser($user); + + // No result found for fake search. + $result = \core_calendar\local\api::get_action_events_by_timesort(0, null, null, 6, false, null, 'Fake search'); + $this->assertEmpty($result); + + // Search for event name called 'Event 1'. + $result = \core_calendar\local\api::get_action_events_by_timesort(0, 8, null, 20, false, null, 'Event 1'); + $this->assertCount(1, $result); + $this->assertEquals('Event 1', $result[0]->get_name()); + + // Search for activity type called 'assign'. + $result = \core_calendar\local\api::get_action_events_by_timesort(0, 8, null, 20, false, null, 'assign'); + $this->assertCount(8, $result); + $this->assertEquals('Event 1', $result[0]->get_name()); + $this->assertEquals('Event 2', $result[1]->get_name()); + $this->assertEquals('Event 3', $result[2]->get_name()); + $this->assertEquals('Event 4', $result[3]->get_name()); + $this->assertEquals('Event 5', $result[4]->get_name()); + $this->assertEquals('Event 6', $result[5]->get_name()); + $this->assertEquals('Event 7', $result[6]->get_name()); + $this->assertEquals('Event 8', $result[7]->get_name()); + } + /** * Requesting calendar events from a given course and time should return all * events with a sort time at or after the requested time. All events prior diff --git a/calendar/upgrade.txt b/calendar/upgrade.txt index 35fda4daa05..38e32781934 100644 --- a/calendar/upgrade.txt +++ b/calendar/upgrade.txt @@ -1,6 +1,11 @@ This files describes API changes in /calendar/* , information provided here is intended especially for developers. +=== 4.0 === +* The following external functions now accepts an optional parameter 'searchvalue' to search the events: + - core_calendar_external::get_calendar_action_events_by_timesort + - core_calendar_external::get_calendar_action_events_by_courses + === 3.10 === * The core_calendar\local\event\value_objects\times_interface class now has new method get_usermidnight_time() which returns the user midnight time for a given event. diff --git a/theme/boost/scss/moodle/blocks.scss b/theme/boost/scss/moodle/blocks.scss index e8f34707eb8..160ed1ef8d9 100644 --- a/theme/boost/scss/moodle/blocks.scss +++ b/theme/boost/scss/moodle/blocks.scss @@ -105,6 +105,14 @@ $card-gutter : $card-deck-margin * 2; height: 5rem; } +.block { + .searchbar { + .icon { + margin-right: 0; + } + } +} + .block .block-cards { .course-info-container { padding: 0.8rem; diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index 28c50bffe3a..af11a7dcd5e 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -12868,6 +12868,9 @@ body.dragging .dragging { .block .empty-placeholder-image-lg { height: 5rem; } +.block .searchbar .icon { + margin-right: 0; } + .block .block-cards .course-info-container { padding: 0.8rem; } diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index f7ee68e73ca..145a0291fa6 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -12868,6 +12868,9 @@ body.dragging .dragging { .block .empty-placeholder-image-lg { height: 5rem; } +.block .searchbar .icon { + margin-right: 0; } + .block .block-cards .course-info-container { padding: 0.8rem; }