テーブルのソート
This commit is contained in:
parent
fa47675790
commit
503f5a6e44
|
|
@ -67,6 +67,28 @@ const CSS = `
|
||||||
white-space: nowrap;
|
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 {
|
.ytpf-td {
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border-bottom: 1px solid var(--yt-spec-10-percent-layer);
|
border-bottom: 1px solid var(--yt-spec-10-percent-layer);
|
||||||
|
|
@ -84,12 +106,14 @@ const CSS = `
|
||||||
.ytpf-col-title { width: auto; }
|
.ytpf-col-title { width: auto; }
|
||||||
.ytpf-col-channel { width: 180px; }
|
.ytpf-col-channel { width: 180px; }
|
||||||
.ytpf-col-duration { width: 80px; font-family: "Roboto Mono", monospace; }
|
.ytpf-col-duration { width: 80px; font-family: "Roboto Mono", monospace; }
|
||||||
.ytpf-col-status { width: 60px; text-align: center; }
|
|
||||||
|
|
||||||
.ytpf-tr--unplayable {
|
.ytpf-tr--unplayable {
|
||||||
opacity: 0.45;
|
opacity: 0.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ytpf-tr--unplayable .ytpf-link {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
.ytpf-link {
|
.ytpf-link {
|
||||||
color: var(--yt-spec-text-primary);
|
color: var(--yt-spec-text-primary);
|
||||||
text-decoration: none;
|
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 {
|
type SortKey = "index" | "title" | "channel" | "duration";
|
||||||
const wrapper = document.createElement("div");
|
type SortDir = "asc" | "desc";
|
||||||
wrapper.className = "ytpf-wrapper";
|
|
||||||
|
|
||||||
// Header bar
|
const columns: { label: string; cls: string; key: SortKey }[] = [
|
||||||
const header = document.createElement("div");
|
{ label: "#", cls: "ytpf-col-index", key: "index" },
|
||||||
header.className = "ytpf-header";
|
{ label: "Title", cls: "ytpf-col-title", key: "title" },
|
||||||
|
{ label: "Channel", cls: "ytpf-col-channel", key: "channel" },
|
||||||
const titleSpan = document.createElement("span");
|
{ label: "Duration", cls: "ytpf-col-duration", key: "duration" },
|
||||||
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 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");
|
function compareFn(key: SortKey, dir: SortDir) {
|
||||||
th.className = `ytpf-th ${col.cls}`;
|
const m = dir === "asc" ? 1 : -1;
|
||||||
th.textContent = col.label;
|
return (a: PlaylistVideo, b: PlaylistVideo): number => {
|
||||||
headRow.appendChild(th);
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
thead.appendChild(headRow);
|
|
||||||
table.appendChild(thead);
|
|
||||||
|
|
||||||
// Tbody
|
function buildRow(video: PlaylistVideo, playlistId: string): HTMLTableRowElement {
|
||||||
const tbody = document.createElement("tbody");
|
|
||||||
const playlistId = data.metadata.playlistId;
|
|
||||||
|
|
||||||
for (const video of data.videos) {
|
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
tr.className = "ytpf-tr";
|
tr.className = "ytpf-tr";
|
||||||
if (!video.isPlayable) {
|
if (!video.isPlayable) tr.classList.add("ytpf-tr--unplayable");
|
||||||
tr.classList.add("ytpf-tr--unplayable");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Index
|
// Index
|
||||||
const tdIndex = document.createElement("td");
|
const tdIndex = document.createElement("td");
|
||||||
|
|
@ -106,15 +75,105 @@ export function renderPlaylistTable(data: PlaylistData): HTMLElement {
|
||||||
}
|
}
|
||||||
tr.appendChild(tdDuration);
|
tr.appendChild(tdDuration);
|
||||||
|
|
||||||
// Status
|
return tr;
|
||||||
const tdStatus = document.createElement("td");
|
|
||||||
tdStatus.className = "ytpf-td ytpf-col-status";
|
|
||||||
tdStatus.textContent = video.isPlayable ? "\u2713" : "\u2717";
|
|
||||||
tr.appendChild(tdStatus);
|
|
||||||
|
|
||||||
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);
|
table.appendChild(tbody);
|
||||||
body.appendChild(table);
|
body.appendChild(table);
|
||||||
wrapper.appendChild(body);
|
wrapper.appendChild(body);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user