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
 * Controls the popover region element.
18
 *
19
 * See template: core/popover_region
20
 *
21
 * @module     core/popover_region_controller
22
 * @copyright  2015 Ryan Wyllie <ryan@moodle.com>
23
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 * @since      3.2
25
 */
26
define(['jquery', 'core/str', 'core/custom_interaction_events'],
27
        function($, str, customEvents) {
28
 
29
    var SELECTORS = {
30
        CONTENT: '.popover-region-content',
31
        CONTENT_CONTAINER: '.popover-region-content-container',
32
        MENU_CONTAINER: '.popover-region-container',
33
        MENU_TOGGLE: '.popover-region-toggle',
34
        CAN_RECEIVE_FOCUS: 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]',
35
    };
36
 
37
    /**
38
     * Constructor for the PopoverRegionController.
39
     *
40
     * @param {jQuery} element object root element of the popover
41
     */
42
    var PopoverRegionController = function(element) {
43
        this.root = $(element);
44
        this.content = this.root.find(SELECTORS.CONTENT);
45
        this.contentContainer = this.root.find(SELECTORS.CONTENT_CONTAINER);
46
        this.menuContainer = this.root.find(SELECTORS.MENU_CONTAINER);
47
        this.menuToggle = this.root.find(SELECTORS.MENU_TOGGLE);
48
        this.isLoading = false;
49
        this.promises = {
50
            closeHandlers: $.Deferred(),
51
            navigationHandlers: $.Deferred(),
52
        };
53
 
54
        // Core event listeners to open and close.
55
        this.registerBaseEventListeners();
56
    };
57
 
58
    /**
59
     * The collection of events triggered by this controller.
60
     *
61
     * @returns {object}
62
     */
63
    PopoverRegionController.prototype.events = function() {
64
        return {
65
            menuOpened: 'popoverregion:menuopened',
66
            menuClosed: 'popoverregion:menuclosed',
67
            startLoading: 'popoverregion:startLoading',
68
            stopLoading: 'popoverregion:stopLoading',
69
        };
70
    };
71
 
72
    /**
73
     * Return the container element for the content element.
74
     *
75
     * @method getContentContainer
76
     * @return {jQuery} object
77
     */
78
    PopoverRegionController.prototype.getContentContainer = function() {
79
        return this.contentContainer;
80
    };
81
 
82
    /**
83
     * Return the content element.
84
     *
85
     * @method getContent
86
     * @return {jQuery} object
87
     */
88
    PopoverRegionController.prototype.getContent = function() {
89
        return this.content;
90
    };
91
 
92
    /**
93
     * Checks if the popover is displayed.
94
     *
95
     * @method isMenuOpen
96
     * @return {bool}
97
     */
98
    PopoverRegionController.prototype.isMenuOpen = function() {
99
        return !this.root.hasClass('collapsed');
100
    };
101
 
102
    /**
103
     * Toggle the visibility of the popover.
104
     *
105
     * @method toggleMenu
106
     */
107
    PopoverRegionController.prototype.toggleMenu = function() {
108
        if (this.isMenuOpen()) {
109
            this.closeMenu();
110
        } else {
111
            this.openMenu();
112
        }
113
    };
114
 
115
    /**
116
     * Hide the popover.
117
     *
118
     * Note: This triggers the menuClosed event.
119
     *
120
     * @method closeMenu
121
     */
122
    PopoverRegionController.prototype.closeMenu = function() {
123
        // We're already closed.
124
        if (!this.isMenuOpen()) {
125
            return;
126
        }
127
 
128
        this.root.addClass('collapsed');
129
        this.menuContainer.attr('aria-expanded', 'false');
130
        this.menuContainer.attr('aria-hidden', 'true');
131
        this.updateButtonAriaLabel();
132
        this.updateFocusItemTabIndex();
133
        this.root.trigger(this.events().menuClosed);
134
    };
135
 
136
    /**
137
     * Show the popover.
138
     *
139
     * Note: This triggers the menuOpened event.
140
     *
141
     * @method openMenu
142
     */
143
    PopoverRegionController.prototype.openMenu = function() {
144
        // We're already open.
145
        if (this.isMenuOpen()) {
146
            return;
147
        }
148
 
149
        this.root.removeClass('collapsed');
150
        this.menuContainer.attr('aria-expanded', 'true');
151
        this.menuContainer.attr('aria-hidden', 'false');
152
        this.updateButtonAriaLabel();
153
        this.updateFocusItemTabIndex();
154
        // Resolve the promises to allow the handlers to be added
155
        // to the DOM, if they have been requested.
156
        this.promises.closeHandlers.resolve();
157
        this.promises.navigationHandlers.resolve();
158
        this.root.trigger(this.events().menuOpened);
159
    };
160
 
161
    /**
162
     * Set the appropriate aria label on the popover toggle.
163
     *
164
     * @method updateButtonAriaLabel
165
     */
166
    PopoverRegionController.prototype.updateButtonAriaLabel = function() {
167
        if (this.isMenuOpen()) {
168
            str.get_string('hidepopoverwindow').done(function(string) {
169
                this.menuToggle.attr('aria-label', string);
170
            }.bind(this));
171
        } else {
172
            str.get_string('showpopoverwindow').done(function(string) {
173
                this.menuToggle.attr('aria-label', string);
174
            }.bind(this));
175
        }
176
    };
177
 
178
    /**
179
     * Set the loading state on this popover.
180
     *
181
     * Note: This triggers the startLoading event.
182
     *
183
     * @method startLoading
184
     */
185
    PopoverRegionController.prototype.startLoading = function() {
186
        this.isLoading = true;
187
        this.getContentContainer().addClass('loading');
188
        this.getContentContainer().attr('aria-busy', 'true');
189
        this.root.trigger(this.events().startLoading);
190
    };
191
 
192
    /**
193
     * Undo the loading state on this popover.
194
     *
195
     * Note: This triggers the stopLoading event.
196
     *
197
     * @method stopLoading
198
     */
199
    PopoverRegionController.prototype.stopLoading = function() {
200
        this.isLoading = false;
201
        this.getContentContainer().removeClass('loading');
202
        this.getContentContainer().attr('aria-busy', 'false');
203
        this.root.trigger(this.events().stopLoading);
204
    };
205
 
206
    /**
207
     * Sets the focus on the menu toggle.
208
     *
209
     * @method focusMenuToggle
210
     */
211
    PopoverRegionController.prototype.focusMenuToggle = function() {
212
        this.menuToggle.focus();
213
    };
214
 
215
    /**
216
     * Check if a content item has focus.
217
     *
218
     * @method contentItemHasFocus
219
     * @return {bool}
220
     */
221
    PopoverRegionController.prototype.contentItemHasFocus = function() {
222
        return this.getContentItemWithFocus().length > 0;
223
    };
224
 
225
    /**
226
     * Return the currently focused content item.
227
     *
228
     * @method getContentItemWithFocus
229
     * @return {jQuery} object
230
     */
231
    PopoverRegionController.prototype.getContentItemWithFocus = function() {
232
        var currentFocus = $(document.activeElement);
233
        var items = this.getContent().children();
234
        var currentItem = items.filter(currentFocus);
235
 
236
        if (!currentItem.length) {
237
            currentItem = items.has(currentFocus);
238
        }
239
 
240
        return currentItem;
241
    };
242
 
243
    /**
244
     * Focus the given content item or the first focusable element within
245
     * the content item.
246
     *
247
     * @method focusContentItem
248
     * @param {object} item The content item jQuery element
249
     */
250
    PopoverRegionController.prototype.focusContentItem = function(item) {
251
        if (item.is(SELECTORS.CAN_RECEIVE_FOCUS)) {
252
            item.focus();
253
        } else {
254
            item.find(SELECTORS.CAN_RECEIVE_FOCUS).first().focus();
255
        }
256
    };
257
 
258
    /**
259
     * Set focus on the first content item in the list.
260
     *
261
     * @method focusFirstContentItem
262
     */
263
    PopoverRegionController.prototype.focusFirstContentItem = function() {
264
        this.focusContentItem(this.getContent().children().first());
265
    };
266
 
267
    /**
268
     * Set focus on the last content item in the list.
269
     *
270
     * @method focusLastContentItem
271
     */
272
    PopoverRegionController.prototype.focusLastContentItem = function() {
273
        this.focusContentItem(this.getContent().children().last());
274
    };
275
 
276
    /**
277
     * Set focus on the content item after the item that currently has focus
278
     * in the list.
279
     *
280
     * @method focusNextContentItem
281
     */
282
    PopoverRegionController.prototype.focusNextContentItem = function() {
283
        var currentItem = this.getContentItemWithFocus();
284
 
285
        if (currentItem.length && currentItem.next()) {
286
            this.focusContentItem(currentItem.next());
287
        }
288
    };
289
 
290
    /**
291
     * Set focus on the content item preceding the item that currently has focus
292
     * in the list.
293
     *
294
     * @method focusPreviousContentItem
295
     */
296
    PopoverRegionController.prototype.focusPreviousContentItem = function() {
297
        var currentItem = this.getContentItemWithFocus();
298
 
299
        if (currentItem.length && currentItem.prev()) {
300
            this.focusContentItem(currentItem.prev());
301
        }
302
    };
303
 
304
    /**
305
     * Register the minimal amount of listeners for the popover to function.
306
     *
307
     * @method registerBaseEventListeners
308
     */
309
    PopoverRegionController.prototype.registerBaseEventListeners = function() {
310
        customEvents.define(this.root, [
311
            customEvents.events.activate,
312
            customEvents.events.escape,
313
        ]);
314
 
315
        // Toggle the popover visibility on activation (click/enter/space) of the toggle button.
316
        this.root.on(customEvents.events.activate, SELECTORS.MENU_TOGGLE, function() {
317
            this.toggleMenu();
318
        }.bind(this));
319
 
320
        // Delay the binding of these handlers until the region has been opened.
321
        this.promises.closeHandlers.done(function() {
322
            // Close the popover if escape is pressed.
323
            this.root.on(customEvents.events.escape, function() {
324
                this.closeMenu();
325
                this.focusMenuToggle();
326
            }.bind(this));
327
 
328
            // Close the popover if any other part of the page is clicked.
329
            $('html').click(function(e) {
330
                var target = $(e.target);
331
                if (!this.root.is(target) && !this.root.has(target).length) {
332
                    this.closeMenu();
333
                }
334
            }.bind(this));
335
 
336
            customEvents.define(this.getContentContainer(), [
337
                customEvents.events.scrollBottom
338
            ]);
339
        }.bind(this));
340
    };
341
 
342
    /**
343
     * Set up the event listeners for keyboard navigating a list of content items.
344
     *
345
     * @method registerListNavigationEventListeners
346
     */
347
    PopoverRegionController.prototype.registerListNavigationEventListeners = function() {
348
        customEvents.define(this.root, [
349
            customEvents.events.down
350
        ]);
351
 
352
        // If the down arrow is pressed then open the menu and focus the first content
353
        // item or focus the next content item if the menu is open.
354
        this.root.on(customEvents.events.down, function(e, data) {
355
            if (!this.isMenuOpen()) {
356
                this.openMenu();
357
                this.focusFirstContentItem();
358
            } else {
359
                if (this.contentItemHasFocus()) {
360
                    this.focusNextContentItem();
361
                } else {
362
                    this.focusFirstContentItem();
363
                }
364
            }
365
 
366
            data.originalEvent.preventDefault();
367
        }.bind(this));
368
 
369
        // Delay the binding of these handlers until the region has been opened.
370
        this.promises.navigationHandlers.done(function() {
371
            customEvents.define(this.root, [
372
                customEvents.events.up,
373
                customEvents.events.home,
374
                customEvents.events.end,
375
            ]);
376
 
377
            // Shift focus to the previous content item if the up key is pressed.
378
            this.root.on(customEvents.events.up, function(e, data) {
379
                this.focusPreviousContentItem();
380
                data.originalEvent.preventDefault();
381
            }.bind(this));
382
 
383
            // Jump focus to the first content item if the home key is pressed.
384
            this.root.on(customEvents.events.home, function(e, data) {
385
                this.focusFirstContentItem();
386
                data.originalEvent.preventDefault();
387
            }.bind(this));
388
 
389
            // Jump focus to the last content item if the end key is pressed.
390
            this.root.on(customEvents.events.end, function(e, data) {
391
                this.focusLastContentItem();
392
                data.originalEvent.preventDefault();
393
            }.bind(this));
394
        }.bind(this));
395
    };
396
 
397
    /**
398
     * Set the appropriate tabindex attribute on the popover toggle.
399
     *
400
     * @method updateFocusItemTabIndex
401
     */
402
    PopoverRegionController.prototype.updateFocusItemTabIndex = function() {
403
        if (this.isMenuOpen()) {
404
            this.menuContainer.find(SELECTORS.CAN_RECEIVE_FOCUS).removeAttr('tabindex');
405
        } else {
406
            this.menuContainer.find(SELECTORS.CAN_RECEIVE_FOCUS).attr('tabindex', '-1');
407
        }
408
    };
409
 
410
    return PopoverRegionController;
411
});