Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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/>.
15
 
16
/**
17
 * Module for viewing a discussion.
18
 *
19
 * @module     mod_forum/discussion
20
 * @copyright  2019 Ryan Wyllie <ryan@moodle.com>
21
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 */
23
define(
24
[
25
    'jquery',
26
    'core/custom_interaction_events',
27
    'mod_forum/selectors',
28
    'core/pubsub',
29
    'mod_forum/forum_events',
30
    'core/str',
31
    'core/notification',
32
],
33
function(
34
    $,
35
    CustomEvents,
36
    Selectors,
37
    PubSub,
38
    ForumEvents,
39
    String,
40
    Notification
41
) {
42
 
43
    /**
44
     * Set the focus on the previous post in the list. Previous post is calculated
45
     * based on position in list as viewed top to bottom.
46
     *
47
     * @param {Object} currentPost The post that currently has focus
48
     */
49
    var focusPreviousPost = function(currentPost) {
50
        // See if there is a previous sibling post.
51
        var prevPost = currentPost.prev(Selectors.post.post);
52
 
53
        if (prevPost.length) {
54
            // The previous post might have replies that appear visually between
55
            // it and the current post (see nested view) so if that's the case
56
            // then the last reply will be the previous post in the list.
57
            var replyPost = prevPost.find(Selectors.post.post).last();
58
 
59
            if (replyPost.length) {
60
                // Focus the last reply.
61
                replyPost.focus();
62
            } else {
63
                // No replies so we can focus straight on the sibling.
64
                prevPost.focus();
65
            }
66
        } else {
67
            // If there are no siblings then jump up the tree to the parent
68
            // post and focus the first parent post we find.
69
            currentPost.parents(Selectors.post.post).first().focus();
70
        }
71
    };
72
 
73
    /**
74
     * Set the focus on the next post in the list. Previous post is calculated
75
     * based on position in list as viewed top to bottom.
76
     *
77
     * @param {Object} currentPost The post that currently has focus
78
     */
79
    var focusNextPost = function(currentPost) {
80
        // The next post in the visual list would be the first reply to this one
81
        // so let's see if we have one.
82
        var replyPost = currentPost.find(Selectors.post.post).first();
83
 
84
        if (replyPost.length) {
85
            // Got a reply.
86
            replyPost.focus();
87
        } else {
88
            // If we don't have a reply then the next post in the visual list would
89
            // be a sibling post (replying to the same parent).
90
            var siblingPost = currentPost.next(Selectors.post.post);
91
 
92
            if (siblingPost.length) {
93
                siblingPost.focus();
94
            } else {
95
                // No siblings either. That means we're the lowest level reply in a thread
96
                // so we need to walk back up the tree of posts and find an ancestor post that
97
                // has a sibling post we can focus.
98
                var parentPosts = currentPost.parents(Selectors.post.post).toArray();
99
 
100
                for (var i = 0; i < parentPosts.length; i++) {
101
                    var ancestorSiblingPost = $(parentPosts[i]).next(Selectors.post.post);
102
 
103
                    if (ancestorSiblingPost.length) {
104
                        ancestorSiblingPost.focus();
105
                        break;
106
                    }
107
                }
108
            }
109
        }
110
    };
111
 
112
    /**
113
     * Check if the element is inside the in page reply section.
114
     *
115
     * @param {Object} element The element to check
116
     * @return {Boolean}
117
     */
118
    var isElementInInPageReplySection = function(element) {
119
        var inPageReply = $(element).closest(Selectors.post.inpageReplyContent);
120
        return inPageReply.length ? true : false;
121
    };
122
 
123
    /**
124
     * Initialise the keyboard accessibility controls for the discussion.
125
     *
126
     * @param {Object} root The discussion root element
127
     */
128
    var initAccessibilityKeyboardNav = function(root) {
129
        var posts = root.find(Selectors.post.post);
130
 
131
        // Take each post action out of the tab index.
132
        posts.each(function(index, post) {
133
            var actions = $(post).find(Selectors.post.action);
134
            var firstAction = actions.first();
135
            actions.attr('tabindex', '-1');
136
            firstAction.attr('tabindex', 0);
137
        });
138
 
139
        CustomEvents.define(root, [
140
            CustomEvents.events.up,
141
            CustomEvents.events.down,
142
            CustomEvents.events.next,
143
            CustomEvents.events.previous,
144
            CustomEvents.events.home,
145
            CustomEvents.events.end,
146
        ]);
147
 
148
        root.on(CustomEvents.events.up, function(e, data) {
149
            var activeElement = document.activeElement;
150
 
151
            if (isElementInInPageReplySection(activeElement)) {
152
                // Focus is currently inside the in page reply section so don't move focus
153
                // to another post.
154
                return;
155
            }
156
 
157
            var focusPost = $(activeElement).closest(Selectors.post.post);
158
 
159
            if (focusPost.length) {
160
                focusPreviousPost(focusPost);
161
            } else {
162
                root.find(Selectors.post.post).first().focus();
163
            }
164
 
165
            data.originalEvent.preventDefault();
166
        });
167
 
168
        root.on(CustomEvents.events.down, function(e, data) {
169
            var activeElement = document.activeElement;
170
 
171
            if (isElementInInPageReplySection(activeElement)) {
172
                // Focus is currently inside the in page reply section so don't move focus
173
                // to another post.
174
                return;
175
            }
176
 
177
            var focusPost = $(activeElement).closest(Selectors.post.post);
178
 
179
            if (focusPost.length) {
180
                focusNextPost(focusPost);
181
            } else {
182
                root.find(Selectors.post.post).first().focus();
183
            }
184
 
185
            data.originalEvent.preventDefault();
186
        });
187
 
188
        root.on(CustomEvents.events.home, function(e, data) {
189
            if (isElementInInPageReplySection(document.activeElement)) {
190
                // Focus is currently inside the in page reply section so don't move focus
191
                // to another post.
192
                return;
193
            }
194
            root.find(Selectors.post.post).first().focus();
195
            data.originalEvent.preventDefault();
196
        });
197
 
198
        root.on(CustomEvents.events.end, function(e, data) {
199
            if (isElementInInPageReplySection(document.activeElement)) {
200
                // Focus is currently inside the in page reply section so don't move focus
201
                // to another post.
202
                return;
203
            }
204
            root.find(Selectors.post.post).last().focus();
205
            data.originalEvent.preventDefault();
206
        });
207
 
208
        root.on(CustomEvents.events.next, Selectors.post.action, function(e, data) {
209
            var currentAction = $(e.target);
210
            var container = currentAction.closest(Selectors.post.actionsContainer);
211
            var actions = container.find(Selectors.post.action);
212
            var nextAction = currentAction.next(Selectors.post.action);
213
 
214
            actions.attr('tabindex', '-1');
215
 
216
            if (!nextAction.length) {
217
                nextAction = actions.first();
218
            }
219
 
220
            nextAction.attr('tabindex', 0);
221
            nextAction.focus();
222
 
223
            data.originalEvent.preventDefault();
224
        });
225
 
226
        root.on(CustomEvents.events.previous, Selectors.post.action, function(e, data) {
227
            var currentAction = $(e.target);
228
            var container = currentAction.closest(Selectors.post.actionsContainer);
229
            var actions = container.find(Selectors.post.action);
230
            var nextAction = currentAction.prev(Selectors.post.action);
231
 
232
            actions.attr('tabindex', '-1');
233
 
234
            if (!nextAction.length) {
235
                nextAction = actions.last();
236
            }
237
 
238
            nextAction.attr('tabindex', 0);
239
            nextAction.focus();
240
 
241
            data.originalEvent.preventDefault();
242
        });
243
 
244
        root.on(CustomEvents.events.home, Selectors.post.action, function(e, data) {
245
            var currentAction = $(e.target);
246
            var container = currentAction.closest(Selectors.post.actionsContainer);
247
            var actions = container.find(Selectors.post.action);
248
            var firstAction = actions.first();
249
 
250
            actions.attr('tabindex', '-1');
251
            firstAction.attr('tabindex', 0);
252
            firstAction.focus();
253
 
254
            e.stopPropagation();
255
            data.originalEvent.preventDefault();
256
        });
257
 
258
        root.on(CustomEvents.events.end, Selectors.post.action, function(e, data) {
259
            var currentAction = $(e.target);
260
            var container = currentAction.closest(Selectors.post.actionsContainer);
261
            var actions = container.find(Selectors.post.action);
262
            var lastAction = actions.last();
263
 
264
            actions.attr('tabindex', '-1');
265
            lastAction.attr('tabindex', 0);
266
            lastAction.focus();
267
 
268
            e.stopPropagation();
269
            data.originalEvent.preventDefault();
270
        });
271
 
272
        PubSub.subscribe(ForumEvents.SUBSCRIPTION_TOGGLED, function(data) {
273
            var subscribed = data.subscriptionState;
274
            var updateMessage = subscribed ? 'discussionsubscribed' : 'discussionunsubscribed';
275
            String.get_string(updateMessage, "forum")
276
                .then(function(s) {
277
                    return Notification.addNotification({
278
                        message: s,
279
                        type: "info"
280
                    });
281
                })
282
                .catch(Notification.exception);
283
        });
284
    };
285
 
286
    return {
287
        init: function(root) {
288
            initAccessibilityKeyboardNav(root);
289
        }
290
    };
291
});