1 // This file is part of Moodle - http://moodle.org/
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 * Module for viewing a discussion.
19 * @module mod_forum/discussion_list
21 * @copyright 2019 Ryan Wyllie <ryan@moodle.com>
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 'core/custom_interaction_events',
37 * Set the focus on the previous post in the list. Previous post is calculated
38 * based on position in list as viewed top to bottom.
40 * @param {Object} currentPost The post that currently has focus
42 var focusPreviousPost = function(currentPost) {
43 // See if there is a previous sibling post.
44 var prevPost = currentPost.prev(Selectors.post.post);
46 if (prevPost.length) {
47 // The previous post might have replies that appear visually between
48 // it and the current post (see nested view) so if that's the case
49 // then the last reply will be the previous post in the list.
50 var replyPost = prevPost.find(Selectors.post.post).last();
52 if (replyPost.length) {
53 // Focus the last reply.
56 // No replies so we can focus straight on the sibling.
60 // If there are no siblings then jump up the tree to the parent
61 // post and focus the first parent post we find.
62 currentPost.parents(Selectors.post.post).first().focus();
67 * Set the focus on the next post in the list. Previous post is calculated
68 * based on position in list as viewed top to bottom.
70 * @param {Object} currentPost The post that currently has focus
72 var focusNextPost = function(currentPost) {
73 // The next post in the visual list would be the first reply to this one
74 // so let's see if we have one.
75 var replyPost = currentPost.find(Selectors.post.post).first();
77 if (replyPost.length) {
81 // If we don't have a reply then the next post in the visual list would
82 // be a sibling post (replying to the same parent).
83 var siblingPost = currentPost.next(Selectors.post.post);
85 if (siblingPost.length) {
88 // No siblings either. That means we're the lowest level reply in a thread
89 // so we need to walk back up the tree of posts and find an ancestor post that
90 // has a sibling post we can focus.
91 currentPost.parents().toArray().forEach(function(parent) {
92 var ancestorSiblingPost = $(parent).next(Selectors.post.post);
94 if (ancestorSiblingPost.length) {
95 ancestorSiblingPost.focus();
104 * Check if the element is inside the in page reply section.
106 * @param {Object} element The element to check
109 var isElementInInPageReplySection = function(element) {
110 var inPageReply = $(element).closest(Selectors.post.inpageReplyContent);
111 return inPageReply.length ? true : false;
115 * Initialise the keyboard accessibility controls for the discussion.
117 * @param {Object} root The discussion root element
119 var initAccessibilityKeyboardNav = function(root) {
120 var posts = root.find(Selectors.post.post);
122 // Take each post action out of the tab index.
123 posts.each(function(index, post) {
124 var actions = $(post).find(Selectors.post.action);
125 var firstAction = actions.first();
126 actions.attr('tabindex', '-1');
127 firstAction.attr('tabindex', 0);
130 CustomEvents.define(root, [
131 CustomEvents.events.up,
132 CustomEvents.events.down,
133 CustomEvents.events.next,
134 CustomEvents.events.previous,
135 CustomEvents.events.home,
136 CustomEvents.events.end,
139 root.on(CustomEvents.events.up, function(e, data) {
140 var activeElement = document.activeElement;
142 if (isElementInInPageReplySection(activeElement)) {
143 // Focus is currently inside the in page reply section so don't move focus
148 var focusPost = $(activeElement).closest(Selectors.post.post);
150 if (focusPost.length) {
151 focusPreviousPost(focusPost);
153 root.find(Selectors.post.post).first().focus();
156 data.originalEvent.preventDefault();
159 root.on(CustomEvents.events.down, function(e, data) {
160 var activeElement = document.activeElement;
162 if (isElementInInPageReplySection(activeElement)) {
163 // Focus is currently inside the in page reply section so don't move focus
168 var focusPost = $(activeElement).closest(Selectors.post.post);
170 if (focusPost.length) {
171 focusNextPost(focusPost);
173 root.find(Selectors.post.post).first().focus();
176 data.originalEvent.preventDefault();
179 root.on(CustomEvents.events.home, function(e, data) {
180 if (isElementInInPageReplySection(document.activeElement)) {
181 // Focus is currently inside the in page reply section so don't move focus
185 root.find(Selectors.post.post).first().focus();
186 data.originalEvent.preventDefault();
189 root.on(CustomEvents.events.end, function(e, data) {
190 if (isElementInInPageReplySection(document.activeElement)) {
191 // Focus is currently inside the in page reply section so don't move focus
195 root.find(Selectors.post.post).last().focus();
196 data.originalEvent.preventDefault();
199 root.on(CustomEvents.events.next, Selectors.post.action, function(e, data) {
200 var currentAction = $(e.target);
201 var container = currentAction.closest(Selectors.post.actionsContainer);
202 var actions = container.find(Selectors.post.action);
203 var nextAction = currentAction.next(Selectors.post.action);
205 actions.attr('tabindex', '-1');
207 if (!nextAction.length) {
208 nextAction = actions.first();
211 nextAction.attr('tabindex', 0);
214 data.originalEvent.preventDefault();
217 root.on(CustomEvents.events.previous, Selectors.post.action, function(e, data) {
218 var currentAction = $(e.target);
219 var container = currentAction.closest(Selectors.post.actionsContainer);
220 var actions = container.find(Selectors.post.action);
221 var nextAction = currentAction.prev(Selectors.post.action);
223 actions.attr('tabindex', '-1');
225 if (!nextAction.length) {
226 nextAction = actions.last();
229 nextAction.attr('tabindex', 0);
232 data.originalEvent.preventDefault();
235 root.on(CustomEvents.events.home, Selectors.post.action, function(e, data) {
236 var currentAction = $(e.target);
237 var container = currentAction.closest(Selectors.post.actionsContainer);
238 var actions = container.find(Selectors.post.action);
239 var firstAction = actions.first();
241 actions.attr('tabindex', '-1');
242 firstAction.attr('tabindex', 0);
246 data.originalEvent.preventDefault();
249 root.on(CustomEvents.events.end, Selectors.post.action, function(e, data) {
250 var currentAction = $(e.target);
251 var container = currentAction.closest(Selectors.post.actionsContainer);
252 var actions = container.find(Selectors.post.action);
253 var lastAction = actions.last();
255 actions.attr('tabindex', '-1');
256 lastAction.attr('tabindex', 0);
260 data.originalEvent.preventDefault();
265 init: function(root) {
266 initAccessibilityKeyboardNav(root);