added basic sorting in track-list and cover-grid

This commit is contained in:
2026-02-22 17:16:01 -05:00
parent 8a2ceb9107
commit 30bbdd7c53
3 changed files with 935 additions and 20 deletions
@@ -61,6 +61,67 @@ const SCROLL_DEBOUNCE_MS = 100;
/** Pixels to change card width per scroll tick. */
const ZOOM_STEP = 16;
/** localStorage keys for sort preferences. */
const SORT_FIELD_KEY = 'cover-grid-sort-field';
const SORT_DIR_KEY = 'cover-grid-sort-direction';
/** Available sort fields for the album grid. */
type AlbumSortField = 'name' | 'artist' | 'year';
/** Sort option definition for the dropdown. */
interface AlbumSortOption {
id: AlbumSortField;
label: string;
comparator: (
a: library.Album,
b: library.Album,
) => number;
}
/** All available sort options for albums. */
const ALBUM_SORT_OPTIONS: AlbumSortOption[] = [
{
id: 'name',
label: 'Name',
comparator: (a, b) =>
a.Name.localeCompare(b.Name),
},
{
id: 'artist',
label: 'Artist',
comparator: (a, b) => {
const cmp = a.ArtistName.localeCompare(
b.ArtistName,
);
if (cmp !== 0) return cmp;
return a.Name.localeCompare(b.Name);
},
},
{
id: 'year',
label: 'Year',
comparator: (a, b) => {
// Albums without a year sort last.
if (!a.Year && !b.Year) {
return a.Name.localeCompare(b.Name);
}
if (!a.Year) return 1;
if (!b.Year) return -1;
const cmp = a.Year - b.Year;
if (cmp !== 0) return cmp;
return a.Name.localeCompare(b.Name);
},
},
];
type SortDirection = 'asc' | 'desc';
@customElement('cover-grid')
export class CoverGrid extends LitElement {
private libraryCtrl = new LibraryController(this);
@@ -186,14 +247,34 @@ export class CoverGrid extends LitElement {
const term =
this.searchCtrl.term.toLowerCase();
if (!term) return this.albums;
let albums: library.Album[];
return this.albums.filter(
(a) =>
a.Name.toLowerCase().includes(term) ||
a.ArtistName.toLowerCase().includes(
term,
),
if (!term) {
albums = this.albums;
} else {
albums = this.albums.filter(
(a) =>
a.Name.toLowerCase().includes(
term,
) ||
a.ArtistName.toLowerCase().includes(
term,
),
);
}
// Apply sort.
const opt = ALBUM_SORT_OPTIONS.find(
(o) => o.id === this.sortField,
);
if (!opt) return albums;
const dir =
this.sortDirection === 'asc' ? 1 : -1;
return [...albums].sort(
(a, b) => dir * opt.comparator(a, b),
);
}
@@ -215,6 +296,120 @@ export class CoverGrid extends LitElement {
position: relative;
}
/* ========================================
* Sort toolbar
* ======================================== */
.sort-toolbar {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
font-size: 12px;
color: var(
--yj-text-secondary,
#b3b3b3
);
border-bottom: 1px solid
var(--yj-border-subtle, #333);
flex-shrink: 0;
user-select: none;
}
.sort-anchor {
display: inline-flex;
align-items: center;
gap: 4px;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
background: transparent;
border: none;
color: inherit;
font: inherit;
}
.sort-anchor:hover {
background: var(
--yj-hover-overlay,
rgba(255, 255, 255, 0.05)
);
}
.sort-anchor .sort-label {
color: var(--yj-text-primary, #fff);
}
.sort-dir-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
cursor: pointer;
border: none;
border-radius: 4px;
background: transparent;
color: var(
--yj-text-secondary,
#b3b3b3
);
font-size: 12px;
padding: 0;
}
.sort-dir-btn:hover {
background: var(
--yj-hover-overlay,
rgba(255, 255, 255, 0.05)
);
color: var(--yj-text-primary, #fff);
}
.sort-dropdown-panel {
background-color: var(
--yj-bg-elevated,
#343a40
);
border: 1px solid
var(--yj-border, #444);
border-radius: 6px;
padding: 4px 0;
box-shadow: 0 8px 24px
rgba(0, 0, 0, 0.5);
min-width: 140px;
}
.sort-dropdown-panel wa-dropdown-item {
cursor: pointer;
--wa-color-text-normal: var(
--yj-text-primary,
#fff
);
font-size: 13px;
}
.sort-dropdown-panel
wa-dropdown-item:hover {
background-color: var(
--yj-hover-overlay,
rgba(255, 255, 255, 0.1)
);
}
.sort-dropdown-panel
wa-dropdown-item.active-sort {
color: var(--yj-accent, #ffd43b);
--wa-color-text-normal: var(
--yj-accent,
#ffd43b
);
}
#sort-dropdown {
z-index: 200;
}
.grid-scroll-container {
flex: 1;
position: relative;
@@ -438,6 +633,21 @@ export class CoverGrid extends LitElement {
@state()
private playlistFilePaths: string[] = [];
/** Current album sort field. */
@state()
private sortField: AlbumSortField = 'name';
/** Current sort direction. */
@state()
private sortDirection: SortDirection = 'asc';
/** Whether the sort dropdown popup is open. */
@state()
private sortDropdownOpen = false;
@query('#sort-dropdown')
private sortDropdownPopup!: HTMLElement;
/** ID of the album whose dropdown is currently open, or null. */
@state()
private expandedAlbumId: number | null = null;
@@ -531,12 +741,135 @@ export class CoverGrid extends LitElement {
private transitionOverlay: HTMLDivElement | null =
null;
/* ====================================================================
* Sort controls
* ==================================================================== */
/** Restore sort preferences from localStorage. */
private restoreSortPreferences() {
try {
const field = localStorage.getItem(
SORT_FIELD_KEY,
);
const dir =
localStorage.getItem(SORT_DIR_KEY);
if (
field &&
ALBUM_SORT_OPTIONS.some(
(o) => o.id === field,
)
) {
this.sortField =
field as AlbumSortField;
}
if (dir === 'asc' || dir === 'desc') {
this.sortDirection = dir;
}
} catch {
// Ignore storage errors.
}
}
/** Persist sort preferences to localStorage. */
private saveSortPreferences() {
try {
localStorage.setItem(
SORT_FIELD_KEY,
this.sortField,
);
localStorage.setItem(
SORT_DIR_KEY,
this.sortDirection,
);
} catch {
// Ignore storage errors.
}
}
/** Set the sort field from the dropdown. */
private onSortDropdownSelect(
field: AlbumSortField,
) {
this.sortField = field;
this.saveSortPreferences();
this.closeSortDropdown();
}
/** Toggle sort direction. */
private toggleSortDirection() {
this.sortDirection =
this.sortDirection === 'asc'
? 'desc'
: 'asc';
this.saveSortPreferences();
}
private toggleSortDropdown() {
if (this.sortDropdownOpen) {
this.closeSortDropdown();
} else {
this.openSortDropdown();
}
}
private async openSortDropdown() {
this.sortDropdownOpen = true;
await this.updateComplete;
const popup = this.sortDropdownPopup;
const anchor =
this.shadowRoot?.querySelector(
'.sort-anchor',
);
if (popup && anchor) {
(popup as any).anchor = anchor;
(popup as any).active = true;
}
}
private closeSortDropdown() {
if (!this.sortDropdownOpen) return;
this.sortDropdownOpen = false;
const popup = this.sortDropdownPopup;
if (popup) {
(popup as any).active = false;
}
}
private sortDropdownCloseHandler = (
e: MouseEvent,
) => {
if (!this.sortDropdownOpen) return;
const path = e.composedPath();
const popup = this.sortDropdownPopup;
if (popup && path.includes(popup)) return;
const anchor =
this.shadowRoot?.querySelector(
'.sort-anchor',
);
if (anchor && path.includes(anchor)) return;
this.closeSortDropdown();
};
/* ====================================================================
* Lifecycle
* ==================================================================== */
override connectedCallback() {
super.connectedCallback();
this.restoreSortPreferences();
this.loadAlbums();
this.cancelScanComplete = EventsOn(
Events.LibraryScanComplete,
@@ -554,6 +887,10 @@ export class CoverGrid extends LitElement {
'mousedown',
this.mousedownCloseHandler,
);
document.addEventListener(
'mousedown',
this.sortDropdownCloseHandler,
);
// error events do not bubble — use capture
// phase to catch <img> load failures.
@@ -579,6 +916,10 @@ export class CoverGrid extends LitElement {
'mousedown',
this.mousedownCloseHandler,
);
document.removeEventListener(
'mousedown',
this.sortDropdownCloseHandler,
);
this.removeEventListener(
'error',
this.onGridImageError,
@@ -2764,6 +3105,90 @@ export class CoverGrid extends LitElement {
this.closeContextMenu();
};
/* ====================================================================
* Render: sort toolbar
* ==================================================================== */
/** Render the sort toolbar above the grid. */
private renderSortToolbar() {
const activeOpt = ALBUM_SORT_OPTIONS.find(
(o) => o.id === this.sortField,
);
const label = activeOpt
? activeOpt.label
: 'Name';
const dirIcon =
this.sortDirection === 'asc'
? 'arrow-up-short-wide'
: 'arrow-down-wide-short';
return html`
<div class="sort-toolbar">
<span>Sort:</span>
<button
class="sort-anchor"
@click=${() =>
this.toggleSortDropdown()}
>
<span class="sort-label">
${label}
</span>
<wa-icon
name="chevron-down"
></wa-icon>
</button>
<button
class="sort-dir-btn"
title="${this.sortDirection === 'asc' ? 'Ascending' : 'Descending'}"
@click=${() =>
this.toggleSortDirection()}
>
<wa-icon
name=${dirIcon}
></wa-icon>
</button>
</div>
${this.renderSortDropdownPopup()}
`;
}
/** Render the sort dropdown popup. */
private renderSortDropdownPopup() {
return html`
<wa-popup
id="sort-dropdown"
placement="bottom-start"
flip
shift
.active=${this.sortDropdownOpen}
>
${this.sortDropdownOpen
? html`
<div
class="sort-dropdown-panel"
>
${ALBUM_SORT_OPTIONS.map(
(opt) => html`
<wa-dropdown-item
class=${this.sortField === opt.id ? 'active-sort' : ''}
@click=${() =>
this.onSortDropdownSelect(
opt.id,
)}
>
${opt.label}
</wa-dropdown-item>
`,
)}
</div>
`
: nothing}
</wa-popup>
`;
}
/* ====================================================================
* Rendering helpers
* ==================================================================== */
@@ -2934,6 +3359,7 @@ export class CoverGrid extends LitElement {
: this.renderSingleGrid();
return html`
${this.renderSortToolbar()}
${this.searchCtrl.term
? html`<div class="search-indicator">
Showing results for
@@ -1,6 +1,21 @@
import type { library } from '@go/models';
import { formatMilliseconds } from '@utils/time';
/** Compares two strings using locale-aware ordering. */
const compareStr = (
a: string,
b: string,
): number => a.localeCompare(b);
/** Compares two numbers, treating 0 as "empty" (sorted last). */
const compareNum = (a: number, b: number): number => {
if (!a && !b) return 0;
if (!a) return 1;
if (!b) return -1;
return a - b;
};
/** Definition for a single displayable column. */
export interface ColumnDef {
/** Unique identifier matching the backend ColumnID. */
@@ -13,6 +28,15 @@ export interface ColumnDef {
defaultWidth: string;
/** Text alignment. Defaults to left. */
align?: 'left' | 'right';
/**
* Comparison function for sorting two tracks by this column.
* Returns negative if a < b, positive if a > b, zero if equal.
* If omitted the column is not sortable.
*/
comparator?: (
a: library.Track,
b: library.Track,
) => number;
}
/** Registry of every available column keyed by ID. */
@@ -22,30 +46,43 @@ export const COLUMN_DEFS: Record<string, ColumnDef> = {
label: 'Track Name',
accessor: (t) => t.TrackName,
defaultWidth: '1fr',
comparator: (a, b) =>
compareStr(a.TrackName, b.TrackName),
},
artistName: {
id: 'artistName',
label: 'Artist',
accessor: (t) => t.ArtistName,
defaultWidth: '1fr',
comparator: (a, b) =>
compareStr(a.ArtistName, b.ArtistName),
},
trackLength: {
id: 'trackLength',
label: 'Duration',
accessor: (t) => formatMilliseconds(t.TrackLength),
defaultWidth: '80px',
comparator: (a, b) =>
Number(a.TrackLength) - Number(b.TrackLength),
},
album: {
id: 'album',
label: 'Album',
accessor: (t) => t.Album,
defaultWidth: '1fr',
comparator: (a, b) =>
compareStr(a.Album, b.Album),
},
genre: {
id: 'genre',
label: 'Genre',
accessor: (t) => (t.Genre ?? []).join(', '),
defaultWidth: '120px',
comparator: (a, b) =>
compareStr(
(a.Genre ?? []).join(', '),
(b.Genre ?? []).join(', '),
),
},
year: {
id: 'year',
@@ -53,12 +90,16 @@ export const COLUMN_DEFS: Record<string, ColumnDef> = {
accessor: (t) =>
t.Year ? String(t.Year) : '',
defaultWidth: '60px',
comparator: (a, b) =>
compareNum(a.Year, b.Year),
},
composer: {
id: 'composer',
label: 'Composer',
accessor: (t) => t.Composer,
defaultWidth: '1fr',
comparator: (a, b) =>
compareStr(a.Composer, b.Composer),
},
trackNumber: {
id: 'trackNumber',
@@ -66,6 +107,8 @@ export const COLUMN_DEFS: Record<string, ColumnDef> = {
accessor: (t) =>
t.TrackNumber ? String(t.TrackNumber) : '',
defaultWidth: '60px',
comparator: (a, b) =>
compareNum(a.TrackNumber, b.TrackNumber),
},
discNumber: {
id: 'discNumber',
@@ -73,18 +116,24 @@ export const COLUMN_DEFS: Record<string, ColumnDef> = {
accessor: (t) =>
t.DiscNumber ? String(t.DiscNumber) : '',
defaultWidth: '60px',
comparator: (a, b) =>
compareNum(a.DiscNumber, b.DiscNumber),
},
filePath: {
id: 'filePath',
label: 'File Path',
accessor: (t) => t.FilePath,
defaultWidth: '1fr',
comparator: (a, b) =>
compareStr(a.FilePath, b.FilePath),
},
fileType: {
id: 'fileType',
label: 'File Type',
accessor: (t) => t.FileType,
defaultWidth: '80px',
comparator: (a, b) =>
compareStr(a.FileType, b.FileType),
},
};
+453 -13
View File
@@ -36,9 +36,13 @@ import '@components/playlist-picker/playlist-picker.js';
import type { PlaylistPicker } from '@components/playlist-picker/playlist-picker.js';
const COLUMN_STORAGE_KEY = 'track-list-column-widths';
const SORT_FIELD_KEY = 'track-list-sort-field';
const SORT_DIR_KEY = 'track-list-sort-direction';
const MIN_COLUMN_WIDTH = 50;
const DEFAULT_FIXED_WIDTH = 80;
type SortDirection = 'asc' | 'desc';
@customElement('track-list')
export class TrackList extends LitElement implements SelectionHost {
private player = new PlayerController(this);
@@ -127,6 +131,21 @@ export class TrackList extends LitElement implements SelectionHost {
@state()
private columnWidths: number[] = [];
/** Column ID to sort by, or null for default order. */
@state()
private sortField: string | null = null;
/** Current sort direction. */
@state()
private sortDirection: SortDirection = 'asc';
/** Whether the sort dropdown popup is open. */
@state()
private sortDropdownOpen = false;
@query('#sort-dropdown')
private sortDropdownPopup!: HTMLElement;
private resizingColumn: number | null = null;
private resizeStartX = 0;
private resizeStartWidths: number[] = [];
@@ -157,16 +176,37 @@ export class TrackList extends LitElement implements SelectionHost {
);
}
// =================================================================
// Sorted tracks
// =================================================================
private get sortedTracks(): library.Track[] {
const tracks = this.filteredTracks;
if (!this.sortField) return tracks;
const col = COLUMN_DEFS[this.sortField];
if (!col?.comparator) return tracks;
const dir =
this.sortDirection === 'asc' ? 1 : -1;
return [...tracks].sort(
(a, b) => dir * col.comparator!(a, b),
);
}
// =================================================================
// SelectionHost interface
// =================================================================
getItemKey(index: number): string | undefined {
return this.filteredTracks[index]?.FilePath;
return this.sortedTracks[index]?.FilePath;
}
getItemCount(): number {
return this.filteredTracks.length;
return this.sortedTracks.length;
}
onSelectionChanged(): void {
@@ -564,19 +604,131 @@ export class TrackList extends LitElement implements SelectionHost {
static override styles = css`
:host {
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
}
.table-container {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ---- Sort toolbar ---- */
.sort-toolbar {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
font-size: 12px;
color: var(--yj-text-secondary, #b3b3b3);
border-bottom: 1px solid
var(--yj-border-subtle, #333);
flex-shrink: 0;
user-select: none;
}
.sort-anchor {
display: inline-flex;
align-items: center;
gap: 4px;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
background: transparent;
border: none;
color: inherit;
font: inherit;
}
.sort-anchor:hover {
background: var(
--yj-hover-overlay,
rgba(255, 255, 255, 0.05)
);
}
.sort-anchor .sort-label {
color: var(--yj-text-primary, #fff);
}
.sort-dir-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
cursor: pointer;
border: none;
border-radius: 4px;
background: transparent;
color: var(--yj-text-secondary, #b3b3b3);
font-size: 12px;
padding: 0;
}
.sort-dir-btn:hover {
background: var(
--yj-hover-overlay,
rgba(255, 255, 255, 0.05)
);
color: var(--yj-text-primary, #fff);
}
.sort-dropdown-panel {
background-color: var(
--yj-bg-elevated,
#343a40
);
border: 1px solid var(--yj-border, #444);
border-radius: 6px;
padding: 4px 0;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
min-width: 140px;
}
.sort-dropdown-panel wa-dropdown-item {
cursor: pointer;
--wa-color-text-normal: var(
--yj-text-primary,
#fff
);
font-size: 13px;
}
.sort-dropdown-panel wa-dropdown-item:hover {
background-color: var(
--yj-hover-overlay,
rgba(255, 255, 255, 0.1)
);
}
.sort-dropdown-panel wa-dropdown-item.active-sort {
color: var(--yj-accent, #ffd43b);
--wa-color-text-normal: var(
--yj-accent,
#ffd43b
);
}
#sort-dropdown {
z-index: 200;
}
/* ---- Header row ---- */
.header-row {
display: grid;
grid-template-columns: var(--grid-cols);
padding: 8px;
font-weight: bold;
color: var(--yj-text-primary, #fff);
border-bottom: 1px solid var(--yj-text-tertiary, #666);
border-bottom: 1px solid
var(--yj-text-tertiary, #666);
flex-shrink: 0;
overflow: hidden;
}
@@ -585,6 +737,20 @@ export class TrackList extends LitElement implements SelectionHost {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
}
.header-cell:hover {
color: var(--yj-accent, #ffd43b);
}
.sort-arrow {
font-size: 10px;
flex-shrink: 0;
color: var(--yj-accent, #ffd43b);
}
.resize-overlay {
@@ -746,6 +912,7 @@ export class TrackList extends LitElement implements SelectionHost {
override connectedCallback() {
super.connectedCallback();
this.restoreSortPreferences();
this.loadTracks();
this.cancelScanComplete = EventsOn(
Events.LibraryScanComplete,
@@ -754,6 +921,7 @@ export class TrackList extends LitElement implements SelectionHost {
document.addEventListener('click', this.closeHandler);
document.addEventListener('contextmenu', this.closeHandler);
document.addEventListener('mousedown', this.mousedownCloseHandler);
document.addEventListener('mousedown', this.sortDropdownCloseHandler);
document.addEventListener('click', this.clearSelectionHandler);
document.addEventListener('mousemove', this.onColResizeMove);
document.addEventListener('mouseup', this.onColResizeEnd);
@@ -778,6 +946,7 @@ export class TrackList extends LitElement implements SelectionHost {
document.removeEventListener('click', this.closeHandler);
document.removeEventListener('contextmenu', this.closeHandler);
document.removeEventListener('mousedown', this.mousedownCloseHandler);
document.removeEventListener('mousedown', this.sortDropdownCloseHandler);
document.removeEventListener('click', this.clearSelectionHandler);
document.removeEventListener('mousemove', this.onColResizeMove);
document.removeEventListener('mouseup', this.onColResizeEnd);
@@ -1062,6 +1231,161 @@ export class TrackList extends LitElement implements SelectionHost {
this.closeContextMenu(true);
};
// =================================================================
// Sort controls
// =================================================================
/** Restore sort preferences from localStorage. */
private restoreSortPreferences() {
try {
const field =
localStorage.getItem(SORT_FIELD_KEY);
const dir =
localStorage.getItem(SORT_DIR_KEY);
if (
field &&
COLUMN_DEFS[field]?.comparator
) {
this.sortField = field;
}
if (dir === 'asc' || dir === 'desc') {
this.sortDirection = dir;
}
} catch {
// Ignore storage errors.
}
}
/** Persist sort preferences to localStorage. */
private saveSortPreferences() {
try {
if (this.sortField) {
localStorage.setItem(
SORT_FIELD_KEY,
this.sortField,
);
} else {
localStorage.removeItem(
SORT_FIELD_KEY,
);
}
localStorage.setItem(
SORT_DIR_KEY,
this.sortDirection,
);
} catch {
// Ignore storage errors.
}
}
/**
* Handle a click on a column header to toggle sorting.
* First click: sort ascending. Second: descending.
* Third: clear sort (back to default order).
*/
private onHeaderCellClick(colId: string) {
const col = COLUMN_DEFS[colId];
if (!col?.comparator) return;
if (this.sortField === colId) {
if (this.sortDirection === 'asc') {
this.sortDirection = 'desc';
} else {
this.sortField = null;
this.sortDirection = 'asc';
}
} else {
this.sortField = colId;
this.sortDirection = 'asc';
}
this.saveSortPreferences();
}
/** Set sort from the dropdown and close it. */
private onSortDropdownSelect(
colId: string | null,
) {
if (colId === null) {
this.sortField = null;
this.sortDirection = 'asc';
} else {
this.sortField = colId;
}
this.saveSortPreferences();
this.closeSortDropdown();
}
/** Toggle sort direction via the toolbar button. */
private toggleSortDirection() {
this.sortDirection =
this.sortDirection === 'asc'
? 'desc'
: 'asc';
this.saveSortPreferences();
}
private toggleSortDropdown() {
if (this.sortDropdownOpen) {
this.closeSortDropdown();
} else {
this.openSortDropdown();
}
}
private async openSortDropdown() {
this.sortDropdownOpen = true;
await this.updateComplete;
const popup = this.sortDropdownPopup;
const anchor = this.shadowRoot?.querySelector(
'.sort-anchor',
);
if (popup && anchor) {
(popup as any).anchor = anchor;
(popup as any).active = true;
}
}
private closeSortDropdown() {
if (!this.sortDropdownOpen) return;
this.sortDropdownOpen = false;
const popup = this.sortDropdownPopup;
if (popup) {
(popup as any).active = false;
}
}
private sortDropdownCloseHandler = (
e: MouseEvent,
) => {
if (!this.sortDropdownOpen) return;
const path = e.composedPath();
const popup = this.sortDropdownPopup;
if (popup && path.includes(popup)) return;
const anchor =
this.shadowRoot?.querySelector(
'.sort-anchor',
);
if (anchor && path.includes(anchor)) return;
this.closeSortDropdown();
};
private isActiveTrack(track: library.Track): boolean {
const currentTrack = this.player.currentTrack;
@@ -1114,19 +1438,134 @@ export class TrackList extends LitElement implements SelectionHost {
`;
};
/** Render the sort toolbar above the header row. */
private renderSortToolbar() {
const activeCol = this.sortField
? COLUMN_DEFS[this.sortField]
: null;
const label = activeCol
? activeCol.label
: 'Default';
const dirIcon =
this.sortDirection === 'asc'
? 'arrow-up-short-wide'
: 'arrow-down-wide-short';
return html`
<div class="sort-toolbar">
<span>Sort:</span>
<button
class="sort-anchor"
@click=${() =>
this.toggleSortDropdown()}
>
<span class="sort-label">
${label}
</span>
<wa-icon
name="chevron-down"
></wa-icon>
</button>
${this.sortField
? html`
<button
class="sort-dir-btn"
title="${this.sortDirection === 'asc' ? 'Ascending' : 'Descending'}"
@click=${() =>
this.toggleSortDirection()}
>
<wa-icon
name=${dirIcon}
></wa-icon>
</button>
`
: nothing}
</div>
${this.renderSortDropdownPopup()}
`;
}
/** Render the sort dropdown popup. */
private renderSortDropdownPopup() {
const cols = this.activeColumns;
return html`
<wa-popup
id="sort-dropdown"
placement="bottom-start"
flip
shift
.active=${this.sortDropdownOpen}
>
${this.sortDropdownOpen
? html`
<div
class="sort-dropdown-panel"
>
<wa-dropdown-item
class=${!this.sortField ? 'active-sort' : ''}
@click=${() =>
this.onSortDropdownSelect(
null,
)}
>
Default
</wa-dropdown-item>
${cols
.filter(
(c) =>
c.comparator,
)
.map(
(col) => html`
<wa-dropdown-item
class=${this.sortField === col.id ? 'active-sort' : ''}
@click=${() =>
this.onSortDropdownSelect(
col.id,
)}
>
${col.label}
</wa-dropdown-item>
`,
)}
</div>
`
: nothing}
</wa-popup>
`;
}
override render() {
const visibleTracks = this.filteredTracks;
const visibleTracks = this.sortedTracks;
const cols = this.activeColumns;
return html`
${this.tracks.length === 0
? html`<p>Loading tracks...</p>`
: html`
${this.renderSortToolbar()}
<div class="table-container">
<div class="header-row">
${cols.map(
(col) => html`
<div class="header-cell ${col.align === 'right' ? 'cell-right' : ''}">
${col.label}
<div
class="header-cell ${col.align === 'right' ? 'cell-right' : ''}"
@click=${() =>
this.onHeaderCellClick(
col.id,
)}
>
<span>${col.label}</span>
${this.sortField === col.id
? html`<span
class="sort-arrow"
>
${this.sortDirection === 'asc' ? '\u25B2' : '\u25BC'}
</span>`
: nothing}
</div>
`,
)}
@@ -1143,27 +1582,28 @@ export class TrackList extends LitElement implements SelectionHost {
.layout=${this.flowLayout}
></lit-virtualizer>
`}
`}
${this.searchCtrl.term && visibleTracks.length > 0
? html`<div class="search-indicator">
? html`<div class="search-indicator">
Showing results for
&ldquo;${this.searchCtrl.term}&rdquo;
</div>`
: nothing}
: nothing}
<div class="resize-overlay">
${this.colBoundaryPositions.map(
(pos, i) => html`
(pos, i) => html`
<div
class="col-resize-handle ${this.resizingColumn === i ? 'active' : ''}"
style="left: ${pos}px"
@mousedown=${(e: MouseEvent) =>
this.onColResizeStart(e, i)}
this.onColResizeStart(e, i)}
></div>
`,
)}
)}
</div>
</div>
`}
<wa-popup
id="context-menu"