Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | 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
 * Moves wrapping navigation items into a more menu.
18
 *
19
 * @module     core/moremenu
20
 * @copyright  2021 Moodle
21
 * @author     Bas Brands <bas@moodle.com>
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
import menu_navigation from "core/menu_navigation";
26
/**
27
 * Moremenu selectors.
28
 */
29
const Selectors = {
30
    regions: {
31
        moredropdown: '[data-region="moredropdown"]',
32
        morebutton: '[data-region="morebutton"]'
33
    },
34
    classes: {
35
        dropdownitem: 'dropdown-item',
36
        dropdownmoremenu: 'dropdownmoremenu',
37
        hidden: 'd-none',
38
        active: 'active',
39
        nav: 'nav',
40
        navlink: 'nav-link',
41
        observed: 'observed',
42
    },
43
    attributes: {
44
        menu: '[role="menu"]',
1441 ariadna 45
        dropdowntoggle: '[data-bs-toggle="dropdown"]'
1 efrain 46
    }
47
};
48
 
49
let isTabListMenu = false;
50
 
51
/**
52
 * Auto Collapse navigation items that wrap into a dropdown menu.
53
 *
54
 * @param {HTMLElement} menu The navbar container.
55
 */
56
const autoCollapse = menu => {
57
 
58
    const maxHeight = menu.parentNode.offsetHeight + 1;
59
 
60
    const moreDropdown = menu.querySelector(Selectors.regions.moredropdown);
61
    const moreButton = menu.querySelector(Selectors.regions.morebutton);
62
 
63
    // If the menu items wrap and the menu height is larger than the height of the
64
    // parent then start pushing navlinks into the moreDropdown.
65
    if (menu.offsetHeight > maxHeight) {
66
        moreButton.classList.remove(Selectors.classes.hidden);
67
 
68
        let menuHeight = 0;
69
        const menuNodes = Array.from(menu.children).reverse();
70
        menuNodes.forEach(item => {
71
            if (!item.classList.contains(Selectors.classes.dropdownmoremenu)) {
72
                // After moving the menu items into the moreDropdown check again
73
                // if the menu height is still larger then the height of the parent.
74
                if (menu.offsetHeight > maxHeight) {
75
                    // Move this node into the more dropdown menu.
76
                    moveIntoMoreDropdown(menu, item, true);
77
                } else if (menuHeight > maxHeight) {
78
                    moveIntoMoreDropdown(menu, item, true);
79
                    menuHeight = 0;
80
                }
81
            } else if (menu.offsetHeight > maxHeight) {
82
                // Assign menu height to be used to check with menu parent.
83
                menuHeight = menu.offsetHeight;
84
            }
85
        });
86
    } else {
87
        // If the menu height is smaller than the height of the parent, then try returning navlinks to the menu.
88
        if ('children' in moreDropdown) {
89
            // Iterate through the nodes within the more dropdown menu.
90
            Array.from(moreDropdown.children).forEach(item => {
91
                // Don't move the node to the more menu if it is explicitly defined that
92
                // this node should be displayed in the more dropdown menu at all times.
93
                if (menu.offsetHeight < maxHeight && item.dataset.forceintomoremenu !== 'true') {
94
                    const lastNode = moreDropdown.removeChild(item);
95
                    // Move this node from the more dropdown menu into the main section of the menu.
96
                    moveOutOfMoreDropdown(menu, lastNode);
97
                }
98
            });
99
            // If there are no more nodes in the more dropdown menu we can hide the moreButton.
100
            if (Array.from(moreDropdown.children).length === 0) {
101
                moreButton.classList.add(Selectors.classes.hidden);
102
            }
103
        }
104
 
105
        if (menu.offsetHeight > maxHeight) {
106
            autoCollapse(menu);
107
        }
108
    }
109
    menu.parentNode.classList.add(Selectors.classes.observed);
110
};
111
 
112
/**
113
 * Move a node into the "more" dropdown menu.
114
 *
115
 * This method forces a given navigation node to be added and displayed within the "more" dropdown menu.
116
 *
117
 * @param {HTMLElement} menu The navbar moremenu.
118
 * @param {HTMLElement} navNode The navigation node.
119
 * @param {boolean} prepend Whether to prepend or append the node to the content in the more dropdown menu.
120
 */
121
const moveIntoMoreDropdown = (menu, navNode, prepend = false) => {
122
    const moreDropdown = menu.querySelector(Selectors.regions.moredropdown);
123
    const dropdownToggle = menu.querySelector(Selectors.attributes.dropdowntoggle);
124
 
125
    const navLink = navNode.querySelector('.' + Selectors.classes.navlink);
126
    // If there are navLinks that contain an active link in the moreDropdown
127
    // make the dropdownToggle in the moreButton active.
128
    if (navLink.classList.contains(Selectors.classes.active)) {
129
        dropdownToggle.classList.add(Selectors.classes.active);
130
        dropdownToggle.setAttribute('tabindex', '0');
131
        navLink.setAttribute('tabindex', '-1'); // So that we don't have a single tabbable menu item.
132
        // Remove aria-selected if the more menu is rendered as a tab list.
133
        if (isTabListMenu) {
134
            navLink.removeAttribute('aria-selected');
135
        }
136
        navLink.setAttribute('aria-current', 'true');
137
    }
138
 
139
    // This will become a menu item instead of a tab.
140
    navLink.setAttribute('role', 'menuitem');
141
 
142
    // Change the styling of the navLink to a dropdownitem and push it into
143
    // the moreDropdown.
144
    navLink.classList.remove(Selectors.classes.navlink);
145
    navLink.classList.add(Selectors.classes.dropdownitem);
146
    if (prepend) {
147
        moreDropdown.prepend(navNode);
148
    } else {
149
        moreDropdown.append(navNode);
150
    }
151
};
152
 
