first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+291
View File
@@ -0,0 +1,291 @@
// 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/>.
/**
* Module for viewing a discussion.
*
* @module mod_forum/discussion
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core/custom_interaction_events',
'mod_forum/selectors',
'core/pubsub',
'mod_forum/forum_events',
'core/str',
'core/notification',
],
function(
$,
CustomEvents,
Selectors,
PubSub,
ForumEvents,
String,
Notification
) {
/**
* Set the focus on the previous post in the list. Previous post is calculated
* based on position in list as viewed top to bottom.
*
* @param {Object} currentPost The post that currently has focus
*/
var focusPreviousPost = function(currentPost) {
// See if there is a previous sibling post.
var prevPost = currentPost.prev(Selectors.post.post);
if (prevPost.length) {
// The previous post might have replies that appear visually between
// it and the current post (see nested view) so if that's the case
// then the last reply will be the previous post in the list.
var replyPost = prevPost.find(Selectors.post.post).last();
if (replyPost.length) {
// Focus the last reply.
replyPost.focus();
} else {
// No replies so we can focus straight on the sibling.
prevPost.focus();
}
} else {
// If there are no siblings then jump up the tree to the parent
// post and focus the first parent post we find.
currentPost.parents(Selectors.post.post).first().focus();
}
};
/**
* Set the focus on the next post in the list. Previous post is calculated
* based on position in list as viewed top to bottom.
*
* @param {Object} currentPost The post that currently has focus
*/
var focusNextPost = function(currentPost) {
// The next post in the visual list would be the first reply to this one
// so let's see if we have one.
var replyPost = currentPost.find(Selectors.post.post).first();
if (replyPost.length) {
// Got a reply.
replyPost.focus();
} else {
// If we don't have a reply then the next post in the visual list would
// be a sibling post (replying to the same parent).
var siblingPost = currentPost.next(Selectors.post.post);
if (siblingPost.length) {
siblingPost.focus();
} else {
// No siblings either. That means we're the lowest level reply in a thread
// so we need to walk back up the tree of posts and find an ancestor post that
// has a sibling post we can focus.
var parentPosts = currentPost.parents(Selectors.post.post).toArray();
for (var i = 0; i < parentPosts.length; i++) {
var ancestorSiblingPost = $(parentPosts[i]).next(Selectors.post.post);
if (ancestorSiblingPost.length) {
ancestorSiblingPost.focus();
break;
}
}
}
}
};
/**
* Check if the element is inside the in page reply section.
*
* @param {Object} element The element to check
* @return {Boolean}
*/
var isElementInInPageReplySection = function(element) {
var inPageReply = $(element).closest(Selectors.post.inpageReplyContent);
return inPageReply.length ? true : false;
};
/**
* Initialise the keyboard accessibility controls for the discussion.
*
* @param {Object} root The discussion root element
*/
var initAccessibilityKeyboardNav = function(root) {
var posts = root.find(Selectors.post.post);
// Take each post action out of the tab index.
posts.each(function(index, post) {
var actions = $(post).find(Selectors.post.action);
var firstAction = actions.first();
actions.attr('tabindex', '-1');
firstAction.attr('tabindex', 0);
});
CustomEvents.define(root, [
CustomEvents.events.up,
CustomEvents.events.down,
CustomEvents.events.next,
CustomEvents.events.previous,
CustomEvents.events.home,
CustomEvents.events.end,
]);
root.on(CustomEvents.events.up, function(e, data) {
var activeElement = document.activeElement;
if (isElementInInPageReplySection(activeElement)) {
// Focus is currently inside the in page reply section so don't move focus
// to another post.
return;
}
var focusPost = $(activeElement).closest(Selectors.post.post);
if (focusPost.length) {
focusPreviousPost(focusPost);
} else {
root.find(Selectors.post.post).first().focus();
}
data.originalEvent.preventDefault();
});
root.on(CustomEvents.events.down, function(e, data) {
var activeElement = document.activeElement;
if (isElementInInPageReplySection(activeElement)) {
// Focus is currently inside the in page reply section so don't move focus
// to another post.
return;
}
var focusPost = $(activeElement).closest(Selectors.post.post);
if (focusPost.length) {
focusNextPost(focusPost);
} else {
root.find(Selectors.post.post).first().focus();
}
data.originalEvent.preventDefault();
});
root.on(CustomEvents.events.home, function(e, data) {
if (isElementInInPageReplySection(document.activeElement)) {
// Focus is currently inside the in page reply section so don't move focus
// to another post.
return;
}
root.find(Selectors.post.post).first().focus();
data.originalEvent.preventDefault();
});
root.on(CustomEvents.events.end, function(e, data) {
if (isElementInInPageReplySection(document.activeElement)) {
// Focus is currently inside the in page reply section so don't move focus
// to another post.
return;
}
root.find(Selectors.post.post).last().focus();
data.originalEvent.preventDefault();
});
root.on(CustomEvents.events.next, Selectors.post.action, function(e, data) {
var currentAction = $(e.target);
var container = currentAction.closest(Selectors.post.actionsContainer);
var actions = container.find(Selectors.post.action);
var nextAction = currentAction.next(Selectors.post.action);
actions.attr('tabindex', '-1');
if (!nextAction.length) {
nextAction = actions.first();
}
nextAction.attr('tabindex', 0);
nextAction.focus();
data.originalEvent.preventDefault();
});
root.on(CustomEvents.events.previous, Selectors.post.action, function(e, data) {
var currentAction = $(e.target);
var container = currentAction.closest(Selectors.post.actionsContainer);
var actions = container.find(Selectors.post.action);
var nextAction = currentAction.prev(Selectors.post.action);
actions.attr('tabindex', '-1');
if (!nextAction.length) {
nextAction = actions.last();
}
nextAction.attr('tabindex', 0);
nextAction.focus();
data.originalEvent.preventDefault();
});
root.on(CustomEvents.events.home, Selectors.post.action, function(e, data) {
var currentAction = $(e.target);
var container = currentAction.closest(Selectors.post.actionsContainer);
var actions = container.find(Selectors.post.action);
var firstAction = actions.first();
actions.attr('tabindex', '-1');
firstAction.attr('tabindex', 0);
firstAction.focus();
e.stopPropagation();
data.originalEvent.preventDefault();
});
root.on(CustomEvents.events.end, Selectors.post.action, function(e, data) {
var currentAction = $(e.target);
var container = currentAction.closest(Selectors.post.actionsContainer);
var actions = container.find(Selectors.post.action);
var lastAction = actions.last();
actions.attr('tabindex', '-1');
lastAction.attr('tabindex', 0);
lastAction.focus();
e.stopPropagation();
data.originalEvent.preventDefault();
});
PubSub.subscribe(ForumEvents.SUBSCRIPTION_TOGGLED, function(data) {
var subscribed = data.subscriptionState;
var updateMessage = subscribed ? 'discussionsubscribed' : 'discussionunsubscribed';
String.get_string(updateMessage, "forum")
.then(function(s) {
return Notification.addNotification({
message: s,
type: "info"
});
})
.catch(Notification.exception);
});
};
return {
init: function(root) {
initAccessibilityKeyboardNav(root);
}
};
});
+151
View File
@@ -0,0 +1,151 @@
// 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/>.
/**
* Module for the list of discussions on when viewing a forum.
*
* @module mod_forum/discussion_list
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([
'jquery',
'core/templates',
'core/str',
'core/notification',
'mod_forum/subscription_toggle',
'mod_forum/selectors',
'mod_forum/repository',
'core/pubsub',
'mod_forum/forum_events',
'core_form/changechecker',
], function(
$,
Templates,
Str,
Notification,
SubscriptionToggle,
Selectors,
Repository,
PubSub,
ForumEvents,
FormChangeChecker
) {
var registerEventListeners = function(root) {
PubSub.subscribe(ForumEvents.SUBSCRIPTION_TOGGLED, function(data) {
var discussionId = data.discussionId;
var subscribed = data.subscriptionState;
var discussionListItem = root.find(Selectors.discussion.item + '[data-discussionid= ' + discussionId + ']');
var subscribedLabel = discussionListItem.find(Selectors.discussion.subscribedLabel);
if (subscribed) {
discussionListItem.addClass('subscribed');
subscribedLabel.removeAttr('hidden');
} else {
discussionListItem.removeClass('subscribed');
subscribedLabel.attr('hidden', true);
}
});
root.on('click', Selectors.post.inpageCancelButton, function(e) {
// Tell formchangechecker to reset the form state.
FormChangeChecker.resetFormDirtyState(e.currentTarget);
});
root.on('click', Selectors.favourite.toggle, function(e) {
e.preventDefault();
var toggleElement = $(this);
var forumId = toggleElement.data('forumid');
var discussionId = toggleElement.data('discussionid');
var subscriptionState = toggleElement.data('targetstate');
Repository.setFavouriteDiscussionState(forumId, discussionId, subscriptionState)
.then(function() {
return location.reload();
})
.catch(Notification.exception);
});
root.on('click', Selectors.pin.toggle, function(e) {
e.preventDefault();
var toggleElement = $(this);
var forumId = toggleElement.data('forumid');
var discussionId = toggleElement.data('discussionid');
var state = toggleElement.data('targetstate');
Repository.setPinDiscussionState(forumId, discussionId, state)
.then(function() {
return location.reload();
})
.catch(Notification.exception);
});
root.on('click', Selectors.lock.toggle, function(e) {
var toggleElement = $(this);
var forumId = toggleElement.data('forumid');
var discussionId = toggleElement.data('discussionid');
var state = toggleElement.data('state');
Repository.setDiscussionLockState(forumId, discussionId, state)
.then(function(context) {
var icon = toggleElement.parents(Selectors.summary.actions).find(Selectors.lock.icon);
var lockedLabel = toggleElement.parents(Selectors.discussion.item).find(Selectors.discussion.lockedLabel);
if (context.locked) {
icon.removeClass('hidden');
lockedLabel.removeAttr('hidden');
} else {
icon.addClass('hidden');
lockedLabel.attr('hidden', true);
}
return context;
})
.then(function(context) {
context.forumid = forumId;
return Templates.render('mod_forum/discussion_lock_toggle', context);
})
.then(function(html, js) {
return Templates.replaceNode(toggleElement, html, js);
})
.then(function() {
return Str.get_string('lockupdated', 'forum')
.done(function(s) {
return Notification.addNotification({
message: s,
type: "info"
});
});
})
.catch(Notification.exception);
e.preventDefault();
});
};
return {
init: function(root) {
SubscriptionToggle.init(root, false, function(toggleElement, context) {
var toggleId = toggleElement.attr('id');
var newTargetState = context.userstate.subscribed ? 0 : 1;
toggleElement.data('targetstate', newTargetState);
var stringKey = context.userstate.subscribed ? 'unsubscribediscussion' : 'subscribediscussion';
return Str.get_string(stringKey, 'mod_forum')
.then(function(string) {
toggleElement.closest('td').find('label[for="' + toggleId + '"]').find('span').text(string);
return string;
});
});
registerEventListeners(root);
}
};
});
+430
View File
@@ -0,0 +1,430 @@
// 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/>.
/**
* Module for viewing a discussion in nested v2 view.
*
* @module mod_forum/discussion_nested_v2
* @copyright 2019 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import $ from 'jquery';
import AutoRows from 'core/auto_rows';
import CustomEvents from 'core/custom_interaction_events';
import * as FormChangeChecker from 'core_form/changechecker';
import Notification from 'core/notification';
import Templates from 'core/templates';
import Discussion from 'mod_forum/discussion';
import InPageReply from 'mod_forum/inpage_reply';
import LockToggle from 'mod_forum/lock_toggle';
import FavouriteToggle from 'mod_forum/favourite_toggle';
import Pin from 'mod_forum/pin_toggle';
import Selectors from 'mod_forum/selectors';
import Subscribe from 'mod_forum/subscription_toggle';
const ANIMATION_DURATION = 150;
/**
* Get the closest post container element from the given element.
*
* @param {Object} element jQuery element to search from
* @return {Object} jQuery element
*/
const getPostContainer = (element) => {
return element.closest(Selectors.post.post);
};
/**
* Get the closest post container element from the given element.
*
* @param {Object} element jQuery element to search from
* @param {Number} id Id of the post to find.
* @return {Object} jQuery element
*/
const getPostContainerById = (element, id) => {
return element.find(`${Selectors.post.post}[data-post-id=${id}]`);
};
/**
* Get the parent post container elements from the given element.
*
* @param {Object} element jQuery element to search from
* @return {Object} jQuery element
*/
const getParentPostContainers = (element) => {
return element.parents(Selectors.post.post);
};
/**
* Get the post content container element from the post container element.
*
* @param {Object} postContainer jQuery element for the post container
* @return {Object} jQuery element
*/
const getPostContentContainer = (postContainer) => {
return postContainer.children().not(Selectors.post.repliesContainer).find(Selectors.post.forumCoreContent);
};
/**
* Get the in page reply container element from the post container element.
*
* @param {Object} postContainer jQuery element for the post container
* @return {Object} jQuery element
*/
const getInPageReplyContainer = (postContainer) => {
return postContainer.children().filter(Selectors.post.inpageReplyContainer);
};
/**
* Get the in page reply form element from the post container element.
*
* @param {Object} postContainer jQuery element for the post container
* @return {Object} jQuery element
*/
const getInPageReplyForm = (postContainer) => {
return getInPageReplyContainer(postContainer).find(Selectors.post.inpageReplyContent);
};
/**
* Get the in page reply create (reply) button element from the post container element.
*
* @param {Object} postContainer jQuery element for the post container
* @return {Object} jQuery element
*/
const getInPageReplyCreateButton = (postContainer) => {
return getPostContentContainer(postContainer).find(Selectors.post.inpageReplyCreateButton);
};
/**
* Get the replies visibility toggle container (show/hide replies button container) element
* from the post container element.
*
* @param {Object} postContainer jQuery element for the post container
* @return {Object} jQuery element
*/
const getRepliesVisibilityToggleContainer = (postContainer) => {
return postContainer.children(Selectors.post.repliesVisibilityToggleContainer);
};
/**
* Get the replies container element from the post container element.
*
* @param {Object} postContainer jQuery element for the post container
* @return {Object} jQuery element
*/
const getRepliesContainer = (postContainer) => {
return postContainer.children(Selectors.post.repliesContainer);
};
/**
* Check if the post has any replies.
*
* @param {Object} postContainer jQuery element for the post container
* @return {Bool}
*/
const hasReplies = (postContainer) => {
return getRepliesContainer(postContainer).children().length > 0;
};
/**
* Get the show replies button element from the replies visibility toggle container element.
*
* @param {Object} replyVisibilityToggleContainer jQuery element for the toggle container
* @return {Object} jQuery element
*/
const getShowRepliesButton = (replyVisibilityToggleContainer) => {
return replyVisibilityToggleContainer.find(Selectors.post.showReplies);
};
/**
* Get the hide replies button element from the replies visibility toggle container element.
*
* @param {Object} replyVisibilityToggleContainer jQuery element for the toggle container
* @return {Object} jQuery element
*/
const getHideRepliesButton = (replyVisibilityToggleContainer) => {
return replyVisibilityToggleContainer.find(Selectors.post.hideReplies);
};
/**
* Check if the replies are visible.
*
* @param {Object} postContainer jQuery element for the post container
* @return {Bool}
*/
const repliesVisible = (postContainer) => {
const repliesContainer = getRepliesContainer(postContainer);
return repliesContainer.is(':visible');
};
/**
* Show the post replies.
*
* @param {Object} postContainer jQuery element for the post container
* @param {Number|null} postIdToSee Id of the post to scroll into view (if any)
*/
const showReplies = (postContainer, postIdToSee = null) => {
const repliesContainer = getRepliesContainer(postContainer);
const replyVisibilityToggleContainer = getRepliesVisibilityToggleContainer(postContainer);
const showButton = getShowRepliesButton(replyVisibilityToggleContainer);
const hideButton = getHideRepliesButton(replyVisibilityToggleContainer);
showButton.addClass('hidden');
hideButton.removeClass('hidden');
repliesContainer.slideDown({
duration: ANIMATION_DURATION,
queue: false,
complete: () => {
if (postIdToSee) {
const postContainerToSee = getPostContainerById(repliesContainer, postIdToSee);
if (postContainerToSee.length) {
postContainerToSee[0].scrollIntoView();
}
}
}
}).css('display', 'none').fadeIn(ANIMATION_DURATION);
};
/**
* Hide the post replies.
*
* @param {Object} postContainer jQuery element for the post container
*/
const hideReplies = (postContainer) => {
const repliesContainer = getRepliesContainer(postContainer);
const replyVisibilityToggleContainer = getRepliesVisibilityToggleContainer(postContainer);
const showButton = getShowRepliesButton(replyVisibilityToggleContainer);
const hideButton = getHideRepliesButton(replyVisibilityToggleContainer);
showButton.removeClass('hidden');
hideButton.addClass('hidden');
repliesContainer.slideUp({
duration: ANIMATION_DURATION,
queue: false
}).fadeOut(ANIMATION_DURATION);
};
/** Variable to hold the showInPageReplyForm function after it's built. */
let showInPageReplyForm = null;
/**
* Build the showInPageReplyForm function with the given additional template context.
*
* @param {Object} additionalTemplateContext Additional render context for the in page reply template.
* @return {Function}
*/
const buildShowInPageReplyFormFunction = (additionalTemplateContext) => {
/**
* Show the in page reply form in the given in page reply container. The form
* display will be animated.
*
* @param {Object} postContainer jQuery element for the post container
*/
return async(postContainer) => {
const inPageReplyContainer = getInPageReplyContainer(postContainer);
const repliesVisibilityToggleContainer = getRepliesVisibilityToggleContainer(postContainer);
const inPageReplyCreateButton = getInPageReplyCreateButton(postContainer);
if (!hasInPageReplyForm(inPageReplyContainer)) {
try {
const html = await renderInPageReplyTemplate(additionalTemplateContext, inPageReplyCreateButton, postContainer);
Templates.appendNodeContents(inPageReplyContainer, html, '');
} catch (e) {
Notification.exception(e);
}
FormChangeChecker.watchForm(postContainer[0].querySelector('form'));
}
inPageReplyCreateButton.fadeOut(ANIMATION_DURATION, () => {
const inPageReplyForm = getInPageReplyForm(postContainer);
inPageReplyForm.slideDown({
duration: ANIMATION_DURATION,
queue: false,
complete: () => {
inPageReplyForm.find('textarea').focus();
}
}).css('display', 'none').fadeIn(ANIMATION_DURATION);
if (repliesVisibilityToggleContainer.length && hasReplies(postContainer)) {
repliesVisibilityToggleContainer.fadeIn(ANIMATION_DURATION);
hideReplies(postContainer);
}
});
};
};
/**
* Hide the in page reply form in the given in page reply container. The form
* display will be animated.
*
* @param {Object} postContainer jQuery element for the post container
* @param {Number|null} postIdToSee Id of the post to scroll into view (if any)
*/
const hideInPageReplyForm = (postContainer, postIdToSee = null) => {
const inPageReplyForm = getInPageReplyForm(postContainer);
const inPageReplyCreateButton = getInPageReplyCreateButton(postContainer);
const repliesVisibilityToggleContainer = getRepliesVisibilityToggleContainer(postContainer);
if (repliesVisibilityToggleContainer.length && hasReplies(postContainer)) {
repliesVisibilityToggleContainer.fadeOut(ANIMATION_DURATION);
if (!repliesVisible(postContainer)) {
showReplies(postContainer, postIdToSee);
}
}
inPageReplyForm.slideUp({
duration: ANIMATION_DURATION,
queue: false,
complete: () => {
inPageReplyCreateButton.fadeIn(ANIMATION_DURATION);
}
}).fadeOut(200);
};
/**
* Check if the in page reply container contains the in page reply form.
*
* @param {Object} inPageReplyContainer jQuery element for the in page reply container
* @return {Bool}
*/
const hasInPageReplyForm = (inPageReplyContainer) => {
return inPageReplyContainer.find(Selectors.post.inpageReplyContent).length > 0;
};
/**
* Render the template to generate the in page reply form HTML.
*
* @param {Object} additionalTemplateContext Additional render context for the in page reply template
* @param {Object} button jQuery element for the reply button that was clicked
* @param {Object} postContainer jQuery element for the post container
* @return {Object} jQuery promise
*/
const renderInPageReplyTemplate = (additionalTemplateContext, button, postContainer) => {
const postContentContainer = getPostContentContainer(postContainer);
const currentSubject = postContentContainer.find(Selectors.post.forumSubject).text();
const currentAuthorName = postContentContainer.find(Selectors.post.authorName).text();
const context = {
postid: postContainer.data('post-id'),
"reply_url": button.attr('data-href'),
sesskey: M.cfg.sesskey,
parentsubject: currentSubject,
parentauthorname: currentAuthorName,
canreplyprivately: button.data('can-reply-privately'),
postformat: InPageReply.CONTENT_FORMATS.MOODLE,
...additionalTemplateContext
};
return Templates.render('mod_forum/inpage_reply_v2', context);
};
/**
* Increment the total reply count in the show/hide replies buttons for the post.
*
* @param {Object} postContainer jQuery element for the post container
*/
const incrementTotalReplyCount = (postContainer) => {
getRepliesVisibilityToggleContainer(postContainer).find(Selectors.post.replyCount).each((index, element) => {
const currentCount = parseInt(element.innerText, 10);
element.innerText = currentCount + 1;
});
};
/**
* Create all of the event listeners for the discussion.
*
* @param {Object} root jQuery element for the discussion container
*/
const registerEventListeners = (root) => {
CustomEvents.define(root, [CustomEvents.events.activate]);
// Auto expanding text area for in page reply.
AutoRows.init(root);
// Reply button is clicked.
root.on(CustomEvents.events.activate, Selectors.post.inpageReplyCreateButton, (e, data) => {
data.originalEvent.preventDefault();
const postContainer = getPostContainer($(e.currentTarget));
showInPageReplyForm(postContainer);
});
// Cancel in page reply button.
root.on(CustomEvents.events.activate, Selectors.post.inpageReplyCancelButton, (e, data) => {
data.originalEvent.preventDefault();
const postContainer = getPostContainer($(e.currentTarget));
hideInPageReplyForm(postContainer);
});
// Show replies button clicked.
root.on(CustomEvents.events.activate, Selectors.post.showReplies, (e, data) => {
data.originalEvent.preventDefault();
const postContainer = getPostContainer($(e.target));
showReplies(postContainer);
});
// Hide replies button clicked.
root.on(CustomEvents.events.activate, Selectors.post.hideReplies, (e, data) => {
data.originalEvent.preventDefault();
const postContainer = getPostContainer($(e.target));
hideReplies(postContainer);
});
// Post created with in page reply.
root.on(InPageReply.EVENTS.POST_CREATED, Selectors.post.inpageSubmitBtn, (e, newPostId) => {
const currentTarget = $(e.currentTarget);
const postContainer = getPostContainer(currentTarget);
const postContainers = getParentPostContainers(currentTarget);
hideInPageReplyForm(postContainer, newPostId);
postContainers.each((index, container) => {
incrementTotalReplyCount($(container));
});
});
};
/**
* Initialise the javascript for the discussion in nested v2 display mode.
*
* @param {Object} root jQuery element for the discussion container
* @param {Object} context Additional render context for the in page reply template
*/
export const init = (root, context) => {
// Build the showInPageReplyForm function with the additional render context.
showInPageReplyForm = buildShowInPageReplyFormFunction(context);
// Add discussion event listeners.
registerEventListeners(root);
// Initialise default discussion javascript (keyboard nav etc).
Discussion.init(root);
// Add in page reply javascript.
InPageReply.init(root);
// Initialise the settings menu javascript.
const discussionToolsContainer = root.find(Selectors.discussion.tools);
LockToggle.init(discussionToolsContainer, false);
FavouriteToggle.init(discussionToolsContainer, false, (toggleElement, response) => {
const newTargetState = response.userstate.favourited ? 0 : 1;
return toggleElement.data('targetstate', newTargetState);
});
Pin.init(discussionToolsContainer, false, (toggleElement, response) => {
const newTargetState = response.pinned ? 0 : 1;
return toggleElement.data('targetstate', newTargetState);
});
Subscribe.init(discussionToolsContainer, false, (toggleElement, response) => {
const newTargetState = response.userstate.subscribed ? 0 : 1;
toggleElement.data('targetstate', newTargetState);
});
};
+78
View File
@@ -0,0 +1,78 @@
// 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/>.
/**
* Handle discussion subscription toggling on a discussion list in
* the forum view.
*
* @module mod_forum/favourite_toggle
* @copyright 2019 Peter Dias <peter@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([
'jquery',
'core/templates',
'core/notification',
'mod_forum/repository',
'mod_forum/selectors',
'core/str',
], function(
$,
Templates,
Notification,
Repository,
Selectors,
String
) {
/**
* Register event listeners for the subscription toggle.
*
* @param {object} root The discussion list root element
* @param {boolean} preventDefault Should the default action of the event be prevented
* @param {function} callback Success callback
*/
var registerEventListeners = function(root, preventDefault, callback) {
root.on('click', Selectors.favourite.toggle, function(e) {
var toggleElement = $(this);
var forumId = toggleElement.data('forumid');
var discussionId = toggleElement.data('discussionid');
var subscriptionState = toggleElement.data('targetstate');
Repository.setFavouriteDiscussionState(forumId, discussionId, subscriptionState)
.then(function(context) {
return callback(toggleElement, context);
})
.then(function() {
return String.get_string("favouriteupdated", "forum")
.done(function(s) {
return Notification.addNotification({
message: s,
type: "info"
});
});
})
.catch(Notification.exception);
if (preventDefault) {
e.preventDefault();
}
});
};
return {
init: registerEventListeners
};
});
+79
View File
@@ -0,0 +1,79 @@
// 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/>.
/**
* Enrolled user selector module.
*
* @module mod_forum/form-user-selector
* @copyright 2019 Shamim Rezaie
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/ajax', 'core/templates'], function($, Ajax, Templates) {
return /** @alias module:mod_forum/form-user-selector */ {
processResults: function(selector, results) {
var users = [];
$.each(results, function(index, user) {
users.push({
value: user.id,
label: user._label
});
});
return users;
},
transport: function(selector, query, success, failure) {
var promise;
var courseid = $(selector).attr('courseid');
var contextid = $(selector).attr('data-contextid');
promise = Ajax.call([{
methodname: 'core_enrol_search_users',
args: {
courseid: courseid,
search: query,
searchanywhere: true,
page: 0,
perpage: 30,
contextid: contextid,
}
}]);
promise[0].then(function(results) {
var promises = [],
i = 0;
// Render the label.
$.each(results, function(index, user) {
promises.push(Templates.render('mod_forum/form-user-selector-suggestion', user));
});
// Apply the label to the results.
return $.when.apply($.when, promises).then(function() {
var args = arguments;
$.each(results, function(index, user) {
user._label = args[i];
i++;
});
success(results);
return;
});
}).fail(failure);
}
};
});
+27
View File
@@ -0,0 +1,27 @@
// 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/>.
/**
* Events for the forum activity.
*
* @module mod_forum/forum_events
* @copyright 2019 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([], function() {
return {
SUBSCRIPTION_TOGGLED: 'mod_forum/subscription_toggle:subscriptionToggled',
};
});
@@ -0,0 +1,127 @@
// 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/>.
/**
* This module handles the creation of a Modal that shows the user's post in context of the entire discussion.
*
* @module mod_forum/grades/expandconversation
* @copyright 2019 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import * as ForumSelectors from './grader/selectors';
import Repository from 'mod_forum/repository';
import {exception as showException} from "core/notification";
import Templates from 'core/templates';
import Modal from 'core/modal_cancel';
import * as ModalEvents from 'core/modal_events';
/**
* Find the Node containing the gradable details from the provided node by searching up the tree.
*
* @param {HTMLElement} node
* @returns {HTMLElement}
*/
const findGradableNode = node => node.closest(ForumSelectors.expandConversation);
/**
* Show the post in context in a modal.
*
* @param {HTMLElement} rootNode The button that has been clicked
* @param {object} param
* @param {bool} [param.focusOnClose=null]
*/
const showPostInContext = async(rootNode, {
focusOnClose = null,
} = {}) => {
const postId = rootNode.dataset.postid;
const discussionId = rootNode.dataset.discussionid;
const discussionName = rootNode.dataset.name;
const experimentalDisplayMode = rootNode.dataset.experimentalDisplayMode == "1";
const [
allPosts,
modal,
] = await Promise.all([
Repository.getDiscussionPosts(parseInt(discussionId)),
Modal.create({
title: discussionName,
large: true,
removeOnClose: true,
returnElement: focusOnClose,
}),
]);
const postsById = new Map(allPosts.posts.map(post => {
post.readonly = true;
post.hasreplies = false;
post.replies = [];
return [post.id, post];
}));
let posts = [];
allPosts.posts.forEach(post => {
if (post.parentid) {
const parent = postsById.get(post.parentid);
if (parent) {
post.parentauthorname = parent.author.fullname;
parent.hasreplies = true;
parent.replies.push(post);
} else {
posts.push(post);
}
} else {
posts.push(post);
}
});
modal.getRoot().on(ModalEvents.bodyRendered, () => {
const relevantPost = modal.getRoot()[0].querySelector(`#p${postId}`);
if (relevantPost) {
relevantPost.scrollIntoView({behavior: "smooth"});
}
});
modal.show();
// Note: We do not use await here because it messes with the Modal transitions.
const templatePromise = Templates.render('mod_forum/grades/grader/discussion/post_modal', {
posts,
experimentaldisplaymode: experimentalDisplayMode
});
modal.setBody(templatePromise);
};
/**
* Register event listeners for the expand conversations button.
*
* @param {HTMLElement} rootNode The root to listen to.
*/
export const registerEventListeners = (rootNode) => {
rootNode.addEventListener('click', (e) => {
const rootNode = findGradableNode(e.target);
if (rootNode) {
e.preventDefault();
try {
showPostInContext(rootNode, {
focusOnClose: e.target,
});
} catch (err) {
showException(err);
}
}
});
};
+231
View File
@@ -0,0 +1,231 @@
// 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/>.
/**
* This module will tie together all of the different calls the gradable module will make.
*
* @module mod_forum/grades/grader
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import * as Selectors from './grader/selectors';
import Repository from 'mod_forum/repository';
import Templates from 'core/templates';
import * as Grader from '../local/grades/grader';
import Notification from 'core/notification';
import CourseRepository from 'core_course/repository';
import {relativeUrl} from 'core/url';
const templateNames = {
contentRegion: 'mod_forum/grades/grader/discussion/posts',
};
/**
* Curried function with CMID set, this is then used in unified grader as a fetch a users content.
*
* @param {Number} cmid
* @param {Bool} experimentalDisplayMode
* @return {Function}
*/
const getContentForUserIdFunction = (cmid, experimentalDisplayMode) => (userid) => {
/**
* Given the parent function is called with the second param set execute the partially executed function.
*
* @param {Number} userid
*/
return Repository.getDiscussionByUserID(userid, cmid)
.then(context => {
// Rebuild the returned data for the template.
context.discussions = context.discussions.map(discussionPostMapper);
context.experimentaldisplaymode = experimentalDisplayMode ? true : false;
return Templates.render(templateNames.contentRegion, context);
})
.catch(Notification.exception);
};
/**
* Curried function with CMID set, this is then used in unified grader as a fetch users call.
* The function curried fetches all users in a course for a given CMID.
*
* @param {Number} courseID
* @param {Number} groupID
* @param {Boolean} onlyActive Whether to fetch only the active enrolled users or all enrolled users in the course.
* @return {Array} Array of users for a given context.
*/
const getGradableUsersForCourseidFunction = (courseID, groupID, onlyActive) => async() => {
const context = await CourseRepository.getGradableUsersFromCourseID(courseID, groupID, onlyActive);
return context.users;
};
const findGradableNode = node => node.closest(Selectors.gradableItem);
/**
* For a discussion we need to manipulate it's posts to hide certain UI elements.
*
* @param {Object} discussion
* @return {Array} name, id, posts
*/
const discussionPostMapper = (discussion) => {
// Map postid => post.
const parentMap = new Map();
discussion.posts.parentposts.forEach(post => parentMap.set(post.id, post));
const userPosts = discussion.posts.userposts.map(post => {
post.readonly = true;
post.hasreplies = false;
post.replies = [];
const parent = post.parentid ? parentMap.get(post.parentid) : null;
if (parent) {
parent.hasreplies = false;
parent.replies = [];
parent.readonly = true;
post.parentauthorname = parent.author.fullname;
}
return {
parent,
post
};
});
return {
...discussion,
posts: userPosts,
};
};
/**
* Launch the Grader.
*
* @param {HTMLElement} rootNode the root HTML element describing what is to be graded
* @param {object} param
* @param {bool} [param.focusOnClose=null]
*/
const launchWholeForumGrading = async(rootNode, {
focusOnClose = null,
} = {}) => {
const data = rootNode.dataset;
const gradingPanelFunctions = await Grader.getGradingPanelFunctions(
'mod_forum',
data.contextid,
data.gradingComponent,
data.gradingComponentSubtype,
data.gradableItemtype
);
const groupID = data.group ? data.group : 0;
const onlyActive = data.gradeOnlyActiveUsers;
await Grader.launch(
getGradableUsersForCourseidFunction(data.courseId, groupID, onlyActive),
getContentForUserIdFunction(data.cmid, data.experimentalDisplayMode == "1"),
gradingPanelFunctions.getter,
gradingPanelFunctions.setter,
{
groupid: data.groupid,
initialUserId: data.initialuserid,
moduleName: data.name,
courseName: data.courseName,
courseUrl: relativeUrl('/course/view.php', {id: data.courseId}),
sendStudentNotifications: data.sendStudentNotifications,
focusOnClose,
}
);
};
/**
* Launch the Grader.
*
* @param {HTMLElement} rootNode the root HTML element describing what is to be graded
* @param {object} param
* @param {bool} [param.focusOnClose=null]
*/
const launchViewGrading = async(rootNode, {
focusOnClose = null,
} = {}) => {
const data = rootNode.dataset;
const gradingPanelFunctions = await Grader.getGradingPanelFunctions(
'mod_forum',
data.contextid,
data.gradingComponent,
data.gradingComponentSubtype,
data.gradableItemtype
);
await Grader.view(
gradingPanelFunctions.getter,
data.userid,
data.name,
{
focusOnClose,
}
);
};
/**
* Register listeners to launch the grading panel.
*/
export const registerLaunchListeners = () => {
document.addEventListener('click', async(e) => {
if (e.target.matches(Selectors.launch)) {
const rootNode = findGradableNode(e.target);
if (!rootNode) {
throw Error('Unable to find a gradable item');
}
if (rootNode.matches(Selectors.gradableItems.wholeForum)) {
// Note: The preventDefault must be before any async function calls because the function becomes async
// at that point and the default action is implemented.
e.preventDefault();
try {
await launchWholeForumGrading(rootNode, {
focusOnClose: e.target,
});
} catch (error) {
Notification.exception(error);
}
} else {
throw Error('Unable to find a valid gradable item');
}
}
if (e.target.matches(Selectors.viewGrade)) {
e.preventDefault();
const rootNode = findGradableNode(e.target);
if (!rootNode) {
throw Error('Unable to find a gradable item');
}
if (rootNode.matches(Selectors.gradableItems.wholeForum)) {
// Note: The preventDefault must be before any async function calls because the function becomes async
// at that point and the default action is implemented.
e.preventDefault();
try {
await launchViewGrading(rootNode, {
focusOnClose: e.target,
});
} catch (error) {
Notification.exception(error);
}
} else {
throw Error('Unable to find a valid gradable item');
}
}
});
};
@@ -0,0 +1,32 @@
// 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/>.
/**
* This module will tie together all of the different calls the gradable module will make.
*
* @module mod_forum/grades/grader/selectors
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export default {
launch: '[data-grade-action="launch"]',
gradableItem: '[data-gradable-itemtype]',
gradableItems: {
wholeForum: '[data-gradable-itemtype="forum"]',
},
expandConversation: '[data-action="view-context"]',
posts: '[data-region="posts"]',
viewGrade: '[data-grade-action="view"]',
};
+206
View File
@@ -0,0 +1,206 @@
// 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/>.
/**
* This module handles the in page replying to forum posts.
*
* @module mod_forum/inpage_reply
* @copyright 2019 Peter Dias
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([
'jquery',
'core/templates',
'core/notification',
'mod_forum/repository',
'mod_forum/selectors',
'core_form/changechecker',
], function(
$,
Templates,
Notification,
Repository,
Selectors,
FormChangeChecker
) {
var DISPLAYCONSTANTS = {
NESTED_V2: 4,
THREADED: 2,
NESTED: 3,
FLAT_OLDEST_FIRST: 1,
FLAT_NEWEST_FIRST: -1
};
var EVENTS = {
POST_CREATED: 'mod_forum-post-created'
};
/**
* Moodle formats taken from the FORMAT_* constants declared in lib/weblib.php.
* @type {Object}
*/
var CONTENT_FORMATS = {
MOODLE: 0
};
/**
* Show the loading icon for the submit button.
*
* @param {Object} button The submit button element
*/
var showSubmitButtonLoadingIcon = function(button) {
var textContainer = button.find(Selectors.post.inpageSubmitBtnText);
var loadingIconContainer = button.find(Selectors.post.loadingIconContainer);
var width = button.outerWidth();
// Fix the width so that the button size doesn't change when we show the loading icon.
button.css('width', width);
textContainer.addClass('hidden');
loadingIconContainer.removeClass('hidden');
};
/**
* Hide the loading icon for the submit button.
*
* @param {Object} button The submit button element
*/
var hideSubmitButtonLoadingIcon = function(button) {
var textContainer = button.find(Selectors.post.inpageSubmitBtnText);
var loadingIconContainer = button.find(Selectors.post.loadingIconContainer);
// Reset the width back to it's default.
button.css('width', '');
textContainer.removeClass('hidden');
loadingIconContainer.addClass('hidden');
};
/**
* Register the event listeners for the submit/cancel buttons of the in page reply.
*
* @param {Object} root The discussion container element.
*/
var registerEventListeners = function(root) {
root.on('click', Selectors.post.inpageSubmitBtn, function(e) {
e.preventDefault();
var submitButton = $(e.currentTarget);
var allButtons = submitButton.parent().find(Selectors.post.inpageReplyButton);
var form = submitButton.parents(Selectors.post.inpageReplyForm).get(0);
var message = form.elements.post.value.trim();
// For now, we consider the inline reply post written using the FORMAT_MOODLE (because a textarea is displayed).
// In the future, other formats should be supported, letting users to use their preferred editor and format.
var messageformat = CONTENT_FORMATS.MOODLE;
// The message post will be converted from messageformat to FORMAT_HTML.
var topreferredformat = true;
var postid = form.elements.reply.value;
var subject = form.elements.subject.value;
var currentRoot = submitButton.closest(Selectors.post.post);
var isprivatereply = form.elements.privatereply != undefined ? form.elements.privatereply.checked : false;
var modeSelector = root.find(Selectors.post.modeSelect);
var mode = modeSelector.length ? parseInt(modeSelector.get(0).value) : null;
var newid;
if (message.length) {
showSubmitButtonLoadingIcon(submitButton);
allButtons.prop('disabled', true);
Repository.addDiscussionPost(postid, subject, message, messageformat, isprivatereply, topreferredformat)
.then(function(context) {
var message = context.messages.reduce(function(carry, message) {
if (message.type == 'success') {
carry += '<p>' + message.message + '</p>';
}
return carry;
}, '');
Notification.addNotification({
message: message,
type: "success"
});
return context;
})
.then(function(context) {
form.reset();
var post = context.post;
newid = post.id;
switch (mode) {
case DISPLAYCONSTANTS.NESTED_V2:
var capabilities = post.capabilities;
var currentAuthorName = currentRoot.children()
.not(Selectors.post.repliesContainer)
.find(Selectors.post.authorName)
.text();
post.parentauthorname = currentAuthorName;
post.showactionmenu = capabilities.view ||
capabilities.controlreadstatus ||
capabilities.edit ||
capabilities.split ||
capabilities.delete ||
capabilities.export ||
post.urls.viewparent;
return Templates.render('mod_forum/forum_discussion_nested_v2_post_reply', post);
case DISPLAYCONSTANTS.THREADED:
return Templates.render('mod_forum/forum_discussion_threaded_post', post);
case DISPLAYCONSTANTS.NESTED:
return Templates.render('mod_forum/forum_discussion_nested_post', post);
default:
return Templates.render('mod_forum/forum_discussion_post', post);
}
})
.then(function(html, js) {
var repliesnode = currentRoot.find(Selectors.post.repliesContainer).first();
if (mode == DISPLAYCONSTANTS.FLAT_NEWEST_FIRST) {
return Templates.prependNodeContents(repliesnode, html, js);
} else {
return Templates.appendNodeContents(repliesnode, html, js);
}
})
.then(function() {
submitButton.trigger(EVENTS.POST_CREATED, newid);
hideSubmitButtonLoadingIcon(submitButton);
allButtons.prop('disabled', false);
// Tell formchangechecker we submitted the form.
FormChangeChecker.resetFormDirtyState(submitButton[0]);
return currentRoot.find(Selectors.post.inpageReplyContent).hide();
})
.then(function() {
location.href = "#p" + newid;
// Reload the page, say if threshold is being set by user those would get reflected through the templates.
location.reload();
})
.catch(function(error) {
hideSubmitButtonLoadingIcon(submitButton);
allButtons.prop('disabled', false);
return Notification.exception(error);
});
}
});
root.on('click', Selectors.post.inpageCancelButton, function(e) {
// Tell formchangechecker to reset the form state.
FormChangeChecker.resetFormDirtyState(e.currentTarget);
});
};
return {
init: function(root) {
registerEventListeners(root);
},
CONTENT_FORMATS: CONTENT_FORMATS,
EVENTS: EVENTS
};
});
+514
View File
@@ -0,0 +1,514 @@
// 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/>.
/**
* This module will tie together all of the different calls the gradable module will make.
*
* @module mod_forum/local/grades/grader
* @copyright 2019 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Templates from 'core/templates';
import Selectors from './local/grader/selectors';
import getUserPicker from './local/grader/user_picker';
import {createLayout as createFullScreenWindow} from 'mod_forum/local/layout/fullscreen';
import getGradingPanelFunctions from './local/grader/gradingpanel';
import {add as addToast} from 'core/toast';
import {addNotification} from 'core/notification';
import {getString} from 'core/str';
import {failedUpdate} from 'core_grades/grades/grader/gradingpanel/normalise';
import {addIconToContainerWithPromise} from 'core/loadingicon';
import {debounce} from 'core/utils';
import {fillInitialValues} from 'core_grades/grades/grader/gradingpanel/comparison';
import Modal from 'core/modal_cancel';
import {subscribe} from 'core/pubsub';
import DrawerEvents from 'core/drawer_events';
const templateNames = {
grader: {
app: 'mod_forum/local/grades/grader',
gradingPanel: {
error: 'mod_forum/local/grades/local/grader/gradingpanel/error',
},
searchResults: 'mod_forum/local/grades/local/grader/user_picker/user_search',
status: 'mod_forum/local/grades/local/grader/status',
},
};
/**
* Helper function that replaces the user picker placeholder with what we get back from the user picker class.
*
* @param {HTMLElement} root
* @param {String} html
*/
const displayUserPicker = (root, html) => {
const pickerRegion = root.querySelector(Selectors.regions.pickerRegion);
Templates.replaceNodeContents(pickerRegion, html, '');
};
/**
* To be removed, this is now done as a part of Templates.renderForPromise()
*
* @param {String} html
* @param {String} js
* @returns {array} An array containing the HTML, and JS.
*/
const fetchContentFromRender = (html, js) => {
return [html, js];
};
/**
* Here we build the function that is passed to the user picker that'll handle updating the user content area
* of the grading interface.
*
* @param {HTMLElement} root
* @param {Function} getContentForUser
* @param {Function} getGradeForUser
* @param {Function} saveGradeForUser
* @return {Function}
*/
const getUpdateUserContentFunction = (root, getContentForUser, getGradeForUser, saveGradeForUser) => {
let firstLoad = true;
return async(user) => {
const spinner = firstLoad ? null : addIconToContainerWithPromise(root);
const [
[html, js],
userGrade,
] = await Promise.all([
getContentForUser(user.id).then(fetchContentFromRender),
getGradeForUser(user.id),
]);
Templates.replaceNodeContents(root.querySelector(Selectors.regions.moduleReplace), html, js);
const [
gradingPanelHtml,
gradingPanelJS
] = await Templates.render(userGrade.templatename, userGrade.grade).then(fetchContentFromRender);
const panelContainer = root.querySelector(Selectors.regions.gradingPanelContainer);
const panel = panelContainer.querySelector(Selectors.regions.gradingPanel);
Templates.replaceNodeContents(panel, gradingPanelHtml, gradingPanelJS);
const form = panel.querySelector('form');
fillInitialValues(form);
form.addEventListener('submit', event => {
saveGradeForUser(user);
event.preventDefault();
});
panelContainer.scrollTop = 0;
firstLoad = false;
if (spinner) {
spinner.resolve();
}
return userGrade;
};
};
/**
* Show the search results container and hide the user picker and body content.
*
* @param {HTMLElement} bodyContainer The container element for the body content
* @param {HTMLElement} userPickerContainer The container element for the user picker
* @param {HTMLElement} searchResultsContainer The container element for the search results
*/
const showSearchResultContainer = (bodyContainer, userPickerContainer, searchResultsContainer) => {
bodyContainer.classList.add('hidden');
userPickerContainer.classList.add('hidden');
searchResultsContainer.classList.remove('hidden');
};
/**
* Hide the search results container and show the user picker and body content.
*
* @param {HTMLElement} bodyContainer The container element for the body content
* @param {HTMLElement} userPickerContainer The container element for the user picker
* @param {HTMLElement} searchResultsContainer The container element for the search results
*/
const hideSearchResultContainer = (bodyContainer, userPickerContainer, searchResultsContainer) => {
bodyContainer.classList.remove('hidden');
userPickerContainer.classList.remove('hidden');
searchResultsContainer.classList.add('hidden');
};
/**
* Toggles the visibility of the user search.
*
* @param {HTMLElement} toggleSearchButton The button that toggles the search
* @param {HTMLElement} searchContainer The container element for the user search
* @param {HTMLElement} searchInput The input element for searching
*/
const showUserSearchInput = (toggleSearchButton, searchContainer, searchInput) => {
searchContainer.classList.remove('collapsed');
toggleSearchButton.setAttribute('aria-expanded', 'true');
toggleSearchButton.classList.add('expand');
toggleSearchButton.classList.remove('collapse');
// Hide the grading info container from screen reader.
const gradingInfoContainer = searchContainer.parentElement.querySelector(Selectors.regions.gradingInfoContainer);
gradingInfoContainer.setAttribute('aria-hidden', 'true');
// Hide the collapse grading drawer button from screen reader.
const collapseGradingDrawer = searchContainer.parentElement.querySelector(Selectors.buttons.collapseGradingDrawer);
collapseGradingDrawer.setAttribute('aria-hidden', 'true');
collapseGradingDrawer.setAttribute('tabindex', '-1');
searchInput.focus();
};
/**
* Toggles the visibility of the user search.
*
* @param {HTMLElement} toggleSearchButton The button that toggles the search
* @param {HTMLElement} searchContainer The container element for the user search
* @param {HTMLElement} searchInput The input element for searching
*/
const hideUserSearchInput = (toggleSearchButton, searchContainer, searchInput) => {
searchContainer.classList.add('collapsed');
toggleSearchButton.setAttribute('aria-expanded', 'false');
toggleSearchButton.classList.add('collapse');
toggleSearchButton.classList.remove('expand');
toggleSearchButton.focus();
// Show the grading info container to screen reader.
const gradingInfoContainer = searchContainer.parentElement.querySelector(Selectors.regions.gradingInfoContainer);
gradingInfoContainer.removeAttribute('aria-hidden');
// Show the collapse grading drawer button from screen reader.
const collapseGradingDrawer = searchContainer.parentElement.querySelector(Selectors.buttons.collapseGradingDrawer);
collapseGradingDrawer.removeAttribute('aria-hidden');
collapseGradingDrawer.setAttribute('tabindex', '0');
searchInput.value = '';
};
/**
* Find the list of users who's names include the given search term.
*
* @param {Array} userList List of users for the grader
* @param {String} searchTerm The search term to match
* @return {Array}
*/
const searchForUsers = (userList, searchTerm) => {
if (searchTerm === '') {
return userList;
}
searchTerm = searchTerm.toLowerCase();
return userList.filter((user) => {
return user.fullname.toLowerCase().includes(searchTerm);
});
};
/**
* Render the list of users in the search results area.
*
* @param {HTMLElement} searchResultsContainer The container element for search results
* @param {Array} users The list of users to display
*/
const renderSearchResults = async(searchResultsContainer, users) => {
const {html, js} = await Templates.renderForPromise(templateNames.grader.searchResults, {users});
Templates.replaceNodeContents(searchResultsContainer, html, js);
};
/**
* Add click handlers to the buttons in the header of the grading interface.
*
* @param {HTMLElement} graderLayout
* @param {Object} userPicker
* @param {Function} saveGradeFunction
* @param {Array} userList List of users for the grader.
*/
const registerEventListeners = (graderLayout, userPicker, saveGradeFunction, userList) => {
const graderContainer = graderLayout.getContainer();
const toggleSearchButton = graderContainer.querySelector(Selectors.buttons.toggleSearch);
const searchInputContainer = graderContainer.querySelector(Selectors.regions.userSearchContainer);
const searchInput = searchInputContainer.querySelector(Selectors.regions.userSearchInput);
const bodyContainer = graderContainer.querySelector(Selectors.regions.bodyContainer);
const userPickerContainer = graderContainer.querySelector(Selectors.regions.pickerRegion);
const searchResultsContainer = graderContainer.querySelector(Selectors.regions.searchResultsContainer);
graderContainer.addEventListener('click', (e) => {
if (e.target.closest(Selectors.buttons.toggleFullscreen)) {
e.stopImmediatePropagation();
e.preventDefault();
graderLayout.toggleFullscreen();
return;
}
if (e.target.closest(Selectors.buttons.closeGrader)) {
e.stopImmediatePropagation();
e.preventDefault();
graderLayout.close();
return;
}
if (e.target.closest(Selectors.buttons.saveGrade)) {
saveGradeFunction(userPicker.currentUser);
}
if (e.target.closest(Selectors.buttons.toggleSearch)) {
if (toggleSearchButton.getAttribute('aria-expanded') === 'true') {
// Search is open so let's close it.
hideUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);
hideSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);
searchResultsContainer.innerHTML = '';
} else {
// Search is closed so let's open it.
showUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);
showSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);
renderSearchResults(searchResultsContainer, userList);
}
return;
}
const selectUserButton = e.target.closest(Selectors.buttons.selectUser);
if (selectUserButton) {
const userId = selectUserButton.getAttribute('data-userid');
const user = userList.find(user => user.id == userId);
userPicker.setUserId(userId);
userPicker.showUser(user);
hideUserSearchInput(toggleSearchButton, searchInputContainer, searchInput);
hideSearchResultContainer(bodyContainer, userPickerContainer, searchResultsContainer);
searchResultsContainer.innerHTML = '';
}
});
// Debounce the search input so that it only executes 300 milliseconds after the user has finished typing.
searchInput.addEventListener('input', debounce(() => {
const users = searchForUsers(userList, searchInput.value);
renderSearchResults(searchResultsContainer, users);
}, 300));
// Remove the right margin of the content container when the grading panel is hidden so that it expands to full-width.
subscribe(DrawerEvents.DRAWER_HIDDEN, (drawerRoot) => {
const gradingPanel = drawerRoot[0];
if (gradingPanel.querySelector(Selectors.regions.gradingPanel)) {
setContentContainerMargin(graderContainer, 0);
}
});
// Bring back the right margin of the content container when the grading panel is shown to give space for the grading panel.
subscribe(DrawerEvents.DRAWER_SHOWN, (drawerRoot) => {
const gradingPanel = drawerRoot[0];
if (gradingPanel.querySelector(Selectors.regions.gradingPanel)) {
setContentContainerMargin(graderContainer, gradingPanel.offsetWidth);
}
});
};
/**
* Adjusts the right margin of the content container.
*
* @param {HTMLElement} graderContainer The container for the grader app.
* @param {Number} rightMargin The right margin value.
*/
const setContentContainerMargin = (graderContainer, rightMargin) => {
const contentContainer = graderContainer.querySelector(Selectors.regions.moduleContainer);
if (contentContainer) {
contentContainer.style.marginRight = `${rightMargin}px`;
}
};
/**
* Get the function used to save a user grade.
*
* @param {HTMLElement} root The container for the grader
* @param {Function} setGradeForUser The function that will be called.
* @return {Function}
*/
const getSaveUserGradeFunction = (root, setGradeForUser) => {
return async(user) => {
try {
root.querySelector(Selectors.regions.gradingPanelErrors).innerHTML = '';
const result = await setGradeForUser(
user.id,
root.querySelector(Selectors.values.sendStudentNotifications).value,
root.querySelector(Selectors.regions.gradingPanel)
);
if (result.success) {
addToast(await getString('grades:gradesavedfor', 'mod_forum', user));
}
if (result.failed) {
displayGradingError(root, user, result.error);
}
return result;
} catch (err) {
displayGradingError(root, user, err);
return failedUpdate(err);
}
};
};
/**
* Display a grading error, typically from a failed save.
*
* @param {HTMLElement} root The container for the grader
* @param {Object} user The user who was errored
* @param {Object} err The details of the error
*/
const displayGradingError = async(root, user, err) => {
const [
{html, js},
errorString
] = await Promise.all([
Templates.renderForPromise(templateNames.grader.gradingPanel.error, {error: err}),
await getString('grades:gradesavefailed', 'mod_forum', {error: err.message, ...user}),
]);
Templates.replaceNodeContents(root.querySelector(Selectors.regions.gradingPanelErrors), html, js);
addToast(errorString, {type: 'warning'});
};
/**
* Launch the grader interface with the specified parameters.
*
* @param {Function} getListOfUsers A function to get the list of users
* @param {Function} getContentForUser A function to get the content for a specific user
* @param {Function} getGradeForUser A function get the grade details for a specific user
* @param {Function} setGradeForUser A function to set the grade for a specific user
* @param {Object} preferences Preferences for the launch function
* @param {Number} preferences.initialUserId
* @param {string} preferences.moduleName
* @param {string} preferences.courseName
* @param {string} preferences.courseUrl
* @param {boolean} preferences.sendStudentNotifications
* @param {null|HTMLElement} preferences.focusOnClose
*/
export const launch = async(getListOfUsers, getContentForUser, getGradeForUser, setGradeForUser, {
initialUserId = null,
moduleName,
courseName,
courseUrl,
sendStudentNotifications,
focusOnClose = null,
} = {}) => {
// We need all of these functions to be executed in series, if one step runs before another the interface
// will not work.
// We need this promise to resolve separately so that we can avoid loading the whole interface if there are no users.
const userList = await getListOfUsers();
if (!userList.length) {
addNotification({
message: await getString('nouserstograde', 'core_grades'),
type: "error",
});
return;
}
// Now that we have confirmed there are at least some users let's boot up the grader interface.
const [
graderLayout,
{html, js},
] = await Promise.all([
createFullScreenWindow({
fullscreen: false,
showLoader: false,
focusOnClose,
}),
Templates.renderForPromise(templateNames.grader.app, {
moduleName,
courseName,
courseUrl,
drawer: {show: true},
defaultsendnotifications: sendStudentNotifications,
}),
]);
const graderContainer = graderLayout.getContainer();
const saveGradeFunction = getSaveUserGradeFunction(graderContainer, setGradeForUser);
Templates.replaceNodeContents(graderContainer, html, js);
const updateUserContent = getUpdateUserContentFunction(graderContainer, getContentForUser, getGradeForUser, saveGradeFunction);
const userIds = userList.map(user => user.id);
const statusContainer = graderContainer.querySelector(Selectors.regions.statusContainer);
// Fetch the userpicker for display.
const userPicker = await getUserPicker(
userList,
async(user) => {
const userGrade = await updateUserContent(user);
const renderContext = {
status: userGrade.hasgrade,
index: userIds.indexOf(user.id) + 1,
total: userList.length
};
Templates.render(templateNames.grader.status, renderContext).then(html => {
statusContainer.innerHTML = html;
return html;
}).catch();
},
saveGradeFunction,
{
initialUserId,
},
);
// Register all event listeners.
registerEventListeners(graderLayout, userPicker, saveGradeFunction, userList);
// Display the newly created user picker.
displayUserPicker(graderContainer, userPicker.rootNode);
};
/**
* Show the grade for a specific user.
*
* @param {Function} getGradeForUser A function get the grade details for a specific user
* @param {Number} userid The ID of a specific user
* @param {String} moduleName the name of the module
* @param {object} param
* @param {null|HTMLElement} param.focusOnClose
*/
export const view = async(getGradeForUser, userid, moduleName, {
focusOnClose = null,
} = {}) => {
const userGrade = await getGradeForUser(userid);
const [
modal,
gradeTemplateData
] = await Promise.all([
Modal.create({
title: moduleName,
large: true,
removeOnClose: true,
returnElement: focusOnClose,
show: true,
body: Templates.render('mod_forum/local/grades/view_grade', userGrade),
}),
renderGradeTemplate(userGrade)
]);
const bodyPromise = await modal.getBodyPromise();
const gradeReplace = bodyPromise[0].querySelector('[data-region="grade-template"]');
Templates.replaceNodeContents(gradeReplace, gradeTemplateData.html, gradeTemplateData.js);
};
const renderGradeTemplate = (userGrade) => Templates.renderForPromise(userGrade.templatename, userGrade.grade);
export {getGradingPanelFunctions};
@@ -0,0 +1,51 @@
// 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/>.
/**
* Grading panel functions.
*
* @module mod_forum/local/grades/local/grader/gradingpanel
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Get the grade panel setter and getter for the current component.
* This function dynamically pulls the relevant gradingpanel JS file defined in the grading method.
* We do this because we do not know until execution time what the grading type is and we do not want to import unused files.
*
* @method
* @param {String} component The component being graded
* @param {Number} context The contextid of the thing being graded
* @param {String} gradingComponent The thing providing the grading type
* @param {String} gradingSubtype The subtype fo the grading component
* @param {String} itemName The name of the thing being graded
* @return {Object}
*/
export default async(component, context, gradingComponent, gradingSubtype, itemName) => {
let gradingMethodHandler = `${gradingComponent}/grades/grader/gradingpanel`;
if (gradingSubtype) {
gradingMethodHandler += `/${gradingSubtype}`;
}
const GradingMethod = await import(gradingMethodHandler);
return {
getter: (userId) => GradingMethod.fetchCurrentGrade(component, context, itemName, userId),
setter: (userId, notifyStudent, formData) => GradingMethod.storeCurrentGrade(
component, context, itemName, userId, notifyStudent, formData),
};
};
@@ -0,0 +1,61 @@
// 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/>.
/**
* Define all of the selectors we will be using on the grading interface.
*
* @module mod_forum/local/grades/local/grader/selectors
* @copyright 2019 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* A small helper function to build queryable data selectors.
* @param {String} name
* @param {String} value
* @return {string}
*/
const getDataSelector = (name, value) => {
return `[data-${name}="${value}"]`;
};
export default {
buttons: {
toggleFullscreen: getDataSelector('action', 'togglefullscreen'),
closeGrader: getDataSelector('action', 'closegrader'),
collapseGradingDrawer: getDataSelector('action', 'collapse-grading-drawer'),
saveGrade: getDataSelector('action', 'savegrade'),
selectUser: getDataSelector('action', 'select-user'),
toggleSearch: getDataSelector('action', 'toggle-search')
},
regions: {
bodyContainer: getDataSelector('region', 'body-container'),
moduleContainer: getDataSelector('region', 'module_content_container'),
moduleReplace: getDataSelector('region', 'module_content'),
pickerRegion: getDataSelector('region', 'user_picker'),
gradingInfoContainer: getDataSelector('region', 'grading-info-container'),
gradingPanel: getDataSelector('region', 'grade'),
gradingPanelContainer: getDataSelector('region', 'grading-panel-container'),
gradingPanelErrors: getDataSelector('region', 'grade-errors'),
searchResultsContainer: getDataSelector('region', 'search-results-container'),
statusContainer: getDataSelector('region', 'status-container'),
userSearchContainer: getDataSelector('region', 'user-search-container'),
userSearchInput: getDataSelector('region', 'user-search-input')
},
values: {
sendStudentNotifications: '[data-region="notification"] input[type="radio"]:checked',
}
};
@@ -0,0 +1,211 @@
// 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/>.
/**
* This module will tie together all of the different calls the gradable module will make.
*
* @module mod_forum/local/grades/local/grader/user_picker
* @copyright 2019 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Templates from 'core/templates';
import Selectors from './user_picker/selectors';
import {getString} from 'core/str';
const templatePath = 'mod_forum/local/grades/local/grader';
/**
* The Grader User Picker.
*
* @class mod_forum/local/grades/local/grader/user_picker
*/
class UserPicker {
/**
* Constructor for the User Picker.
*
* @constructor mod_forum/local/grades/local/grader/user_picker
* @param {Array} userList List of users
* @param {Function} showUserCallback The callback used to display the user
* @param {Function} preChangeUserCallback The callback to use before changing user
*/
constructor(userList, showUserCallback, preChangeUserCallback) {
this.userList = userList;
this.showUserCallback = showUserCallback;
this.preChangeUserCallback = preChangeUserCallback;
this.currentUserIndex = 0;
// Ensure that render is bound correctly.
this.render = this.render.bind(this);
this.setUserId = this.setUserId.bind(this);
}
/**
* Set the current userid without rendering the change.
* To show the user, call showUser too.
*
* @param {Number} userId
*/
setUserId(userId) {
// Determine the current index based on the user ID.
const userIndex = this.userList.findIndex(user => {
return user.id === parseInt(userId);
});
if (userIndex === -1) {
throw Error(`User with id ${userId} not found`);
}
this.currentUserIndex = userIndex;
}
/**
* Render the user picker.
*/
async render() {
// Create the root node.
this.root = document.createElement('div');
const {html, js} = await this.renderNavigator();
Templates.replaceNodeContents(this.root, html, js);
// Call the showUser function to show the first user immediately.
await this.showUser(this.currentUser);
// Ensure that the event listeners are all bound.
this.registerEventListeners();
}
/**
* Render the navigator itself.
*
* @returns {Promise}
*/
renderNavigator() {
return Templates.renderForPromise(`${templatePath}/user_picker`, {});
}
/**
* Render the current user details for the picker.
*
* @param {Object} context The data used to render the user picker.
* @returns {Promise}
*/
renderUserChange(context) {
return Templates.renderForPromise(`${templatePath}/user_picker/user`, context);
}
/**
* Show the specified user in the picker.
*
* @param {Object} user
*/
async showUser(user) {
const [{html, js}] = await Promise.all([this.renderUserChange(user), this.showUserCallback(user)]);
const userRegion = this.root.querySelector(Selectors.regions.userRegion);
Templates.replaceNodeContents(userRegion, html, js);
// Update the hidden now-grading region so screen readers can announce the user that's currently being graded.
const currentUserRegion = this.root.querySelector(Selectors.regions.currentUser);
currentUserRegion.textContent = await getString('nowgradinguser', 'mod_forum', user.fullname);
}
/**
* Register the event listeners for the user picker.
*/
registerEventListeners() {
this.root.addEventListener('click', async(e) => {
const button = e.target.closest(Selectors.actions.changeUser);
if (button) {
const result = await this.preChangeUserCallback(this.currentUser);
if (!result.failed) {
this.updateIndex(parseInt(button.dataset.direction));
await this.showUser(this.currentUser);
}
}
});
}
/**
* Update the current user index.
*
* @param {Number} direction
* @returns {Number}}
*/
updateIndex(direction) {
this.currentUserIndex += direction;
// Loop around the edges.
if (this.currentUserIndex < 0) {
this.currentUserIndex = this.userList.length - 1;
} else if (this.currentUserIndex > this.userList.length - 1) {
this.currentUserIndex = 0;
}
return this.currentUserIndex;
}
/**
* Get the details of the user currently shown with the total number of users, and the 1-indexed count of the
* current user.
*
* @returns {Object}
*/
get currentUser() {
return {
...this.userList[this.currentUserIndex],
total: this.userList.length,
displayIndex: this.currentUserIndex + 1,
};
}
/**
* Get the root node for the User Picker.
*
* @returns {HTMLElement}
*/
get rootNode() {
return this.root;
}
}
/**
* Create a new user picker.
*
* @param {Array} users The list of users
* @param {Function} showUserCallback The function to call to show a specific user
* @param {Function} preChangeUserCallback The fucntion to call to save the grade for the current user
* @param {Number} [currentUserID] The userid of the current user
* @returns {UserPicker}
*/
export default async(
users,
showUserCallback,
preChangeUserCallback,
{
initialUserId = null,
} = {}
) => {
const userPicker = new UserPicker(users, showUserCallback, preChangeUserCallback);
if (initialUserId) {
userPicker.setUserId(initialUserId);
}
await userPicker.render();
return userPicker;
};
@@ -0,0 +1,33 @@
// 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/>.
/**
* Define all of the selectors we will be using on the grading interface.
*
* @module mod_forum/local/grades/local/grader/user_picker/selectors
* @copyright 2019 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export default {
regions: {
currentUser: '[data-region="user_picker/current_user"]',
userRegion: '[data-region="user_picker/user"]',
},
actions: {
changeUser: '[data-action="change-user"]',
}
};
@@ -0,0 +1,251 @@
// 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/>.
/**
* Full screen window layout.
*
* @module mod_forum/local/layout/fullscreen
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {addIconToContainer} from 'core/loadingicon';
import {addToastRegion} from 'core/toast';
import * as FocusLockManager from 'core/local/aria/focuslock';
/**
* Get the composed layout.
*
* @method
* @param {string} templateName
* @param {object} context
* @returns {LayoutHelper}
*/
export const createLayout = ({
fullscreen = true,
showLoader = false,
focusOnClose = null,
} = {}) => {
const container = document.createElement('div');
document.body.append(container);
container.classList.add('layout');
container.classList.add('fullscreen');
container.setAttribute('role', 'application');
addToastRegion(container);
// Lock scrolling on the document body.
lockBodyScroll();
// Lock tab control.
FocusLockManager.trapFocus(container);
const helpers = getLayoutHelpers(container, FocusLockManager, focusOnClose);
if (showLoader) {
helpers.showLoadingIcon();
}
if (fullscreen) {
helpers.requestFullscreen();
}
return helpers;
};
/**
* LayoutHelper A helper object containing functions for managing the current fullscreen layout
*
* @typedef {object}
* @property {Function} close A function to close the fullscreen layout
* @property {Function} toggleFullscreen A function to toggle the fullscreen from active to disabled and back
* @property {Function} requestFullscreen Make a request to the browser to make the window full screen.
* Note: This must be called in response to a direct user action
* @property {Function} exitFullscreen Exit the fullscreen mode
* @property {Function} getContainer Get the container of the fullscreen layout
* @property {Function} setContent Set the content of the fullscreen layout
* @property {Function} showLoadingIcon Display the loading icon
* @property {Function} hideLoadingIcon Hide the loading icon
*/
/**
* Get the layout helpers.
*
* @method
* @private
* @param {HTMLElement} layoutNode
* @param {FocusLockManager} FocusLockManager
* @param {Boolean} focusOnClose
* @returns {LayoutHelper}
*/
const getLayoutHelpers = (layoutNode, FocusLockManager, focusOnClose) => {
const contentNode = document.createElement('div');
layoutNode.append(contentNode);
const loadingNode = document.createElement('div');
layoutNode.append(loadingNode);
/**
* Close and destroy the window container.
*/
const close = () => {
exitFullscreen();
unlockBodyScroll();
FocusLockManager.untrapFocus();
layoutNode.remove();
if (focusOnClose) {
try {
focusOnClose.focus();
} catch (e) {
// eslint-disable-line
}
}
};
/**
* Attempt to make the conatiner full screen.
*/
const requestFullscreen = () => {
if (layoutNode.requestFullscreen) {
layoutNode.requestFullscreen();
} else if (layoutNode.msRequestFullscreen) {
layoutNode.msRequestFullscreen();
} else if (layoutNode.mozRequestFullscreen) {
layoutNode.mozRequestFullscreen();
} else if (layoutNode.webkitRequestFullscreen) {
layoutNode.webkitRequestFullscreen();
} else {
// Not supported.
// Hack to make this act like full-screen as much as possible.
layoutNode.setTop(0);
}
};
/**
* Exit full screen but do not close the container fully.
*/
const exitFullscreen = () => {
if (document.exitRequestFullScreen) {
if (document.fullScreenElement !== layoutNode) {
return;
}
document.exitRequestFullScreen();
} else if (document.msExitFullscreen) {
if (document.msFullscreenElement !== layoutNode) {
return;
}
document.msExitFullscreen();
} else if (document.mozCancelFullScreen) {
if (document.mozFullScreenElement !== layoutNode) {
return;
}
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
if (document.webkitFullscreenElement !== layoutNode) {
return;
}
document.webkitExitFullscreen();
}
};
const toggleFullscreen = () => {
if (document.exitRequestFullScreen) {
if (document.fullScreenElement === layoutNode) {
exitFullscreen();
} else {
requestFullscreen();
}
} else if (document.msExitFullscreen) {
if (document.msFullscreenElement === layoutNode) {
exitFullscreen();
} else {
requestFullscreen();
}
} else if (document.mozCancelFullScreen) {
if (document.mozFullScreenElement === layoutNode) {
exitFullscreen();
} else {
requestFullscreen();
}
} else if (document.webkitExitFullscreen) {
if (document.webkitFullscreenElement === layoutNode) {
exitFullscreen();
} else {
requestFullscreen();
}
}
};
/**
* Get the Node which is fullscreen.
*
* @return {Element}
*/
const getContainer = () => {
return contentNode;
};
const setContent = (content) => {
hideLoadingIcon();
// Note: It would be better to use replaceWith, but this is not compatible with IE.
let child = contentNode.lastElementChild;
while (child) {
contentNode.removeChild(child);
child = contentNode.lastElementChild;
}
contentNode.append(content);
};
const showLoadingIcon = () => {
addIconToContainer(loadingNode);
};
const hideLoadingIcon = () => {
// Hide the loading container.
let child = loadingNode.lastElementChild;
while (child) {
loadingNode.removeChild(child);
child = loadingNode.lastElementChild;
}
};
/**
* @return {Object}
*/
return {
close,
toggleFullscreen,
requestFullscreen,
exitFullscreen,
getContainer,
setContent,
showLoadingIcon,
hideLoadingIcon,
};
};
const lockBodyScroll = () => {
document.querySelector('body').classList.add('overflow-hidden');
};
const unlockBodyScroll = () => {
document.querySelector('body').classList.remove('overflow-hidden');
};
+5
View File
@@ -0,0 +1,5 @@
import {createLayout as createFullScreenWindow} from './layout/fullscreen';
export {
createFullScreenWindow
};
+65
View File
@@ -0,0 +1,65 @@
// 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/>.
/**
* Handle the manual locking of individual discussions
*
* @module mod_forum/lock_toggle
* @copyright 2019 Peter Dias <peter@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([
'jquery',
'core/templates',
'core/notification',
'mod_forum/repository',
'mod_forum/selectors',
], function(
$,
Templates,
Notification,
Repository,
Selectors
) {
/**
* Register event listeners for the subscription toggle.
*
* @param {object} root The discussion list root element
* @param {boolean} preventDefault Should the default action of the event be prevented
*/
var registerEventListeners = function(root, preventDefault) {
root.on('click', Selectors.lock.toggle, function(e) {
var toggleElement = $(this);
var forumId = toggleElement.data('forumid');
var discussionId = toggleElement.data('discussionid');
var state = toggleElement.data('state');
Repository.setDiscussionLockState(forumId, discussionId, state)
.then(function() {
return location.reload();
})
.catch(Notification.exception);
if (preventDefault) {
e.preventDefault();
}
});
};
return {
init: registerEventListeners
};
});
+84
View File
@@ -0,0 +1,84 @@
// 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/>.
/**
* This module is the highest level module for the calendar. It is
* responsible for initialising all of the components required for
* the calendar to run. It also coordinates the interaction between
* components by listening for and responding to different events
* triggered within the calendar UI.
*
* @module mod_forum/pin_toggle
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([
'jquery',
'core/ajax',
'core/str',
'core/templates',
'core/notification',
'mod_forum/repository',
'mod_forum/selectors',
'core/str',
], function(
$,
Ajax,
Str,
Templates,
Notification,
Repository,
Selectors,
String
) {
/**
* Registery event listeners for the pin toggle.
*
* @param {object} root The calendar root element
* @param {boolean} preventDefault Should the default action of the event be prevented
* @param {function} callback Success callback
*/
var registerEventListeners = function(root, preventDefault, callback) {
root.on('click', Selectors.pin.toggle, function(e) {
var toggleElement = $(this);
var forumid = toggleElement.data('forumid');
var discussionid = toggleElement.data('discussionid');
var pinstate = toggleElement.data('targetstate');
Repository.setPinDiscussionState(forumid, discussionid, pinstate)
.then(function(context) {
return callback(toggleElement, context);
})
.then(function() {
return String.get_string("pinupdated", "forum")
.done(function(s) {
return Notification.addNotification({
message: s,
type: "info"
});
});
})
.fail(Notification.exception);
if (preventDefault) {
e.preventDefault();
}
});
};
return {
init: registerEventListeners
};
});
+102
View File
@@ -0,0 +1,102 @@
// 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/>.
/**
* This module is the highest level module for the calendar. It is
* responsible for initialising all of the components required for
* the calendar to run. It also coordinates the interaction between
* components by listening for and responding to different events
* triggered within the calendar UI.
*
* @module mod_forum/posts_list
* @copyright 2019 Peter Dias
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([
'jquery',
'core/templates',
'core/notification',
'core/pending',
'mod_forum/selectors',
'mod_forum/inpage_reply',
'core_form/changechecker',
], function(
$,
Templates,
Notification,
Pending,
Selectors,
InPageReply,
FormChangeChecker
) {
var registerEventListeners = function(root, throttlingwarningmsg) {
root.on('click', Selectors.post.inpageReplyLink, function(e) {
e.preventDefault();
// After adding a reply a url hash is being generated that scrolls (points) to the newly added reply.
// The hash being present causes this scrolling behavior to the particular reply to persists even when
// another, non-related in-page replay link is being clicked which ultimately causes a bad user experience.
// A particular solution for this problem would be changing the browser's history state when a url hash is
// present.
if (window.location.hash) {
// Remove the fragment identifier from the url.
var url = window.location.href.split('#')[0];
history.pushState({}, document.title, url);
}
var pending = new Pending('inpage-reply');
var currentTarget = $(e.currentTarget).parents(Selectors.post.forumCoreContent);
var currentSubject = currentTarget.find(Selectors.post.forumSubject);
var currentRoot = $(e.currentTarget).parents(Selectors.post.forumContent);
var context = {
postid: $(currentRoot).data('post-id'),
"reply_url": $(e.currentTarget).attr('href'),
sesskey: M.cfg.sesskey,
parentsubject: currentSubject.data('replySubject'),
canreplyprivately: $(e.currentTarget).data('can-reply-privately'),
postformat: InPageReply.CONTENT_FORMATS.MOODLE,
throttlingwarningmsg: throttlingwarningmsg
};
if (!currentRoot.find(Selectors.post.inpageReplyContent).length) {
Templates.render('mod_forum/inpage_reply', context)
.then(function(html, js) {
return Templates.appendNodeContents(currentTarget, html, js);
})
.then(function() {
return currentRoot.find(Selectors.post.inpageReplyContent)
.slideToggle(300, pending.resolve).find('textarea').focus();
})
.then(function() {
FormChangeChecker.watchFormById(`inpage-reply-${context.postid}`);
return;
})
.catch(Notification.exception);
} else {
var form = currentRoot.find(Selectors.post.inpageReplyContent);
form.slideToggle(300, pending.resolve);
if (form.is(':visible')) {
form.find('textarea').focus();
}
}
});
};
return {
init: function(root, throttlingwarningmsg) {
registerEventListeners(root, throttlingwarningmsg);
InPageReply.init(root);
}
};
});
+165
View File
@@ -0,0 +1,165 @@
// 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/>.
/**
* Forum repository class to encapsulate all of the AJAX requests that subscribe or unsubscribe
* can be sent for forum.
*
* @module mod_forum/repository
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['core/ajax'], function(Ajax) {
/**
* Set the subscription state for a discussion in a forum.
*
* @param {number} forumId ID of the forum the discussion belongs to
* @param {number} discussionId ID of the discussion with the subscription state
* @param {boolean} targetState Set the subscribed state. True == subscribed; false == unsubscribed.
* @return {object} jQuery promise
*/
var setDiscussionSubscriptionState = function(forumId, discussionId, targetState) {
var request = {
methodname: 'mod_forum_set_subscription_state',
args: {
forumid: forumId,
discussionid: discussionId,
targetstate: targetState
}
};
return Ajax.call([request])[0];
};
var addDiscussionPost = function(postid, subject, message, messageformat, isprivatereply, topreferredformat) {
var request = {
methodname: 'mod_forum_add_discussion_post',
args: {
postid: postid,
message: message,
messageformat: messageformat,
subject: subject,
options: [{
name: "private",
value: isprivatereply,
}, {
name: "topreferredformat",
value: topreferredformat,
}]
}
};
return Ajax.call([request])[0];
};
/**
* Set the favourite state for a discussion in a forum.
*
* @param {number} forumId ID of the forum the discussion belongs to
* @param {number} discussionId ID of the discussion with the subscription state
* @param {null|date} targetState Set the favourite state. True == favourited; false == unfavourited.
* @return {object} jQuery promise
*/
var setFavouriteDiscussionState = function(forumId, discussionId, targetState) {
var request = {
methodname: 'mod_forum_toggle_favourite_state',
args: {
discussionid: discussionId,
targetstate: targetState
}
};
return Ajax.call([request])[0];
};
var setDiscussionLockState = function(forumId, discussionId, targetState) {
var request = {
methodname: 'mod_forum_set_lock_state',
args: {
forumid: forumId,
discussionid: discussionId,
targetstate: targetState}
};
return Ajax.call([request])[0];
};
/**
* Set the pinned state for the discussion provided.
*
* @param {number} forumid
* @param {number} discussionid
* @param {boolean} targetstate
* @return {*|Promise}
*/
var setPinDiscussionState = function(forumid, discussionid, targetstate) {
var request = {
methodname: 'mod_forum_set_pin_state',
args: {
discussionid: discussionid,
targetstate: targetstate
}
};
return Ajax.call([request])[0];
};
/**
* Get the discussions for the user and cmid provided.
*
* @param {number} userid
* @param {number} cmid
* @param {string} sortby
* @param {string} sortdirection
* @return {*|Promise}
*/
var getDiscussionByUserID = function(userid, cmid, sortby = 'modified', sortdirection = 'DESC') {
var request = {
methodname: 'mod_forum_get_discussion_posts_by_userid',
args: {
userid: userid,
cmid: cmid,
sortby: sortby,
sortdirection: sortdirection,
},
};
return Ajax.call([request])[0];
};
/**
* Get the posts for the discussion ID provided.
*
* @param {number} discussionId
* @param {String} sortby
* @param {String} sortdirection
* @return {*|Promise}
*/
var getDiscussionPosts = function(discussionId, sortby = 'created', sortdirection = 'ASC') {
var request = {
methodname: 'mod_forum_get_discussion_posts',
args: {
discussionid: discussionId,
sortby: sortby,
sortdirection: sortdirection,
},
};
return Ajax.call([request])[0];
};
return {
setDiscussionSubscriptionState: setDiscussionSubscriptionState,
addDiscussionPost: addDiscussionPost,
setDiscussionLockState: setDiscussionLockState,
setFavouriteDiscussionState: setFavouriteDiscussionState,
setPinDiscussionState: setPinDiscussionState,
getDiscussionByUserID: getDiscussionByUserID,
getDiscussionPosts: getDiscussionPosts,
};
});
+75
View File
@@ -0,0 +1,75 @@
// 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/>.
/**
* Common CSS selectors for the forum UI.
*
* @module mod_forum/selectors
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([], function() {
return {
subscription: {
toggle: "[data-type='subscription-toggle'][data-action='toggle']",
},
summary: {
actions: "[data-container='discussion-summary-actions']"
},
post: {
post: '[data-region="post"]',
action: '[data-region="post-action"]',
actionsContainer: '[data-region="post-actions-container"]',
authorName: '[data-region="author-name"]',
forumCoreContent: "[data-region-content='forum-post-core']",
forumContent: "[data-content='forum-post']",
forumSubject: "[data-region-content='forum-post-core-subject']",
inpageCancelButton: "button[name='cancelbtn']",
inpageReplyButton: "button",
inpageReplyLink: "[data-action='collapsible-link']",
inpageReplyCancelButton: "[data-action='cancel-inpage-reply']",
inpageReplyCreateButton: "[data-action='create-inpage-reply']",
inpageReplyContainer: '[data-region="inpage-reply-container"]',
inpageReplyContent: "[data-content='inpage-reply-content']",
inpageReplyForm: "form[data-content='inpage-reply-form']",
inpageSubmitBtn: "[data-action='forum-inpage-submit']",
inpageSubmitBtnText: "[data-region='submit-text']",
loadingIconContainer: "[data-region='loading-icon-container']",
repliesContainer: "[data-region='replies-container']",
replyCount: '[data-region="reply-count"]',
modeSelect: "select[name='mode']",
showReplies: '[data-action="show-replies"]',
hideReplies: '[data-action="hide-replies"]',
repliesVisibilityToggleContainer: '[data-region="replies-visibility-toggle-container"]'
},
lock: {
toggle: "[data-action='toggle'][data-type='lock-toggle']",
icon: "[data-region='locked-icon']"
},
favourite: {
toggle: "[data-type='favorite-toggle'][data-action='toggle']",
},
pin: {
toggle: "[data-type='pin-toggle'][data-action='toggle']",
},
discussion: {
tools: '[data-container="discussion-tools"]',
item: '[data-region="discussion-list-item"]',
lockedLabel: "[data-region='locked-label']",
subscribedLabel: "[data-region='subscribed-label']",
timedLabel: "[data-region='timed-label']",
},
};
});
+75
View File
@@ -0,0 +1,75 @@
// 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/>.
/**
* Handle discussion subscription toggling on a discussion list in
* the forum view.
*
* @module mod_forum/subscription_toggle
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define([
'jquery',
'core/templates',
'core/notification',
'mod_forum/repository',
'mod_forum/selectors',
'core/pubsub',
'mod_forum/forum_events',
], function(
$,
Templates,
Notification,
Repository,
Selectors,
PubSub,
ForumEvents
) {
/**
* Register event listeners for the subscription toggle.
*
* @param {object} root The discussion list root element
* @param {boolean} preventDefault Should the default action of the event be prevented
* @param {function} callback Success callback
*/
var registerEventListeners = function(root, preventDefault, callback) {
root.on('click', Selectors.subscription.toggle, function(e) {
var toggleElement = $(this);
var forumId = toggleElement.data('forumid');
var discussionId = toggleElement.data('discussionid');
var subscriptionState = toggleElement.data('targetstate');
Repository.setDiscussionSubscriptionState(forumId, discussionId, subscriptionState)
.then(function(context) {
PubSub.publish(ForumEvents.SUBSCRIPTION_TOGGLED, {
discussionId: discussionId,
subscriptionState: subscriptionState
});
return callback(toggleElement, context);
})
.catch(Notification.exception);
if (preventDefault) {
e.preventDefault();
}
});
};
return {
init: registerEventListeners
};
});