Skip to content

Commit

Permalink
11/Input/Radio MultiSelect Searchable
Browse files Browse the repository at this point in the history
  • Loading branch information
catenglaender committed Jan 28, 2025
1 parent 6fbcee0 commit 2cb2340
Show file tree
Hide file tree
Showing 31 changed files with 1,318 additions and 27 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/**
* This file is part of ILIAS, a powerful learning management system
* published by ILIAS open source e-Learning e.V.
*
* ILIAS is licensed with the GPL-3.0,
* see https://www.gnu.org/licenses/gpl-3.0.en.html
* You should have received a copy of said license along with the
* source code, too.
*
* If this is not the case or you just want to try ILIAS, you'll find
* us at:
* https://www.ilias.de
* https://github.com/ILIAS-eLearning
*/

/**
* Searchable Select Component
* JS features:
* - search bar input filters (hides) list items
* - button to clear the filter
* - expanding and collapsing the component (hiding and showing elements) with button triggers
* SCSS features:
* - pushing checked items to the top of the list using flex-box order
* - component expanding animation
* - item switching position animation
* @author Ferdinand Engländer <[email protected]>
*/
export default class SearchableInputContext {
/**
* @type {HTMLFieldSetElement}
*/
inputFieldContext;

/**
* @type {HTMLInputElement}
*/
searchbar;

/**
* @type {HTMLFieldSetElement}
*/
selectionComponent;

/**
* @type {string}
*/
listType;

/**
* @type {HTMLElement}
*/
itemList;

/**
* @type {NodeList}
*/
items;

/**
* @type {HTMLButtonElement}
*/
engageDisengageToggle;

/**
* @type {HTMLSpanElement}
*/
toggleExpandText;

/**
* @type {HTMLSpanElement}
*/
toggleCollapseText;

/**
* @type {HTMLButtonElement}
*/
clearSearchButton;

/**
* @type {HTMLDivElement}
*/
scrollContainer;

/**
* @type {boolean}
*/
#isFiltered;

/**
* @type {HTMLDivElement}
*/
messageNoMatch;

constructor(inputFieldContext) {
/* DOM Elements */
this.inputFieldContext = inputFieldContext;
this.scrollContainer = this.inputFieldContext.querySelector('.c-input--searchable__field');
this.searchbar = this.inputFieldContext.querySelector('.c-input--searchable__search-input input');
this.listType = this.inputFieldContext.getAttribute('data-il-ui-component');
this.itemList = this.inputFieldContext.querySelector('.c-field--searchable__list');
this.items = this.itemList.querySelectorAll('.c-field--searchable__item');
this.messageNoMatch = this.inputFieldContext.querySelector('.message-no-match');

/* Buttons */
this.clearSearchButton = this.inputFieldContext.querySelector('.c-input--searchable__clear-search');
this.engageDisengageToggle = this.inputFieldContext.querySelector('.c-input--searchable__visibility-toggle');
this.toggleExpandText = this.engageDisengageToggle.querySelector('.text-expand');
this.toggleCollapseText = this.engageDisengageToggle.querySelector('.text-collapse');

/* Initialize states */
this.isEngaged = false; // will also set isFiltered false

/* Event Listeners */
this.filterItemsSearch = this.filterItemsSearch.bind(this);
this.searchbar.addEventListener('input', this.filterItemsSearch);

this.clearSearchButton.addEventListener('click', () => { this.isFiltered = false; });

this.toggleVisibility = this.toggleVisibility.bind(this);
this.engageDisengageToggle.addEventListener('click', this.toggleVisibility);

if (this.listType === 'radio-field-input') {
this.scrollListToTop = this.scrollListToTop.bind(this);
this.items.forEach((item) => {
item.addEventListener('change', this.scrollListToTop);
});
}
}

/**
* Getter for isFiltered state
* @returns {boolean}
*/
get isFiltered() {
return this.#isFiltered;
}

/**
* Setter for isFiltered state
* @param {boolean} value
*/
set isFiltered(value) {
if (this.#isFiltered === value) return;
this.#isFiltered = value;
if (value) {
this.clearSearchButton.style.removeProperty('display');
} else {
this.searchbar.value = '';
this.clearSearchButton.style.display = 'none';
this.messageNoMatch.style.display = 'none';
this.resetItemsDisplay();
}
}

toggleVisibility() {
if (this.isEngaged) {
this.isEngaged = false;
this.inputFieldContext.classList.remove('engaged');
this.isFiltered = false;
this.engageDisengageToggle.setAttribute('aria-expanded', 'false');
this.toggleExpandText.style.removeProperty('display');
this.toggleCollapseText.style.display = 'none';
} else {
this.isEngaged = true;
this.inputFieldContext.classList.add('engaged');
this.engageDisengageToggle.setAttribute('aria-expanded', 'true');
this.toggleExpandText.style.display = 'none';
this.toggleCollapseText.style.removeProperty('display');
}
}

/**
* Filter items based on search input
* @param {Event} event
*/
filterItemsSearch(event) {
const value = event.target.value.toLowerCase();
this.isFiltered = !!value; // negates any search term input to false then flips it to true

let foundMatch = false;
this.items.forEach((item) => {
const itemText = item.textContent.toLowerCase();
const isMatch = itemText.includes(value);
if (isMatch) {
foundMatch = true;
showItem(item);
} else {
hideItem(item);
}
});
if (value !== '' && foundMatch === false) {
this.messageNoMatch.style.removeProperty('display');
} else if (value === '' || foundMatch) {
this.messageNoMatch.style.display = 'none';
}
}

/**
* Reset the display of all items
*/
resetItemsDisplay() {
this.items.forEach((item) => showItem(item));
}

scrollListToTop() {
this.scrollContainer.scrollTo({
top: 0,
behavior: 'smooth',
});
}
}

/**
* Show a specific item
* @param {HTMLElement} item
*/
function showItem(item) {
item.style.removeProperty('display');
}

/**
* Hide a specific item
* @param {HTMLElement} item
*/
function hideItem(item) {
item.style.display = 'none';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import SearchableInputContext from './searchablecontext.class.js';

/**
* @author Ferdinand Engländer <[email protected]>
*/
export default class SearchableInputContextFactory {
/**
* @type {Array<string, SearchableInputContext>}
*/
instances = [];

/**
* @param {HTMLElement} searchableField
* @return {void}
* @throws {Error} if the input was already initialized.
*/
init(searchableField) {
if (undefined !== this.instances[searchableField.id]) {
throw new Error(`A searchable field with input-id '${searchableField.id}' has already been initialized.`);
}
this.instances[searchableField.id] = new SearchableInputContext(searchableField);
}

/**
* @param {string} inputID
* @return {SearchableInputContext|null}
*/
get(inputID) {
return this.instances[inputID] ?? null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@
*/

import il from 'ilias';
import TextareaFactory from './Textarea/textarea.factory';
import MarkdownFactory from './Markdown/markdown.factory';
import TextareaFactory from './Textarea/textarea.factory.js';
import MarkdownFactory from './Markdown/markdown.factory.js';
import SearchableInputContextFactory from './SearchableContext/searchablecontext.factory.js';

il.UI = il.UI || {};
il.UI.Input = il.UI.Input || {};

(function (Input) {
Input.textarea = new TextareaFactory();
Input.markdown = new MarkdownFactory();
Input.searchableinputcontext = new SearchableInputContextFactory();
}(il.UI.Input));
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
div.ilc_section_Card,
a.ilc_section_Card {
min-height: 250px;
position: relative;
margin: 15px;
padding-top: 15px;
padding-right: 15px;
padding-bottom: 15px;
padding-left: 15px;
box-shadow: 5px 5px 40px rgba(0, 0, 0, 0.2);
border-radius: 15px;
max-width: 400px;
}
div.ilc_section_Citation,
a.ilc_section_Citation {
padding-right: 30px;
padding-top: 35px;
margin-bottom: 10px;
margin-top: 40px;
position: relative;
border-width: 2px;
border-color: #4C6586;
border-style: solid;
background-color: #FFFFFF;
border-radius: 3px;
position: relative !important;
padding-left: 20px;
padding-bottom: 15px;
}
div.ilc_section_Citation::before,
a.ilc_section_Citation::before {
background-color: #eceff4;
left: 15px;
background-position: center center;
border-width: 2px;
border-color: #FFFFFF;
border-style: solid;
content: "";
display: block;
border-radius: 50px;
background-image: url("assets/images/citation.svg");
background-repeat: no-repeat;
position: absolute;
top: -32px;
width: 60px;
height: 60px;
}

div.ilc_section_Example,
a.ilc_section_Example {
padding-right: 30px;
padding-bottom: 15px;
padding-left: 20px;
background-position: left center;
margin-bottom: 10px;
margin-top: 40px;
position: relative;
border-width: 2px;
border-color: #4C6586;
border-style: solid;
border-radius: 3px;
position: relative !important;
padding-top: 35px;
}
div.ilc_section_Example::before,
a.ilc_section_Example::before {
background-color: #eceff4;
background-position: center center;
left: 15px;
background-image: url("assets/images/example.svg");
background-repeat: no-repeat;
position: absolute;
top: -32px;
width: 60px;
height: 60px;
border-width: 2px;
border-color: #FFFFFF;
border-style: solid;
right: 0px;
content: "";
display: block;
border-radius: 50px;
}
div.ilc_section_Excursus,
a.ilc_section_Excursus {
padding-right: 30px;
margin-bottom: 10px;
margin-top: 40px;
border-style: solid;
border-width: 2px;
position: relative;
padding-top: 35px;
border-color: #4C6586;
padding-left: 20px;
border-radius: 3px;
position: relative !important;
padding-bottom: 15px;
}
div.ilc_section_Excursus::before,
a.ilc_section_Excursus::before {
border-width: 2px;
border-color: #FFFFFF;
border-style: solid;
background-color: #eceff4;
background-position: center center;
left: 15px;
content: "";
display: block;
border-radius: 50px;
background-image: url("assets/images/excursus.svg");
background-repeat: no-repeat;
position: absolute;
top: -32px;
width: 60px;
height: 60px;
}
Loading

0 comments on commit 2cb2340

Please sign in to comment.