312 lines
12 KiB
PHP

{{--
可打字搜索的多选筛选(按钮 + 复选面板)。
用法:
<div class="upstream-data-field cw-ms" data-name="store" data-initial='["A"]'>
<span>店铺</span>
<span class="cw-ms__box">
<button type="button" class="cw-ms__btn" data-role="btn" aria-expanded="false">
<span class="cw-ms__label">全部</span><span class="cw-ms__caret">▾</span>
</button>
<span class="cw-ms__panel" data-role="panel" hidden></span>
<span class="cw-ms__values" data-role="values"></span>
</span>
</div>
window.CaiWuMultiSelect.create(el, { options: [...], searchable: true });
勾选结果会写成 <input type="hidden" name="store[]"> 随表单提交;一个都不勾=全部。
--}}
<style>
.cw-ms { position: relative; }
.cw-ms__box { position: relative; display: block; }
.cw-ms__btn {
display: flex;
width: 100%;
height: 2.125rem;
align-items: center;
justify-content: space-between;
gap: 0.4rem;
padding: 0 0.625rem;
border: 1px solid var(--vip-border, #d8d2c7);
border-radius: 0.5rem;
background: var(--vip-surface, #fff);
color: var(--vip-content-ink, #171717);
font: inherit;
font-size: 0.8125rem;
text-align: left;
cursor: pointer;
}
.cw-ms__btn:hover { border-color: #c4b79f; }
.cw-ms__btn[aria-expanded="true"] { border-color: #856632; box-shadow: 0 0 0 3px rgba(133, 102, 50, 0.2); }
.cw-ms__btn--on { font-weight: 600; color: #6f5427; }
.cw-ms__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cw-ms__caret { flex: 0 0 auto; font-size: 0.625rem; color: #8a8377; }
.cw-ms__panel {
position: absolute;
top: calc(100% + 0.3rem);
left: 0;
z-index: 70;
min-width: 16rem;
max-height: 16rem;
overflow: auto;
padding: 0.35rem;
background: #fffdf8;
border: 1px solid #e4dccd;
border-radius: 8px;
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.14);
}
.cw-ms__panel[hidden] { display: none; }
.cw-ms__values { display: none; }
.cw-ms__empty { padding: 0.4rem 0.5rem; font-size: 0.75rem; color: #8a8377; }
.cw-ms__search {
width: 100%;
height: 1.9rem;
min-width: 0;
padding: 0 0.5rem;
border: 1px solid #ddd5c7;
border-radius: 0.4rem;
background: #fff;
color: #3d3a35;
font: inherit;
font-size: 0.8125rem;
outline: none;
}
.cw-ms__search:focus { border-color: #856632; box-shadow: 0 0 0 3px rgba(133, 102, 50, 0.2); }
.cw-ms__opts { display: block; }
.cw-ms__head { display: flex; gap: 0.35rem; padding: 0.15rem 0.15rem 0.4rem; margin-bottom: 0.25rem; border-bottom: 1px solid #efe8db; }
.cw-ms__head button {
flex: 1 1 auto;
height: auto;
padding: 0.2rem 0.35rem;
border: 1px solid #e4dccd;
border-radius: 6px;
background: #fff;
color: #3d3a35;
font: inherit;
font-size: 0.6875rem;
cursor: pointer;
}
.cw-ms__head button:hover { background: #f6f2e9; }
.cw-ms__opt {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.3rem 0.35rem;
border-radius: 6px;
font-size: 0.8125rem;
color: #3d3a35;
cursor: pointer;
}
.cw-ms__opt:hover { background: #f6f2e9; }
.cw-ms__opt span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* 联动统计:选项右侧显示当前条件下的行数(不传 counts 的页面不显示)。 */
.cw-ms__opt span:first-of-type { flex: 1 1 auto; min-width: 0; }
.cw-ms__opt .cw-ms__count { flex: 0 0 auto; color: #8a8377; font-size: 0.6875rem; font-variant-numeric: tabular-nums; }
.cw-ms__panel .cw-ms__opt input {
position: relative;
flex: 0 0 auto;
width: 0.95rem;
height: 0.95rem;
min-width: 0;
margin: 0;
padding: 0;
border: 1px solid #cfc6b4;
border-radius: 4px;
background: #fff;
appearance: none;
-webkit-appearance: none;
cursor: pointer;
}
.cw-ms__panel .cw-ms__opt input:checked { border-color: #856632; background: #856632; }
.cw-ms__panel .cw-ms__opt input:checked::after {
content: '✓';
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 0.6875rem;
line-height: 1;
}
</style>
<script>
(function () {
if (window.CaiWuMultiSelect) { return; }
var instances = [];
function esc(value) {
return String(value == null ? '' : value).replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
function create(root, opts) {
opts = opts || {};
var name = root.getAttribute('data-name') || '';
var btn = root.querySelector('[data-role="btn"]');
var labelEl = btn.querySelector('.cw-ms__label');
var panel = root.querySelector('[data-role="panel"]');
var valuesEl = root.querySelector('[data-role="values"]');
var candidates = (opts.options || []).slice();
var counts = opts.counts || {};
var countsLoading = false;
var selectedList = [];
var keyword = '';
var optsEl = null;
var searchEl = null;
try {
(JSON.parse(root.getAttribute('data-initial') || '[]') || []).forEach(function (v) {
if (selectedList.indexOf(v) < 0) { selectedList.push(String(v)); }
});
} catch (e) { /* 脏数据忽略 */ }
function isSel(v) { return selectedList.indexOf(v) >= 0; }
function countText(v) {
var total = counts[v];
if (total === null || total === undefined) {
return countsLoading ? '统计中…' : '';
}
return String(total).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
function listed() {
if (!keyword) { return candidates.slice(); }
return candidates.filter(function (v) { return String(v).toLowerCase().indexOf(keyword) >= 0; });
}
function buildShell() {
if (optsEl) { return; }
panel.innerHTML = (opts.searchable
? '<span class="cw-ms__head"><input type="search" class="cw-ms__search" placeholder="'
+ esc(opts.searchPlaceholder || '打字找关键字') + '"></span>'
: '')
+ '<span class="cw-ms__head">'
+ '<button type="button" data-ms-act="all">全选</button>'
+ '<button type="button" data-ms-act="none">清空</button>'
+ '</span>'
+ '<span class="cw-ms__opts"></span>';
optsEl = panel.querySelector('.cw-ms__opts');
searchEl = panel.querySelector('.cw-ms__search');
if (searchEl) {
searchEl.addEventListener('input', function () {
keyword = searchEl.value.trim().toLowerCase();
renderOptions();
});
}
}
function renderOptions() {
if (!optsEl) { return; }
var items = listed();
if (!items.length) {
optsEl.innerHTML = '<div class="cw-ms__empty">'
+ (keyword ? '没有匹配的项,换个关键字试试' : '暂无可选项') + '</div>';
return;
}
var html = items.slice(0, 300).map(function (v) {
return '<label class="cw-ms__opt"><input type="checkbox" value="' + esc(v) + '"'
+ (isSel(v) ? ' checked' : '') + '><span>' + esc(v) + '</span>'
+ '<span class="cw-ms__count">' + esc(countText(v)) + '</span></label>';
}).join('');
if (items.length > 300) {
html += '<div class="cw-ms__empty">还有 ' + (items.length - 300) + ' 项,再打精确一点</div>';
}
optsEl.innerHTML = html;
}
function sync() {
if (!selectedList.length) {
labelEl.textContent = '全部';
btn.classList.remove('cw-ms__btn--on');
} else if (selectedList.length === 1) {
labelEl.textContent = selectedList[0];
btn.classList.add('cw-ms__btn--on');
} else {
labelEl.textContent = selectedList[0] + ' 等 ' + selectedList.length + ' 项';
btn.classList.add('cw-ms__btn--on');
}
btn.title = selectedList.length ? selectedList.join('、') : '全部';
// 目录表把控件放在 query 表单外面(靠 form 属性关联),这里支持传 formId。
var formAttr = opts.formId ? ' form="' + esc(opts.formId) + '"' : '';
valuesEl.innerHTML = selectedList.map(function (v) {
return '<input type="hidden" name="' + esc(name) + '[]"' + formAttr + ' value="' + esc(v) + '">';
}).join('');
}
function closePanel() {
panel.hidden = true;
btn.setAttribute('aria-expanded', 'false');
}
var api = {
root: root,
values: function () { return selectedList.slice(); },
close: closePanel,
setOptions: function (list, nextCounts) {
candidates = (list || []).slice();
if (nextCounts) { counts = nextCounts; }
countsLoading = false;
renderOptions();
sync();
},
setLoading: function (loading) {
countsLoading = Boolean(loading);
renderOptions();
}
};
btn.addEventListener('click', function (ev) {
ev.stopPropagation();
if (panel.hidden) {
instances.forEach(function (ms) { if (ms !== api) { ms.close(); } });
buildShell();
renderOptions();
panel.hidden = false;
btn.setAttribute('aria-expanded', 'true');
if (searchEl) { searchEl.focus(); }
} else {
closePanel();
}
});
panel.addEventListener('click', function (ev) {
ev.stopPropagation();
var act = ev.target.closest('button[data-ms-act]');
if (!act) { return; }
if (act.getAttribute('data-ms-act') === 'all') {
listed().forEach(function (v) { if (!isSel(v)) { selectedList.push(v); } });
} else {
selectedList = [];
keyword = '';
if (searchEl) { searchEl.value = ''; }
}
renderOptions();
sync();
});
panel.addEventListener('change', function (ev) {
var cb = ev.target.closest('input[type="checkbox"]');
if (!cb) { return; }
if (cb.checked) {
if (!isSel(cb.value)) { selectedList.push(cb.value); }
} else {
selectedList = selectedList.filter(function (v) { return v !== cb.value; });
}
sync();
});
instances.push(api);
sync();
return api;
}
document.addEventListener('click', function (ev) {
instances.forEach(function (ms) { if (!ms.root.contains(ev.target)) { ms.close(); } });
});
document.addEventListener('keydown', function (ev) {
if (ev.key === 'Escape') { instances.forEach(function (ms) { ms.close(); }); }
});
window.CaiWuMultiSelect = { create: create };
})();
</script>