Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 
 

153 rindas
4.6 KiB

  1. import { esc } from './utils.js';
  2. export class SearchableSelect {
  3. constructor(container, { searchPlaceholder = '...' } = {}) {
  4. this.container = container;
  5. this.value = '';
  6. this.label = '';
  7. this.groups = [];
  8. this.open = false;
  9. this.highlightIdx = -1;
  10. this.placeholder = container.dataset.placeholder || '...';
  11. this.onSelect = null;
  12. this.container.innerHTML = `
  13. <button type="button" class="ss__trigger">
  14. <span class="ss__value">${esc(this.placeholder)}</span>
  15. <svg class="ss__arrow" viewBox="0 0 10 6"><path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.3" fill="none" stroke-linecap="round"/></svg>
  16. </button>
  17. <div class="ss__dropdown" hidden>
  18. <input type="text" class="ss__search" placeholder="${esc(searchPlaceholder)}">
  19. <div class="ss__list"></div>
  20. </div>`;
  21. this.trigger = container.querySelector('.ss__trigger');
  22. this.valueEl = container.querySelector('.ss__value');
  23. this.dropdown = container.querySelector('.ss__dropdown');
  24. this.search = container.querySelector('.ss__search');
  25. this.list = container.querySelector('.ss__list');
  26. this.trigger.addEventListener('click', (e) => {
  27. e.stopPropagation();
  28. this.toggle();
  29. });
  30. this.search.addEventListener('input', () => this.render());
  31. this.search.addEventListener('keydown', (e) => this.onKeydown(e));
  32. this.list.addEventListener('click', (e) => {
  33. const item = e.target.closest('[data-value]');
  34. if (item) this.select(item.dataset.value, item.dataset.label);
  35. });
  36. document.addEventListener('click', (e) => {
  37. if (this.open && !this.container.contains(e.target)) this.close();
  38. });
  39. }
  40. setGroups(groups) {
  41. this.groups = groups;
  42. }
  43. setValue(val) {
  44. for (const g of this.groups) {
  45. for (const item of g.items) {
  46. if (String(item.id) === String(val)) {
  47. this.value = String(item.id);
  48. this.label = item.name;
  49. this.valueEl.textContent = item.name;
  50. this.valueEl.classList.add('ss__value--selected');
  51. return;
  52. }
  53. }
  54. }
  55. this.value = '';
  56. this.label = '';
  57. this.valueEl.textContent = this.placeholder;
  58. this.valueEl.classList.remove('ss__value--selected');
  59. }
  60. getValue() { return this.value; }
  61. toggle() {
  62. this.open ? this.close() : this.openDropdown();
  63. }
  64. openDropdown() {
  65. this.open = true;
  66. this.dropdown.hidden = false;
  67. this.search.value = '';
  68. this.highlightIdx = -1;
  69. this.render();
  70. this.search.focus();
  71. }
  72. close() {
  73. this.open = false;
  74. this.dropdown.hidden = true;
  75. }
  76. select(val, label) {
  77. this.value = val;
  78. this.label = label;
  79. this.valueEl.textContent = label;
  80. this.valueEl.classList.add('ss__value--selected');
  81. this.close();
  82. if (this.onSelect) this.onSelect(val, label);
  83. }
  84. focus() {
  85. this.openDropdown();
  86. }
  87. render() {
  88. const q = this.search.value.toLowerCase().trim();
  89. let html = '';
  90. let idx = 0;
  91. const visibleItems = [];
  92. for (const g of this.groups) {
  93. const filtered = g.items.filter(item =>
  94. !q || item.name.toLowerCase().includes(q) || g.label.toLowerCase().includes(q)
  95. );
  96. if (!filtered.length) continue;
  97. if (g.label) {
  98. html += `<div class="ss__group">${esc(g.label)}</div>`;
  99. }
  100. for (const item of filtered) {
  101. const active = String(item.id) === this.value ? ' ss__item--active' : '';
  102. const hl = idx === this.highlightIdx ? ' ss__item--highlight' : '';
  103. html += `<div class="ss__item${active}${hl}" data-value="${item.id}" data-label="${esc(item.name)}" data-idx="${idx}">${esc(item.name)}</div>`;
  104. visibleItems.push(item);
  105. idx++;
  106. }
  107. }
  108. this.list.innerHTML = html || `<div class="ss__empty">–</div>`;
  109. this.visibleCount = visibleItems.length;
  110. }
  111. onKeydown(e) {
  112. if (e.key === 'ArrowDown') {
  113. e.preventDefault();
  114. this.highlightIdx = Math.min(this.highlightIdx + 1, this.visibleCount - 1);
  115. this.render();
  116. this.scrollToHighlight();
  117. } else if (e.key === 'ArrowUp') {
  118. e.preventDefault();
  119. this.highlightIdx = Math.max(this.highlightIdx - 1, 0);
  120. this.render();
  121. this.scrollToHighlight();
  122. } else if (e.key === 'Enter') {
  123. e.preventDefault();
  124. const el = this.list.querySelector(`[data-idx="${this.highlightIdx}"]`);
  125. if (el) this.select(el.dataset.value, el.dataset.label);
  126. } else if (e.key === 'Escape') {
  127. this.close();
  128. }
  129. }
  130. scrollToHighlight() {
  131. const el = this.list.querySelector('.ss__item--highlight');
  132. if (el) el.scrollIntoView({ block: 'nearest' });
  133. }
  134. }