Created
October 29, 2025 16:54
-
-
Save TobiGa/2e7927cd165edefc44520543ccb2c6a4 to your computer and use it in GitHub Desktop.
// js part of the custom element
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // This file is part of Moodle - http://moodle.org/ | |
| // | |
| // Moodle is free software: you can redistribute it and/or modify | |
| // it under the terms of the GNU General Public License as published by | |
| // the Free Software Foundation, either version 3 of the License, or | |
| // (at your option) any later version. | |
| // | |
| // Moodle is distributed in the hope that it will be useful, | |
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| // GNU General Public License for more details. | |
| // | |
| // You should have received a copy of the GNU General Public License | |
| // along with Moodle. If not, see <http://www.gnu.org/licenses/>. | |
| /** | |
| * Module for class. | |
| * | |
| * This custom select works with a moodle textinput "[name='customchar1']", | |
| * which will store the option. The custom selects purpose is improved user interface. | |
| * | |
| * @module enrol_class/customselect | |
| * @copyright 2024 ISB Bayern | |
| * @author Tobias Garske | |
| * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
| */ | |
| import Pending from 'core/pending'; | |
| const customSelect = document.querySelector(".select.enrol_class"); | |
| const customInput = document.getElementById("custom-input"); | |
| const hiddenInput = document.querySelector("[name='customint4']"); | |
| const customDropdown = document.querySelector(".enrol_class #select-dropdown"); | |
| const filters = document.querySelector(".enrol_class .filters"); | |
| const filtersList = document.querySelectorAll(".enrol_class .filters .filter"); | |
| const optionsList = document.querySelectorAll(".enrol_class .select-dropdown li.options"); | |
| let focusedIndex = -1; | |
| let optionsListVisible = optionsList; | |
| /** | |
| * Initialize custom form. | |
| * @method init | |
| */ | |
| export const init = () => { | |
| const pendingPromise = new Pending('enrol_class/customselect'); | |
| customform(); | |
| pendingPromise.resolve(); | |
| }; | |
| const customform = () => { | |
| // Add a click event to select button. | |
| customInput.addEventListener("click", () => { | |
| // Add/remove active class on the container element. | |
| customSelect.classList.toggle("active"); | |
| // Update the aria-expanded attribute based on the current state. | |
| customInput.setAttribute( | |
| "aria-expanded", | |
| customInput.getAttribute("aria-expanded") === "true" ? "false" : "true" | |
| ); | |
| }); | |
| // Add events to options to set moodle "hidden" input. | |
| optionsList.forEach((option) => { | |
| option.addEventListener("click", (e) => { | |
| // Set hidden input. | |
| hiddenInput.value = e.target.dataset.id; | |
| // Close custom select form element. | |
| customSelect.classList.remove("active"); | |
| }); | |
| }); | |
| // Add events to filters. | |
| filtersList.forEach((filter) => { | |
| filter.addEventListener("click", (e) => { | |
| // Filter list. | |
| filtergroups(e.target.dataset); | |
| }); | |
| }); | |
| // Add events to changing input. | |
| customInput.addEventListener('input', function(e) { | |
| // Filter list. | |
| filterinput(e.target.value); | |
| // Make sure list is visible, can be hidden because of focus click. | |
| if (!customSelect.classList.contains("active")) { | |
| customSelect.classList.add("active"); | |
| } | |
| // Unset hidden value, if inputstring is changed. | |
| hiddenInput.value = ''; | |
| // Unset selected option in mock form. | |
| let selected = document.querySelector(".enrol_class .select-dropdown li.options[aria-selected='true']"); | |
| if (selected) { | |
| selected.setAttribute('aria-selected', 'false'); | |
| } | |
| }); | |
| // Add events to certain keystrokes on input and dropdown. | |
| customInput.addEventListener('keydown', function(e) { | |
| moveFocus(e); | |
| }); | |
| // Hide options when clicking outside of select dropdown. | |
| window.addEventListener('mouseup', function(event) { | |
| if ( | |
| event.target != customInput && | |
| event.target != customDropdown && | |
| event.target.parentNode != customDropdown && | |
| event.target.parentNode != filters | |
| ) { | |
| customSelect.classList.remove("active"); | |
| } | |
| }); | |
| /** | |
| * | |
| * @param {Array} dataset of clicked filter. | |
| */ | |
| const filtergroups = (dataset) => { | |
| togglefilter(dataset); | |
| // Read all active filters. | |
| const filtersActiveList = document.querySelectorAll(".enrol_class .filters .filter[data-active='active']"); | |
| const arrFilters = []; | |
| filtersActiveList.forEach((filter) => { | |
| arrFilters.push(filter.dataset.filter); | |
| }); | |
| // Read input. | |
| let inputValue = ''; | |
| if (customInput.value != '') { | |
| inputValue = customInput.value; | |
| } | |
| // Change visibility accordingly. | |
| if (inputValue.length == 0) { | |
| optionsList.forEach((option) => { | |
| if (arrFilters.length !== 0) { | |
| arrFilters.forEach((filter) => { | |
| if (compare(option.dataset.filtertype, filter)) { | |
| show(option); | |
| } else { | |
| hide(option); | |
| } | |
| }); | |
| } else { | |
| show(option); | |
| } | |
| }); | |
| } else { | |
| filterinputandgroups(inputValue, arrFilters); | |
| } | |
| updateOptionsVisibleIndex(); | |
| }; | |
| /** | |
| * | |
| * @param {String} input | |
| */ | |
| const filterinput = (input) => { | |
| // Read all active filters. | |
| const filtersActiveList = document.querySelectorAll(".enrol_class .filters .filter[data-active='active']"); | |
| const arrFilters = []; | |
| filtersActiveList.forEach((filter) => { | |
| arrFilters.push(filter.dataset.filter); | |
| }); | |
| if (arrFilters.length == 0) { | |
| // Change visibility according to input. | |
| optionsList.forEach((option) => { | |
| if (compare(option.dataset.value, input)) { | |
| show(option); | |
| } else { | |
| hide(option); | |
| } | |
| }); | |
| } else { | |
| filterinputandgroups(input, arrFilters); | |
| } | |
| updateOptionsVisibleIndex(); | |
| }; | |
| /** | |
| * @param {String} input | |
| * @param {Array} arrFilters | |
| */ | |
| const filterinputandgroups = (input, arrFilters) => { | |
| // Change visibility according to filter and input. | |
| optionsList.forEach((option) => { | |
| arrFilters.forEach((filter) => { | |
| if (arrFilters.length !== 0) { | |
| // If a value is set, don`t try to filter for value, only filter. | |
| if (hiddenInput.value != '') { | |
| if (compare(option.dataset.filtertype, filter)) { | |
| show(option); | |
| } else { | |
| hide(option); | |
| } | |
| } else { | |
| if (compare(option.dataset.filtertype, filter) && compare(option.dataset.value, input)) { | |
| show(option); | |
| } else { | |
| hide(option); | |
| } | |
| } | |
| } else if (compare(option.dataset.value, input)) { | |
| show(option); | |
| } | |
| }); | |
| if (arrFilters.length == 0) { | |
| show(option); | |
| } | |
| }); | |
| updateOptionsVisibleIndex(); | |
| }; | |
| /** | |
| * @param {String} target | |
| * @param {String} value | |
| */ | |
| const compare = (target, value) => { | |
| // Convert value value to lowercase for case-insensitive comparison | |
| let valueValueLower = value.toLowerCase(); | |
| let targetValueLower = target.toLowerCase(); | |
| if (targetValueLower.includes(valueValueLower)) { | |
| return true; | |
| } else { | |
| return false; | |
| } | |
| }; | |
| /** | |
| * @param {Array} target option. | |
| */ | |
| const show = (target) => { | |
| target.style.display = 'flex'; | |
| if (!target.classList.contains("onlist")) { | |
| target.classList.add("onlist"); | |
| } | |
| }; | |
| /** | |
| * @param {Array} target option. | |
| */ | |
| const hide = (target) => { | |
| target.style.display = 'none'; | |
| if (target.classList.contains("onlist")) { | |
| target.classList.remove("onlist"); | |
| } | |
| }; | |
| /** | |
| * @param {Array} dataset of clicked filter. | |
| */ | |
| const togglefilter = (dataset) => { | |
| // Toggle filter or choose new filter. | |
| if (dataset.active === "active") { | |
| dataset.active = "inactive"; | |
| } else { | |
| // Deactivate others and activate clicked. | |
| filtersList.forEach((filter) => { | |
| filter.dataset.active = "inactive"; | |
| }); | |
| dataset.active = "active"; | |
| } | |
| }; | |
| /** | |
| * Function to get keystrokes for moving the focus. | |
| * @param {Array} e event | |
| */ | |
| const moveFocus = (e) => { | |
| switch (e.key) { | |
| case 'ArrowDown': | |
| updateFocus("down"); | |
| // Make sure list is visible, can be hidden because of focus click. | |
| if (!customSelect.classList.contains("active")) { | |
| customSelect.classList.add("active"); | |
| } | |
| e.preventDefault(); | |
| break; | |
| case 'ArrowUp': | |
| updateFocus("up"); | |
| e.preventDefault(); | |
| break; | |
| case 'Enter': | |
| hitEnter(); | |
| e.preventDefault(); | |
| break; | |
| case 'Tab': | |
| // Close custom select form element. | |
| customSelect.classList.remove("active"); | |
| break; | |
| } | |
| }; | |
| /** | |
| * Function to update the focus based on key press. | |
| * @param {String} direction | |
| */ | |
| const updateFocus = (direction) => { | |
| // Remove the focused class from the currently focused item. | |
| if (focusedIndex >= 0) { | |
| optionsListVisible[focusedIndex].classList.remove("focused"); | |
| } | |
| // Calculate the index of the new focused item. | |
| if (direction === "up") { | |
| focusedIndex = Math.max(focusedIndex - 1, 0); | |
| } else if (direction === "down") { | |
| focusedIndex = Math.min(focusedIndex + 1, optionsListVisible.length - 1); | |
| } | |
| // Add the focused class to the new focused item. | |
| optionsListVisible[focusedIndex].classList.add("focused"); | |
| // Make sure the focused item is in view. | |
| optionsListVisible[focusedIndex].scrollIntoView({ behavior: "smooth", block: "nearest" }); | |
| }; | |
| /** | |
| * Function select focussed element. | |
| */ | |
| const hitEnter = () => { | |
| let focusedElements = document.querySelectorAll(".enrol_class .select-dropdown li.options.focused.onlist"); | |
| // Check if the list exists and contains exactly one element | |
| if (focusedElements.length === 1) { | |
| // Set inputs. | |
| customInput.value = focusedElements[0].dataset.value; | |
| hiddenInput.value = focusedElements[0].dataset.id; | |
| // Close custom select form element. | |
| customSelect.classList.remove("active"); | |
| } else { | |
| // Multiple foci, unset all. | |
| optionsList.forEach((option) => { | |
| option.classList.remove('focused'); | |
| }); | |
| } | |
| }; | |
| /** | |
| * Function to update the visible list representation: optionsListVisible. | |
| */ | |
| const updateOptionsVisibleIndex = () => { | |
| optionsListVisible = document.querySelectorAll(".enrol_class .select-dropdown li.options.onlist"); | |
| }; | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment