1a45edb634e1c77b2edae9aebe2569b556ad4b4e
[moodle.git] / mod / forum / amd / src / discussion.js
1 // This file is part of Moodle - http://moodle.org/
2 //
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.
7 //
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.
12 //
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/>.
16 /**
17  * Module for viewing a discussion.
18  *
19  * @module     mod_forum/discussion_list
20  * @package    mod_forum
21  * @copyright  2019 Ryan Wyllie <ryan@moodle.com>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
24 define(
25 [
26     'jquery',
27     'core/custom_interaction_events',
28     'mod_forum/selectors'
29 ],
30 function(
31     $,
32     CustomEvents,
33     Selectors
34 ) {
36     /**
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.
39      *
40      * @param {Object} currentPost The post that currently has focus
41      */
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.
54                 replyPost.focus();
55             } else {
56                 // No replies so we can focus straight on the sibling.
57                 prevPost.focus();
58             }
59         } else {
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();
63         }
64     };
66     /**
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.
69      *
70      * @param {Object} currentPost The post that currently has focus
71      */
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) {
78             // Got a reply.
79             replyPost.focus();
80         } else {
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) {
86                 siblingPost.focus();
87             } else {
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();
96                         return;
97                     }
98                 });
99             }
100         }
101     };
103     /**
104      * Initialise the keyboard accessibility controls for the discussion.
105      *
106      * @param {Object} root The discussion root element
107      */
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);
117         });
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,
126         ]);
128         root.on(CustomEvents.events.up, function() {
129             var focusPost = $(document.activeElement).closest(Selectors.post.post);
131             if (focusPost.length) {
132                 focusPreviousPost(focusPost);
133             } else {
134                 root.find(Selectors.post.post).first().focus();
135             }
136         });
138         root.on(CustomEvents.events.down, function() {
139             var focusPost = $(document.activeElement).closest(Selectors.post.post);
141             if (focusPost.length) {
142                 focusNextPost(focusPost);
143             } else {
144                 root.find(Selectors.post.post).first().focus();
145             }
146         });
148         root.on(CustomEvents.events.home, function() {
149             root.find(Selectors.post.post).first().focus();
150         });
152         root.on(CustomEvents.events.end, function() {
153             root.find(Selectors.post.post).last().focus();
154         });
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();
166             }
168             nextAction.attr('tabindex', 0);
169             nextAction.focus();
171             data.originalEvent.preventDefault();
172         });
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();
184             }
186             nextAction.attr('tabindex', 0);
187             nextAction.focus();
189             data.originalEvent.preventDefault();
190         });
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);
200             firstAction.focus();
202             e.stopPropagation();
203             data.originalEvent.preventDefault();
204         });
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);
214             lastAction.focus();
216             e.stopPropagation();
217             data.originalEvent.preventDefault();
218         });
219     };
221     return {
222         init: function(root) {
223             initAccessibilityKeyboardNav(root);
224         }
225     };
226 });