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
 * 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);
104
        }
105
    }
106
 
107
    /**
108
     * Sets the selected value.
109
     * @param {string} value The value to set.
110
     */
111
    setSelectedValue(value) {
112
        const selected = this.panel.querySelector(Selectors.selectedOption);
113
        if (selected && selected.dataset.value === value) {
114
            return;
115
        }
116
        if (selected) {
117
            this._updateOptionChecked(selected, false);
118
        }
119
        const option = this.panel.querySelector(`${Selectors.option}[data-value="${value}"]`);
120
        if (option) {
121
            this._updateOptionChecked(option, true);
122
        }
123
        if (this.isButtonSyncEnabled()) {
124
            this.syncButtonText();
125
        }
126
        // Emit standard radio button event with the selected option.
127
        this.element.dispatchEvent(new Event('change'));
128
    }
129
 
130
    /**
131
     * Update the option checked content.
132
     * @private
133
     * @param {HTMLElement} option the option element to set
134
     * @param {Boolean} checked the new checked value
135
     */
136
    _updateOptionChecked(option, checked) {
137
        option.setAttribute('aria-selected', checked.toString());
138
        option.classList.toggle(Classes.selected, checked);
139
        option.classList.toggle(Classes.disabled, checked);
140
 
141
        const optionItem = option.closest(Selectors.optionItem);
142
        if (optionItem) {
143
            this._updateOptionItemChecked(optionItem, checked);
144
        }
145
 
146
        if (checked) {
147
            this.element.dataset.value = option.dataset.value;
148
        } else if (this.element.dataset.value === option.dataset.value) {
149
            delete this.element.dataset.value;
150
        }
151
    }
152
 
153
    /**
154
     * Update the option item checked content.
155
     * @private
156
     * @param {HTMLElement} optionItem
157
     * @param {Boolean} checked
158
     */
159
    _updateOptionItemChecked(optionItem, checked) {
160
        const selectedClasses = optionItem.dataset.selectedClasses ?? Classes.selected;
161
        for (const selectedClass of selectedClasses.split(' ')) {
162
            optionItem.classList.toggle(selectedClass, checked);
163
        }
164
        if (checked) {
165
            optionItem.dataset.selected = checked;
166
        } else {
167
            delete optionItem?.dataset.selected;
168
        }
169
        const checkedIcon = optionItem.querySelector(Selectors.checkedIcon);
170
        if (checkedIcon) {
171
            checkedIcon.classList.toggle(Classes.hidden, !checked);
172
        }
173
        const uncheckedIcon = optionItem.querySelector(Selectors.uncheckedIcon);
174
        if (uncheckedIcon) {
175
            uncheckedIcon.classList.toggle(Classes.hidden, checked);
176
        }
177
    }
178
 
179
 
180
    /**
181
     * Return the selected value.
182
     * @returns {string|null} The selected value.
183
     */
184
    getSelectedValue() {
185
        const selected = this.panel.querySelector(Selectors.selectedOption);
186
        return selected?.dataset.value ?? null;
187
    }
188
 
189
    /**
190
     * Set the button sync value.
191
     *
192
     * If the sync is enabled, the button text will show the selected option.
193
     *
194
     * @param {Boolean} value The value to set.
195
     */
196
    setButtonSyncEnabled(value) {
197
        if (value) {
198
            this.element.dataset.buttonSync = 'true';
199
        } else {
200
            delete this.element.dataset.buttonSync;
201
        }
202
        if (value) {
203
            this.syncButtonText();
204
        }
205
    }
206
 
207
    /**
208
     * Return if the button sync is enabled.
209
     * @returns {Boolean} The button sync value.
210
     */
211
    isButtonSyncEnabled() {
212
        return this.element.dataset.buttonSync == 'true';
213
    }
214
 
215
    /**
216
     * Sync the button text with the selected option.
217
     */
218
    syncButtonText() {
219
        const selected = this.panel.querySelector(Selectors.selectedOption);
220
        if (!selected) {
221
            return;
222
        }
223
        let newText = selected.textContent;
224
        const optionIcon = this._getOptionIcon(selected);
225
        if (optionIcon) {
226
            newText = optionIcon.innerHTML + newText;
227
        }
228
        this.button.innerHTML = newText;
229
    }
230
 
231
    /**
232
     * Set the update status value.
233
     *
234
     * @param {Boolean} value The value to set.
235
     */
236
    setUpdateStatusEnabled(value) {
237
        if (value) {
238
            this.element.dataset.updateStatus = 'true';
239
        } else {
240
            delete this.element.dataset.updateStatus;
241
        }
242
    }
243
 
244
    /**
245
     * Return if the update status is enabled.
246
     * @returns {Boolean} The update status value.
247
     */
248
    isUpdateStatusEnabled() {
249
        return this.element.dataset.updateStatus == 'true';
250
    }
251
 
252
    _getOptionIcon(option) {
253
        const optionItem = option.closest(Selectors.optionItem);
254
        if (!optionItem) {
255
            return null;
256
        }
257
        return optionItem.querySelector(Selectors.optionIcon);
258
    }
259
 
260
}
261
 
262
/**
263
 * Get the dropdown dialog instance form a selector.
264
 * @param {string} selector The query selector to init.
265
 * @returns {DropdownStatus|null} The dropdown dialog instance if any.
266
 */
267
export const getDropdownStatus = (selector) => {
268
    const dropdownElement = document.querySelector(selector);
269
    if (!dropdownElement) {
270
        return null;
271
    }
272
    return new DropdownStatus(dropdownElement);
273
};
274
 
275
/**
276
 * Initialize module.
277
 *
278
 * @method
279
 * @param {string} selector The query selector to init.
280
 */
281
export const init = (selector) => {
282
    const dropdown = getDropdownStatus(selector);
283
    if (!dropdown) {
284
        throw new Error(`Dopdown status element not found: ${selector}`);
285
    }
286
    dropdown.init();
287
};