Sindbad~EG File Manager
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Manage the courses view for the overview block.
*
* @copyright 2018 Bas Brands <bas@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'block_myoverview/repository',
'core/paged_content_factory',
'core/pubsub',
'core/custom_interaction_events',
'core/notification',
'core/templates',
'core_course/events',
'block_myoverview/selectors',
'core/paged_content_events',
'core/aria',
],
function(
$,
Repository,
PagedContentFactory,
PubSub,
CustomEvents,
Notification,
Templates,
CourseEvents,
Selectors,
PagedContentEvents,
Aria
) {
var SELECTORS = {
COURSE_REGION: '[data-region="course-view-content"]',
ACTION_HIDE_COURSE: '[data-action="hide-course"]',
ACTION_SHOW_COURSE: '[data-action="show-course"]',
ACTION_ADD_FAVOURITE: '[data-action="add-favourite"]',
ACTION_REMOVE_FAVOURITE: '[data-action="remove-favourite"]',
FAVOURITE_ICON: '[data-region="favourite-icon"]',
ICON_IS_FAVOURITE: '[data-region="is-favourite"]',
ICON_NOT_FAVOURITE: '[data-region="not-favourite"]',
PAGED_CONTENT_CONTAINER: '[data-region="page-container"]'
};
var TEMPLATES = {
COURSES_CARDS: 'block_myoverview/view-cards',
COURSES_LIST: 'block_myoverview/view-list',
COURSES_SUMMARY: 'block_myoverview/view-summary',
NOCOURSES: 'core_course/no-courses'
};
var GROUPINGS = {
GROUPING_ALLINCLUDINGHIDDEN: 'allincludinghidden',
GROUPING_ALL: 'all',
GROUPING_INPROGRESS: 'inprogress',
GROUPING_FUTURE: 'future',
GROUPING_PAST: 'past',
GROUPING_FAVOURITES: 'favourites',
GROUPING_HIDDEN: 'hidden'
};
var NUMCOURSES_PERPAGE = [12, 24, 48, 96, 0];
var loadedPages = [];
var courseOffset = 0;
var lastPage = 0;
var lastLimit = 0;
var namespace = null;
/**
* Get filter values from DOM.
*
* @param {object} root The root element for the courses view.
* @return {filters} Set filters.
*/
var getFilterValues = function(root) {
var courseRegion = root.find(Selectors.courseView.region);
return {
display: courseRegion.attr('data-display'),
grouping: courseRegion.attr('data-grouping'),
sort: courseRegion.attr('data-sort'),
displaycategories: courseRegion.attr('data-displaycategories'),
customfieldname: courseRegion.attr('data-customfieldname'),
customfieldvalue: courseRegion.attr('data-customfieldvalue'),
};
};
// We want the paged content controls below the paged content area.
// and the controls should be ignored while data is loading.
var DEFAULT_PAGED_CONTENT_CONFIG = {
ignoreControlWhileLoading: true,
controlPlacementBottom: true,
persistentLimitKey: 'block_myoverview_user_paging_preference'
};
/**
* Get enrolled courses from backend.
*
* @param {object} filters The filters for this view.
* @param {int} limit The number of courses to show.
* @return {promise} Resolved with an array of courses.
*/
var getMyCourses = function(filters, limit) {
return Repository.getEnrolledCoursesByTimeline({
offset: courseOffset,
limit: limit,
classification: filters.grouping,
sort: filters.sort,
customfieldname: filters.customfieldname,
customfieldvalue: filters.customfieldvalue
});
};
/**
* Get the container element for the favourite icon.
*
* @param {Object} root The course overview container
* @param {Number} courseId Course id number
* @return {Object} The favourite icon container
*/
var getFavouriteIconContainer = function(root, courseId) {
return root.find(SELECTORS.FAVOURITE_ICON + '[data-course-id="' + courseId + '"]');
};
/**
* Get the paged content container element.
*
* @param {Object} root The course overview container
* @param {Number} index Rendered page index.
* @return {Object} The rendered paged container.
*/
var getPagedContentContainer = function(root, index) {
return root.find('[data-region="paged-content-page"][data-page="' + index + '"]');
};
/**
* Get the course id from a favourite element.
*
* @param {Object} root The favourite icon container element.
* @return {Number} Course id.
*/
var getCourseId = function(root) {
return root.attr('data-course-id');
};
/**
* Hide the favourite icon.
*
* @param {Object} root The favourite icon container element.
* @param {Number} courseId Course id number.
*/
var hideFavouriteIcon = function(root, courseId) {
var iconContainer = getFavouriteIconContainer(root, courseId);
var isFavouriteIcon = iconContainer.find(SELECTORS.ICON_IS_FAVOURITE);
isFavouriteIcon.addClass('hidden');
Aria.hide(isFavouriteIcon);
var notFavourteIcon = iconContainer.find(SELECTORS.ICON_NOT_FAVOURITE);
notFavourteIcon.removeClass('hidden');
Aria.unhide(notFavourteIcon);
};
/**
* Show the favourite icon.
*
* @param {Object} root The course overview container.
* @param {Number} courseId Course id number.
*/
var showFavouriteIcon = function(root, courseId) {
var iconContainer = getFavouriteIconContainer(root, courseId);
var isFavouriteIcon = iconContainer.find(SELECTORS.ICON_IS_FAVOURITE);
isFavouriteIcon.removeClass('hidden');
Aria.unhide(isFavouriteIcon);
var notFavourteIcon = iconContainer.find(SELECTORS.ICON_NOT_FAVOURITE);
notFavourteIcon.addClass('hidden');
Aria.hide(notFavourteIcon);
};
/**
* Get the action menu item
*
* @param {Object} root root The course overview container
* @param {Number} courseId Course id.
* @return {Object} The add to favourite menu item.
*/
var getAddFavouriteMenuItem = function(root, courseId) {
return root.find('[data-action="add-favourite"][data-course-id="' + courseId + '"]');
};
/**
* Get the action menu item
*
* @param {Object} root root The course overview container
* @param {Number} courseId Course id.
* @return {Object} The remove from favourites menu item.
*/
var getRemoveFavouriteMenuItem = function(root, courseId) {
return root.find('[data-action="remove-favourite"][data-course-id="' + courseId + '"]');
};
/**
* Add course to favourites
*
* @param {Object} root The course overview container
* @param {Number} courseId Course id number
*/
var addToFavourites = function(root, courseId) {
var removeAction = getRemoveFavouriteMenuItem(root, courseId);
var addAction = getAddFavouriteMenuItem(root, courseId);
setCourseFavouriteState(courseId, true).then(function(success) {
if (success) {
PubSub.publish(CourseEvents.favourited, courseId);
removeAction.removeClass('hidden');
addAction.addClass('hidden');
showFavouriteIcon(root, courseId);
} else {
Notification.alert('Starring course failed', 'Could not change favourite state');
}
return;
}).catch(Notification.exception);
};
/**
* Remove course from favourites
*
* @param {Object} root The course overview container
* @param {Number} courseId Course id number
*/
var removeFromFavourites = function(root, courseId) {
var removeAction = getRemoveFavouriteMenuItem(root, courseId);
var addAction = getAddFavouriteMenuItem(root, courseId);
setCourseFavouriteState(courseId, false).then(function(success) {
if (success) {
PubSub.publish(CourseEvents.unfavorited, courseId);
removeAction.addClass('hidden');
addAction.removeClass('hidden');
hideFavouriteIcon(root, courseId);
} else {
Notification.alert('Starring course failed', 'Could not change favourite state');
}
return;
}).catch(Notification.exception);
};
/**
* Get the action menu item
*
* @param {Object} root root The course overview container
* @param {Number} courseId Course id.
* @return {Object} The hide course menu item.
*/
var getHideCourseMenuItem = function(root, courseId) {
return root.find('[data-action="hide-course"][data-course-id="' + courseId + '"]');
};
/**
* Get the action menu item
*
* @param {Object} root root The course overview container
* @param {Number} courseId Course id.
* @return {Object} The show course menu item.
*/
var getShowCourseMenuItem = function(root, courseId) {
return root.find('[data-action="show-course"][data-course-id="' + courseId + '"]');
};
/**
* Hide course
*
* @param {Object} root The course overview container
* @param {Number} courseId Course id number
*/
var hideCourse = function(root, courseId) {
var hideAction = getHideCourseMenuItem(root, courseId);
var showAction = getShowCourseMenuItem(root, courseId);
var filters = getFilterValues(root);
setCourseHiddenState(courseId, true);
// Remove the course from this view as it is now hidden and thus not covered by this view anymore.
// Do only if we are not in "All" view mode where really all courses are shown.
if (filters.grouping != GROUPINGS.GROUPING_ALLINCLUDINGHIDDEN) {
hideElement(root, courseId);
}
hideAction.addClass('hidden');
showAction.removeClass('hidden');
};
/**
* Show course
*
* @param {Object} root The course overview container
* @param {Number} courseId Course id number
*/
var showCourse = function(root, courseId) {
var hideAction = getHideCourseMenuItem(root, courseId);
var showAction = getShowCourseMenuItem(root, courseId);
var filters = getFilterValues(root);
setCourseHiddenState(courseId, null);
// Remove the course from this view as it is now shown again and thus not covered by this view anymore.
// Do only if we are not in "All" view mode where really all courses are shown.
if (filters.grouping != GROUPINGS.GROUPING_ALLINCLUDINGHIDDEN) {
hideElement(root, courseId);
}
hideAction.removeClass('hidden');
showAction.addClass('hidden');
};
/**
* Set the courses hidden status and push to repository
*
* @param {Number} courseId Course id to favourite.
* @param {Bool} status new hidden status.
* @return {Promise} Repository promise.
*/
var setCourseHiddenState = function(courseId, status) {
// If the given status is not hidden, the preference has to be deleted with a null value.
if (status === false) {
status = null;
}
return Repository.updateUserPreferences({
preferences: [
{
type: 'block_myoverview_hidden_course_' + courseId,
value: status
}
]
});
};
/**
* Reset the loadedPages dataset to take into account the hidden element
*
* @param {Object} root The course overview container
* @param {Number} id The course id number
*/
var hideElement = function(root, id) {
var pagingBar = root.find('[data-region="paging-bar"]');
var jumpto = parseInt(pagingBar.attr('data-active-page-number'));
// Get a reduced dataset for the current page.
var courseList = loadedPages[jumpto];
var reducedCourse = courseList.courses.reduce(function(accumulator, current) {
if (id != current.id) {
accumulator.push(current);
}
return accumulator;
}, []);
// Get the next page's data if loaded and pop the first element from it
if (loadedPages[jumpto + 1] != undefined) {
var newElement = loadedPages[jumpto + 1].courses.slice(0, 1);
// Adjust the dataset for the reset of the pages that are loaded
loadedPages.forEach(function(courseList, index) {
if (index > jumpto) {
var popElement = [];
if (loadedPages[index + 1] != undefined) {
popElement = loadedPages[index + 1].courses.slice(0, 1);
}
loadedPages[index].courses = $.merge(loadedPages[index].courses.slice(1), popElement);
}
});
reducedCourse = $.merge(reducedCourse, newElement);
}
// Check if the next page is the last page and if it still has data associated to it
if (lastPage == jumpto + 1 && loadedPages[jumpto + 1].courses.length == 0) {
var pagedContentContainer = root.find('[data-region="paged-content-container"]');
PagedContentFactory.resetLastPageNumber($(pagedContentContainer).attr('id'), jumpto);
}
loadedPages[jumpto].courses = reducedCourse;
// Reduce the course offset
courseOffset--;
// Render the paged content for the current
var pagedContentPage = getPagedContentContainer(root, jumpto);
renderCourses(root, loadedPages[jumpto]).then(function(html, js) {
return Templates.replaceNodeContents(pagedContentPage, html, js);
}).catch(Notification.exception);
// Delete subsequent pages in order to trigger the callback
loadedPages.forEach(function(courseList, index) {
if (index > jumpto) {
var page = getPagedContentContainer(root, index);
page.remove();
}
});
};
/**
* Set the courses favourite status and push to repository
*
* @param {Number} courseId Course id to favourite.
* @param {Bool} status new favourite status.
* @return {Promise} Repository promise.
*/
var setCourseFavouriteState = function(courseId, status) {
return Repository.setFavouriteCourses({
courses: [
{
'id': courseId,
'favourite': status
}
]
}).then(function(result) {
if (result.warnings.length == 0) {
loadedPages.forEach(function(courseList) {
courseList.courses.forEach(function(course, index) {
if (course.id == courseId) {
courseList.courses[index].isfavourite = status;
}
});
});
return true;
} else {
return false;
}
}).catch(Notification.exception);
};
/**
* Render the dashboard courses.
*
* @param {object} root The root element for the courses view.
* @param {array} coursesData containing array of returned courses.
* @return {promise} jQuery promise resolved after rendering is complete.
*/
var renderCourses = function(root, coursesData) {
var filters = getFilterValues(root);
var currentTemplate = '';
if (filters.display == 'card') {
currentTemplate = TEMPLATES.COURSES_CARDS;
} else if (filters.display == 'list') {
currentTemplate = TEMPLATES.COURSES_LIST;
} else {
currentTemplate = TEMPLATES.COURSES_SUMMARY;
}
// Whether the course category should be displayed in the course item.
coursesData.courses = coursesData.courses.map(function(course) {
course.showcoursecategory = filters.displaycategories == 'on' ? true : false;
return course;
});
if (coursesData.courses.length) {
return Templates.render(currentTemplate, {
courses: coursesData.courses,
});
} else {
var nocoursesimg = root.find(Selectors.courseView.region).attr('data-nocoursesimg');
return Templates.render(TEMPLATES.NOCOURSES, {
nocoursesimg: nocoursesimg
});
}
};
/**
* Return the callback to be passed to the subscribe event
*
* @param {Number} limit The paged limit that is passed through the event
*/
var setLimit = function(limit) {
this.find(Selectors.courseView.region).attr('data-paging', limit);
};
/**
* Intialise the paged list and cards views on page load.
* Returns an array of paged contents that we would like to handle here
*
* @param {object} root The root element for the courses view
* @param {string} namespace The namespace for all the events attached
*/
var registerPagedEventHandlers = function(root, namespace) {
var event = namespace + PagedContentEvents.SET_ITEMS_PER_PAGE_LIMIT;
PubSub.subscribe(event, setLimit.bind(root));
};
/**
* Intialise the courses list and cards views on page load.
*
* @param {object} root The root element for the courses view.
*/
var initializePagedContent = function(root) {
namespace = "block_myoverview_" + root.attr('id') + "_" + Math.random();
var pagingLimit = parseInt(root.find(Selectors.courseView.region).attr('data-paging'), 10);
var itemsPerPage = NUMCOURSES_PERPAGE.map(function(value) {
var active = false;
if (value == pagingLimit) {
active = true;
}
return {
value: value,
active: active
};
});
// Filter out all pagination options which are too large for the amount of courses user is enrolled in.
var totalCourseCount = parseInt(root.find(Selectors.courseView.region).attr('data-totalcoursecount'), 10);
itemsPerPage = itemsPerPage.filter(function(pagingOption) {
return pagingOption.value < totalCourseCount || pagingOption.value === 0;
});
var filters = getFilterValues(root);
var config = $.extend({}, DEFAULT_PAGED_CONTENT_CONFIG);
config.eventNamespace = namespace;
var pagedContentPromise = PagedContentFactory.createWithLimit(
itemsPerPage,
function(pagesData, actions) {
var promises = [];
pagesData.forEach(function(pageData) {
var currentPage = pageData.pageNumber;
var limit = (pageData.limit > 0) ? pageData.limit : 0;
// Reset local variables if limits have changed
if (lastLimit != limit) {
loadedPages = [];
courseOffset = 0;
lastPage = 0;
}
if (lastPage == currentPage) {
// If we are on the last page and have it's data then load it from cache
actions.allItemsLoaded(lastPage);
promises.push(renderCourses(root, loadedPages[currentPage]));
return;
}
lastLimit = limit;
// Get 2 pages worth of data as we will need it for the hidden functionality.
if (loadedPages[currentPage + 1] == undefined) {
if (loadedPages[currentPage] == undefined) {
limit *= 2;
}
}
var pagePromise = getMyCourses(
filters,
limit
).then(function(coursesData) {
var courses = coursesData.courses;
var nextPageStart = 0;
var pageCourses = [];
// If current page's data is loaded make sure we max it to page limit
if (loadedPages[currentPage] != undefined) {
pageCourses = loadedPages[currentPage].courses;
var currentPageLength = pageCourses.length;
if (currentPageLength < pageData.limit) {
nextPageStart = pageData.limit - currentPageLength;
pageCourses = $.merge(loadedPages[currentPage].courses, courses.slice(0, nextPageStart));
}
} else {
// When the page limit is zero, there is only one page of courses, no start for next page.
nextPageStart = pageData.limit || false;
pageCourses = (pageData.limit > 0) ? courses.slice(0, pageData.limit) : courses;
}
// Finished setting up the current page
loadedPages[currentPage] = {
courses: pageCourses
};
// Set up the next page (if there is more than one page).
var remainingCourses = nextPageStart !== false ? courses.slice(nextPageStart, courses.length) : [];
if (remainingCourses.length) {
loadedPages[currentPage + 1] = {
courses: remainingCourses
};
}
// Set the last page to either the current or next page
if (loadedPages[currentPage].courses.length < pageData.limit || !remainingCourses.length) {
lastPage = currentPage;
actions.allItemsLoaded(currentPage);
} else if (loadedPages[currentPage + 1] != undefined
&& loadedPages[currentPage + 1].courses.length < pageData.limit) {
lastPage = currentPage + 1;
}
courseOffset = coursesData.nextoffset;
return renderCourses(root, loadedPages[currentPage]);
})
.catch(Notification.exception);
promises.push(pagePromise);
});
return promises;
},
config
);
pagedContentPromise.then(function(html, js) {
registerPagedEventHandlers(root, namespace);
return Templates.replaceNodeContents(root.find(Selectors.courseView.region), html, js);
}).catch(Notification.exception);
};
/**
* Listen to, and handle events for the myoverview block.
*
* @param {Object} root The myoverview block container element.
*/
var registerEventListeners = function(root) {
CustomEvents.define(root, [
CustomEvents.events.activate
]);
root.on(CustomEvents.events.activate, SELECTORS.ACTION_ADD_FAVOURITE, function(e, data) {
var favourite = $(e.target).closest(SELECTORS.ACTION_ADD_FAVOURITE);
var courseId = getCourseId(favourite);
addToFavourites(root, courseId);
data.originalEvent.preventDefault();
});
root.on(CustomEvents.events.activate, SELECTORS.ACTION_REMOVE_FAVOURITE, function(e, data) {
var favourite = $(e.target).closest(SELECTORS.ACTION_REMOVE_FAVOURITE);
var courseId = getCourseId(favourite);
removeFromFavourites(root, courseId);
data.originalEvent.preventDefault();
});
root.on(CustomEvents.events.activate, SELECTORS.FAVOURITE_ICON, function(e, data) {
data.originalEvent.preventDefault();
});
root.on(CustomEvents.events.activate, SELECTORS.ACTION_HIDE_COURSE, function(e, data) {
var target = $(e.target).closest(SELECTORS.ACTION_HIDE_COURSE);
var courseId = getCourseId(target);
hideCourse(root, courseId);
data.originalEvent.preventDefault();
});
root.on(CustomEvents.events.activate, SELECTORS.ACTION_SHOW_COURSE, function(e, data) {
var target = $(e.target).closest(SELECTORS.ACTION_SHOW_COURSE);
var courseId = getCourseId(target);
showCourse(root, courseId);
data.originalEvent.preventDefault();
});
};
/**
* Intialise the courses list and cards views on page load.
*
* @param {object} root The root element for the courses view.
*/
var init = function(root) {
root = $(root);
loadedPages = [];
lastPage = 0;
courseOffset = 0;
initializePagedContent(root);
if (!root.attr('data-init')) {
registerEventListeners(root);
root.attr('data-init', true);
}
};
/**
* Reset the courses views to their original
*
* state on first page load.courseOffset
*
* This is called when configuration has changed for the event lists
* to cause them to reload their data.
*
* @param {Object} root The root element for the timeline view.
*/
var reset = function(root) {
if (loadedPages.length > 0) {
loadedPages.forEach(function(courseList, index) {
var pagedContentPage = getPagedContentContainer(root, index);
renderCourses(root, courseList).then(function(html, js) {
return Templates.replaceNodeContents(pagedContentPage, html, js);
}).catch(Notification.exception);
});
} else {
init(root);
}
};
return {
init: init,
reset: reset
};
});
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists