Live updates during a cycle; Filter & sort dropdown on phones; layout polish
- Worker publishes the rows swept so far every few seconds (LivePublishSeconds) with a DataVersion stamp; the dashboard reloads on each new version, so tables, the Overview and open dialogs update mid-cycle. Shared files are written to a temp file and swapped in, so readers never see half a file. - Phones / tablets: one Filter & sort button opens a column list instead of a row of chips, on every table. - Details: a wrapped Simple / Advanced toggle sits on the left. - Card fields: fixed-height dialog; field list and add list share it evenly.
This commit is contained in:
1 parent
7242f8f8af
commit
9dc2d88726
8 files changed
+149
-31
No files matched your search
+50
-14
@@ -318,35 +318,71 @@
|
||||
bar.className = 'xf-bar';
|
||||
table.parentNode.insertBefore(bar, table);
|
||||
}
|
||||
// One "Filter & sort" button (plus Card fields / Clear) instead of a chip per column:
|
||||
// the column list opens as a sheet, so the bar stays a single short row
|
||||
const sortedCol = cols.find(c => { const d = sortDirOf(ad, c.idx); return d === 'asc' || d === 'desc'; });
|
||||
const parts = [];
|
||||
if (ad.custom) parts.push('<button type="button" class="xf-chip xf-fields" data-act="fields">☰ Card fields</button>');
|
||||
cols.forEach(c => {
|
||||
if (!c.filterable && !c.sortable) return;
|
||||
const on = activeKeys.has(ad.colKey(c.idx));
|
||||
const n = on ? filters[ad.key][ad.colKey(c.idx)].size : 0;
|
||||
const dir = sortDirOf(ad, c.idx);
|
||||
const arrow = dir === 'asc' ? ' ↑' : dir === 'desc' ? ' ↓' : '';
|
||||
parts.push(`<button type="button" class="xf-chip${on ? ' xf-on' : ''}" data-idx="${c.idx}">${on ? FUNNEL_SVG : ''}<span></span>${on ? ` <b>${n}</b>` : ''}${arrow}${CARET_SVG}</button>`);
|
||||
});
|
||||
if (active.length) parts.push('<button type="button" class="xf-chip xf-clear" data-act="clear">Clear all</button>');
|
||||
parts.push(`<button type="button" class="xf-chip xf-filterbtn${active.length ? ' xf-on' : ''}" data-act="pick">${FUNNEL_SVG}<span>Filter & sort</span>${active.length ? ` <b>${active.length}</b>` : ''}${sortedCol ? ' · ' + (sortDirOf(ad, sortedCol.idx) === 'asc' ? '↑' : '↓') : ''}${CARET_SVG}</button>`);
|
||||
if (active.length) parts.push('<button type="button" class="xf-chip xf-clear" data-act="clear">Clear</button>');
|
||||
const sig = JSON.stringify([parts, cols.map(c => c.label)]);
|
||||
if (bar.dataset.sig !== sig) {
|
||||
bar.innerHTML = parts.join('');
|
||||
// labels via textContent (column names can come from user-chosen fields)
|
||||
bar.querySelectorAll('.xf-chip[data-idx]').forEach(chip => {
|
||||
chip.querySelector('span').textContent = cols[+chip.dataset.idx].label;
|
||||
});
|
||||
bar.dataset.sig = sig;
|
||||
bar.onclick = e => {
|
||||
const chip = e.target.closest('.xf-chip');
|
||||
if (!chip) return;
|
||||
if (chip.dataset.act === 'clear') { delete filters[ad.key]; ad.rerun(); return; }
|
||||
if (chip.dataset.act === 'fields') { openFieldEditor(); return; }
|
||||
openMenu(table, +chip.dataset.idx, chip);
|
||||
if (chip.dataset.act === 'pick') openColumnPicker(table, chip);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Phones / tablets: pick a column, then its usual sort + filter menu opens
|
||||
function openColumnPicker(table, anchor) {
|
||||
closeMenu();
|
||||
const ad = adapter(table);
|
||||
if (!ad) return;
|
||||
const cols = columnsOf(table, ad).filter(c => c.filterable || c.sortable);
|
||||
const activeKeys = new Set(activeFilters(ad, columnsOf(table, ad)).map(c => ad.colKey(c.idx)));
|
||||
backdrop = el('div', 'xf-backdrop'); backdrop.onclick = closeMenu;
|
||||
menu = el('div', 'xf-menu xf-sheet xf-picker');
|
||||
menuCtx = { picker: true, table };
|
||||
const head = el('div', 'xf-head');
|
||||
head.appendChild(el('span', 'xf-title', 'Filter & sort'));
|
||||
const x = el('button', 'xf-x', '\u00d7'); x.type = 'button'; x.title = 'Close'; x.onclick = closeMenu;
|
||||
head.appendChild(x);
|
||||
menu.appendChild(head);
|
||||
const list = el('div', 'xf-pick-list');
|
||||
cols.forEach(c => {
|
||||
const b = el('button', 'xf-pick-col'); b.type = 'button';
|
||||
b.appendChild(el('span', 'xf-pick-name', c.label));
|
||||
const bits = [];
|
||||
const on = activeKeys.has(ad.colKey(c.idx));
|
||||
if (on) { const f = filters[ad.key][ad.colKey(c.idx)]; bits.push(f.size + ' selected'); }
|
||||
const d = sortDirOf(ad, c.idx);
|
||||
if (d === 'asc') bits.push('sorted \u2191'); else if (d === 'desc') bits.push('sorted \u2193');
|
||||
const st = el('span', 'xf-pick-state', bits.join(' \u00b7 '));
|
||||
if (on) b.classList.add('xf-on');
|
||||
b.appendChild(st);
|
||||
b.insertAdjacentHTML('beforeend', '<svg class="xf-pick-chev" viewBox="0 0 16 16" aria-hidden="true"><path d="M6 3.5 10.5 8 6 12.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>');
|
||||
b.onclick = () => { closeMenu(); openMenu(table, c.idx, anchor); };
|
||||
list.appendChild(b);
|
||||
});
|
||||
menu.appendChild(list);
|
||||
if (activeKeys.size) {
|
||||
const actions = el('div', 'xf-actions');
|
||||
const clr = el('button', 'xf-cancel', 'Clear all filters'); clr.type = 'button';
|
||||
clr.onclick = () => { delete filters[ad.key]; closeMenu(); ad.rerun(); };
|
||||
actions.appendChild(clr);
|
||||
menu.appendChild(actions);
|
||||
}
|
||||
document.body.appendChild(backdrop);
|
||||
document.body.appendChild(menu);
|
||||
document.addEventListener('keydown', onMenuKey, true);
|
||||
}
|
||||
|
||||
// Effective sort direction of a column: a size/free-space sort overrides the table's own
|
||||
function sortDirOf(ad, i) {
|
||||
const ns = nativeSort[ad.key];
|
||||
|
||||
Reference in new issue
Block a user