-
Notifications
You must be signed in to change notification settings - Fork 346
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
11/Input/Radio MultiSelect Searchable
- Loading branch information
1 parent
6fbcee0
commit 2cb2340
Showing
31 changed files
with
1,318 additions
and
27 deletions.
There are no files selected for viewing
2 changes: 1 addition & 1 deletion
2
components/ILIAS/UI/resources/js/Input/Field/dist/input.factory.min.js
Large diffs are not rendered by default.
Oops, something went wrong.
227 changes: 227 additions & 0 deletions
227
...onents/ILIAS/UI/resources/js/Input/Field/src/SearchableContext/searchablecontext.class.js
This file contains 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
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'; | ||
} |
31 changes: 31 additions & 0 deletions
31
...ents/ILIAS/UI/resources/js/Input/Field/src/SearchableContext/searchablecontext.factory.js
This file contains 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
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; | ||
} | ||
} |
This file contains 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
116 changes: 116 additions & 0 deletions
116
components/ILIAS/UI/resources/ui-examples/css/radio_searchable_section_style.css
This file contains 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
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; | ||
} |
Oops, something went wrong.