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
 * Dropdown status JS controls.
18
 *
19
 * The status controls enable extra configurarions for the dropdown like:
20
 * - Sync the button text with the selected option.
21
 * - Update the status of the button when the selected option changes. This will
22
 *   trigger a "change" event when the status changes.
23
 *
24
 * @module      core/local/dropdown/status
25
 * @copyright   2023 Ferran Recio <ferran@moodle.com>
26
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27
 */
28
 
29
import {DropdownDialog} from 'core/local/dropdown/dialog';
30
 
31
const Selectors = {
32
    checkedIcon: '[data-for="checkedIcon"]',
33
    option: '[role="option"]',
34
    optionItem: '[data-optionnumber]',
35
    optionIcon: '.option-icon',
36
    selectedOption: '[role="option"][aria-selected="true"]',
37
    uncheckedIcon: '[data-for="uncheckedIcon"]',
38
};
39
 
40
const Classes = {
41
    selected: 'selected',
42
    disabled: 'disabled',
43
    hidden: 'd-none',
44
};
45
 
46
/**
47
 * Dropdown dialog class.
48
 * @private
49
 */
50
export class DropdownStatus extends DropdownDialog {
51
    /**
52
     * Constructor.
53
     * @param {HTMLElement} element The element to initialize.
54
     */
55
    constructor(element) {
56
        super(element);
57
        this.buttonSync = element.dataset.buttonSync == 'true';
58
        this.updateStatus = element.dataset.updateStatus == 'true';
59
    }
60
 
61
    /**
62
     * Initialize the subpanel element.
63
     *
64
     * This method adds the event listeners to the subpanel and the position classes.
65
     * @private
66
     */
67
    init() {
68
        super.init();
69
 
70
        if (this.element.dataset.dropdownStatusInitialized) {
71
            return;
72
        }
73
 
74
        this.panel.addEventListener('click', this._contentClickHandler.bind(this));
75
 
76
        if (this.element.dataset.buttonSync == 'true') {
77
            this.setButtonSyncEnabled(true);
78
        }
79
        if (this.element.dataset.updateStatus == 'true') {
80
            this.setUpdateStatusEnabled(true);
81
        }
82
 
83
        this.element.dataset.dropdownStatusInitialized = true;
84
    }
85
 
86
    /**
87
     * Handle click events on the status content.
88
     * @param {Event} event The event.
89
     * @private
90
     */
91
    _contentClickHandler(event) {
92
        const option = event.target.closest(Selectors.option);
93
        if (!option) {
94
            return;
95
        }
96
        if (option.getAttribute('aria-disabled') === 'true') {
97
            return;
98
        }
99
        if (option.getAttribute('aria-selected') === 'true') {
100
            return;
101
        }
102
        if (this.isUpdateStatusEnabled()) {
103
            this.setSelectedValue(option.dataset.value);
1441 ariadna 104
            this.button.focus();
1 efrain 105
        }
106
    }
107
 
108
    /**
109
     * Sets the selected value.
110
     * @param {string} value The value to set.
111
     */
112
    setSelectedValue(value) {
113
        const selected = this.panel.querySelector(Selectors.selectedOption);
114
        if (selected && selected.dataset.value === value) {
115
            return;
116
        }
117
        if (selected) {
118
            this._updateOptionChecked(selected, false);
119
        }
120
        const option = this.panel.querySelector(`${Selectors.option}[data-value="${value}"]`);
121
        if (option) {
122
            this._updateOptionChecked(option, true);
123
        }
124
        if (this.isButtonSyncEnabled()) {
125
            this.syncButtonText();
126
        }
127
        // Emit standard radio button event with the selected option.
128
        this.element.dispatchEvent(new Event('change'));
129
    }
130
 
131
    /**
132
     * Update the option checked content.
133
     * @private
134
     * @param {HTMLElement} option the option element to set
135
     * @param {Boolean} checked the new checked value
136
     */
137
    _updateOptionChecked(option, checked) {
138
        option.setAttribute('aria-selected', checked.toString());
139
        option.classList.toggle(Classes.selected, checked);
140
        option.classList.toggle(Classes.disabled, checked);
141
 
142
        const optionItem = option.closest(Selectors.optionItem);
143
        if (optionItem) {
144
            this._updateOptionItemChecked(optionItem, checked);
145
        }
146
 
147
        if (checked) {
148
            this.element.dataset.value = option.dataset.value;
149
        } else if (this.element.dataset.value === option.dataset.value) {
150
            delete this.element.dataset.value;
151
        }
152
    }
153
 
154
    /**
155
     * Update the option item checked content.
156
     * @private
157
     * @param {HTMLElement} optionItem
158
     * @param {Boolean} checked
159
     */
160
    _updateOptionItemChecked(optionItem, checked) {
161
        const selectedClasses = optionItem.dataset.selectedClasses ?? Classes.selected;
162
        for (const selectedClass of selectedClasses.split(' ')) {
163
            optionItem.classList.toggle(selectedClass, checked);
164
        }
165
        if (checked) {
166
            optionItem.dataset.selected = checked;
167
        } else {
168
            delete optionItem?.dataset.selected;
169
        }
170
        const checkedIcon = optionItem.querySelector(Selectors.checkedIcon);
171
        if (checkedIcon) {
172
            checkedIcon.classList.toggle(Classes.hidden, !checked);
173
        }
174
        const uncheckedIcon = optionItem.querySelector(Selectors.uncheckedIcon);
175
        if (uncheckedIcon) {
176
            uncheckedIcon.classList.toggle(Classes.hidden, checked);
177
        }
178
    }
179
 
180
 
181
    /**
182
     * Return the selected value.
183
     * @returns {string|null} The selected value.
184
     */
185
    getSelectedValue() {
186
        const selected = this.panel.querySelector(Selectors.selectedOption);
187
        return selected?.dataset.value ?? null;
188
    }
189
 
190
    /**
191
     * Set the button sync value.
192
     *
193
     * If the sync is enabled, the button text will show the selected option.
194
     *
195
     * @param {Boolean} value The value to set.
196
     */
197
    setButtonSyncEnabled(value) {
198
        if (value) {
199
            this.element.dataset.buttonSync = 'true';
200
        } else {
201
            delete this.element.dataset.buttonSync;
202
        }
203
        if (value) {
204
            this.syncButtonText();
205
        }
206
    }
207
 
208
    /**
209
     * Return if the button sync is enabled.
210
     * @returns {Boolean} The button sync value.
211
     */
212
    isButtonSyncEnabled() {
213
        return this.element.dataset.buttonSync == 'true';
214
    }
215
 
216
    /**
217
     * Sync the button text with the selected option.
218
     */
219
    syncButtonText() {
220
        const selected = this.panel.querySelector(Selectors.selectedOption);
221
        if (!selected) {
222
            return;
223
        }
224
        let newText = selected.textContent;
225
        const optionIcon = this._getOptionIcon(selected);
226
        if (optionIcon) {
227
            newText = optionIcon.innerHTML + newText;
228
        }
229
        this.button.innerHTML = newText;
230
    }
231
 
232
    /**
233
     * Set the update status value.
234
     *
235
     * @param {Boolean} value The value to set.
236
     */
237
    setUpdateStatusEnabled(value) {
238
        if (value) {
239
            this.element.dataset.updateStatus = 'true';
240
        } else {
241
            delete this.element.dataset.updateStatus;
242
        }
243
    }
244
 
245
    /**
246
     * Return if the update status is enabled.
247
     * @returns {Boolean} The update status value.
248
     */
249
    isUpdateStatusEnabled() {
250
        return this.element.dataset.updateStatus == 'true';
251
    }
252
 
253
    _getOptionIcon(option) {
254
        const optionItem = option.closest(Selectors.optionItem);
255
        if (!optionItem) {
256
            return null;
257
        }
258
        return optionItem.querySelector(Selectors.optionIcon);
259
    }
260
 
261
}
262
 
263
/**
264
 * Get the dropdown dialog instance form a selector.
265
 * @param {string} selector The query selector to init.
266
 * @returns {DropdownStatus|null} The dropdown dialog instance if any.
267
 */
268
export const getDropdownStatus = (selector) => {
269
    const dropdownElement = document.querySelector(selector);
270
    if (!dropdownElement) {
271
        return null;
272
    }
273
    return new DropdownStatus(dropdownElement);
274
};
275
 
276
/**
277
 * Initialize module.
278
 *
279
 * @method
280
 * @param {string} selector The query selector to init.
281
 */
282
export const init = (selector) => {
283
    const dropdown = getDropdownStatus(selector);
284
    if (!dropdown) {
285
        throw new Error(`Dopdown status element not found: ${selector}`);
286
    }
287
    dropdown.init();
288
};