テーブルのソート
This commit is contained in:
parent
fa47675790
commit
503f5a6e44
|
|
@ -67,6 +67,28 @@ const CSS = `
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ytpf-th--sortable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ytpf-th--sortable:hover {
|
||||
color: var(--yt-spec-text-primary);
|
||||
}
|
||||
|
||||
.ytpf-th--sortable::after {
|
||||
content: "";
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.ytpf-th[data-sort-dir="asc"]::after {
|
||||
content: " \\25B2";
|
||||
}
|
||||
|
||||
.ytpf-th[data-sort-dir="desc"]::after {
|
||||
content: " \\25BC";
|
||||
}
|
||||
|
||||
.ytpf-td {
|
||||
padding: 6px 12px;
|
||||
border-bottom: 1px solid var(--yt-spec-10-percent-layer);
|
||||
|
|
@ -84,12 +106,14 @@ const CSS = `
|
|||
.ytpf-col-title { width: auto; }
|
||||
.ytpf-col-channel { width: 180px; }
|
||||
.ytpf-col-duration { width: 80px; font-family: "Roboto Mono", monospace; }
|
||||
.ytpf-col-status { width: 60px; text-align: center; }
|
||||
|
||||
.ytpf-tr--unplayable {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.ytpf-tr--unplayable .ytpf-link {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.ytpf-link {
|
||||
color: var(--yt-spec-text-primary);
|
||||
text-decoration: none;
|
||||
|
|
|
|||
|
|
@ -1,66 +1,35 @@
|
|||
import type { PlaylistData } from "../../types/playlist";
|
||||
import type { PlaylistData, PlaylistVideo } from "../../types/playlist";
|
||||
|
||||
export function renderPlaylistTable(data: PlaylistData): HTMLElement {
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "ytpf-wrapper";
|
||||
type SortKey = "index" | "title" | "channel" | "duration";
|
||||
type SortDir = "asc" | "desc";
|
||||
|
||||
// Header bar
|
||||
const header = document.createElement("div");
|
||||
header.className = "ytpf-header";
|
||||
const columns: { label: string; cls: string; key: SortKey }[] = [
|
||||
{ label: "#", cls: "ytpf-col-index", key: "index" },
|
||||
{ label: "Title", cls: "ytpf-col-title", key: "title" },
|
||||
{ label: "Channel", cls: "ytpf-col-channel", key: "channel" },
|
||||
{ label: "Duration", cls: "ytpf-col-duration", key: "duration" },
|
||||
];
|
||||
|
||||
const titleSpan = document.createElement("span");
|
||||
titleSpan.className = "ytpf-header-title";
|
||||
titleSpan.textContent = `${data.metadata.title} — ${data.extractedCount} videos`;
|
||||
|
||||
const metaSpan = document.createElement("span");
|
||||
metaSpan.className = "ytpf-header-meta";
|
||||
if (data.metadata.totalDurationText) {
|
||||
metaSpan.textContent = data.metadata.totalDurationText;
|
||||
function compareFn(key: SortKey, dir: SortDir) {
|
||||
const m = dir === "asc" ? 1 : -1;
|
||||
return (a: PlaylistVideo, b: PlaylistVideo): number => {
|
||||
switch (key) {
|
||||
case "index":
|
||||
return (a.index - b.index) * m;
|
||||
case "title":
|
||||
return a.title.localeCompare(b.title) * m;
|
||||
case "channel":
|
||||
return a.channel.name.localeCompare(b.channel.name) * m;
|
||||
case "duration":
|
||||
return ((a.durationSeconds ?? -1) - (b.durationSeconds ?? -1)) * m;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const toggle = document.createElement("span");
|
||||
toggle.className = "ytpf-toggle";
|
||||
toggle.textContent = "\u25BC";
|
||||
|
||||
header.append(titleSpan, metaSpan, toggle);
|
||||
wrapper.appendChild(header);
|
||||
|
||||
// Body (table container)
|
||||
const body = document.createElement("div");
|
||||
body.className = "ytpf-body";
|
||||
|
||||
const table = document.createElement("table");
|
||||
table.className = "ytpf-table";
|
||||
|
||||
// Thead
|
||||
const thead = document.createElement("thead");
|
||||
const headRow = document.createElement("tr");
|
||||
const columns = [
|
||||
{ label: "#", cls: "ytpf-col-index" },
|
||||
{ label: "Title", cls: "ytpf-col-title" },
|
||||
{ label: "Channel", cls: "ytpf-col-channel" },
|
||||
{ label: "Duration", cls: "ytpf-col-duration" },
|
||||
{ label: "Status", cls: "ytpf-col-status" },
|
||||
];
|
||||
for (const col of columns) {
|
||||
const th = document.createElement("th");
|
||||
th.className = `ytpf-th ${col.cls}`;
|
||||
th.textContent = col.label;
|
||||
headRow.appendChild(th);
|
||||
}
|
||||
thead.appendChild(headRow);
|
||||
table.appendChild(thead);
|
||||
|
||||
// Tbody
|
||||
const tbody = document.createElement("tbody");
|
||||
const playlistId = data.metadata.playlistId;
|
||||
|
||||
for (const video of data.videos) {
|
||||
function buildRow(video: PlaylistVideo, playlistId: string): HTMLTableRowElement {
|
||||
const tr = document.createElement("tr");
|
||||
tr.className = "ytpf-tr";
|
||||
if (!video.isPlayable) {
|
||||
tr.classList.add("ytpf-tr--unplayable");
|
||||
}
|
||||
if (!video.isPlayable) tr.classList.add("ytpf-tr--unplayable");
|
||||
|
||||
// Index
|
||||
const tdIndex = document.createElement("td");
|
||||
|
|
@ -106,15 +75,105 @@ export function renderPlaylistTable(data: PlaylistData): HTMLElement {
|
|||
}
|
||||
tr.appendChild(tdDuration);
|
||||
|
||||
// Status
|
||||
const tdStatus = document.createElement("td");
|
||||
tdStatus.className = "ytpf-td ytpf-col-status";
|
||||
tdStatus.textContent = video.isPlayable ? "\u2713" : "\u2717";
|
||||
tr.appendChild(tdStatus);
|
||||
return tr;
|
||||
}
|
||||
|
||||
tbody.appendChild(tr);
|
||||
export function renderPlaylistTable(data: PlaylistData): HTMLElement {
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.className = "ytpf-wrapper";
|
||||
|
||||
// Header bar
|
||||
const header = document.createElement("div");
|
||||
header.className = "ytpf-header";
|
||||
|
||||
const titleSpan = document.createElement("span");
|
||||
titleSpan.className = "ytpf-header-title";
|
||||
titleSpan.textContent = `${data.metadata.title} — ${data.extractedCount} videos`;
|
||||
|
||||
const metaSpan = document.createElement("span");
|
||||
metaSpan.className = "ytpf-header-meta";
|
||||
if (data.metadata.totalDurationText) {
|
||||
metaSpan.textContent = data.metadata.totalDurationText;
|
||||
}
|
||||
|
||||
const toggle = document.createElement("span");
|
||||
toggle.className = "ytpf-toggle";
|
||||
toggle.textContent = "\u25BC";
|
||||
|
||||
header.append(titleSpan, metaSpan, toggle);
|
||||
wrapper.appendChild(header);
|
||||
|
||||
// Body (table container)
|
||||
const body = document.createElement("div");
|
||||
body.className = "ytpf-body";
|
||||
|
||||
const table = document.createElement("table");
|
||||
table.className = "ytpf-table";
|
||||
|
||||
// Thead
|
||||
const thead = document.createElement("thead");
|
||||
const headRow = document.createElement("tr");
|
||||
const thElements: HTMLTableCellElement[] = [];
|
||||
|
||||
for (const col of columns) {
|
||||
const th = document.createElement("th");
|
||||
th.className = `ytpf-th ytpf-th--sortable ${col.cls}`;
|
||||
th.dataset.sortKey = col.key;
|
||||
th.textContent = col.label;
|
||||
thElements.push(th);
|
||||
headRow.appendChild(th);
|
||||
}
|
||||
thead.appendChild(headRow);
|
||||
table.appendChild(thead);
|
||||
|
||||
// Tbody
|
||||
const tbody = document.createElement("tbody");
|
||||
const playlistId = data.metadata.playlistId;
|
||||
const videos = [...data.videos];
|
||||
|
||||
function renderRows() {
|
||||
tbody.textContent = "";
|
||||
for (const video of videos) {
|
||||
tbody.appendChild(buildRow(video, playlistId));
|
||||
}
|
||||
}
|
||||
|
||||
renderRows();
|
||||
|
||||
// Sort state
|
||||
let sortKey: SortKey | null = null;
|
||||
let sortDir: SortDir = "asc";
|
||||
|
||||
function updateSortIndicators() {
|
||||
for (const th of thElements) {
|
||||
const key = th.dataset.sortKey;
|
||||
if (key === sortKey) {
|
||||
th.dataset.sortDir = sortDir;
|
||||
} else {
|
||||
delete th.dataset.sortDir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
headRow.addEventListener("click", (e) => {
|
||||
const th = (e.target as HTMLElement).closest<HTMLTableCellElement>(
|
||||
"th[data-sort-key]",
|
||||
);
|
||||
if (!th) return;
|
||||
|
||||
const key = th.dataset.sortKey as SortKey;
|
||||
if (sortKey === key) {
|
||||
sortDir = sortDir === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
sortKey = key;
|
||||
sortDir = "asc";
|
||||
}
|
||||
|
||||
videos.sort(compareFn(sortKey, sortDir));
|
||||
updateSortIndicators();
|
||||
renderRows();
|
||||
});
|
||||
|
||||
table.appendChild(tbody);
|
||||
body.appendChild(table);
|
||||
wrapper.appendChild(body);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user