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