feat: add market row selection column
This commit is contained in:
parent
45e5bb781b
commit
91d8347b76
@ -11,6 +11,7 @@ import type {
|
|||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const BACKEND_COLUMN_KEY = "backendMetrics";
|
const BACKEND_COLUMN_KEY = "backendMetrics";
|
||||||
|
const SELECTION_COLUMN_KEY = "selection";
|
||||||
const SINGLE_COLUMN_KEY = "singleVideoAfterSearchRate";
|
const SINGLE_COLUMN_KEY = "singleVideoAfterSearchRate";
|
||||||
const PERSONAL_COLUMN_KEY = "personalVideoAfterSearchRate";
|
const PERSONAL_COLUMN_KEY = "personalVideoAfterSearchRate";
|
||||||
const ACTION_HEADER_TEXT = "操作";
|
const ACTION_HEADER_TEXT = "操作";
|
||||||
@ -86,12 +87,14 @@ export interface MarketRowDom {
|
|||||||
price21To60s?: string;
|
price21To60s?: string;
|
||||||
rates?: AfterSearchRates;
|
rates?: AfterSearchRates;
|
||||||
row: HTMLElement;
|
row: HTMLElement;
|
||||||
|
selectionCheckbox: HTMLInputElement;
|
||||||
singleCell: HTMLElement;
|
singleCell: HTMLElement;
|
||||||
visibilityTargets: HTMLElement[];
|
visibilityTargets: HTMLElement[];
|
||||||
orderTargets: RowOrderTarget[];
|
orderTargets: RowOrderTarget[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MarketTableDom {
|
export interface MarketTableDom {
|
||||||
|
headerSelectionCheckbox: HTMLInputElement | null;
|
||||||
rows: MarketRowDom[];
|
rows: MarketRowDom[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,6 +266,8 @@ function syncSyntheticMarketTable(root: ParentNode): MarketTableDom | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectionHeader = ensureSyntheticHeaderCell(header, SELECTION_COLUMN_KEY, "");
|
||||||
|
const headerSelectionCheckbox = ensureSelectionHeaderControl(selectionHeader);
|
||||||
ensureSyntheticHeaderCell(header, SINGLE_COLUMN_KEY, "单视频看后搜率");
|
ensureSyntheticHeaderCell(header, SINGLE_COLUMN_KEY, "单视频看后搜率");
|
||||||
ensureSyntheticHeaderCell(header, PERSONAL_COLUMN_KEY, "个人视频看后搜率");
|
ensureSyntheticHeaderCell(header, PERSONAL_COLUMN_KEY, "个人视频看后搜率");
|
||||||
BACKEND_METRIC_COLUMNS.forEach(({ field, label }) => {
|
BACKEND_METRIC_COLUMNS.forEach(({ field, label }) => {
|
||||||
@ -273,6 +278,8 @@ function syncSyntheticMarketTable(root: ParentNode): MarketTableDom | null {
|
|||||||
const rows = Array.from(body.querySelectorAll("[data-market-row]")).map(
|
const rows = Array.from(body.querySelectorAll("[data-market-row]")).map(
|
||||||
(rowElement) => {
|
(rowElement) => {
|
||||||
const row = rowElement as HTMLElement;
|
const row = rowElement as HTMLElement;
|
||||||
|
const selectionCell = ensureSyntheticRowCell(row, SELECTION_COLUMN_KEY);
|
||||||
|
const selectionCheckbox = ensureSelectionRowControl(selectionCell);
|
||||||
const singleCell = ensureSyntheticRowCell(row, SINGLE_COLUMN_KEY);
|
const singleCell = ensureSyntheticRowCell(row, SINGLE_COLUMN_KEY);
|
||||||
const personalCell = ensureSyntheticRowCell(row, PERSONAL_COLUMN_KEY);
|
const personalCell = ensureSyntheticRowCell(row, PERSONAL_COLUMN_KEY);
|
||||||
const backendMetricsCells = Object.fromEntries(
|
const backendMetricsCells = Object.fromEntries(
|
||||||
@ -303,6 +310,7 @@ function syncSyntheticMarketTable(root: ParentNode): MarketTableDom | null {
|
|||||||
?.textContent?.trim() ?? "",
|
?.textContent?.trim() ?? "",
|
||||||
rates: undefined,
|
rates: undefined,
|
||||||
row,
|
row,
|
||||||
|
selectionCheckbox,
|
||||||
singleCell,
|
singleCell,
|
||||||
visibilityTargets: [row]
|
visibilityTargets: [row]
|
||||||
} satisfies MarketRowDom;
|
} satisfies MarketRowDom;
|
||||||
@ -310,6 +318,7 @@ function syncSyntheticMarketTable(root: ParentNode): MarketTableDom | null {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
headerSelectionCheckbox,
|
||||||
rows
|
rows
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -435,12 +444,16 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
|
|||||||
bodySection,
|
bodySection,
|
||||||
getDirectChildIndex(headerSection, authorHeader)
|
getDirectChildIndex(headerSection, authorHeader)
|
||||||
);
|
);
|
||||||
|
const authorHeaderSection = getIndexedChild(
|
||||||
|
headerSection,
|
||||||
|
getDirectChildIndex(headerSection, authorHeader)
|
||||||
|
);
|
||||||
const rightSection = getIndexedChild(
|
const rightSection = getIndexedChild(
|
||||||
bodySection,
|
bodySection,
|
||||||
getDirectChildIndex(headerSection, actionHeader)
|
getDirectChildIndex(headerSection, actionHeader)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!authorSection || !rightSection) {
|
if (!authorSection || !authorHeaderSection || !rightSection) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const middleBodySection = findPreviousNativeSection(rightSection) ?? rightSection;
|
const middleBodySection = findPreviousNativeSection(rightSection) ?? rightSection;
|
||||||
@ -462,6 +475,19 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rowCount = getDirectContentCells(authorColumn).length;
|
const rowCount = getDirectContentCells(authorColumn).length;
|
||||||
|
const selectionHeaderCell = ensureDivHeaderCell(
|
||||||
|
authorHeaderSection,
|
||||||
|
authorHeader,
|
||||||
|
SELECTION_COLUMN_KEY,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
const headerSelectionCheckbox = ensureSelectionHeaderControl(selectionHeaderCell);
|
||||||
|
const selectionColumn = ensureDivBodyColumn(
|
||||||
|
authorSection,
|
||||||
|
authorColumn,
|
||||||
|
SELECTION_COLUMN_KEY,
|
||||||
|
rowCount
|
||||||
|
);
|
||||||
const headerTemplateCell =
|
const headerTemplateCell =
|
||||||
getDirectHeaderCells(middleHeaderSection).at(-1) ??
|
getDirectHeaderCells(middleHeaderSection).at(-1) ??
|
||||||
findPreviousHeaderCell(actionHeader) ??
|
findPreviousHeaderCell(actionHeader) ??
|
||||||
@ -510,6 +536,8 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
|
|||||||
) as Record<BackendMetricField, HTMLElement>;
|
) as Record<BackendMetricField, HTMLElement>;
|
||||||
syncContainerWidth(pluginHeaderSection);
|
syncContainerWidth(pluginHeaderSection);
|
||||||
syncContainerWidth(pluginBodySection);
|
syncContainerWidth(pluginBodySection);
|
||||||
|
syncContainerWidth(authorHeaderSection);
|
||||||
|
syncContainerWidth(authorSection);
|
||||||
ensureVisibleHorizontalScroll(headerSection);
|
ensureVisibleHorizontalScroll(headerSection);
|
||||||
ensureVisibleHorizontalScroll(bodySection);
|
ensureVisibleHorizontalScroll(bodySection);
|
||||||
ensureScrollHint(root, headerSection);
|
ensureScrollHint(root, headerSection);
|
||||||
@ -525,6 +553,7 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
|
|||||||
: []
|
: []
|
||||||
);
|
);
|
||||||
const authorCells = getDirectContentCells(authorColumn);
|
const authorCells = getDirectContentCells(authorColumn);
|
||||||
|
const selectionCells = getDirectContentCells(selectionColumn);
|
||||||
const singleCells = getDirectContentCells(singleColumn);
|
const singleCells = getDirectContentCells(singleColumn);
|
||||||
const personalCells = getDirectContentCells(personalColumn);
|
const personalCells = getDirectContentCells(personalColumn);
|
||||||
const backendMetricCellsByField = Object.fromEntries(
|
const backendMetricCellsByField = Object.fromEntries(
|
||||||
@ -539,6 +568,7 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
|
|||||||
const remainingSerializedMarketRows = [...readSerializedMarketRows(root.ownerDocument)];
|
const remainingSerializedMarketRows = [...readSerializedMarketRows(root.ownerDocument)];
|
||||||
|
|
||||||
const rows = authorCells.flatMap((authorCell, index) => {
|
const rows = authorCells.flatMap((authorCell, index) => {
|
||||||
|
const selectionCell = selectionCells[index] ?? null;
|
||||||
const singleCell = singleCells[index] ?? null;
|
const singleCell = singleCells[index] ?? null;
|
||||||
const personalCell = personalCells[index] ?? null;
|
const personalCell = personalCells[index] ?? null;
|
||||||
const backendMetricsCells = Object.fromEntries(
|
const backendMetricsCells = Object.fromEntries(
|
||||||
@ -548,12 +578,14 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
|
|||||||
])
|
])
|
||||||
) as Record<BackendMetricField, HTMLElement | null>;
|
) as Record<BackendMetricField, HTMLElement | null>;
|
||||||
if (
|
if (
|
||||||
|
!selectionCell ||
|
||||||
!singleCell ||
|
!singleCell ||
|
||||||
!personalCell ||
|
!personalCell ||
|
||||||
Object.values(backendMetricsCells).some((cell) => cell === null)
|
Object.values(backendMetricsCells).some((cell) => cell === null)
|
||||||
) {
|
) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
const selectionCheckbox = ensureSelectionRowControl(selectionCell);
|
||||||
|
|
||||||
const alignedRowCells = allBodyColumns.map(
|
const alignedRowCells = allBodyColumns.map(
|
||||||
(column) => getDirectContentCells(column)[index] ?? null
|
(column) => getDirectContentCells(column)[index] ?? null
|
||||||
@ -612,6 +644,7 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
|
|||||||
price21To60s,
|
price21To60s,
|
||||||
rates: fallbackMarketRow?.rates,
|
rates: fallbackMarketRow?.rates,
|
||||||
row: authorCell,
|
row: authorCell,
|
||||||
|
selectionCheckbox,
|
||||||
singleCell,
|
singleCell,
|
||||||
visibilityTargets: rowCells
|
visibilityTargets: rowCells
|
||||||
} satisfies MarketRowDom
|
} satisfies MarketRowDom
|
||||||
@ -619,6 +652,7 @@ function syncDivGridRoot(root: HTMLElement): MarketTableDom | null {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
headerSelectionCheckbox,
|
||||||
rows
|
rows
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -640,7 +674,11 @@ function ensureSyntheticHeaderCell(
|
|||||||
const nextCell = header.ownerDocument.createElement("div");
|
const nextCell = header.ownerDocument.createElement("div");
|
||||||
nextCell.dataset.marketHeaderCell = field;
|
nextCell.dataset.marketHeaderCell = field;
|
||||||
nextCell.textContent = label;
|
nextCell.textContent = label;
|
||||||
header.appendChild(nextCell);
|
if (field === SELECTION_COLUMN_KEY) {
|
||||||
|
header.insertBefore(nextCell, header.firstChild);
|
||||||
|
} else {
|
||||||
|
header.appendChild(nextCell);
|
||||||
|
}
|
||||||
return nextCell;
|
return nextCell;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -655,7 +693,11 @@ function ensureSyntheticRowCell(row: HTMLElement, field: string): HTMLElement {
|
|||||||
|
|
||||||
const nextCell = row.ownerDocument.createElement(field === BACKEND_COLUMN_KEY ? "div" : "span");
|
const nextCell = row.ownerDocument.createElement(field === BACKEND_COLUMN_KEY ? "div" : "span");
|
||||||
nextCell.dataset.marketRowCell = field;
|
nextCell.dataset.marketRowCell = field;
|
||||||
row.appendChild(nextCell);
|
if (field === SELECTION_COLUMN_KEY) {
|
||||||
|
row.insertBefore(nextCell, row.firstChild);
|
||||||
|
} else {
|
||||||
|
row.appendChild(nextCell);
|
||||||
|
}
|
||||||
return nextCell;
|
return nextCell;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -727,9 +769,12 @@ function syncDivColumnCells(
|
|||||||
|
|
||||||
const templateCell =
|
const templateCell =
|
||||||
templateCells[index] ?? templateCells[templateCells.length - 1] ?? null;
|
templateCells[index] ?? templateCells[templateCells.length - 1] ?? null;
|
||||||
const nextCell = templateCell
|
const nextCell =
|
||||||
? cloneElementShallow(templateCell)
|
field === SELECTION_COLUMN_KEY
|
||||||
: createBareContentCell(column.ownerDocument);
|
? createBareContentCell(column.ownerDocument)
|
||||||
|
: templateCell
|
||||||
|
? cloneElementShallow(templateCell)
|
||||||
|
: createBareContentCell(column.ownerDocument);
|
||||||
nextCell.dataset.marketRowCell = field;
|
nextCell.dataset.marketRowCell = field;
|
||||||
applyColumnWidth(nextCell, field);
|
applyColumnWidth(nextCell, field);
|
||||||
applyPluginContentCellStyles(nextCell);
|
applyPluginContentCellStyles(nextCell);
|
||||||
@ -756,6 +801,54 @@ function applyPluginContentCellStyles(cell: HTMLElement): void {
|
|||||||
cell.style.whiteSpace = "nowrap";
|
cell.style.whiteSpace = "nowrap";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureSelectionHeaderControl(cell: HTMLElement): HTMLInputElement {
|
||||||
|
cell.textContent = "";
|
||||||
|
cell.style.gap = "6px";
|
||||||
|
cell.style.justifyContent = "center";
|
||||||
|
|
||||||
|
const checkbox = ensureSelectionCheckbox(cell, "header");
|
||||||
|
const label = cell.querySelector(
|
||||||
|
'[data-market-selection-label="header"]'
|
||||||
|
) as HTMLElement | null;
|
||||||
|
if (label) {
|
||||||
|
label.textContent = "全选";
|
||||||
|
return checkbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextLabel = cell.ownerDocument.createElement("span");
|
||||||
|
nextLabel.dataset.marketSelectionLabel = "header";
|
||||||
|
nextLabel.textContent = "全选";
|
||||||
|
nextLabel.style.fontSize = "12px";
|
||||||
|
cell.appendChild(nextLabel);
|
||||||
|
return checkbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureSelectionRowControl(cell: HTMLElement): HTMLInputElement {
|
||||||
|
cell.textContent = "";
|
||||||
|
cell.style.justifyContent = "center";
|
||||||
|
return ensureSelectionCheckbox(cell, "row");
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureSelectionCheckbox(
|
||||||
|
container: HTMLElement,
|
||||||
|
kind: "header" | "row"
|
||||||
|
): HTMLInputElement {
|
||||||
|
const existingCheckbox = container.querySelector(
|
||||||
|
`[data-market-selection-checkbox="${kind}"]`
|
||||||
|
) as HTMLInputElement | null;
|
||||||
|
if (existingCheckbox) {
|
||||||
|
existingCheckbox.type = "checkbox";
|
||||||
|
return existingCheckbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkbox = container.ownerDocument.createElement("input");
|
||||||
|
checkbox.type = "checkbox";
|
||||||
|
checkbox.dataset.marketSelectionCheckbox = kind;
|
||||||
|
checkbox.style.cursor = "pointer";
|
||||||
|
container.appendChild(checkbox);
|
||||||
|
return checkbox;
|
||||||
|
}
|
||||||
|
|
||||||
function getOwnerDocument(root: ParentNode): Document | null {
|
function getOwnerDocument(root: ParentNode): Document | null {
|
||||||
if ("ownerDocument" in root && root.ownerDocument) {
|
if ("ownerDocument" in root && root.ownerDocument) {
|
||||||
return root.ownerDocument;
|
return root.ownerDocument;
|
||||||
@ -1434,6 +1527,11 @@ function readRateCellText(value: string | undefined): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyColumnWidth(element: HTMLElement, field: string): void {
|
function applyColumnWidth(element: HTMLElement, field: string): void {
|
||||||
|
if (field === SELECTION_COLUMN_KEY) {
|
||||||
|
element.style.minWidth = "56px";
|
||||||
|
element.style.width = "56px";
|
||||||
|
}
|
||||||
|
|
||||||
if (field === BACKEND_COLUMN_KEY) {
|
if (field === BACKEND_COLUMN_KEY) {
|
||||||
element.style.minWidth = "240px";
|
element.style.minWidth = "240px";
|
||||||
element.style.width = "240px";
|
element.style.width = "240px";
|
||||||
|
|||||||
@ -39,6 +39,9 @@ describe("market-dom-sync", () => {
|
|||||||
const table = syncMarketTable(document);
|
const table = syncMarketTable(document);
|
||||||
|
|
||||||
expect(table).not.toBeNull();
|
expect(table).not.toBeNull();
|
||||||
|
expect(
|
||||||
|
document.querySelector('[data-market-header-cell="selection"]')
|
||||||
|
).not.toBeNull();
|
||||||
expect(
|
expect(
|
||||||
document.querySelector('[data-market-header-cell="singleVideoAfterSearchRate"]')
|
document.querySelector('[data-market-header-cell="singleVideoAfterSearchRate"]')
|
||||||
).not.toBeNull();
|
).not.toBeNull();
|
||||||
@ -63,7 +66,9 @@ describe("market-dom-sync", () => {
|
|||||||
expect(
|
expect(
|
||||||
document.querySelector('[data-market-header-cell="cpSearch"]')
|
document.querySelector('[data-market-header-cell="cpSearch"]')
|
||||||
).not.toBeNull();
|
).not.toBeNull();
|
||||||
expect(document.querySelectorAll("[data-market-row-cell]").length).toBe(16);
|
expect(document.querySelectorAll("[data-market-row-cell]").length).toBe(18);
|
||||||
|
expect(table?.headerSelectionCheckbox).not.toBeNull();
|
||||||
|
expect(table?.rows[0]?.selectionCheckbox).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders loading, success, missing, and failed states", () => {
|
test("renders loading, success, missing, and failed states", () => {
|
||||||
@ -170,7 +175,11 @@ describe("market-dom-sync", () => {
|
|||||||
throw new Error("Expected market table");
|
throw new Error("Expected market table");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expect(table.headerSelectionCheckbox).not.toBeNull();
|
||||||
|
expect(table.rows[0]?.selectionCheckbox).not.toBeNull();
|
||||||
expect(readRightHeaderTexts()).toEqual(["21-60s报价", "操作"]);
|
expect(readRightHeaderTexts()).toEqual(["21-60s报价", "操作"]);
|
||||||
|
expect(readSelectionHeaderText()).toBe("全选");
|
||||||
|
expect(readSelectionRowCheckboxCount()).toBe(2);
|
||||||
expect(readPluginHeaderTexts()).toEqual([
|
expect(readPluginHeaderTexts()).toEqual([
|
||||||
"单视频看后搜率",
|
"单视频看后搜率",
|
||||||
"个人视频看后搜率",
|
"个人视频看后搜率",
|
||||||
@ -926,6 +935,18 @@ function readScrollHintText() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readSelectionHeaderText() {
|
||||||
|
return (
|
||||||
|
document
|
||||||
|
.querySelector('[data-market-header-cell="selection"]')
|
||||||
|
?.textContent?.trim() ?? ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSelectionRowCheckboxCount() {
|
||||||
|
return document.querySelectorAll('[data-market-selection-checkbox="row"]').length;
|
||||||
|
}
|
||||||
|
|
||||||
function readAuthorNames() {
|
function readAuthorNames() {
|
||||||
const authorColumn = document.querySelector(
|
const authorColumn = document.querySelector(
|
||||||
'[data-testid="author-section"] .content-column'
|
'[data-testid="author-section"] .content-column'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user