Skip to content

Commit e607a97

Browse files
committed
creatable, and searh is working. Added keyboard support
1 parent f38aba1 commit e607a97

8 files changed

Lines changed: 666 additions & 508 deletions

File tree

dist/css/dselect.css

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/css/dselect.css.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/css/dselect.min.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/js/dselect.js

Lines changed: 106 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -66,52 +66,104 @@ function dselectSearch(event, input, classElement, classToggler, creatable, loca
6666
}
6767
}));
6868
}
69+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
70+
event.preventDefault();
71+
dselectNavigate(event.key, itemsContainer, classElement);
72+
return;
73+
}
74+
if (event.key === "Enter") {
75+
event.preventDefault();
76+
const highlightedItem = itemsContainer.querySelector(".dropdown-item.dselect-highlighted");
77+
if (highlightedItem && !highlightedItem.classList.contains("d-none")) {
78+
dselectUpdate(highlightedItem, classElement, classToggler);
79+
return;
80+
} else if (creatable && filterValue !== "" && items.filter((item) => !item.classList.contains("d-none")).length === 0) {
81+
const toggler = target.nextElementSibling.querySelector(`.${classToggler}`);
82+
const existingOption = Array.from(target.options).find((option) => option.value === input.value.trim());
83+
if (!existingOption) {
84+
target.insertAdjacentHTML("afterbegin", `<option value="${input.value.trim()}" selected>${input.value.trim()}</option>`);
85+
} else {
86+
existingOption.selected = true;
87+
}
88+
target.dispatchEvent(new Event("change"));
89+
input.value = "";
90+
const searchEvent = new Event("keyup");
91+
searchEvent.key = "";
92+
input.dispatchEvent(searchEvent);
93+
toggler.click();
94+
toggler.focus();
95+
return;
96+
}
97+
return;
98+
}
6999
headers.forEach((header) => header.classList.add("d-none"));
100+
items.forEach((item) => item.classList.remove("dselect-highlighted"));
70101
items.forEach((item) => {
71102
const filterText = item.textContent.toLowerCase();
72-
const isVisible = filterText.includes(filterValue);
103+
const isVisible = filterValue === "" || filterText.includes(filterValue);
73104
item.classList.toggle("d-none", !isVisible);
74105
if (isVisible) {
75-
let currentHeader = item.previousElementSibling;
76-
while (currentHeader && !currentHeader.classList.contains("dropdown-header")) {
77-
currentHeader.classList.remove("d-none");
78-
currentHeader = currentHeader.previousElementSibling;
106+
let currentElement = item.previousElementSibling;
107+
while (currentElement) {
108+
if (currentElement.classList.contains("dropdown-header")) {
109+
currentElement.classList.remove("d-none");
110+
break;
111+
}
112+
currentElement = currentElement.previousElementSibling;
79113
}
80114
}
81115
});
82116
headers.forEach((header) => {
83117
const filterText = header.textContent.toLowerCase();
84-
const isVisible = filterText.includes(filterValue);
85-
header.classList.toggle("d-none", !isVisible);
86-
if (isVisible) {
118+
if (filterValue !== "" && filterText.includes(filterValue)) {
119+
header.classList.remove("d-none");
87120
let currentItem = header.nextElementSibling;
88121
while (currentItem && !currentItem.classList.contains("dropdown-header")) {
89-
currentItem.classList.remove("d-none");
122+
if (currentItem.classList.contains("dropdown-item")) {
123+
currentItem.classList.remove("d-none");
124+
}
90125
currentItem = currentItem.nextElementSibling;
91126
}
92127
}
93128
});
94129
const foundItems = items.filter((item) => !item.classList.contains("d-none") && !item.hasAttribute("hidden"));
95-
if (foundItems.length === 0) {
130+
if (foundItems.length === 0 && filterValue !== "") {
96131
noResults.classList.remove("d-none");
97132
itemsContainer.classList.add("d-none");
98133
if (creatable) {
99-
noResults.innerHTML = localization.replace("[searched-term]", input.value);
100-
if (event.key === "Enter") {
101-
const toggler = target.nextElementSibling.querySelector(`.${classToggler}`);
102-
target.insertAdjacentHTML("afterbegin", `<option value="${input.value}" selected>${input.value}</option>`);
103-
target.dispatchEvent(new Event("change"));
104-
input.value = "";
105-
input.dispatchEvent(new Event("keyup"));
106-
toggler.click();
107-
toggler.focus();
108-
}
134+
noResults.innerHTML = localization.replace("[searched-term]", `<strong>${input.value}</strong>`);
109135
}
110136
} else {
111137
noResults.classList.add("d-none");
112138
itemsContainer.classList.remove("d-none");
139+
if (foundItems.length > 0 && filterValue !== "") {
140+
foundItems[0].classList.add("dselect-highlighted");
141+
}
113142
}
114143
}
144+
function dselectNavigate(direction, itemsContainer, classElement) {
145+
const visibleItems = Array.from(itemsContainer.querySelectorAll(".dropdown-item")).filter((item) => !item.classList.contains("d-none") && !item.hasAttribute("hidden"));
146+
if (visibleItems.length === 0)
147+
return;
148+
const currentHighlighted = itemsContainer.querySelector(".dropdown-item.dselect-highlighted");
149+
let newIndex = 0;
150+
if (currentHighlighted) {
151+
const currentIndex = visibleItems.indexOf(currentHighlighted);
152+
currentHighlighted.classList.remove("dselect-highlighted");
153+
if (direction === "ArrowDown") {
154+
newIndex = currentIndex < visibleItems.length - 1 ? currentIndex + 1 : 0;
155+
} else if (direction === "ArrowUp") {
156+
newIndex = currentIndex > 0 ? currentIndex - 1 : visibleItems.length - 1;
157+
}
158+
} else {
159+
newIndex = direction === "ArrowUp" ? visibleItems.length - 1 : 0;
160+
}
161+
visibleItems[newIndex].classList.add("dselect-highlighted");
162+
visibleItems[newIndex].scrollIntoView({
163+
block: "nearest",
164+
behavior: "smooth"
165+
});
166+
}
115167
function dselectClear(button, classElement) {
116168
const target = button.closest(`.${classElement}`).previousElementSibling;
117169
if (target) {
@@ -159,7 +211,7 @@ function dselect(el, option = {}) {
159211
const customSize = el.dataset.dselectSize || option.size || defaultSize;
160212
let size = customSize !== "" ? ` form-select-${customSize}` : "";
161213
const classToggler = `form-select${size}`;
162-
const searchInput = search ? `<input onkeydown="return event.key !== 'Enter'" onkeyup="dselectSearch(event, this, '${classElement}', '${classToggler}', ${creatable}, '${addOptionPlaceholder}')" type="text" class="form-control ${searchExtraClass}" placeholder="${searchPlaceholder}" autofocus>` : "";
214+
const searchInput = search ? `<input onkeydown="dselectSearch(event, this, '${classElement}', '${classToggler}', ${creatable}, '${addOptionPlaceholder.replace(/'/g, "\\'")}');" onkeyup="dselectSearch(event, this, '${classElement}', '${classToggler}', ${creatable}, '${addOptionPlaceholder.replace(/'/g, "\\'")}')" oninput="dselectSearch(event, this, '${classElement}', '${classToggler}', ${creatable}, '${addOptionPlaceholder.replace(/'/g, "\\'")}')" type="text" class="form-control ${searchExtraClass}" placeholder="${searchPlaceholder}" autofocus>` : "";
163215
function attrBool(attr) {
164216
const attribute = `data-dselect-${attr}`;
165217
if (!el.hasAttribute(attribute))
@@ -189,11 +241,11 @@ function dselect(el, option = {}) {
189241
} else {
190242
for (const option2 of selectedOptions) {
191243
tag.push(`
192-
<div class="${classTag}" data-dselect-value="${option2.value}">
193-
${option2.text}
194-
<svg onclick="dselectRemoveTag(this, '${classElement}', '${classToggler}')" class="${classTagRemove}" width="14" height="14" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/></svg>
195-
</div>
196-
`);
244+
<div class="${classTag}" data-dselect-value="${option2.value}">
245+
${option2.text}
246+
<svg onclick="dselectRemoveTag(this, '${classElement}', '${classToggler}')" class="${classTagRemove}" width="14" height="14" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/></svg>
247+
</div>
248+
`);
197249
}
198250
}
199251
return tag.join("");
@@ -238,13 +290,13 @@ function dselect(el, option = {}) {
238290
imgSize = "1.2rem";
239291
}
240292
text = `<span class="d-flex align-items-center">
241-
<img src="${img}" style="max-height:${imgSize}; width:auto;">
242-
<span class="ps-2">${text}</span>
243-
</span>`;
293+
<img src="${img}" style="max-height:${imgSize}; width:auto;">
294+
<span class="ps-2">${text}</span>
295+
</span>`;
244296
}
245297
items.push(`<button${hidden} class="dropdown-item${active}${btnClass}" ${disableitem} data-dselect-value="${value}" type="button" onclick="dselectUpdate(this, '${classElement}', '${classToggler}')" ${disabled}>
246-
${text}
247-
</button>`);
298+
${text}
299+
</button>`);
248300
}
249301
}
250302
items = items.join("");
@@ -256,30 +308,30 @@ function dselect(el, option = {}) {
256308
return className !== "form-select" && className !== "form-select-sm" && className !== "form-select-lg";
257309
}).join(" ");
258310
const clearBtn = clearable && !el.multiple ? `
259-
<button type="button" class="btn ${classClearBtn}" title="Clear selection" onclick="dselectClear(this, '${classElement}')">
260-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" fill="none">
261-
<path d="M13 1L0.999999 13" stroke-width="2" stroke="currentColor"></path>
262-
<path d="M1 1L13 13" stroke-width="2" stroke="currentColor"></path>
263-
</svg>
264-
</button>
265-
` : "";
311+
<button type="button" class="btn ${classClearBtn}" title="Clear selection" onclick="dselectClear(this, '${classElement}')">
312+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" fill="none">
313+
<path d="M13 1L0.999999 13" stroke-width="2" stroke="currentColor"></path>
314+
<path d="M1 1L13 13" stroke-width="2" stroke="currentColor"></path>
315+
</svg>
316+
</button>
317+
` : "";
266318
const items = itemTags(el.querySelectorAll("*"));
267319
const template = `
268-
<div class="dropdown ${classElement} ${additionalClass}">
269-
<button class="${classToggler} ${!el.multiple && clearable ? classTogglerClearable : ""}" data-dselect-text="${!el.multiple && selectedText(el.options)}" type="button" data-bs-toggle="dropdown" aria-expanded="false"${autoclose}>
270-
${selectedTag(el.options, el.multiple)}
271-
</button>
272-
<div class="dropdown-menu">
273-
<div class="d-flex flex-column">
274-
${searchInput}
275-
<div class="dselect-items" style="max-height:${maxHeight};overflow:auto">
276-
${items}
277-
</div>
278-
<div class="${classNoResults} ${items.length ? "d-none" : ""}">${noResultsPlaceholder}</div>
279-
</div>
280-
</div>
281-
${clearBtn}
282-
</div>`;
320+
<div class="dropdown ${classElement} ${additionalClass}">
321+
<button class="${classToggler} ${!el.multiple && clearable ? classTogglerClearable : ""}" data-dselect-text="${!el.multiple && selectedText(el.options)}" type="button" data-bs-toggle="dropdown" aria-expanded="false"${autoclose}>
322+
${selectedTag(el.options, el.multiple)}
323+
</button>
324+
<div class="dropdown-menu">
325+
<div class="d-flex flex-column">
326+
${searchInput}
327+
<div class="dselect-items" style="max-height:${maxHeight};overflow:auto">
328+
${items}
329+
</div>
330+
<div class="${classNoResults} d-none">${noResultsPlaceholder}</div>
331+
</div>
332+
</div>
333+
${clearBtn}
334+
</div>`;
283335
removePrev();
284336
el.insertAdjacentHTML("afterend", template);
285337
const dropdownElement = el.nextElementSibling;
@@ -307,6 +359,7 @@ if (typeof window !== "undefined") {
307359
window.dselectUpdate = dselectUpdate;
308360
window.dselectRemoveTag = dselectRemoveTag;
309361
window.dselectSearch = dselectSearch;
362+
window.dselectNavigate = dselectNavigate;
310363
window.dselectClear = dselectClear;
311364
window.dselect = dselect;
312365
}

0 commit comments

Comments
 (0)