テーブルのソート
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,4 +1,82 @@
|
|||
import type { PlaylistData } from "../../types/playlist";
|
||||
import type { PlaylistData, PlaylistVideo } from "../../types/playlist";
|
||||
|
||||
type SortKey = "index" | "title" | "channel" | "duration";
|
||||
type SortDir = "asc" | "desc";
|
||||
|
||||
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" },
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
// Index
|
||||
const tdIndex = document.createElement("td");
|
||||
tdIndex.className = "ytpf-td ytpf-col-index";
|
||||
tdIndex.textContent = String(video.index + 1);
|
||||
tr.appendChild(tdIndex);
|
||||
|
||||
// Title
|
||||
const tdTitle = document.createElement("td");
|
||||
tdTitle.className = "ytpf-td ytpf-col-title";
|
||||
const titleLink = document.createElement("a");
|
||||
titleLink.className = "ytpf-link";
|
||||
titleLink.href = `/watch?v=${video.videoId}&list=${playlistId}`;
|
||||
titleLink.textContent = video.title;
|
||||
titleLink.title = video.title;
|
||||
tdTitle.appendChild(titleLink);
|
||||
tr.appendChild(tdTitle);
|
||||
|
||||
// Channel
|
||||
const tdChannel = document.createElement("td");
|
||||
tdChannel.className = "ytpf-td ytpf-col-channel";
|
||||
if (video.channel.url) {
|
||||
const chLink = document.createElement("a");
|
||||
chLink.className = "ytpf-link";
|
||||
chLink.href = video.channel.url;
|
||||
chLink.textContent = video.channel.name;
|
||||
tdChannel.appendChild(chLink);
|
||||
} else {
|
||||
tdChannel.textContent = video.channel.name;
|
||||
}
|
||||
tr.appendChild(tdChannel);
|
||||
|
||||
// Duration
|
||||
const tdDuration = document.createElement("td");
|
||||
tdDuration.className = "ytpf-td ytpf-col-duration";
|
||||
if (video.isLive) {
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "ytpf-live";
|
||||
badge.textContent = "LIVE";
|
||||
tdDuration.appendChild(badge);
|
||||
} else {
|
||||
tdDuration.textContent = video.durationText ?? "--";
|
||||
}
|
||||
tr.appendChild(tdDuration);
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
export function renderPlaylistTable(data: PlaylistData): HTMLElement {
|
||||
const wrapper = document.createElement("div");
|
||||
|
|
@ -35,17 +113,14 @@ export function renderPlaylistTable(data: PlaylistData): HTMLElement {
|
|||
// 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" },
|
||||
];
|
||||
const thElements: HTMLTableCellElement[] = [];
|
||||
|
||||
for (const col of columns) {
|
||||
const th = document.createElement("th");
|
||||
th.className = `ytpf-th ${col.cls}`;
|
||||
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);
|
||||
|
|
@ -54,67 +129,51 @@ export function renderPlaylistTable(data: PlaylistData): HTMLElement {
|
|||
// Tbody
|
||||
const tbody = document.createElement("tbody");
|
||||
const playlistId = data.metadata.playlistId;
|
||||
const videos = [...data.videos];
|
||||
|
||||
for (const video of data.videos) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.className = "ytpf-tr";
|
||||
if (!video.isPlayable) {
|
||||
tr.classList.add("ytpf-tr--unplayable");
|
||||
function renderRows() {
|
||||
tbody.textContent = "";
|
||||
for (const video of videos) {
|
||||
tbody.appendChild(buildRow(video, playlistId));
|
||||
}
|
||||
|
||||
// Index
|
||||
const tdIndex = document.createElement("td");
|
||||
tdIndex.className = "ytpf-td ytpf-col-index";
|
||||
tdIndex.textContent = String(video.index + 1);
|
||||
tr.appendChild(tdIndex);
|
||||
|
||||
// Title
|
||||
const tdTitle = document.createElement("td");
|
||||
tdTitle.className = "ytpf-td ytpf-col-title";
|
||||
const titleLink = document.createElement("a");
|
||||
titleLink.className = "ytpf-link";
|
||||
titleLink.href = `/watch?v=${video.videoId}&list=${playlistId}`;
|
||||
titleLink.textContent = video.title;
|
||||
titleLink.title = video.title;
|
||||
tdTitle.appendChild(titleLink);
|
||||
tr.appendChild(tdTitle);
|
||||
|
||||
// Channel
|
||||
const tdChannel = document.createElement("td");
|
||||
tdChannel.className = "ytpf-td ytpf-col-channel";
|
||||
if (video.channel.url) {
|
||||
const chLink = document.createElement("a");
|
||||
chLink.className = "ytpf-link";
|
||||
chLink.href = video.channel.url;
|
||||
chLink.textContent = video.channel.name;
|
||||
tdChannel.appendChild(chLink);
|
||||
} else {
|
||||
tdChannel.textContent = video.channel.name;
|
||||
}
|
||||
tr.appendChild(tdChannel);
|
||||
|
||||
// Duration
|
||||
const tdDuration = document.createElement("td");
|
||||
tdDuration.className = "ytpf-td ytpf-col-duration";
|
||||
if (video.isLive) {
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "ytpf-live";
|
||||
badge.textContent = "LIVE";
|
||||
tdDuration.appendChild(badge);
|
||||
} else {
|
||||
tdDuration.textContent = video.durationText ?? "--";
|
||||
}
|
||||
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);
|
||||
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
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