diff --git a/blocks/timeline/amd/build/event_list.min.js b/blocks/timeline/amd/build/event_list.min.js index 80f6e91d651..4dc15d4e858 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/str","core/user_date","block_timeline/calendar_events_repository"],function(a,b,c,d,e,f){var g=!1,h={EMPTY_MESSAGE:"[data-region=\"empty-message\"]",ROOT:"[data-region=\"event-list-container\"]",EVENT_LIST_CONTENT:"[data-region=\"event-list-content\"]",EVENT_LIST_WRAPPER:"[data-region=\"event-list-wrapper\"]",EVENT_LIST_LOADING_PLACEHOLDER:"[data-region=\"event-list-loading-placeholder\"]",TIMELINE_BLOCK:"[data-region=\"timeline\"]",TIMELINE_SEARCH:"[data-region=\"search-input\"]",MORE_ACTIVITIES_BUTTON:"[data-action=\"more-events\"]",MORE_ACTIVITIES_BUTTON_CONTAINER:"[data-region=\"more-events-button-container\"]"},i={EVENT_LIST_CONTENT:"block_timeline/event-list-content",MORE_ACTIVITIES_BUTTON:"block_timeline/event-list-loadmore",LOADING_ICON:"core/loading"},j=function(a){a.find(h.EVENT_LIST_CONTENT).addClass("hidden");a.find(h.EMPTY_MESSAGE).removeClass("hidden")},k=function(a){a.find(h.EVENT_LIST_CONTENT).removeClass("hidden");a.find(h.EMPTY_MESSAGE).addClass("hidden")},l=function(a){a.find(h.EVENT_LIST_CONTENT).empty()},m=function(a){var b={},c={courseview:g,eventsbyday:[]};a.forEach(function(a){var c=a.timeusermidnight;if(b[c]){b[c].push(a)}else{b[c]=[a]}});Object.keys(b).forEach(function(a){var d=b[a];c.eventsbyday.push({dayTimestamp:a,events:d})});return c},n=function(a){var b=m(a),d=i.EVENT_LIST_CONTENT;return c.render(d,b)},o=function(a,b,c,d,e,g,h){var i=d!=void 0?a+d*86400:!1,j={starttime:a+c*86400,limit:b};if(e){j.aftereventid=e}if(i){j.endtime=i}if(h){j.searchvalue=h}if(g){j.courseid=g;return f.queryByCourse(j)}else{return f.queryByTime(j)}},p=function(a,c,d,e,f,g,h,i,j){return q(a,d,e,f,g,h,i,j).then(function(a){if(a.calendarEvents.length){var b=a.calendarEvents.at(-1).id,d=a.calendarEvents.at(-1).timeusermidnight;c.resolve({hasContent:!0,lastId:b,lastTimeStamp:d,loadedAll:a.loadedAll});return n(a.calendarEvents,e)}else{c.resolve({hasContent:!1,lastId:0,lastTimeStamp:0,loadedAll:!0});return a.calendarEvents}}).catch(b.exception)},q=function(a,b,c,d,f,g,h,i){var j=o(c,b+1,g,h,d,f,i),k=[],l=!0;return j.then(function(d){if(!d.events.length){return{calendarEvents:k,loadedAll:l}}var f=document.querySelector("[data-filtername='overdue']"),g=f&&f.getAttribute("aria-current");k=d.events.filter(function(a){if("open"==a.eventtype||"opensubmission"==a.eventtype){var b=e.getUserMidnightForTimestamp(a.timesort,c);return b>c}return!g||a.overdue});l=k.length<=b;if(!l){k.pop()}if(k.length){var h=k.at(-1).id;t(a,h)}return{calendarEvents:k,loadedAll:l}})},r=function(d){var e=parseInt(d.attr("data-midnight"),10),f=d.attr("data-course-id"),g=parseInt(d.attr("data-days-offset"),10),j=d.attr("data-days-limit"),k=s(d),l=d.find(h.EVENT_LIST_WRAPPER),m=d.closest(h.TIMELINE_BLOCK).find(h.TIMELINE_SEARCH).val(),o=q(d,10,e,k,f,g,j,m);o.then(function(e){if(e.calendarEvents.length){var f=n(e.calendarEvents),g=u(d);f.then(function(b,f){b=a(b);b.find("[data-timestamp=\"".concat(g,"\"]")).remove();c.appendNodeContents(l,b.html(),f);if(!e.loadedAll){c.render(i.MORE_ACTIVITIES_BUTTON,{}).then(function(a){l.append(a);v(d,e.calendarEvents.at(-1).timeusermidnight);y(d);return a}).catch(function(){return!1})}return b}).catch(b.exception)}return e}).then(function(){return x(d)}).catch(b.exception)},s=function(a){return parseInt(a.attr("data-lazyload-offset"),10)},t=function(a,b){a.attr("data-lazyload-offset",b)},u=function(a){return parseInt(a.attr("data-timestamp"),10)},v=function(a,b){a.attr("data-timestamp",b)},w=function(a){var b=a.find(h.MORE_ACTIVITIES_BUTTON);b.prop("disabled",!0);c.render(i.LOADING_ICON,{}).then(function(a){b.append(a);return a}).catch(function(){return!1})},x=function(a){var b=a.find(h.MORE_ACTIVITIES_BUTTON_CONTAINER);b.remove()},y=function(a){var b=a.find(h.MORE_ACTIVITIES_BUTTON);b.on("click",function(){w(a);r(a)})};return{init:function init(d){var e=1c}return!g||a.overdue});l=k.length<=b;if(!l){k.pop()}if(k.length){var h=k.at(-1).id;u(a,h)}return{calendarEvents:k,loadedAll:l}})},s=function(d){var e=parseInt(d.attr("data-midnight"),10),f=d.attr("data-course-id"),g=parseInt(d.attr("data-days-offset"),10),h=d.attr("data-days-limit"),k=t(d),l=d.find(i.EVENT_LIST_WRAPPER),m=d.closest(i.TIMELINE_BLOCK).find(i.TIMELINE_SEARCH).val(),n=r(d,10,e,k,f,g,h,m);n.then(function(e){if(e.calendarEvents.length){var f=o(e.calendarEvents),g=v(d);f.then(function(b,f){b=a(b);b.find("[data-timestamp=\"".concat(g,"\"]")).remove();c.appendNodeContents(l,b.html(),f);if(!e.loadedAll){c.render(j.MORE_ACTIVITIES_BUTTON,{}).then(function(a){l.append(a);w(d,e.calendarEvents.at(-1).timeusermidnight);z(d);return a}).catch(function(){return!1})}return b}).catch(b.exception)}return e}).then(function(){return y(d)}).catch(b.exception)},t=function(a){return parseInt(a.attr("data-lazyload-offset"),10)},u=function(a,b){a.attr("data-lazyload-offset",b)},v=function(a){return parseInt(a.attr("data-timestamp"),10)},w=function(a,b){a.attr("data-timestamp",b)},x=function(a){var b=a.find(i.MORE_ACTIVITIES_BUTTON);b.prop("disabled",!0);c.render(j.LOADING_ICON,{}).then(function(a){b.append(a);return a}).catch(function(){return!1})},y=function(a){var b=a.find(i.MORE_ACTIVITIES_BUTTON_CONTAINER);b.remove()},z=function(a){var b=a.find(i.MORE_ACTIVITIES_BUTTON);b.on("click",function(){x(a);s(a)})};return{init:function init(d){var e=1.\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/str',\n 'core/user_date',\n 'block_timeline/calendar_events_repository'\n],\nfunction(\n $,\n Notification,\n Templates,\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_WRAPPER: '[data-region=\"event-list-wrapper\"]',\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 MORE_ACTIVITIES_BUTTON: '[data-action=\"more-events\"]',\n MORE_ACTIVITIES_BUTTON_CONTAINER: '[data-region=\"more-events-button-container\"]'\n };\n\n var TEMPLATES = {\n EVENT_LIST_CONTENT: 'block_timeline/event-list-content',\n MORE_ACTIVITIES_BUTTON: 'block_timeline/event-list-loadmore',\n LOADING_ICON: 'core/loading'\n };\n\n /** @type {number} The total items will be shown on the first load. */\n const DEFAULT_LAZY_LOADING_ITEMS_FIRST_LOAD = 5;\n /** @type {number} The total items will be shown when click on the Show more activities button. */\n const DEFAULT_LAZY_LOADING_ITEMS_OTHER_LOAD = 10;\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 * @return {object}\n */\n var buildTemplateContext = function(calendarEvents) {\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 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 * @return {promise} Resolved with HTML and JS strings.\n */\n var render = function(calendarEvents) {\n var templateContext = buildTemplateContext(calendarEvents);\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 * Create a lazy-loading region for the calendar events in the given root element.\n *\n * @param {object} root The event list container element.\n * @param {object} additionalConfig Additional config options to pass to pagedContentFactory.\n */\n var init = function(root, 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 // Create the lazy loading content element.\n return createLazyLoadingContent(root, firstLoad,\n DEFAULT_LAZY_LOADING_ITEMS_FIRST_LOAD, midnight, 0, courseId, daysOffset, daysLimit, searchValue)\n .then(function(html, js) {\n firstLoad.then(function(data) {\n if (!data.hasContent) {\n loadingPlaceholder.addClass('hidden');\n // If we didn't get any data then show the empty data message.\n return hideContent(root);\n }\n\n html = $(html);\n // Hide the content for now.\n html.addClass('hidden');\n // Replace existing elements with the newly created lazy-loading region.\n Templates.replaceNodeContents(eventListContent, html, js);\n\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 (!data.loadedAll) {\n Templates.render(TEMPLATES.MORE_ACTIVITIES_BUTTON, {}).then(function(html) {\n eventListContent.append(html);\n setLastTimestamp(root, data.lastTimeStamp);\n // Init the event handler.\n initEventListener(root);\n return html;\n }).catch(function() {\n return false;\n });\n }\n\n return data;\n })\n .catch(function() {\n return false;\n });\n\n return html;\n })\n .catch(Notification.exception);\n };\n\n /**\n * Create a lazy-loading content element for showing the event list for the initial load.\n *\n * @param {object} root The event list container element.\n * @param {object} firstLoad A jQuery promise to be resolved after the first set of data is loaded.\n * @param {int} itemLimit Limit the number of items.\n * @param {Number} midnight The user's midnight time in unix timestamp.\n * @param {int} lastId The last event ID for each loaded page. Page number is key, id is 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 const createLazyLoadingContent = (root, firstLoad, itemLimit, midnight, lastId,\n courseId, daysOffset, daysLimit, searchValue) => {\n return loadEventsForLazyLoading(\n root,\n itemLimit,\n midnight,\n lastId,\n courseId,\n daysOffset,\n daysLimit,\n searchValue\n ).then(data => {\n if (data.calendarEvents.length) {\n const lastEventId = data.calendarEvents.at(-1).id;\n const lastTimeStamp = data.calendarEvents.at(-1).timeusermidnight;\n firstLoad.resolve({\n hasContent: true,\n lastId: lastEventId,\n lastTimeStamp: lastTimeStamp,\n loadedAll: data.loadedAll\n });\n return render(data.calendarEvents, midnight);\n } else {\n firstLoad.resolve({\n hasContent: false,\n lastId: 0,\n lastTimeStamp: 0,\n loadedAll: true\n });\n return data.calendarEvents;\n }\n }).catch(Notification.exception);\n };\n\n /**\n * Handle the request from the lazy-loading region.\n * Uses the given data like course id, offset... to request the events from the server.\n *\n * @param {object} root The event list container element.\n * @param {int} itemLimit Limit the number of items.\n * @param {Number} midnight The user's midnight time in unix timestamp.\n * @param {int} lastId The last event ID for each loaded page.\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 const loadEventsForLazyLoading = (root, itemLimit, midnight, lastId, courseId, daysOffset, daysLimit, searchValue) => {\n // Load one more than the given limit so that we can tell if there\n // is more content to load after this.\n const eventsPromise = load(midnight, itemLimit + 1, daysOffset, daysLimit, lastId, courseId, searchValue);\n let calendarEvents = [];\n let loadedAll = true;\n\n return eventsPromise.then(result => {\n if (!result.events.length) {\n return {calendarEvents, loadedAll};\n }\n\n // Determine if the overdue filter is applied.\n const overdueFilter = document.querySelector(\"[data-filtername='overdue']\");\n const filterByOverdue = (overdueFilter && overdueFilter.getAttribute('aria-current'));\n\n calendarEvents = result.events.filter(event => {\n if (event.eventtype == 'open' || event.eventtype == 'opensubmission') {\n const dayTimestamp = UserDate.getUserMidnightForTimestamp(event.timesort, midnight);\n return dayTimestamp > midnight;\n }\n // When filtering by overdue, we fetch all events due today, in case any have elapsed already and are overdue.\n // This means if filtering by overdue, some events fetched might not be required (eg if due later today).\n return (!filterByOverdue || event.overdue);\n });\n\n loadedAll = calendarEvents.length <= itemLimit;\n\n if (!loadedAll) {\n // Remove the last element from the array because it isn't\n // needed in this result set.\n calendarEvents.pop();\n }\n\n if (calendarEvents.length) {\n const lastEventId = calendarEvents.at(-1).id;\n setOffset(root, lastEventId);\n }\n\n return {calendarEvents, loadedAll};\n });\n };\n\n /**\n * Load new events and append to current list.\n *\n * @param {object} root The event list container element.\n */\n const loadMoreEvents = root => {\n const midnight = parseInt(root.attr('data-midnight'), 10);\n const courseId = root.attr('data-course-id');\n const daysOffset = parseInt(root.attr('data-days-offset'), 10);\n const daysLimit = root.attr('data-days-limit');\n const lastId = getOffset(root);\n const eventListWrapper = root.find(SELECTORS.EVENT_LIST_WRAPPER);\n const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val();\n const eventsPromise = loadEventsForLazyLoading(\n root,\n DEFAULT_LAZY_LOADING_ITEMS_OTHER_LOAD,\n midnight,\n lastId,\n courseId,\n daysOffset,\n daysLimit,\n searchValue\n );\n eventsPromise.then(data => {\n if (data.calendarEvents.length) {\n const renderPromise = render(data.calendarEvents);\n const lastTimestamp = getLastTimestamp(root);\n renderPromise.then((html, js) => {\n html = $(html);\n\n // Remove the date heading if it has the same value as the previous one.\n html.find(`[data-timestamp=\"${lastTimestamp}\"]`).remove();\n Templates.appendNodeContents(eventListWrapper, html.html(), js);\n\n if (!data.loadedAll) {\n Templates.render(TEMPLATES.MORE_ACTIVITIES_BUTTON, {}).then(html => {\n eventListWrapper.append(html);\n setLastTimestamp(root, data.calendarEvents.at(-1).timeusermidnight);\n // Init the event handler.\n initEventListener(root);\n\n return html;\n }).catch(() => {\n return false;\n });\n }\n\n return html;\n }).catch(Notification.exception);\n }\n\n return data;\n }).then(() => {\n return disableMoreActivitiesButtonLoading(root);\n }).catch(Notification.exception);\n };\n\n /**\n * Return the offset value for lazy loading fetching.\n *\n * @param {object} element The event list container element.\n * @return {Number} Offset value.\n */\n const getOffset = element => {\n return parseInt(element.attr('data-lazyload-offset'), 10);\n };\n\n /**\n * Set the offset value for lazy loading fetching.\n *\n * @param {object} element The event list container element.\n * @param {Number} offset Offset value.\n */\n const setOffset = (element, offset) => {\n element.attr('data-lazyload-offset', offset);\n };\n\n /**\n * Return the timestamp value for lazy loading fetching.\n *\n * @param {object} element The event list container element.\n * @return {Number} Timestamp value.\n */\n const getLastTimestamp = element => {\n return parseInt(element.attr('data-timestamp'), 10);\n };\n\n /**\n * Set the timestamp value for lazy loading fetching.\n *\n * @param {object} element The event list container element.\n * @param {Number} timestamp Timestamp value.\n */\n const setLastTimestamp = (element, timestamp) => {\n element.attr('data-timestamp', timestamp);\n };\n\n /**\n * Add the \"Show more activities\" button and remove and loading spinner.\n *\n * @param {object} root The event list container element.\n */\n const enableMoreActivitiesButtonLoading = root => {\n const loadMoreButton = root.find(SELECTORS.MORE_ACTIVITIES_BUTTON);\n loadMoreButton.prop('disabled', true);\n Templates.render(TEMPLATES.LOADING_ICON, {}).then(html => {\n loadMoreButton.append(html);\n return html;\n }).catch(() => {\n // It's not important if this false so just do so silently.\n return false;\n });\n };\n\n /**\n * Remove the \"Show more activities\" button and remove and loading spinner.\n *\n * @param {object} root The event list container element.\n */\n const disableMoreActivitiesButtonLoading = root => {\n const loadMoreButtonContainer = root.find(SELECTORS.MORE_ACTIVITIES_BUTTON_CONTAINER);\n loadMoreButtonContainer.remove();\n };\n\n /**\n * Event initialise.\n *\n * @param {object} root The event list container element.\n */\n const initEventListener = root => {\n const loadMoreButton = root.find(SELECTORS.MORE_ACTIVITIES_BUTTON);\n loadMoreButton.on('click', () => {\n enableMoreActivitiesButtonLoading(root);\n loadMoreEvents(root);\n });\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","Str","UserDate","CalendarEventsRepository","Pending","courseview","SELECTORS","EMPTY_MESSAGE","ROOT","EVENT_LIST_CONTENT","EVENT_LIST_WRAPPER","EVENT_LIST_LOADING_PLACEHOLDER","TIMELINE_BLOCK","TIMELINE_SEARCH","MORE_ACTIVITIES_BUTTON","MORE_ACTIVITIES_BUTTON_CONTAINER","TEMPLATES","LOADING_ICON","hideContent","root","find","addClass","removeClass","showContent","emptyContent","empty","buildTemplateContext","calendarEvents","eventsByDay","templateContext","eventsbyday","forEach","calendarEvent","dayTimestamp","timeusermidnight","push","Object","keys","events","render","templateName","load","midnight","limit","daysOffset","daysLimit","lastId","courseId","searchValue","endTime","args","starttime","aftereventid","endtime","searchvalue","courseid","queryByCourse","queryByTime","createLazyLoadingContent","firstLoad","itemLimit","loadEventsForLazyLoading","then","data","length","lastEventId","at","id","lastTimeStamp","resolve","hasContent","loadedAll","catch","exception","eventsPromise","result","overdueFilter","document","querySelector","filterByOverdue","getAttribute","filter","event","eventtype","getUserMidnightForTimestamp","timesort","overdue","pop","setOffset","loadMoreEvents","parseInt","attr","getOffset","eventListWrapper","closest","val","renderPromise","lastTimestamp","getLastTimestamp","html","js","remove","appendNodeContents","append","setLastTimestamp","initEventListener","disableMoreActivitiesButtonLoading","element","offset","timestamp","enableMoreActivitiesButtonLoading","loadMoreButton","prop","loadMoreButtonContainer","on","init","additionalConfig","pendingPromise","Deferred","eventListContent","loadingPlaceholder","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/str',\n 'core/user_date',\n 'block_timeline/calendar_events_repository',\n 'core/pending'\n],\nfunction(\n $,\n Notification,\n Templates,\n Str,\n UserDate,\n CalendarEventsRepository,\n Pending\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_WRAPPER: '[data-region=\"event-list-wrapper\"]',\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 MORE_ACTIVITIES_BUTTON: '[data-action=\"more-events\"]',\n MORE_ACTIVITIES_BUTTON_CONTAINER: '[data-region=\"more-events-button-container\"]'\n };\n\n var TEMPLATES = {\n EVENT_LIST_CONTENT: 'block_timeline/event-list-content',\n MORE_ACTIVITIES_BUTTON: 'block_timeline/event-list-loadmore',\n LOADING_ICON: 'core/loading'\n };\n\n /** @type {number} The total items will be shown on the first load. */\n const DEFAULT_LAZY_LOADING_ITEMS_FIRST_LOAD = 5;\n /** @type {number} The total items will be shown when click on the Show more activities button. */\n const DEFAULT_LAZY_LOADING_ITEMS_OTHER_LOAD = 10;\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 * @return {object}\n */\n var buildTemplateContext = function(calendarEvents) {\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 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 * @return {promise} Resolved with HTML and JS strings.\n */\n var render = function(calendarEvents) {\n var templateContext = buildTemplateContext(calendarEvents);\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 * Create a lazy-loading region for the calendar events in the given root element.\n *\n * @param {object} root The event list container element.\n * @param {object} additionalConfig Additional config options to pass to pagedContentFactory.\n */\n var init = function(root, additionalConfig = {}) {\n const pendingPromise = new Pending('block/timeline:event-init');\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 // Create the lazy loading content element.\n return createLazyLoadingContent(root, firstLoad,\n DEFAULT_LAZY_LOADING_ITEMS_FIRST_LOAD, midnight, 0, courseId, daysOffset, daysLimit, searchValue)\n .then(function(html, js) {\n firstLoad.then(function(data) {\n if (!data.hasContent) {\n loadingPlaceholder.addClass('hidden');\n // If we didn't get any data then show the empty data message.\n return hideContent(root);\n }\n\n html = $(html);\n // Hide the content for now.\n html.addClass('hidden');\n // Replace existing elements with the newly created lazy-loading region.\n Templates.replaceNodeContents(eventListContent, html, js);\n\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 (!data.loadedAll) {\n Templates.render(TEMPLATES.MORE_ACTIVITIES_BUTTON, {}).then(function(html) {\n eventListContent.append(html);\n setLastTimestamp(root, data.lastTimeStamp);\n // Init the event handler.\n initEventListener(root);\n return html;\n }).catch(function() {\n return false;\n });\n }\n\n return data;\n })\n .catch(function() {\n return false;\n });\n\n return html;\n }).then(() => {\n return pendingPromise.resolve();\n })\n .catch(Notification.exception);\n };\n\n /**\n * Create a lazy-loading content element for showing the event list for the initial load.\n *\n * @param {object} root The event list container element.\n * @param {object} firstLoad A jQuery promise to be resolved after the first set of data is loaded.\n * @param {int} itemLimit Limit the number of items.\n * @param {Number} midnight The user's midnight time in unix timestamp.\n * @param {int} lastId The last event ID for each loaded page. Page number is key, id is 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 const createLazyLoadingContent = (root, firstLoad, itemLimit, midnight, lastId,\n courseId, daysOffset, daysLimit, searchValue) => {\n return loadEventsForLazyLoading(\n root,\n itemLimit,\n midnight,\n lastId,\n courseId,\n daysOffset,\n daysLimit,\n searchValue\n ).then(data => {\n if (data.calendarEvents.length) {\n const lastEventId = data.calendarEvents.at(-1).id;\n const lastTimeStamp = data.calendarEvents.at(-1).timeusermidnight;\n firstLoad.resolve({\n hasContent: true,\n lastId: lastEventId,\n lastTimeStamp: lastTimeStamp,\n loadedAll: data.loadedAll\n });\n return render(data.calendarEvents, midnight);\n } else {\n firstLoad.resolve({\n hasContent: false,\n lastId: 0,\n lastTimeStamp: 0,\n loadedAll: true\n });\n return data.calendarEvents;\n }\n }).catch(Notification.exception);\n };\n\n /**\n * Handle the request from the lazy-loading region.\n * Uses the given data like course id, offset... to request the events from the server.\n *\n * @param {object} root The event list container element.\n * @param {int} itemLimit Limit the number of items.\n * @param {Number} midnight The user's midnight time in unix timestamp.\n * @param {int} lastId The last event ID for each loaded page.\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 const loadEventsForLazyLoading = (root, itemLimit, midnight, lastId, courseId, daysOffset, daysLimit, searchValue) => {\n // Load one more than the given limit so that we can tell if there\n // is more content to load after this.\n const eventsPromise = load(midnight, itemLimit + 1, daysOffset, daysLimit, lastId, courseId, searchValue);\n let calendarEvents = [];\n let loadedAll = true;\n\n return eventsPromise.then(result => {\n if (!result.events.length) {\n return {calendarEvents, loadedAll};\n }\n\n // Determine if the overdue filter is applied.\n const overdueFilter = document.querySelector(\"[data-filtername='overdue']\");\n const filterByOverdue = (overdueFilter && overdueFilter.getAttribute('aria-current'));\n\n calendarEvents = result.events.filter(event => {\n if (event.eventtype == 'open' || event.eventtype == 'opensubmission') {\n const dayTimestamp = UserDate.getUserMidnightForTimestamp(event.timesort, midnight);\n return dayTimestamp > midnight;\n }\n // When filtering by overdue, we fetch all events due today, in case any have elapsed already and are overdue.\n // This means if filtering by overdue, some events fetched might not be required (eg if due later today).\n return (!filterByOverdue || event.overdue);\n });\n\n loadedAll = calendarEvents.length <= itemLimit;\n\n if (!loadedAll) {\n // Remove the last element from the array because it isn't\n // needed in this result set.\n calendarEvents.pop();\n }\n\n if (calendarEvents.length) {\n const lastEventId = calendarEvents.at(-1).id;\n setOffset(root, lastEventId);\n }\n\n return {calendarEvents, loadedAll};\n });\n };\n\n /**\n * Load new events and append to current list.\n *\n * @param {object} root The event list container element.\n */\n const loadMoreEvents = root => {\n const midnight = parseInt(root.attr('data-midnight'), 10);\n const courseId = root.attr('data-course-id');\n const daysOffset = parseInt(root.attr('data-days-offset'), 10);\n const daysLimit = root.attr('data-days-limit');\n const lastId = getOffset(root);\n const eventListWrapper = root.find(SELECTORS.EVENT_LIST_WRAPPER);\n const searchValue = root.closest(SELECTORS.TIMELINE_BLOCK).find(SELECTORS.TIMELINE_SEARCH).val();\n const eventsPromise = loadEventsForLazyLoading(\n root,\n DEFAULT_LAZY_LOADING_ITEMS_OTHER_LOAD,\n midnight,\n lastId,\n courseId,\n daysOffset,\n daysLimit,\n searchValue\n );\n eventsPromise.then(data => {\n if (data.calendarEvents.length) {\n const renderPromise = render(data.calendarEvents);\n const lastTimestamp = getLastTimestamp(root);\n renderPromise.then((html, js) => {\n html = $(html);\n\n // Remove the date heading if it has the same value as the previous one.\n html.find(`[data-timestamp=\"${lastTimestamp}\"]`).remove();\n Templates.appendNodeContents(eventListWrapper, html.html(), js);\n\n if (!data.loadedAll) {\n Templates.render(TEMPLATES.MORE_ACTIVITIES_BUTTON, {}).then(html => {\n eventListWrapper.append(html);\n setLastTimestamp(root, data.calendarEvents.at(-1).timeusermidnight);\n // Init the event handler.\n initEventListener(root);\n\n return html;\n }).catch(() => {\n return false;\n });\n }\n\n return html;\n }).catch(Notification.exception);\n }\n\n return data;\n }).then(() => {\n return disableMoreActivitiesButtonLoading(root);\n }).catch(Notification.exception);\n };\n\n /**\n * Return the offset value for lazy loading fetching.\n *\n * @param {object} element The event list container element.\n * @return {Number} Offset value.\n */\n const getOffset = element => {\n return parseInt(element.attr('data-lazyload-offset'), 10);\n };\n\n /**\n * Set the offset value for lazy loading fetching.\n *\n * @param {object} element The event list container element.\n * @param {Number} offset Offset value.\n */\n const setOffset = (element, offset) => {\n element.attr('data-lazyload-offset', offset);\n };\n\n /**\n * Return the timestamp value for lazy loading fetching.\n *\n * @param {object} element The event list container element.\n * @return {Number} Timestamp value.\n */\n const getLastTimestamp = element => {\n return parseInt(element.attr('data-timestamp'), 10);\n };\n\n /**\n * Set the timestamp value for lazy loading fetching.\n *\n * @param {object} element The event list container element.\n * @param {Number} timestamp Timestamp value.\n */\n const setLastTimestamp = (element, timestamp) => {\n element.attr('data-timestamp', timestamp);\n };\n\n /**\n * Add the \"Show more activities\" button and remove and loading spinner.\n *\n * @param {object} root The event list container element.\n */\n const enableMoreActivitiesButtonLoading = root => {\n const loadMoreButton = root.find(SELECTORS.MORE_ACTIVITIES_BUTTON);\n loadMoreButton.prop('disabled', true);\n Templates.render(TEMPLATES.LOADING_ICON, {}).then(html => {\n loadMoreButton.append(html);\n return html;\n }).catch(() => {\n // It's not important if this false so just do so silently.\n return false;\n });\n };\n\n /**\n * Remove the \"Show more activities\" button and remove and loading spinner.\n *\n * @param {object} root The event list container element.\n */\n const disableMoreActivitiesButtonLoading = root => {\n const loadMoreButtonContainer = root.find(SELECTORS.MORE_ACTIVITIES_BUTTON_CONTAINER);\n loadMoreButtonContainer.remove();\n };\n\n /**\n * Event initialise.\n *\n * @param {object} root The event list container element.\n */\n const initEventListener = root => {\n const loadMoreButton = root.find(SELECTORS.MORE_ACTIVITIES_BUTTON);\n loadMoreButton.on('click', () => {\n enableMoreActivitiesButtonLoading(root);\n loadMoreEvents(root);\n });\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/src/event_list.js b/blocks/timeline/amd/src/event_list.js index 745ba3a8ef6..b819fdd9b7c 100644 --- a/blocks/timeline/amd/src/event_list.js +++ b/blocks/timeline/amd/src/event_list.js @@ -28,7 +28,8 @@ define( 'core/templates', 'core/str', 'core/user_date', - 'block_timeline/calendar_events_repository' + 'block_timeline/calendar_events_repository', + 'core/pending' ], function( $, @@ -36,7 +37,8 @@ function( Templates, Str, UserDate, - CalendarEventsRepository + CalendarEventsRepository, + Pending ) { var SECONDS_IN_DAY = 60 * 60 * 24; @@ -215,6 +217,7 @@ function( * @param {object} additionalConfig Additional config options to pass to pagedContentFactory. */ var init = function(root, additionalConfig = {}) { + const pendingPromise = new Pending('block/timeline:event-init'); root = $(root); courseview = !!additionalConfig.courseview; @@ -286,6 +289,8 @@ function( }); return html; + }).then(() => { + return pendingPromise.resolve(); }) .catch(Notification.exception); }; diff --git a/blocks/timeline/tests/behat/block_timeline_courses.feature b/blocks/timeline/tests/behat/block_timeline_courses.feature index 0eed9d06cbe..94d17ad209e 100644 --- a/blocks/timeline/tests/behat/block_timeline_courses.feature +++ b/blocks/timeline/tests/behat/block_timeline_courses.feature @@ -42,8 +42,8 @@ Feature: The timeline block allows users to see upcoming courses And I click on "Next 30 days" "link" in the "Timeline" "block" And I click on "Sort timeline items" "button" in the "Timeline" "block" When I click on "Sort by courses" "link" in the "Timeline" "block" - Then I should see "Course 1" in the "Timeline" "block" - And I should see "Course 2" in the "Timeline" "block" + Then I should see "Course 1" in the ".block_timeline [data-region='view-courses']" "css_element" + And I should see "Course 2" in the ".block_timeline [data-region='view-courses']" "css_element" And I should see "Show more courses" in the "Timeline" "block" And "Test choice 1" "link" should exist in the "Timeline" "block" And I should see "Choice closes" in the "Timeline" "block"