Skip to content

Instantly share code, notes, and snippets.

@TobiGa
Created October 29, 2025 16:54
Show Gist options
  • Save TobiGa/2e7927cd165edefc44520543ccb2c6a4 to your computer and use it in GitHub Desktop.
Save TobiGa/2e7927cd165edefc44520543ccb2c6a4 to your computer and use it in GitHub Desktop.
// js part of the custom element
// 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