312 lines
12 KiB
PHP
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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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>
|