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 * Initialise the keyboard accessibility controls for the discussion.
106 * @param {Object} root The discussion root element
108 var initAccessibilityKeyboardNav = function(root) {
109 var posts = root.find(Selectors.post.post);
111 // Take each post action out of the tab index.
112 posts.each(function(index, post) {
113 var actions = $(post).find(Selectors.post.action);
114 var firstAction = actions.first();
115 actions.attr('tabindex', '-1');
116 firstAction.attr('tabindex', 0);
119 CustomEvents.define(root, [
120 CustomEvents.events.up,
121 CustomEvents.events.down,
122 CustomEvents.events.next,
123 CustomEvents.events.previous,
124 CustomEvents.events.home,
125 CustomEvents.events.end,
128 root.on(CustomEvents.events.up, function() {
129 var focusPost = $(document.activeElement).closest(Selectors.post.post);
131 if (focusPost.length) {
132 focusPreviousPost(focusPost);
134 root.find(Selectors.post.post).first().focus();
138 root.on(CustomEvents.events.down, function() {
139 var focusPost = $(document.activeElement).closest(Selectors.post.post);
141 if (focusPost.length) {
142 focusNextPost(focusPost);
144 root.find(Selectors.post.post).first().focus();
148 root.on(CustomEvents.events.home, function() {
149 root.find(Selectors.post.post).first().focus();
152 root.on(CustomEvents.events.end, function() {
153 root.find(Selectors.post.post).last().focus();
156 root.on(CustomEvents.events.next, Selectors.post.action, function(e, data) {
157 var currentAction = $(e.target);
158 var container = currentAction.closest(Selectors.post.actionsContainer);
159 var actions = container.find(Selectors.post.action);
160 var nextAction = currentAction.next(Selectors.post.action);
162 actions.attr('tabindex', '-1');
164 if (!nextAction.length) {
165 nextAction = actions.first();
168 nextAction.attr('tabindex', 0);
171 data.originalEvent.preventDefault();
174 root.on(CustomEvents.events.previous, Selectors.post.action, function(e, data) {
175 var currentAction = $(e.target);
176 var container = currentAction.closest(Selectors.post.actionsContainer);
177 var actions = container.find(Selectors.post.action);
178 var nextAction = currentAction.prev(Selectors.post.action);
180 actions.attr('tabindex', '-1');
182 if (!nextAction.length) {
183 nextAction = actions.last();
186 nextAction.attr('tabindex', 0);
189 data.originalEvent.preventDefault();
192 root.on(CustomEvents.events.home, Selectors.post.action, function(e, data) {
193 var currentAction = $(e.target);
194 var container = currentAction.closest(Selectors.post.actionsContainer);
195 var actions = container.find(Selectors.post.action);
196 var firstAction = actions.first();
198 actions.attr('tabindex', '-1');
199 firstAction.attr('tabindex', 0);
203 data.originalEvent.preventDefault();
206 root.on(CustomEvents.events.end, Selectors.post.action, function(e, data) {
207 var currentAction = $(e.target);
208 var container = currentAction.closest(Selectors.post.actionsContainer);
209 var actions = container.find(Selectors.post.action);
210 var lastAction = actions.last();
212 actions.attr('tabindex', '-1');
213 lastAction.attr('tabindex', 0);
217 data.originalEvent.preventDefault();
222 init: function(root) {
223 initAccessibilityKeyboardNav(root);