153
/**
154
 * Move a node out of the "more" dropdown menu.
155
 *
156
 * This method forces a given node from the "more" dropdown menu to be displayed in the main section of the menu.
157
 *
158
 * @param {HTMLElement} menu The navbar moremenu.
159
 * @param {HTMLElement} navNode The navigation node.
160
 */
161
const moveOutOfMoreDropdown = (menu, navNode) => {
162
    const moreButton = menu.querySelector(Selectors.regions.morebutton);
163
    const dropdownToggle = menu.querySelector(Selectors.attributes.dropdowntoggle);
164
    const navLink = navNode.querySelector('.' + Selectors.classes.dropdownitem);
165
 
166
    // If the more menu is rendered as a tab list,
167
    // this will become a tab instead of a menuitem when moved out of the more menu dropdown.
168
    if (isTabListMenu) {
169
        navLink.setAttribute('role', 'tab');
170
    }
171
 
172
    // Stop displaying the active state on the dropdownToggle if
173
    // the active navlink is removed.
174
    if (navLink.classList.contains(Selectors.classes.active)) {
175
        dropdownToggle.classList.remove(Selectors.classes.active);
176
        dropdownToggle.setAttribute('tabindex', '-1');
177
        navLink.setAttribute('tabindex', '0');
178
        if (isTabListMenu) {
179
            // Replace aria selection state when necessary.
180
            navLink.removeAttribute('aria-current');
181
            navLink.setAttribute('aria-selected', 'true');
182
        }
183
    }
184
    navLink.classList.remove(Selectors.classes.dropdownitem);
185
    navLink.classList.add(Selectors.classes.navlink);
186
    menu.insertBefore(navNode, moreButton);
187
};
188
 
189
/**
190
 * Initialise the more menus.
191
 *
192
 * @param {HTMLElement} menu The navbar moremenu.
193
 */
194
export default menu => {
195
    isTabListMenu = menu.getAttribute('role') === 'tablist';
196
 
197
    // Select the first menu item if there's nothing initially selected.
198
    const hash = window.location.hash;
199
    if (!hash) {
200
        const itemRole = isTabListMenu ? 'tab' : 'menuitem';
201
        const menuListItem = menu.firstElementChild;
202
        const roleSelector = `[role=${itemRole}]`;
203
        const menuItem = menuListItem.querySelector(roleSelector);
204
        const ariaAttribute = isTabListMenu ? 'aria-selected' : 'aria-current';
205
        if (!menu.querySelector(`[${ariaAttribute}='true']`)) {
206
            menuItem.setAttribute(ariaAttribute, 'true');
207
            menuItem.setAttribute('tabindex', '0');
208
        }
209
    }
210
 
211
    // Pre-populate the "more" dropdown menu with navigation nodes which are set to be displayed in this menu
212
    // by default at all times.
213
    if ('children' in menu) {
214
        const moreButton = menu.querySelector(Selectors.regions.morebutton);
215
        const menuNodes = Array.from(menu.children);
216
        menuNodes.forEach((item) => {
217
            if (!item.classList.contains(Selectors.classes.dropdownmoremenu) &&
218
                    item.dataset.forceintomoremenu === 'true') {
219
                // Append this node into the more dropdown menu.
220
                moveIntoMoreDropdown(menu, item, false);
221
                // After adding the node into the more dropdown menu, make sure that the more dropdown menu button
222
                // is displayed.
223
                if (moreButton.classList.contains(Selectors.classes.hidden)) {
224
                    moreButton.classList.remove(Selectors.classes.hidden);
225
                }
226
            }
227
        });
228
    }
229
    // Populate the more dropdown menu with additional nodes if necessary, depending on the current screen size.
230
    autoCollapse(menu);
231
    menu_navigation(menu);
232
 
233
    // When the screen size changes make sure the menu still fits.
234
    window.addEventListener('resize', () => {
235
        autoCollapse(menu);
236
        menu_navigation(menu);
237
    });
238
 
1441 ariadna 239
    const toggledropdown = e => e.stopPropagation();
1 efrain 240
 
1441 ariadna 241
    // If there are dropdowns in the "More" menu, add an event listener on click to prevent the menu from closing.
242
    document.querySelector('.' + Selectors.classes.dropdownmoremenu).addEventListener('show.bs.dropdown', () => {
1 efrain 243
        const moreDropdown = menu.querySelector(Selectors.regions.moredropdown);
244
        moreDropdown.querySelectorAll('.dropdown').forEach((dropdown) => {
245
            dropdown.removeEventListener('click', toggledropdown, true);
246
            dropdown.addEventListener('click', toggledropdown, true);
247
        });
248
    });
249
};