multi-select.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. 'use strict';
  2. // 通用多选下拉(字段 / 店铺 / 学校共用):按钮 + 搜索 + 复选列表 + 全选/清空
  3. // 用法:window.createMultiSelect(rootEl, { label, items:[{key,label,hint}], selected, allowEmpty, emptyLabel, onChange })
  4. (function () {
  5. function esc(value) {
  6. const div = document.createElement('div');
  7. div.textContent = String(value == null ? '' : value);
  8. return div.innerHTML;
  9. }
  10. function createMultiSelect(root, { label, items, selected, allowEmpty, emptyLabel, onChange }) {
  11. const sel = new Set(selected || []);
  12. let keyword = '';
  13. root.innerHTML = `<button type="button" class="ops-multi-btn" aria-expanded="false"></button>
  14. <div class="ops-multi-menu" hidden>
  15. <input type="text" class="ops-multi-search" placeholder="搜索${esc(label)}">
  16. <div class="ops-multi-list"></div>
  17. <div class="ops-multi-foot"><button type="button" data-all>全选</button><button type="button" data-none>清空</button></div>
  18. </div>`;
  19. const btn = root.querySelector('.ops-multi-btn');
  20. const menu = root.querySelector('.ops-multi-menu');
  21. const list = root.querySelector('.ops-multi-list');
  22. const search = root.querySelector('.ops-multi-search');
  23. function refreshButton() {
  24. let text;
  25. if (!sel.size) text = `${label} <em>${emptyLabel || '全部'}</em>`;
  26. else if (sel.size === 1) {
  27. const one = items.find((i) => sel.has(i.key));
  28. text = `${label} <em>${esc(one ? one.label : '1')}</em>`;
  29. } else text = `${label} <em>${sel.size} 项</em>`;
  30. btn.innerHTML = text + '<span class="ops-multi-caret">▾</span>';
  31. }
  32. function renderList() {
  33. const kw = keyword.trim().toLowerCase();
  34. const filtered = items.filter((i) => !kw
  35. || String(i.label).toLowerCase().includes(kw)
  36. || String(i.hint || '').toLowerCase().includes(kw));
  37. list.innerHTML = filtered.length
  38. ? filtered.map((i) => `<label class="ops-multi-item">
  39. <input type="checkbox" value="${esc(i.key)}"${sel.has(i.key) ? ' checked' : ''}>
  40. <span>${esc(i.label)}</span>${i.hint ? `<em>${esc(i.hint)}</em>` : ''}
  41. </label>`).join('')
  42. : '<div class="ops-multi-empty">没有匹配项</div>';
  43. list.querySelectorAll('input[type="checkbox"]').forEach((input) => {
  44. input.addEventListener('change', () => {
  45. if (input.checked) sel.add(input.value);
  46. else sel.delete(input.value);
  47. if (!allowEmpty && !sel.size) {
  48. input.checked = true;
  49. sel.add(input.value);
  50. return;
  51. }
  52. refreshButton();
  53. onChange([...sel]);
  54. });
  55. });
  56. }
  57. btn.addEventListener('click', (e) => {
  58. e.stopPropagation();
  59. const willOpen = menu.hidden;
  60. document.querySelectorAll('.ops-multi-menu').forEach((m) => { m.hidden = true; });
  61. menu.hidden = !willOpen;
  62. btn.setAttribute('aria-expanded', String(willOpen));
  63. if (willOpen) { renderList(); search.focus(); }
  64. });
  65. search.addEventListener('input', () => { keyword = search.value; renderList(); });
  66. root.querySelector('[data-all]').addEventListener('click', () => {
  67. items.forEach((i) => sel.add(i.key));
  68. refreshButton();
  69. renderList();
  70. onChange([...sel]);
  71. });
  72. root.querySelector('[data-none]').addEventListener('click', () => {
  73. sel.clear();
  74. if (!allowEmpty && items[0]) sel.add(items[0].key);
  75. refreshButton();
  76. renderList();
  77. onChange([...sel]);
  78. });
  79. refreshButton();
  80. return { refresh: () => { refreshButton(); renderList(); } };
  81. }
  82. let dismissBound = false;
  83. function bindMultiSelectDismiss() {
  84. if (dismissBound) return;
  85. dismissBound = true;
  86. document.addEventListener('click', (event) => {
  87. document.querySelectorAll('.ops-multi').forEach((root) => {
  88. const menu = root.querySelector('.ops-multi-menu');
  89. if (!menu || menu.hidden) return;
  90. if (root.contains(event.target)) return;
  91. menu.hidden = true;
  92. const btn = root.querySelector('.ops-multi-btn');
  93. if (btn) btn.setAttribute('aria-expanded', 'false');
  94. });
  95. });
  96. document.addEventListener('keydown', (event) => {
  97. if (event.key !== 'Escape') return;
  98. document.querySelectorAll('.ops-multi-menu').forEach((menu) => { menu.hidden = true; });
  99. });
  100. }
  101. window.createMultiSelect = createMultiSelect;
  102. window.bindMultiSelectDismiss = bindMultiSelectDismiss;
  103. })();