csdsolutions-csdjs
Version:
Libreria per i progetti di CSD Solutions
635 lines (549 loc) • 22.5 kB
JavaScript
function initializeCustomSelect(selectElement) {
const $select = $CSD(selectElement);
const groupOptions = $select.find("optgroup");
const options = $select.find("option");
const idSelect = $select.attr("id");
const placeholder =
$select.attr("placeholder") ||
options.filter('[selected][value=""]').text() ||
"Seleziona un'opzione";
const classCssExtra = [];
selectElement.classList.contains("compact")
? classCssExtra.push("csd-select-compact")
: "";
selectElement.classList.contains("checkmark")
? classCssExtra.push("csd-select-checkmark")
: "";
selectElement.classList.contains("checkmark-all")
? classCssExtra.push("csd-select-checkmark")
: "";
selectElement.classList.contains("chips")
? classCssExtra.push("csd-select-chips")
: "";
// Crea il wrapper personalizzato
const $wrapper = $CSD('<div class="csd-select-wrapper"></div>');
const $customSelect = $CSD(
'<div class="csd-custom-select ' + classCssExtra.join(" ") + '"></div>'
);
const isMultipleSelect = selectElement.hasAttribute("multiple");
let $display = $CSD('<div class="csd-select-display"></div>');
$display.html('<label class="csd-label-select">' + placeholder + "</label>");
if (selectElement.classList.contains("editable")) {
$display = $CSD(
'<input type="text" class="csd-searchbar-input csd-select-display csd-select-editable csd-input" placeholder="Seleziona un\'opzione">'
);
}
let checkmark = false;
if (
selectElement.classList.contains("checkmark") ||
selectElement.classList.contains("checkmark-all")
) {
checkmark = '<ion-icon name="checkmark-sharp"></ion-icon>';
}
const searchbarHtml = selectElement.classList.contains("checkmark-all")
? '<div class="csd-select-searchbar-checkbox"></div><input type="text" class="csd-searchbar-input csd-input" placeholder="Cerca..." id="searchInput">'
: '<input type="text" class="csd-searchbar-input csd-input" placeholder="Cerca..." id="searchInput"><ion-icon class="csd-searchbar-icon" name="search-sharp">';
const $searchbar = $CSD('<div class="csd-select-searchbar">').html(
searchbarHtml
);
const $arrow = $CSD(
'<div class="csd-select-arrow"><ion-icon class="csd-select-arrow-icon" name="chevron-down-sharp"></ion-icon></div>'
);
const $clearButton = $CSD(
'<div class="csd-select-clearbutton"><ion-icon name="close-sharp"></ion-icon></div>'
);
const $optionsContainer = $CSD(
'<div class="csd-select-options ' + classCssExtra.join(" ") + '"></div>'
);
// Hide original select
$select.css('display', 'none');
if (selectElement.classList.contains("searchable")) {
$optionsContainer.append($searchbar);
}
// Add option groups
groupOptions.each(function () {
const $group = $CSD(this);
const $groupLabel = $CSD(
'<div class="csd-select-group-label"><div class="csd-select-group-label-text">' +
$group.attr("label") +
"</div></div>"
);
const $groupOptions = $CSD('<div class="csd-select-group-options"></div>');
$group.find("option").each(function () {
const $option = $CSD(this);
const value = $option.attr('value');
const text = $option.text();
let htmlText = text;
if (checkmark) htmlText = checkmark + text;
const $customOption = $CSD(
`<div class="csd-select-option" data-value="${value}">${htmlText}</div>`
);
$groupOptions.append($customOption);
});
$groupOptions.prepend($groupLabel);
$optionsContainer.append($groupOptions);
});
// Add individual options
if (groupOptions.length === 0) {
options.each(function () {
const $option = $CSD(this);
const value = $option.val();
const text = $option.text();
let htmlText = text;
if (checkmark) htmlText = checkmark + text;
if (value) {
const $customOption = $CSD(
`<div class="csd-select-option" data-value="${value}">${htmlText}</div>`
);
$optionsContainer.append($customOption);
}
});
}
// Add components to wrapper
$customSelect.append($display);
$customSelect.append($arrow).append($clearButton);
$wrapper.append($customSelect);
$wrapper.append($optionsContainer);
// Insert wrapper into DOM
$select.after($wrapper);
// Add keyboard navigation
$CSD(document).off("keydown").on("keydown", function (e) {
const $activeWrapper = $CSD(".csd-select-wrapper").filter(function () {
return $CSD(this).find(".csd-select-options").hasClass("visible");
});
if ($activeWrapper.length) {
handleKeyboardNavigation($activeWrapper, e);
}
});
// Close all options when clicking outside
$CSD(document).on("click", function (e) {
if (!e.target.closest('.csd-select-wrapper')) {
$CSD(".csd-select-options").removeClass("visible");
filterOptions($CSD(".csd-select-wrapper"), "");
$CSD(".csd-searchbar-input:not(.csd-select-editable)").val("");
}
});
// Add event listeners using event delegation
if (!selectElement.classList.contains("editable")) {
$wrapper.on("click", ".csd-select-display", function (e) {
e.stopPropagation();
const $thisWrapper = $CSD(this).closest(".csd-select-wrapper");
const $thisOptionsContainer = $thisWrapper.find(".csd-select-options");
$CSD(".csd-select-options").not($thisOptionsContainer).removeClass("visible");
calculateDropdownPosition($CSD(this), $thisOptionsContainer);
$thisOptionsContainer.toggleClass("visible");
if ($searchbar.length) $searchbar.find("input").focus();
});
}
$wrapper.on("click", ".csd-select-arrow", function (e) {
e.stopPropagation();
const $thisWrapper = $CSD(this).closest(".csd-select-wrapper");
const $thisDisplay = $thisWrapper.find(".csd-select-display");
const $thisOptionsContainer = $thisWrapper.find(".csd-select-options");
calculateDropdownPosition($thisDisplay, $thisOptionsContainer);
$CSD(".csd-select-options").not($thisOptionsContainer).removeClass("visible");
$thisOptionsContainer.toggleClass("visible");
});
$wrapper.on("click", ".csd-select-clearbutton", function (e) {
e.stopPropagation();
const $thisWrapper = $CSD(this).closest(".csd-select-wrapper");
selectOption($thisWrapper, "", placeholder, idSelect);
});
$wrapper.on("click", ".csd-select-searchbar", function (e) {
e.stopPropagation();
});
// Add events for both searchbar and display
$wrapper.on("input", ".csd-searchbar-input", function (e) {
e.stopPropagation();
const $thisWrapper = $CSD(this).closest(".csd-select-wrapper");
const searchValue = $CSD(this).val();
filterOptions($thisWrapper, searchValue);
});
$wrapper.on("input", ".csd-select-editable", function (e) {
e.stopPropagation();
const $thisWrapper = $CSD(this).closest(".csd-select-wrapper");
const $thisOptionsContainer = $thisWrapper.find(".csd-select-options");
calculateDropdownPosition($CSD(this), $thisOptionsContainer);
$thisOptionsContainer.addClass("visible");
const searchValue = $CSD(this).val();
filterOptions($thisWrapper, searchValue);
});
// Handle option events
$wrapper.on("mouseenter", ".csd-select-option", function () {
const $thisWrapper = $CSD(this).closest(".csd-select-wrapper");
$thisWrapper.find(".csd-select-option").removeClass("option-focus");
$CSD(this).addClass("option-focus");
});
$wrapper.on("mouseleave", ".csd-select-option", function () {
$CSD(this).removeClass("option-focus");
});
$wrapper.on("click", ".csd-select-option", function (e) {
e.stopPropagation();
const $thisWrapper = $CSD(this).closest(".csd-select-wrapper");
const value = $CSD(this).data("value");
const text = $CSD(this).text().replace(/^✓\s*/, "");
selectOption($thisWrapper, value, text, idSelect, isMultipleSelect);
});
// Add events for checkbox in searchbar if present
if (selectElement.classList.contains("checkmark-all")) {
// Aggiungiamo la checkbox al searchbar di questo specifico select
const $thisSearchbar = $wrapper.find(".csd-select-searchbar");
const $checkbox = $thisSearchbar.find(".csd-select-searchbar-checkbox");
$wrapper.on("click", ".csd-select-searchbar-checkbox", function (e) {
e.stopPropagation();
const $thisWrapper = $CSD(this).closest(".csd-select-wrapper");
const $thisSearchbarCheckbox = $CSD(this);
const $allOptions = $thisWrapper.find(".csd-select-option");
const $visibleOptions = $allOptions.filter(":visible");
const allSelected = $allOptions.length === $allOptions.filter(".selected").length;
if (allSelected) {
$allOptions.each(function () {
const $option = $CSD(this);
if ($option.hasClass("selected")) {
const value = $option.data("value").toString();
const text = $option.text().replace(/^✓\s*/, "");
selectOption($thisWrapper, value, text, idSelect, isMultipleSelect);
}
});
} else {
$visibleOptions.each(function () {
const $option = $CSD(this);
if (!$option.hasClass("selected")) {
const value = $option.data("value").toString();
const text = $option.text().replace(/^✓\s*/, "");
selectOption($thisWrapper, value, text, idSelect, isMultipleSelect);
}
});
}
// Aggiorniamo solo la checkbox di questo select
updateCheckboxIcon($thisSearchbarCheckbox, $thisWrapper);
});
// Delegate click event for chips
$wrapper.on("click", ".delete-chip", function (e) {
e.preventDefault();
e.stopPropagation();
const $thisWrapper = $CSD(this).closest(".csd-select-wrapper");
const $chip = $CSD(this).closest(".csd-select-chip");
const text = $chip.find(".csd-label-select").text();
selectOption($thisWrapper, $chip.data("value").toString(), text, idSelect, isMultipleSelect);
});
}
// Searchbar with clear button
const searchInput = $wrapper.find("#searchInput");
const clearButton = $wrapper.find("#clearButton");
if (searchInput.length && clearButton.length) {
searchInput.on("input", function () {
clearButton.toggleClass("visible", this.value !== "");
});
clearButton.on("click", function () {
searchInput.val("");
$CSD(this).removeClass("visible");
});
}
}
// Funzione per aggiornare l'icona della checkbox
function updateCheckboxIcon($checkbox, $wrapper) {
const $allOptions = $wrapper.find(".csd-select-option");
const selectedCount = $allOptions.filter(".selected").length;
if (selectedCount === 0) {
$checkbox.find("ion-icon").remove();
} else if (selectedCount === $allOptions.length) {
$checkbox.html("<ion-icon name='checkmark-outline'></ion-icon>");
} else {
$checkbox.html("<ion-icon name='remove-outline'></ion-icon>");
}
}
function handleKeyboardNavigation($wrapper, e) {
const $optionsContainer = $wrapper.find(".csd-select-options");
const $options = $wrapper.find(".csd-select-option:visible");
// Miglioriamo la selezione dell'elemento corrente
let $currentHovered = $wrapper.find(".csd-select-option.option-focus");
if (!$currentHovered.length) {
// Prima cerchiamo un elemento con hover
$currentHovered = $wrapper.find(".csd-select-option:hover");
// Se non c'è hover, cerchiamo l'elemento selezionato
if (!$currentHovered.length) {
$currentHovered = $wrapper.find(".csd-select-option.selected");
if ($currentHovered.length) {
$currentHovered.addClass("option-focus");
}
}
}
if (!$optionsContainer.hasClass("visible") || !$options.length) return;
const updateHover = ($newHovered) => {
// Rimuoviamo la classe da tutti gli elementi
$options.each(function() {
$CSD(this).removeClass("option-focus");
});
if ($newHovered && $newHovered.length) {
$newHovered.addClass("option-focus");
const container = $optionsContainer.elements[0];
const hoveredOption = $newHovered.elements[0];
// Calcoliamo la posizione relativa dell'elemento rispetto al container
const containerTop = container.scrollTop;
const containerHeight = container.clientHeight;
const optionTop = hoveredOption.offsetTop;
const optionHeight = hoveredOption.offsetHeight;
const extraScrollPixels = 5;
// Scroll verso l'alto se necessario
if (optionTop < containerTop) {
container.scrollTop = Math.max(0, optionTop - extraScrollPixels);
}
// Scroll verso il basso se necessario
else if (optionTop + optionHeight > containerTop + containerHeight) {
container.scrollTop = Math.min(
container.scrollHeight - containerHeight,
optionTop + optionHeight - containerHeight + extraScrollPixels
);
}
}
};
const getVisibleOptions = () => {
const visibleOptions = [];
$options.each(function() {
visibleOptions.push($CSD(this));
});
return visibleOptions;
};
const getCurrentIndex = () => {
const visibleOptions = getVisibleOptions();
const currentFocused = $wrapper.find(".csd-select-option.option-focus");
if (!currentFocused.length) return -1;
for (let i = 0; i < visibleOptions.length; i++) {
if (visibleOptions[i].elements[0] === currentFocused.elements[0]) {
return i;
}
}
return -1;
};
switch (e.key) {
case "ArrowDown":
e.preventDefault();
const visibleOptionsDown = getVisibleOptions();
const currentDownIndex = getCurrentIndex();
if (currentDownIndex === -1) {
// Se non c'è focus, partiamo dall'elemento selezionato o dal primo
const $selected = $wrapper.find(".csd-select-option.selected");
if ($selected.length) {
const selectedIndex = visibleOptionsDown.findIndex(opt =>
opt.elements[0] === $selected.elements[0]
);
if (selectedIndex > -1) {
updateHover(visibleOptionsDown[selectedIndex]);
} else {
updateHover(visibleOptionsDown[0]);
}
} else {
updateHover(visibleOptionsDown[0]);
}
} else if (currentDownIndex === visibleOptionsDown.length - 1) {
updateHover(visibleOptionsDown[0]); // Torna al primo
} else {
updateHover(visibleOptionsDown[currentDownIndex + 1]);
}
break;
case "ArrowUp":
e.preventDefault();
const visibleOptionsUp = getVisibleOptions();
const currentUpIndex = getCurrentIndex();
if (currentUpIndex === -1) {
// Se non c'è focus, partiamo dall'elemento selezionato o dall'ultimo
const $selected = $wrapper.find(".csd-select-option.selected");
if ($selected.length) {
const selectedIndex = visibleOptionsUp.findIndex(opt =>
opt.elements[0] === $selected.elements[0]
);
if (selectedIndex > -1) {
updateHover(visibleOptionsUp[selectedIndex]);
} else {
updateHover(visibleOptionsUp[visibleOptionsUp.length - 1]);
}
} else {
updateHover(visibleOptionsUp[visibleOptionsUp.length - 1]);
}
} else if (currentUpIndex === 0) {
updateHover(visibleOptionsUp[visibleOptionsUp.length - 1]); // Vai all'ultimo
} else {
updateHover(visibleOptionsUp[currentUpIndex - 1]);
}
break;
case "Enter":
case "Tab":
const currentFocused = $wrapper.find(".csd-select-option.option-focus");
if (currentFocused.length) {
e.preventDefault();
const value = currentFocused.data("value");
const text = currentFocused.text().replace(/^✓\s*/, "");
const idSelect = $wrapper.prev(".csd-select").attr("id");
const isMultipleSelect = $wrapper.prev(".csd-select").prop("multiple");
selectOption($wrapper, value, text, idSelect, isMultipleSelect);
currentFocused.removeClass("option-focus");
$optionsContainer.removeClass("visible");
}
break;
case "Escape":
$options.each(function() {
$CSD(this).removeClass("option-focus");
});
$optionsContainer.removeClass("visible");
break;
}
}
function calculateDropdownPosition($display, $optionsContainer) {
const position = $display.offset();
const viewportHeight = $CSD(window).height();
const scrollTop = $CSD(window).scrollTop();
const displayHeight = $display.outerHeight();
const optionsHeight = $optionsContainer.outerHeight(); // Altezza effettiva del dropdown
// Calcola lo spazio disponibile sotto
const spaceBelow =
viewportHeight - (position.top - scrollTop + displayHeight);
// Se lo spazio sotto è minore dell'altezza delle opzioni, posiziona sopra
if (spaceBelow < optionsHeight) {
$optionsContainer.addClass("position-above").removeClass("position-below");
} else {
$optionsContainer.addClass("position-below").removeClass("position-above");
}
}
function selectOption(
$wrapper,
value,
text,
idSelect,
isMultipleSelect = false
) {
const isEditable = $wrapper.find(".csd-select-editable").length > 0;
const isChipsSelect = $wrapper.find(".csd-select-chips").length > 0;
value = value.toString();
let $display = $wrapper.find(".csd-select-display");
if (isEditable) $display = $wrapper.find(".csd-select-editable");
const $originalSelect = $wrapper.prev(".csd-select");
const $clearButton = $wrapper.find(".csd-select-clearbutton");
const $originalSelectById = $CSD("#" + idSelect);
// Recupera i valori precedenti, assicurandosi che siano un array
let oldValue = $originalSelect.val() || (isMultipleSelect ? [] : "");
if (!Array.isArray(oldValue))
oldValue = oldValue.split(",").filter((v) => v); // Assicura che sia un array
const placeholder =
$originalSelect.attr("data-placeholder") || "Seleziona un'opzione";
const oldText = $display.find(".csd-label-select").text() || "";
let newValue = [...oldValue]; // Copy existing values
let newText = oldText;
if (isMultipleSelect) {
if (newValue.includes(value)) {
// Remove value and text if already present
newValue = newValue.filter((v) => v !== value);
newText = newText
.split(",")
.filter((t) => t.trim() !== text)
.join(", ");
} else {
// Add value and text if not present
if (newValue.includes(placeholder)) {
newValue = newValue.filter((v) => v !== placeholder);
newText = newText.replace(placeholder, "").trim();
}
if (newText.includes(placeholder)) {
newValue = newValue.filter((v) => v !== placeholder);
newText = newText.replace(placeholder, "").trim();
}
newValue.push(value);
newText = (newText ? newText + ", " : "") + text;
}
} else {
// For single select, replace the value
newValue = [value];
newText = text;
}
$originalSelect.val(newValue).trigger("change");
// Update display
if (!isChipsSelect || newValue.length == 0 || newValue[0] == "") {
$display.html(
'<label class="csd-label-select">' + (newText || placeholder) + "</label>"
);
} else {
let newHTML = "";
let $options = $wrapper.find(".csd-select-options");
newValue.forEach((value) => {
const text = $options
.find('.csd-select-option[data-value="' + value + '"]')
.text();
newHTML +=
'<div class="csd-select-chip" data-value="' +
value +
'"><label class="csd-label-select">' +
text +
'</label><ion-icon class="delete-chip" name="close-circle-outline"></ion-icon></div>';
});
if (newHTML == "") {
$display.html(
'<label class="csd-label-select">' + placeholder + "</label>"
);
} else {
$display.html('<div class="csd-select-chips">' + newHTML + "</div>");
}
$display.find(".delete-chip").on("click", function (e) {
e.preventDefault();
e.stopPropagation();
const $chip = $CSD(this).closest(".csd-select-chip");
const text = $chip.find(".csd-label-select").text();
selectOption(
$wrapper,
$chip.data("value").toString(),
text,
idSelect,
isMultipleSelect
);
});
}
if (isEditable) $display.val(newText || "");
if (isEditable && newValue.length === 0) $display.val("");
// Handle reset button visibility
const hasValue = newValue.length > 0 && newValue[0] !== "";
toggleVisibility($clearButton, hasValue);
// Update options state
$wrapper.find(".csd-select-option").each(function () {
const $option = $CSD(this);
const optionValue = $option.data("value").toString();
if (newValue.includes(optionValue)) {
$option.addClass("selected");
} else {
$option.removeClass("selected");
}
});
// Update checkbox state if present
const $searchbarCheckbox = $wrapper.find(".csd-select-searchbar-checkbox");
if ($searchbarCheckbox.length) {
updateCheckboxIcon($searchbarCheckbox, $wrapper);
}
if (!isMultipleSelect)
$wrapper.find(".csd-select-options").removeClass("visible");
}
function toggleVisibility($element, condition) {
$element.toggleClass("visible", condition);
}
function filterOptions($wrapper, searchValue) {
// Converte il valore di ricerca in minuscolo per un confronto case-insensitive
searchValue = searchValue.toLowerCase();
// Conta quante opzioni corrispondono al filtro
let matchCount = 0;
// Filtra le opzioni della select
$wrapper.find(".csd-select-option").each(function () {
const optionText = $CSD(this).text().toLowerCase();
const isMatch = optionText.includes(searchValue);
$CSD(this).toggle(isMatch); // Mostra o nasconde l'opzione
if (isMatch) matchCount++;
});
// Gestione del messaggio "Nessun risultato trovato"
let $noResultsMessage = $wrapper.find(".csd-no-results");
if ($noResultsMessage.length === 0) {
// Se il messaggio non esiste, lo crea
$noResultsMessage = $CSD(
'<div class="csd-no-results" style="display:none">Nessun risultato trovato</div>'
);
$wrapper.find(".csd-select-options").append($noResultsMessage);
}
// Mostra o nasconde il messaggio in base al risultato
$noResultsMessage.toggle(matchCount === 0);
}
export { initializeCustomSelect };