// A single column in the Miller-column navigator. // // Each column holds a list of Item objects. Selecting an item calls // item.onSelect() and marks the item as active (highlighted). The column // also supports optional search filtering. export default class Column { constructor({ title, items = [], searchable = true }) { this.title = title; this._items = items; this._active = null; // currently selected item element this._root = null; this._list = null; this._searchable = searchable; this._filter = ''; this._actionsEl = null; } // Build and return the column DOM element. mount() { const col = document.createElement('div'); col.className = 'mwse-col'; this._root = col; const header = document.createElement('div'); header.className = 'mwse-col__header'; header.textContent = this.title; col.appendChild(header); if (this._searchable && this._items.length > 6) { const search = document.createElement('input'); search.type = 'text'; search.className = 'mwse-col__search'; search.placeholder = 'Filter…'; search.addEventListener('input', () => { this._filter = search.value.toLowerCase(); this._renderItems(); }); col.appendChild(search); } const list = document.createElement('div'); list.className = 'mwse-col__list'; this._list = list; col.appendChild(list); this._renderItems(); return col; } // Replace the item list (re-renders the list area). setItems(items) { this._items = items; this._active = null; this._filter = ''; this._renderItems(); } // Add a persistent action button below the list. addAction(label, className, onClick) { if (!this._root) return; if (!this._actionsEl) { this._actionsEl = document.createElement('div'); this._actionsEl.className = 'mwse-col__actions'; this._root.appendChild(this._actionsEl); } const btn = document.createElement('button'); btn.className = `mwse-btn ${className ?? ''}`; btn.textContent = label; btn.addEventListener('click', onClick); this._actionsEl.appendChild(btn); } // Force a re-render (e.g. after external state changes meta text). refresh() { this._renderItems(); } // ---- Private -------------------------------------------------------- _renderItems() { if (!this._list) return; this._list.innerHTML = ''; const visible = this._filter ? this._items.filter(i => i.label.toLowerCase().includes(this._filter)) : this._items; for (const item of visible) { this._list.appendChild(this._buildItem(item)); } } _buildItem(item) { const row = document.createElement('div'); row.className = 'mwse-item'; const icon = document.createElement('span'); icon.className = 'mwse-item__icon'; icon.textContent = item.icon ?? '○'; row.appendChild(icon); const body = document.createElement('div'); body.className = 'mwse-item__body'; const label = document.createElement('div'); label.className = 'mwse-item__label'; label.textContent = item.label; body.appendChild(label); if (item.meta !== undefined) { const meta = document.createElement('div'); meta.className = 'mwse-item__meta'; meta.textContent = typeof item.meta === 'function' ? item.meta() : item.meta; body.appendChild(meta); item._metaEl = meta; // for refresh() } row.appendChild(body); if (item.hasChildren !== false) { const arrow = document.createElement('span'); arrow.className = 'mwse-item__arrow'; arrow.textContent = '›'; row.appendChild(arrow); } row.addEventListener('click', () => { // Deactivate previous selection. if (this._active) this._active.classList.remove('mwse-item--active'); row.classList.add('mwse-item--active'); this._active = row; item.onSelect?.(item); }); return row; } }