dselect.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. function dselectUpdate(button, classElement, classToggler) {
  2. const value = button.dataset.dselectValue;
  3. const target = button.closest(`.${classElement}`).previousElementSibling;
  4. const toggler = target.nextElementSibling.getElementsByClassName(classToggler)[0];
  5. const input = target.nextElementSibling.querySelector("input");
  6. if (target.multiple) {
  7. Array.from(target.options).filter((option) => option.value === value)[0].selected = true;
  8. } else {
  9. target.value = value;
  10. }
  11. if (target.multiple) {
  12. toggler.click();
  13. }
  14. target.dispatchEvent(new Event("change"));
  15. toggler.focus();
  16. if (input) {
  17. input.value = "";
  18. }
  19. }
  20. function dselectRemoveTag(button, classElement, classToggler) {
  21. const value = button.parentNode.dataset.dselectValue;
  22. const target = button.closest(`.${classElement}`).previousElementSibling;
  23. const toggler = target.nextElementSibling.getElementsByClassName(classToggler)[0];
  24. const input = target.nextElementSibling.querySelector("input");
  25. Array.from(target.options).filter((option) => option.value === value)[0].selected = false;
  26. target.dispatchEvent(new Event("change"));
  27. toggler.click();
  28. if (input) {
  29. input.value = "";
  30. }
  31. }
  32. function dselectSearch(event, input, classElement, classToggler, creatable) {
  33. const filterValue = input.value.toLowerCase().trim();
  34. const itemsContainer = input.nextElementSibling;
  35. const headers = itemsContainer.querySelectorAll(".dropdown-header");
  36. const items = itemsContainer.querySelectorAll(".dropdown-item");
  37. const noResults = itemsContainer.nextElementSibling;
  38. headers.forEach((i) => i.classList.add("d-none"));
  39. for (const item of items) {
  40. const filterText = item.textContent;
  41. if (filterText.toLowerCase().indexOf(filterValue) > -1) {
  42. item.classList.remove("d-none");
  43. let header = item;
  44. while (header = header.previousElementSibling) {
  45. if (header.classList.contains("dropdown-header")) {
  46. header.classList.remove("d-none");
  47. break;
  48. }
  49. }
  50. } else {
  51. item.classList.add("d-none");
  52. }
  53. }
  54. const found = Array.from(items).filter((i) => !i.classList.contains("d-none") && !i.hasAttribute("hidden"));
  55. if (found.length < 1) {
  56. noResults.classList.remove("d-none");
  57. itemsContainer.classList.add("d-none");
  58. if (creatable) {
  59. noResults.innerHTML = `Press Enter to add "<strong>${input.value}</strong>"`;
  60. if (event.key === "Enter") {
  61. const target = input.closest(`.${classElement}`).previousElementSibling;
  62. const toggler = target.nextElementSibling.getElementsByClassName(classToggler)[0];
  63. target.insertAdjacentHTML("afterbegin", `<option value="${input.value}" selected>${input.value}</option>`);
  64. target.dispatchEvent(new Event("change"));
  65. input.value = "";
  66. input.dispatchEvent(new Event("keyup"));
  67. toggler.click();
  68. toggler.focus();
  69. }
  70. }
  71. } else {
  72. noResults.classList.add("d-none");
  73. itemsContainer.classList.remove("d-none");
  74. }
  75. }
  76. function dselectClear(button, classElement) {
  77. const target = button.closest(`.${classElement}`).previousElementSibling;
  78. Array.from(target.options).forEach((option) => option.selected = false);
  79. target.dispatchEvent(new Event("change"));
  80. }
  81. function dselect(el, option = {}) {
  82. el.style.display = "none";
  83. const classElement = "dselect-wrapper";
  84. const classNoResults = "dselect-no-results";
  85. const classTag = "dselect-tag";
  86. const classTagRemove = "dselect-tag-remove";
  87. const classPlaceholder = "dselect-placeholder";
  88. const classClearBtn = "dselect-clear";
  89. const classTogglerClearable = "dselect-clearable";
  90. const defaultSearch = false;
  91. const defaultCreatable = false;
  92. const defaultClearable = false;
  93. const defaultMaxHeight = "360px";
  94. const defaultSize = "";
  95. const search = attrBool("search") || option.search || defaultSearch;
  96. const creatable = attrBool("creatable") || option.creatable || defaultCreatable;
  97. const clearable = attrBool("clearable") || option.clearable || defaultClearable;
  98. const maxHeight = el.dataset.dselectMaxHeight || option.maxHeight || defaultMaxHeight;
  99. let size = el.dataset.dselectSize || option.size || defaultSize;
  100. size = size !== "" ? ` form-select-${size}` : "";
  101. const classToggler = `form-select${size}`;
  102. const searchInput = search ? `<input onkeydown="return event.key !== 'Enter'" onkeyup="dselectSearch(event, this, '${classElement}', '${classToggler}', ${creatable})" type="text" class="form-control" placeholder="Search" autofocus>` : "";
  103. function attrBool(attr) {
  104. const attribute = `data-dselect-${attr}`;
  105. if (!el.hasAttribute(attribute))
  106. return null;
  107. const value = el.getAttribute(attribute);
  108. return value.toLowerCase() === "true";
  109. }
  110. function removePrev() {
  111. if (el.nextElementSibling && el.nextElementSibling.classList && el.nextElementSibling.classList.contains(classElement)) {
  112. el.nextElementSibling.remove();
  113. }
  114. }
  115. function isPlaceholder(option2) {
  116. return option2.getAttribute("value") === "";
  117. }
  118. function selectedTag(options, multiple) {
  119. if (multiple) {
  120. const selectedOptions = Array.from(options).filter((option2) => option2.selected && !isPlaceholder(option2));
  121. const placeholderOption = Array.from(options).filter((option2) => isPlaceholder(option2));
  122. let tag = [];
  123. if (selectedOptions.length === 0) {
  124. const text = placeholderOption.length ? placeholderOption[0].textContent : "&nbsp;";
  125. tag.push(`<span class="${classPlaceholder}">${text}</span>`);
  126. } else {
  127. for (const option2 of selectedOptions) {
  128. tag.push(`
  129. <div class="${classTag}" data-dselect-value="${option2.value}">
  130. ${option2.text}
  131. <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>
  132. </div>
  133. `);
  134. }
  135. }
  136. return tag.join("");
  137. } else {
  138. const selectedOption = options[options.selectedIndex];
  139. return isPlaceholder(selectedOption) ? `<span class="${classPlaceholder}">${selectedOption.innerHTML}</span>` : selectedOption.innerHTML;
  140. }
  141. }
  142. function selectedText(options) {
  143. const selectedOption = options[options.selectedIndex];
  144. return isPlaceholder(selectedOption) ? "" : selectedOption.textContent;
  145. }
  146. function itemTags(options) {
  147. let items = [];
  148. for (const option2 of options) {
  149. if (option2.tagName === "OPTGROUP") {
  150. items.push(`<h6 class="dropdown-header">${option2.getAttribute("label")}</h6>`);
  151. } else {
  152. const hidden = isPlaceholder(option2) ? " hidden" : "";
  153. const active = option2.selected ? " active" : "";
  154. const disabled = el.multiple && option2.selected ? " disabled" : "";
  155. const value = option2.value;
  156. const text = option2.textContent;
  157. items.push(`<button${hidden} class="dropdown-item${active}" data-dselect-value="${value}" type="button" onclick="dselectUpdate(this, '${classElement}', '${classToggler}')"${disabled}>${text}</button>`);
  158. }
  159. }
  160. items = items.join("");
  161. return items;
  162. }
  163. function createDom() {
  164. const autoclose = el.multiple ? ' data-bs-auto-close="outside"' : "";
  165. const additionalClass = Array.from(el.classList).filter((className) => {
  166. return className !== "form-select" && className !== "form-select-sm" && className !== "form-select-lg";
  167. }).join(" ");
  168. const clearBtn = clearable && !el.multiple ? `
  169. <button type="button" class="btn ${classClearBtn}" title="Clear selection" onclick="dselectClear(this, '${classElement}')">
  170. <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" fill="none">
  171. <path d="M13 1L0.999999 13" stroke-width="2" stroke="currentColor"></path>
  172. <path d="M1 1L13 13" stroke-width="2" stroke="currentColor"></path>
  173. </svg>
  174. </button>
  175. ` : "";
  176. const template = `
  177. <div class="dropdown ${classElement} ${additionalClass}">
  178. <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}>
  179. ${selectedTag(el.options, el.multiple)}
  180. </button>
  181. <div class="dropdown-menu">
  182. <div class="d-flex flex-column">
  183. ${searchInput}
  184. <div class="dselect-items" style="max-height:${maxHeight};overflow:auto">
  185. ${itemTags(el.querySelectorAll("*"))}
  186. </div>
  187. <div class="${classNoResults} d-none">No results found</div>
  188. </div>
  189. </div>
  190. ${clearBtn}
  191. </div>
  192. `;
  193. removePrev();
  194. el.insertAdjacentHTML("afterend", template);
  195. }
  196. createDom();
  197. function updateDom() {
  198. const dropdown = el.nextElementSibling;
  199. const toggler = dropdown.getElementsByClassName(classToggler)[0];
  200. const dSelectItems = dropdown.getElementsByClassName("dselect-items")[0];
  201. toggler.innerHTML = selectedTag(el.options, el.multiple);
  202. dSelectItems.innerHTML = itemTags(el.querySelectorAll("*"));
  203. if (!el.multiple) {
  204. toggler.dataset.dselectText = selectedText(el.options);
  205. }
  206. }
  207. el.addEventListener("change", updateDom);
  208. }