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
 * Dropdown status JS controls.
18
 *
19
 * @module      core/local/dropdown/dialog
20
 * @copyright   2023 Ferran Recio <ferran@moodle.com>
21
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 */
23
 
24
// The jQuery module is only used for interacting with Bootstrap 4. It can be removed when MDL-71979 is integrated.
25
import jQuery from 'jquery';
26
import {
27
    firstFocusableElement,
28
    lastFocusableElement,
29
    previousFocusableElement,
30
    nextFocusableElement,
31
} from 'core/pagehelpers';
32
import Pending from 'core/pending';
33
 
34
const Selectors = {
35
    dropdownButton: '[data-for="dropdowndialog_button"]',
36
    dropdownDialog: '[data-for="dropdowndialog_dialog"]',
37
};
38
 
39
/**
40
 * Dropdown dialog class.
41
 * @private
42
 */
43
export class DropdownDialog {
44
    /**
45
     * Constructor.
46
     * @param {HTMLElement} element The element to initialize.
47
     */
48
    constructor(element) {
49
        this.element = element;
50
        this.button = element.querySelector(Selectors.dropdownButton);
51
        this.panel = element.querySelector(Selectors.dropdownDialog);
52
    }
53
 
54
    /**
55
     * Initialize the subpanel element.
56
     *
57
     * This method adds the event listeners to the subpanel and the position classes.
58
     */
59
    init() {
60
        if (this.element.dataset.dropdownDialogInitialized) {
61
            return;
62
        }
63
 
64
        // Menu Item events.
65
        this.button.addEventListener('keydown', this._buttonKeyHandler.bind(this));
66
        // Subpanel content events.
67
        this.panel.addEventListener('keydown', this._contentKeyHandler.bind(this));
68
 
69
        this.element.dataset.dropdownDialogInitialized = true;
70
    }
71
 
72
    /**
73
     * Dropdown button key handler.
74
     * @param {Event} event
75
     * @private
76
     */
77
    _buttonKeyHandler(event) {
78
        if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
79
            event.stopPropagation();
80
            event.preventDefault();
81
            this.setVisible(false);
82
            return;
83
        }
84
 
85
        if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
86
            event.stopPropagation();
87
            event.preventDefault();
88
            this.setVisible(true);
89
            this._focusPanelContent();
90
        }
91
    }
92
 
93
    /**
94
     * Sub panel content key handler.
95
     * @param {Event} event
96
     * @private
97
     */
98
    _contentKeyHandler(event) {
99
        let newFocus = null;
100
 
101
        if (event.key === 'End') {
102
            newFocus = lastFocusableElement(this.panel);
103
        }
104
        if (event.key === 'Home') {
105
            newFocus = firstFocusableElement(this.panel);
106
        }
107
        if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
108
            newFocus = previousFocusableElement(this.panel, false);
109
            if (!newFocus) {
110
                newFocus = this.button;
111
            }
112
        }
113
        if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
114
            newFocus = nextFocusableElement(this.panel, false);
115
        }
116
        if (newFocus !== null) {
117
            event.stopPropagation();
118
            event.preventDefault();
119
            newFocus.focus();
120
        }
121
    }
122
 
123
    /**
124
     * Focus on the first focusable element of the subpanel.
125
     * @private
126
     */
127
    _focusPanelContent() {
128
        const pendingPromise = new Pending('core/dropdown/dialog:focuscontent');
129
        // Some Bootstrap events are triggered after the click event.
130
        // To prevent this from affecting the focus we wait a bit.
131
        setTimeout(() => {
132
            const firstFocusable = firstFocusableElement(this.panel);
133
            if (firstFocusable) {
134
                firstFocusable.focus();
135
            }
136
            pendingPromise.resolve();
137
        }, 100);
138
    }
139
 
140
    /**
141
     * Set the visibility of a subpanel.
142
     * @param {Boolean} visible true if the subpanel should be visible.
143
     */
144
    setVisible(visible) {
145
        if (visible === this.isVisible()) {
146
            return;
147
        }
148
        // All jQuery in this code can be replaced when MDL-71979 is integrated.
149
        jQuery(this.button).dropdown('toggle');
150
    }
151
 
152
    /**
153
     * Get the visibility of a subpanel.
154
     * @returns {Boolean} true if the subpanel is visible.
155
     */
156
    isVisible() {
157
        return this.button.getAttribute('aria-expanded') === 'true';
158
    }
159
 
160
    /**
161
     * Set the content of the button.
162
     * @param {String} content
163
     */
164
    setButtonContent(content) {
165
        this.button.innerHTML = content;
166
    }
167
 
168
    /**
169
     * Set the disabled state of the button.
170
     * @param {Boolean} disabled
171
     */
172
    setButtonDisabled(disabled) {
173
        if (disabled) {
174
            this.button.setAttribute('disabled', 'disabled');
175
        } else {
176
            this.button.removeAttribute('disabled');
177
        }
178
    }
179
 
180
    /**
181
     * Return the main dropdown HTML element.
182
     * @returns {HTMLElement} The element.
183
     */
184
    getElement() {
185
        return this.element;
186
    }
187
}
188
 
189
/**
190
 * Get the dropdown dialog instance from a selector.
191
 * @param {string} selector The query selector to init.
192
 * @returns {DropdownDialog|null} The dropdown dialog instance if any.
193
 */
194
export const getDropdownDialog = (selector) => {
195
    const dropdownElement = document.querySelector(selector);
196
    if (!dropdownElement) {
197
        return null;
198
    }
199
    return new DropdownDialog(dropdownElement);
200
};
201
 
202
/**
203
 * Initialize module.
204
 *
205
 * @method
206
 * @param {string} selector The query selector to init.
207
 */
208
export const init = (selector) => {
209
    const dropdown = getDropdownDialog(selector);
210
    if (!dropdown) {
211
        throw new Error(`Dopdown dialog element not found: ${selector}`);
212
    }
213
    dropdown.init();
214
};