I'm trying to write a custom select element with it's own class. If there are multiple elemnets with unique ID on the page, only the last click event listener working for some reason.
For every new select i create it's own class. In the constructor i add a click event listener, assigned to it's unique id.
constructor(options,changeCallback) {
this.name = options.name;
this.id = options.id;
this.isMulti = options.isMulti;
if (this.id.substring(0,1) !== "#") { this.id = "#" this.id; }
this.wrapper = document.querySelector(this.id);
this.wrapper.addEventListener("click",this.open.bind(this));
}
It should work, since I add the listener to it's ID.
I have tried to remove the listener before i add it.
this.wrapper.removeEventListener("click",this.open.bind(this));
this.wrapper.addEventListener("click",this.open.bind(this));
I have tried to select the 0 th element but it produced an error.
this.wrapper = document.querySelector(this.id)[0];
EDIT: Added code snippet to demonstrate the problem.
/* SELECT CLASS */
class hsSelect {
optionTemplate = `<hsOption %attributes% >
<span >%text%</span>
<span >
<span ></span>
</span>
</hsOption>`;
containerTemplate = `
<div id="%selectID%">
<div ></div>
<div ></div>
<div >
<button action="none">NONE</button>
<button action="all">ALL</button>
</div>
</div>`;
selectedOptions = {};
isMulti = false;
data = [];
isOpen = false;
hsSelectWindowWrapper = null;
constructor(options, changeCallback) {
this.name = options.name;
this.id = options.id;
this.isMulti = options.isMulti;
this.addContainer();
if (this.id.substring(0, 1) !== "#") {
this.id = "#" this.id;
}
// select by id
this.wrapper = document.querySelector(this.id);
this.hsSelectWindowWrapper = document.querySelector(`${this.id}_container`);
if (!this.wrapper) {
this.hsSelectWindowWrapper.remove();
console.error(
`[HS_Select] - No HSH Select with the specified ID: ${this.id}`
);
return;
}
this.hsDisplay = this.wrapper.querySelector(".hsDisplay");
console.log("Adding evt listener to: ", this.wrapper);
this.wrapper.addEventListener("click", this.open.bind(this));
let hshSelectTools = this.hsSelectWindowWrapper.querySelector(
".hshSelectTools"
);
if (this.isMulti) {
let toolButtons = this.hsSelectWindowWrapper.querySelectorAll(
".hshSelectTool"
);
for (const toolBtn of toolButtons) {
toolBtn.addEventListener("click", this.toolBtnClick.bind(this));
}
hshSelectTools.style.display = "flex";
} else {
hshSelectTools.style.display = "none";
}
//this.hsSelectWindowWrapper.removeEventListener("click",this.close.bind(this));
this.hsSelectWindowWrapper.addEventListener("click", this.close.bind(this));
this.setCallback(changeCallback);
}
addContainer() {
let body = document.querySelector("body");
let newTemplate = this.containerTemplate.replace(
"%selectID%",
`${this.id}_container`
);
body.innerHTML = newTemplate;
}
toolBtnClick(e) {
let actionAttr = e.target.getAttribute("action");
if (actionAttr == "none") {
this.deselectAll(e);
} else if (actionAttr == "all") {
this.selectAll();
}
}
close(e) {
if (e.target.classList.contains("hsSelectWindowWrapper")) {
this.isOpen = false;
this.hsSelectWindowWrapper.classList.remove("open");
}
}
change() {
if (this.changeCallback) {
this.changeCallback(this.selectedOptions);
}
}
open(e) {
console.log("Opening");
if (
e.target.classList.contains("hsSelect") ||
e.target.classList.contains("hsDisplay")
) {
if (!this.isOpen) {
this.refreshCurrSelecteds();
this.hsSelectWindowWrapper.classList.add("open");
this.isOpen = true;
}
}
e.stopPropagation();
}
fill(optionData) {
this.data = optionData;
let options = this.hsSelectWindowWrapper.querySelector(".hsSelectWindow");
options.innerHTML = "";
let index = 0;
for (const data of this.data) {
let option = this.optionTemplate;
let attributes = data.attributes;
let attributesNode = "";
for (const attribute of attributes) {
let key = Object.keys(attribute)[0];
let value = attribute[key];
attributesNode = `${key}="${value}" `;
}
attributesNode = `index="${index}"`;
option = option.replace("%attributes%", attributesNode);
option = option.replace("%text%", data.text);
options.innerHTML = option;
index ;
}
let optionsList = options.querySelectorAll(".hsOption");
for (let i = 0; i < optionsList.length; i ) {
optionsList[i].addEventListener("click", this.optionClick.bind(this));
}
}
optionClick(e) {
// add .selected class to .hsOption that was clicked
let options = this.hsSelectWindowWrapper.querySelectorAll(".hsOption");
let clickedOption = e.target.closest(".hsOption");
let index = parseInt(clickedOption.getAttribute("index"));
if (!this.isMulti) {
for (let i = 0; i < options.length; i ) {
options[i].classList.remove("selected");
}
this.selectedOptions = [];
let optionText = clickedOption.querySelector(".hsOptionText");
this.hsDisplay.innerHTML = optionText.innerHTML;
clickedOption.classList.add("selected");
this.addToSelected(index);
} else {
if (clickedOption.classList.contains("selected")) {
clickedOption.classList.remove("selected");
this.removeFromSelected(index);
} else {
clickedOption.classList.add("selected");
this.addToSelected(index);
}
let keys = Object.keys(this.selectedOptions);
this.hsDisplay.innerHTML = `${keys.length} selected`;
}
this.change();
this.refreshCurrSelecteds();
}
addToSelected(index) {
this.selectedOptions[index] = this.data[index];
}
removeFromSelected(index) {
delete this.selectedOptions[index];
}
refreshCurrSelecteds() {
let hshCurrSelecteds = this.hsSelectWindowWrapper.querySelector(
".hshCurrSelecteds"
);
let keys = Object.keys(this.selectedOptions);
hshCurrSelecteds.innerHTML = `${keys.length} selected`;
if (this.isMulti) {
this.hsDisplay.innerHTML = `${keys.length} selected`;
}
if (keys.length == 0) {
this.hsDisplay.innerHTML = "None selected";
}
}
selectAll() {
for (let i = 0; i < this.data.length; i ) {
let option = this.hsSelectWindowWrapper.querySelector(
`.hsOption[index="${i}"]`
);
option.classList.add("selected");
this.addToSelected(i);
}
this.change();
this.refreshCurrSelecteds();
}
deselectAll(shouldCallback = true) {
for (let i = 0; i < this.data.length; i ) {
let option = this.hsSelectWindowWrapper.querySelector(
`.hsOption[index="${i}"]`
);
option.classList.remove("selected");
this.removeFromSelected(i);
}
if (shouldCallback) {
this.change();
}
this.refreshCurrSelecteds();
}
setCallback(callback) {
if (callback) {
this.changeCallback = callback;
}
}
getName() {
return this.name;
}
getID() {
return this.id;
}
getSelected() {
let keys = Object.keys(this.selectedOptions);
let selected = [];
for (let i = 0; i < keys.length; i ) {
selected.push(this.selectedOptions[keys[i]]);
}
return selected;
}
select(attributesToSearch) {
if (this.data.length < 1) {
console.error(`[HS_Select] - There are no options to select.`);
return;
}
this.deselectAll(false);
//console.log("Selecting attributes:", attributesToSearch);
for (const data of this.data) {
for (const attribute of data.attributes) {
let key = Object.keys(attribute)[0];
let value = attribute[key];
for (const searchAttr of attributesToSearch) {
let searchKey = Object.keys(searchAttr)[0];
let searchValue = searchAttr[searchKey];
if (key == searchKey && value == searchValue) {
let index = this.data.indexOf(data);
let option = this.hsSelectWindowWrapper.querySelector(
`.hsOption[index="${index}"]`
);
option.classList.add("selected");
this.addToSelected(index);
//console.log(`Selected: ${key} = ${value}`);
}
}
}
}
}
}
/* SELECT CLASS END */
/* TEST JS */
let testOptions = [
{
text: "One",
attributes: [{ value: "1" }]
},
{ text: "Two", attributes: [{ value: "2" }] },
{ text: "Three", attributes: [{ value: "3" }] },
{ text: "Four", attributes: [{ value: "4" }] },
{ text: "Five", attributes: [{ value: "5" }] },
{ text: "Six", attributes: [{ value: "6" }] },
{ text: "Seven", attributes: [{ value: "7" }] },
{ text: "Eight", attributes: [{ value: "8" }] },
{ text: "Nine", attributes: [{ value: "9" }] },
{ text: "Ten", attributes: [{ value: "10" }] },
{ text: "Eleven", attributes: [{ value: "11" }] },
{ text: "Twelve", attributes: [{ value: "12" }] },
{ text: "Thirteen", attributes: [{ value: "13" }] },
{ text: "Fourteen", attributes: [{ value: "14" }] },
{ text: "Fifteen", attributes: [{ value: "15" }] },
{ text: "Sixteen", attributes: [{ value: "16" }] },
{ text: "Seventeen", attributes: [{ value: "17" }] },
{ text: "Eighteen", attributes: [{ value: "18" }] },
{ text: "Nineteen", attributes: [{ value: "19" }] },
{ text: "Twenty", attributes: [{ value: "20" }] },
{ text: "Twenty One", attributes: [{ value: "21" }, { address: 15 }] }
];
let selectOne = new hsSelect(
{
id: "testSelect_One",
name: "select",
isMulti: false
},
function (selecteds) {
console.log(selectOne.getName(), " changed to ", selecteds);
}
);
selectOne.fill(testOptions);
let selectMulti = new hsSelect(
{
id: "testSelect_Two",
name: "select Multi",
isMulti: true
},
function (selecteds) {
console.log(selectMulti.getName(), " changed to ", selecteds);
}
);
selectMulti.fill(testOptions);
selectMulti.select([{ value: 20 }, { value: 8 }, { address: 15 }]);
let selectThree = new hsSelect(
{
id: "testSelect_Three",
name: "select",
isMulti: true
},
function (selecteds) {
console.log(selectThree.getName(), " changed to ", selecteds);
}
);
selectThree.fill(testOptions);
/* TEST JS END */
/* MAIN CSS */
*,*::after,*::before {
box-sizing: border-box;
}
body{
font-family: sans-serif;
font-size: 16px;
line-height: 1.5;
color: #333;
background-color: #fff;
padding: 0px;
margin: 0px;
width: 100%;
height: 100vh;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
}
*::-webkit-scrollbar {
width: 5px;
}
*::-webkit-scrollbar-track {
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
border-radius: 0.3rem;
}
*::-webkit-scrollbar-thumb {
border-radius: 10px;
-webkit-box-shadow: inset 0 0 6px rgb(69 108 179);
box-shadow: inset 0 0 6px rgb(69 108 179);
}
.noselect {
-webkit-touch-callout: none;
/* iOS Safari */
-webkit-user-select: none;
/* Safari */
-khtml-user-select: none;
/* Konqueror HTML */
-moz-user-select: none;
/* Old versions of Firefox */
-ms-user-select: none;
/* Internet Explorer/Edge */
user-select: none;
/* Non-prefixed version, currently
supported by Chrome, Edge, Opera and Firefox */
}
/* MAIN CSS END */
/* SELECT CSS */
.hsSelect {
padding: 10px;
border-radius: 0.5em;
min-width: 100px;
min-height: 40px;
max-width: 200px;
cursor: pointer;
box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px;
outline: none;
-webkit-tap-highlight-color: transparent !important;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.hsSelectWindowWrapper{
display: none;
}
.hsSelectWindowWrapper.open {
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 15px;
height: 100%;
background-color: #5674a9ba;
padding: 10px;
z-index: 9999;
}
.hsSelectWindow {
display: block;
border-radius: 0.5em;
padding: 10px;
background-color: whitesmoke;
box-shadow: rgb(12 108 158 / 64%) 0px 1px 4px;
width: 500px;
max-height: 75%;
overflow: auto;
display: flex;
flex-direction: column;
}
.hsOption {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 5px;
min-height: 35px;
border-radius: 0.5em;
cursor: pointer;
}
.hsOption:hover{
background-color: rgba(69, 108, 179, 0.1);
}
span.hsOptionText {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 70%;
color: rgb(69 108 179);
font-size: 1em;
font-weight: bold;
}
span.hsOptionCheckWrapper {
width: 25px;
height: 25px;
box-shadow: rgba(69, 107, 179, 0.541) 0px 0px 4px;
border-radius: 0.3em;
z-index: 2;
padding: 2px;
}
.hsOptionCheck{
transition: background-color 0.2s ease-in-out;
}
.hsOption.selected .hsOptionCheck {
display: block;
width: 100%;
border-radius: 0.3em;
background-color: rgb(69, 108, 179);
height: 100%;
}
.hshCurrSelecteds {
padding: 5px;
border-bottom: 1px solid blanchedalmond;
font-size: 1em;
font-weight: bold;
color: whitesmoke;
}
.hshSelectTools {
width: 300px;
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-items: center;
}
button.hshSelectTool {
border: none;
padding: 10px;
border-radius: 0.3em;
font-weight: bold;
color: whitesmoke;
background-color: #456cb3;
cursor: pointer;
}
button.hshSelectTool:active{
transform: scale(0.95);
}
@media (max-width: 768px) {
.hsSelectWindow {
width: 95%;
}
}
/* SELECT CSS END */
<hsSelect id="testSelect_One" >
<span >None selected</span>
</hsSelect>
<hsSelect id="testSelect_Two" >
<span >None selected</span>
</hsSelect>
<hsSelect id="testSelect_Three" >
<span >None selected</span>
</hsSelect>
CodePudding user response:
The problem was this method:
addContainer(){
let body = document.querySelector("body");
let newTemplate = this.containerTemplate.replace("%selectID%",`${this.id}_container`);
body.innerHTML = newTemplate;
}
When i added the template to the body, it modified the entire body and removed the elements where the event listener was added. So i had to use the insertAdjacentHTML
function to do it properly.
Here is the corrected method:
addContainer(){
let body = document.querySelector("body");
let newTemplate = this.containerTemplate.replace("%selectID%",`${this.id}_container`);
body.insertAdjacentHTML("beforeend", newTemplate);
}
This way, the dom is not reparshed and the listeners stay there. Another way i could have done this is delegated listeners to the body.
Here is a more detailed description about insertAdjacentHTML
at this link.