Commit 77df0cbd authored by Iker Hurtado's avatar Iker Hurtado
Browse files

New multiselect autocomplete textfield (AutocompleteMultiselectTextfield.js)...

New multiselect autocomplete textfield (AutocompleteMultiselectTextfield.js) that shows the items selected (without dropping the component dropdown)
(unfinished)
parent 5ff199f4
Pipeline #93413 skipped with stage
...@@ -522,11 +522,7 @@ div.title span.unfolded::before{ ...@@ -522,11 +522,7 @@ div.title span.unfolded::before{
}*/ }*/
.textfield-filter{
padding: 6px;
border: 1px solid #DDD;
width: 200px;
}
.material-name-autocomplete-textfield, .material-name-autocomplete-dropdown{ .material-name-autocomplete-textfield, .material-name-autocomplete-dropdown{
...@@ -535,20 +531,20 @@ div.title span.unfolded::before{ ...@@ -535,20 +531,20 @@ div.title span.unfolded::before{
/* Autocomplete component */ /* Autocomplete components */
.AutocompleteTextField{ .AutocompleteTextField{
display: inline-block; display: inline-block;
} }
.AutocompleteTextField-dropdown{ .AutocompleteTextField-dropdown, .AutocompleteMultiselectTextfield-dropdown{
font-size: 0.9em; font-size: 0.9em;
position: absolute; position: absolute;
z-index: 99; z-index: 99;
box-shadow: 1px 1px 4px gray; box-shadow: 1px 1px 4px gray;
} }
.AutocompleteTextField-dropdown > div { .AutocompleteTextField-dropdown > div, .AutocompleteMultiselectTextfield-dropdown > div {
padding: 2px 10px 2px 10px; padding: 2px 10px 2px 10px;
cursor: pointer; cursor: pointer;
background-color: #DDD; background-color: #DDD;
...@@ -559,6 +555,32 @@ div.title span.unfolded::before{ ...@@ -559,6 +555,32 @@ div.title span.unfolded::before{
border-color: #E56400 !important; border-color: #E56400 !important;
} }
/*
.AutocompleteMultiselectTextfield{
}
*/
.AutocompleteMultiselectTextfield-selected-box{
padding: 3px;
}
.selectedItemLabel{
display: inline-block;
/*border: 1px solid gray;*/
box-shadow: 1px 1px 2px #BBB;
border-radius: 4px;
padding: 1px 5px;
cursor: pointer;
}
.structure_type-autocomplete-multiselect-textfield input[type="text"]{
padding: 6px;
border: 1px solid #DDD;
width: 200px;
}
/* To remove /* To remove
.autocomplete-em { .autocomplete-em {
......
...@@ -26,255 +26,218 @@ ...@@ -26,255 +26,218 @@
class AutocompleteMultiselectTextfield { class AutocompleteMultiselectTextfield {
constructor(name = "") { constructor(id = "", placeholder = '', allowEmptyInput = false) {
this.classPostfix = name; this.id = id;
this.element = document.createElement('input'); this.element = document.createElement('div');
this.element.type = 'text'; //this.element.style.display = 'inline';
this.element.className = 'autocomplete-multiselectlist-' + this.classPostfix; this.element.className = `AutocompleteMultiselectTextfield ${id}-autocomplete-multiselect-textfield`;
this.currentFocus = -1;
this.selectListener = undefined; this.element.innerHTML = `
<input type="text" placeholder="${placeholder}" />
/* the items member variable keeps track of the selected options <div class="AutocompleteMultiselectTextfield-selected-box"></div>
by storing a (sorted) list of all possible values and their selection state. <div class="AutocompleteMultiselectTextfield-dropdown ${this.id}-autocomplete-multiselect-dropdown"></div>
A list entry looks like {"value": "A1", "selected": false} `;
*/
this.items = []; this.input = this.element.querySelector('input');
} this.selectedItemsBox = this.element.querySelector('.AutocompleteMultiselectTextfield-selected-box');
this.listContainer = this.element.querySelector('.AutocompleteMultiselectTextfield-dropdown');
replaceElement(oldElement) {
oldElement.parentElement.replaceChild(this.element, oldElement); this.selectListener;
}
// state
setSelectListener(listener) { this.valueList; // List of autocomplete (possible) values
this.selectListener = listener; this.selectedValues = new Set();
} this.allowEmptyInput = allowEmptyInput;
autocomplete(allAcValues) { // event management
/* the autocomplete function takes an array of possible autocomplete values. //
in the following we will use 'ac' as abbrevation for 'autocomplete' // react to clicking into the textfield
*/ this.input.addEventListener("click", (e) => {
this._processInput();
/* store possible values and initialize them as not selected */ e.stopPropagation();
for (var i = 0; i < allAcValues.length; i++) { });
this.items.push({value: allAcValues[i],
selected: false}
);
}
/* process input when someone writes in the text field:*/ this.input.addEventListener("input", (e) => {
this.element.addEventListener("input", (e) => { this._processInput();
this._processInput(allAcValues);
}); });
/* react to keyboard navigation */ /*
this.element.addEventListener("keydown", (e) => { // react to keyboard navigation
this.input.addEventListener("keydown", (e) => {
if (e.keyCode == 40) { // arrow DOWN if (e.keyCode == 40) { // arrow DOWN
this._setActive(this.currentFocus + 1); if (this._getActiveListItem())
this._setActiveListItem(this._getActiveListItem().nextSibling);
} else if (e.keyCode == 38) { // arrow UP } else if (e.keyCode == 38) { // arrow UP
this._setActive(this.currentFocus - 1); if (this._getActiveListItem())
} else if (e.keyCode == 27) { // ESC key this._setActiveListItem(this._getActiveListItem().previousSibling);
this.element.value = '';
this._closeAllLists();
} }
}); });
/* react to enter key */ // react to enter key
this.element.addEventListener("keypress", (e) => { this.input.addEventListener("keypress", e => {
if (e.keyCode == 13) { // ENTER if (e.keyCode == 13) { // ENTER
/* simulate a click on the "active" item:*/ // simulate a click on the "active" item
this._clickActive(); this._clickOnActiveItem();//_clickActive();
} }
}); });
/* react to klicking into the textfield */
this.element.addEventListener("click", (e) => {
this._processInput();
e.stopPropagation();
// close lists when someone clicks in the document
document.addEventListener("click", e => {
console.log('close lists when someone clicks in the document')
this._cleanList();// this._closeAllLists();
}); });
It doesn't worki properly because it closes the dropdown when you click on it
*/
// close lists when someone clicks in the document:*/ this.selectedItemsBox.addEventListener("click", e => {
document.addEventListener("click", (e) => { let itemLabel = event.target.closest('span'); // (1)
this.element.value = ''; this._toggleValue(itemLabel.dataset.value);//.textContent);
this._closeAllLists();
}); });
}
getSelected() {
let values = [];
for (let item of this.items) {
if (item.selected) {
values.push(item.value);
}
}
return values;
}
_processInput() { this.listContainer.addEventListener("click", e => {
let currentInput = this.element.value; let listItem = event.target.closest('div'); // (1)
/*close any already open lists of autocompleted values*/ this._toggleItem(listItem);//.textContent);
this._closeAllLists();
/*create a DIV element that will contain the items (values):*/
let listContainer = document.createElement("DIV");
listContainer.setAttribute("id", "autocomplete-list");
listContainer.classList.add("autocomplete-items");
listContainer.classList.add("autocomplete-items-"+ this.classPostfix);
/*append the DIV element as a child of the autocomplete container:*/
this.element.parentNode.appendChild(listContainer);
/* keyboard interaction */
listContainer.setAttribute("tabindex", "0");
listContainer.addEventListener("keydown", (e) => {
if (e.keyCode == 40) { // arrow DOWN
this._setActive(this.currentFocus + 1);
e.preventDefault();
} else if (e.keyCode == 38) { // arrow UP
this._setActive(this.currentFocus - 1);
e.preventDefault();
} else if (e.keyCode == 27) { // ESC key
this.element.value = '';
this._closeAllLists();
}
}); });
listContainer.addEventListener("keypress", (e) => {
if (e.keyCode == 13) { // ENTER this.listContainer.addEventListener("mouseover", e => {
/* simulate a click on the "active" item:*/ let listItem = event.target.closest('div'); // (1)
this._clickActive(); this._setActiveListItem(listItem);
}
}); });
}
/* show all items matching the input text */
let acItemIndex = 0;
for (let item of this.items) {
/*check if the item contains the same letters as the text field value:*/
let acValue = item.value;
let acSelected = item.selected;
let pos = 0;
if (currentInput) {
pos = acValue.toUpperCase().search(currentInput.toUpperCase());
}
/* if there is no input text given, pos = 0 and thus an item is generated */
if (pos >= 0){
let listItem = this._generateListItem(acValue,
acSelected,
currentInput,
acItemIndex)
listContainer.appendChild(listItem);
/* check if a valid option was completely entered.
if so, set the focus to the element corresponding to the input */
if (acValue.toUpperCase() === currentInput.toUpperCase()) {
this._setActive(acItemIndex);
}
acItemIndex++;
}
}
getValues(){
return [];//this.input.value;
} }
_generateListItem(acText, selected, inputText, itemIndex) {
/*create a DIV element for each matching element:*/ resetValue(){
let listItem = document.createElement("div"); this.input.value = '';
let itemCheckbox = document.createElement("input"); }
itemCheckbox.type = "checkbox";
itemCheckbox.checked = selected;
/* TODO: check why catching this event is necessary */ disable(bool){
itemCheckbox.addEventListener("click", e => { this.input.disabled = bool;
listItem.click(); }
e.stopPropagation();
});
listItem.appendChild(itemCheckbox); setAutocompleteList(valueList){
/*make the matching letters bold:*/ this.valueList = valueList;
if (inputText && inputText != ""){ }
let pos = acText.toUpperCase().search(inputText.toUpperCase());
listItem.appendChild(document.createTextNode(acText.substr(0, pos)));
let emText = document.createElement("span"); setSelectListener(listener) {
emText.className = "autocomplete-em"; this.selectListener = listener;
emText.innerHTML = acText.substr(pos, inputText.length); }
/* TODO: check why catching this event is necessary */
emText.addEventListener("click", e => {
listItem.click(); _processInput() {
e.stopPropagation(); const currentInput = this.input.value;
}); // close any already open lists of autocompleted values
listItem.appendChild(emText); this._cleanList();//this._closeAllLists();
listItem.appendChild(document.createTextNode(acText.substr(pos + inputText.length)));
} else { // in case of an empty input field
listItem.appendChild(document.createTextNode(acText)); if (!this.allowEmptyInput && !currentInput) {
return false;
} }
/* clicking on the AS list item puts selects the corresponding name for searching */ // for each autocomplete value
listItem.addEventListener("click", (e) => { let counter = 0;
let checkbox = e.target.getElementsByTagName("input")[0]; const matchingValues = this.valueList.filter( value => {
this._toggleSelect(acText, checkbox); const matching = value.toUpperCase().includes(currentInput.toUpperCase());
e.stopPropagation(); if (matching) counter++;
return counter <= 15 && matching;
}); });
//console.log('matchingValues', matchingValues)
this.listContainer.innerHTML = '';
matchingValues.forEach( value => {
const listItem = generateListItem(value, currentInput, this.selectedValues.has(value));
this.listContainer.append(listItem);
// check if a valid option was completely entered. if so, set the focus to the element corresponding to the input
if (value.toUpperCase() === currentInput.toUpperCase())
this._setActiveListItem(listItem);
function generateListItem(value, inputText, present) {
const listItem = document.createElement("div");
let innerHTML = `<input type="checkbox" data-value="${value}" ${present ? 'checked' : ''}>`;
if (inputText){
const pos = value.toUpperCase().indexOf(inputText.toUpperCase()); // console.log('pos', pos)
innerHTML +=
`${value.substring(0, pos)}<strong>${value.substring(pos, pos+inputText.length)}</strong>${value.substring(pos + inputText.length)}`;
}else
innerHTML += value;
listItem.innerHTML = innerHTML;
return listItem;
}
/* hovering puts the focus on the related list item */
listItem.addEventListener("mouseover", (e) => {
this._setActive(itemIndex);
}); });
return listItem;
} }
// _setText(value) {
// /*insert the value for the autocomplete text field:*/ _toggleValue(value) {
// this.element.value = value; const listItem = this.listContainer.querySelector('input[data-value="'+value+'"]').parentElement;
// /* notify listener */ this._toggleItem(listItem);
// if (this.selectListener) { }
// this.selectListener();
// }
// _toggleItem(listItem) {
// /*close the list of autocompleted values, const value = listItem.textContent;
// (or any other open lists of autocompleted values)*/ const present = this.selectedValues.has(value);
// this._closeAllLists();
// } listItem.querySelector('input').checked = !present;
_toggleSelect(value, checkbox) { if (present){
let newSelected; this.selectedValues.delete(value);
for (let item of this.items) { this.selectedItemsBox.querySelector('span[data-value="'+value+'"]').remove();
if (item.value == value) { }else{
newSelected = !item.selected; this.selectedValues.add(value);
item.selected = newSelected; this.selectedItemsBox.append(createSelectedItemLabel(value));
break; }
} //console.log('_addValue: this.selectedValues', this.selectedValues)
if (this.selectListener) this.selectListener();
function createSelectedItemLabel(value){
const label = document.createElement('span');
label.className = 'selectedItemLabel';
label.dataset.value = value;
label.innerHTML = `${value} ❌`;
return label;
} }
checkbox.checked = newSelected;
} }
_setActive(index) {
let listItems = document.getElementById("autocomplete-list")
.getElementsByTagName("div");
/* remove the active status from all list items */
Array.from(listItems).forEach(item => {
item.classList.remove("autocomplete-active");
});
/* ensure to stay in the list _getActiveListItem(){
out of boundary indices are mapped to the closest border */ return this.listContainer.querySelector('.autocomplete-active');
let newFocus = Math.max(0, index); }
newFocus = Math.min(newFocus, listItems.length-1);
this.currentFocus = newFocus;
/* mark the active status by a style class */ _setActiveListItem(element){
listItems[newFocus].classList.add("autocomplete-active"); const currentActiveItem = this.listContainer.querySelector('.autocomplete-active');
if (currentActiveItem) currentActiveItem.classList.remove('autocomplete-active');
element.classList.add('autocomplete-active');
} }
_clickActive() {
if (this.currentFocus > -1) { _clickOnActiveItem() {
let listItems = document.getElementById("autocomplete-list") const activeItem = this.listContainer.querySelector('.autocomplete-active');
.getElementsByTagName("div"); if (activeItem) activeItem.click();
listItems[this.currentFocus].click();
}
} }
_closeAllLists() {
/*close all autocomplete lists in the document */ _cleanList() {
let allAcLists = document.getElementsByClassName("autocomplete-items"); this.listContainer.innerHTML = '';
for (let acList of allAcLists) {
acList.parentNode.removeChild(acList);
}
this.currentFocus = -1;
} }
} }
......
...@@ -31,8 +31,8 @@ class AutocompleteTextfield { ...@@ -31,8 +31,8 @@ class AutocompleteTextfield {
this.element.className = `AutocompleteTextField ${id}-autocomplete-textfield`; this.element.className = `AutocompleteTextField ${id}-autocomplete-textfield`;
this.element.innerHTML = ` this.element.innerHTML = `
<input type="text" placeholder="${placeholder}" /> <!-- class="autocomplete-textfield-${this.id}" /> --> <input type="text" placeholder="${placeholder}" />
<div class="AutocompleteTextField-dropdown ${this.id}-autocomplete-dropdown"></div> <!-- autocomplete-items- --> <div class="AutocompleteTextField-dropdown ${this.id}-autocomplete-dropdown"></div>
`; `;
this.input = this.element.querySelector('input'); this.input = this.element.querySelector('input');
...@@ -145,14 +145,14 @@ class AutocompleteTextfield { ...@@ -145,14 +145,14 @@ class AutocompleteTextfield {
if (value.toUpperCase() === currentInput.toUpperCase()) if (value.toUpperCase() === currentInput.toUpperCase())
this._setActiveListItem(listItem); this._setActiveListItem(listItem);
function generateListItem(value, inputText, itemIndex) { function generateListItem(value, inputText) {
const listItem = document.createElement("div"); const listItem = document.createElement("div");
if (inputText){ if (inputText){
const pos = value.toUpperCase().indexOf(inputText.toUpperCase()); // console.log('pos', pos) const pos = value.toUpperCase().indexOf(inputText.toUpperCase()); // console.log('pos', pos)
listItem.innerHTML += listItem.innerHTML +=
`${value.substring(0, pos)}<strong>${value.substring(pos, pos+inputText.length)}</strong>${value.substring(pos + inputText.length)}`; `${value.substring(0, pos)}<strong>${value.substring(pos, pos+inputText.length)}</strong>${value.substring(pos + inputText.length)}`;
}else }else
listItem.innerHTML = acText; listItem.innerHTML = value;
return listItem; return listItem;
} }
......
...@@ -282,20 +282,18 @@ class AutocompleteField{ ...@@ -282,20 +282,18 @@ class AutocompleteField{
</div>`; </div>`;
this.autocomplete = new AutocompleteMultiselectTextfield(id); this.autocomplete = new AutocompleteMultiselectTextfield(id, 'Search and select options', true);
this.autocomplete.element.placeholder = "Search and select options";
this.autocomplete.element.classList.add('textfield-filter');
this.element.append(this.autocomplete.element) this.element.append(this.autocomplete.element)
let r1 = util.serverReq(util.getSuggestionURL(this.fieldId), (e) => {