Files
star-chart-search-enhancer/src/content/market/audience-profile-field-dialog.ts
T

296 lines
9.5 KiB
TypeScript

import type { AudienceProfileCsvFieldGroup } from "./audience-profile-csv";
export function promptForAudienceProfileFields(
document: Document,
groups: AudienceProfileCsvFieldGroup[],
selectedHeaders: string[]
): Promise<string[] | null> {
return new Promise((resolve) => {
const selectableHeaders = groups.flatMap((group) => group.headers);
const selectedHeaderSet = new Set(
selectedHeaders.filter((header) => selectableHeaders.includes(header))
);
if (selectedHeaderSet.size === 0) {
selectableHeaders.forEach((header) => selectedHeaderSet.add(header));
}
const overlay = document.createElement("div");
overlay.dataset.audienceProfileFieldDialog = "overlay";
applyOverlayStyles(overlay);
const dialog = document.createElement("section");
applyDialogStyles(dialog);
const title = document.createElement("h2");
applyTitleStyles(title);
const hint = document.createElement("p");
hint.textContent = "基础字段会固定保留。取消勾选后,本次及后续CSV将不包含对应列。";
applyHintStyles(hint);
const toolbar = document.createElement("div");
applyToolbarStyles(toolbar);
const selectAllButton = document.createElement("button");
selectAllButton.type = "button";
selectAllButton.textContent = "全选";
applySecondaryButtonStyles(selectAllButton);
const resetButton = document.createElement("button");
resetButton.type = "button";
resetButton.textContent = "恢复默认";
applySecondaryButtonStyles(resetButton);
toolbar.append(selectAllButton, resetButton);
const groupContainer = document.createElement("div");
applyGroupContainerStyles(groupContainer);
const fieldInputs: HTMLInputElement[] = [];
groups.forEach((group) => {
const groupSection = document.createElement("section");
groupSection.dataset.audienceProfileFieldDialogGroup = "section";
applyGroupSectionStyles(groupSection);
const groupHeader = document.createElement("label");
applyGroupHeaderStyles(groupHeader);
const groupInput = document.createElement("input");
groupInput.type = "checkbox";
const groupTitle = document.createElement("span");
groupTitle.textContent = group.label;
groupHeader.append(groupInput, groupTitle);
const fieldList = document.createElement("div");
applyFieldListStyles(fieldList);
const groupFieldInputs = group.headers.map((header) => {
const fieldLabel = document.createElement("label");
applyFieldLabelStyles(fieldLabel);
const input = document.createElement("input");
input.type = "checkbox";
input.value = header;
input.dataset.audienceProfileFieldDialogField = "checkbox";
input.checked = selectedHeaderSet.has(header);
const text = document.createElement("span");
text.textContent = header;
fieldLabel.append(input, text);
fieldList.append(fieldLabel);
fieldInputs.push(input);
return input;
});
const syncGroupInput = () => {
const checkedCount = groupFieldInputs.filter((input) => input.checked).length;
groupInput.checked = checkedCount === groupFieldInputs.length;
groupInput.indeterminate = checkedCount > 0 && checkedCount < groupFieldInputs.length;
};
groupInput.addEventListener("change", () => {
groupFieldInputs.forEach((input) => {
input.checked = groupInput.checked;
});
syncTitle();
});
groupFieldInputs.forEach((input) => {
input.addEventListener("change", () => {
syncGroupInput();
syncTitle();
});
});
syncGroupInput();
groupSection.append(groupHeader, fieldList);
groupContainer.append(groupSection);
});
const actions = document.createElement("div");
applyActionsStyles(actions);
const cancelButton = document.createElement("button");
cancelButton.type = "button";
cancelButton.textContent = "取消";
applySecondaryButtonStyles(cancelButton);
const confirmButton = document.createElement("button");
confirmButton.type = "button";
confirmButton.dataset.audienceProfileFieldDialogSave = "button";
confirmButton.textContent = "保存";
applyPrimaryButtonStyles(confirmButton);
actions.append(cancelButton, confirmButton);
dialog.append(title, hint, toolbar, groupContainer, actions);
overlay.append(dialog);
document.body.appendChild(overlay);
function syncTitle() {
const checkedCount = fieldInputs.filter((input) => input.checked).length;
title.textContent = `可选字段(已选 ${checkedCount}/${fieldInputs.length} 个字段)`;
}
function close(value: string[] | null) {
overlay.remove();
resolve(value);
}
selectAllButton.addEventListener("click", () => {
fieldInputs.forEach((input) => {
input.checked = true;
});
syncTitle();
syncAllGroupInputs(dialog);
});
resetButton.addEventListener("click", () => {
fieldInputs.forEach((input) => {
input.checked = true;
});
syncTitle();
syncAllGroupInputs(dialog);
});
cancelButton.addEventListener("click", () => close(null));
confirmButton.addEventListener("click", () => {
const nextHeaders = fieldInputs
.filter((input) => input.checked)
.map((input) => input.value);
close(nextHeaders);
});
overlay.addEventListener("click", (event) => {
if (event.target === overlay) {
close(null);
}
});
syncTitle();
});
}
function syncAllGroupInputs(dialog: HTMLElement): void {
dialog
.querySelectorAll('[data-audience-profile-field-dialog-group="section"]')
.forEach((section) => {
const groupInput = section.querySelector(":scope > label > input");
const fieldInputs = Array.from(
section.querySelectorAll(":scope > div input")
) as HTMLInputElement[];
if (!(groupInput instanceof HTMLInputElement) || fieldInputs.length === 0) {
return;
}
const checkedCount = fieldInputs.filter((input) => input.checked).length;
groupInput.checked = checkedCount === fieldInputs.length;
groupInput.indeterminate = checkedCount > 0 && checkedCount < fieldInputs.length;
});
}
function applyOverlayStyles(overlay: HTMLElement): void {
overlay.style.position = "fixed";
overlay.style.inset = "0";
overlay.style.zIndex = "2147483647";
overlay.style.display = "flex";
overlay.style.alignItems = "center";
overlay.style.justifyContent = "center";
overlay.style.background = "rgba(15, 23, 42, 0.38)";
}
function applyDialogStyles(dialog: HTMLElement): void {
dialog.style.width = "680px";
dialog.style.maxWidth = "calc(100vw - 32px)";
dialog.style.maxHeight = "calc(100vh - 48px)";
dialog.style.display = "flex";
dialog.style.flexDirection = "column";
dialog.style.background = "#ffffff";
dialog.style.borderRadius = "8px";
dialog.style.boxShadow = "0 18px 45px rgba(15, 23, 42, 0.22)";
dialog.style.padding = "20px";
dialog.style.boxSizing = "border-box";
}
function applyTitleStyles(title: HTMLElement): void {
title.style.margin = "0 0 8px";
title.style.fontSize = "18px";
title.style.fontWeight = "700";
title.style.color = "#1f2329";
}
function applyHintStyles(hint: HTMLElement): void {
hint.style.margin = "0 0 12px";
hint.style.fontSize = "13px";
hint.style.lineHeight = "20px";
hint.style.color = "#64748b";
}
function applyToolbarStyles(toolbar: HTMLElement): void {
toolbar.style.display = "flex";
toolbar.style.gap = "8px";
toolbar.style.marginBottom = "12px";
}
function applyGroupContainerStyles(container: HTMLElement): void {
container.style.display = "flex";
container.style.flexDirection = "column";
container.style.gap = "10px";
container.style.overflow = "auto";
container.style.paddingRight = "4px";
}
function applyGroupSectionStyles(section: HTMLElement): void {
section.style.border = "1px solid #e5e7eb";
section.style.borderRadius = "8px";
section.style.padding = "10px";
}
function applyGroupHeaderStyles(label: HTMLElement): void {
label.style.display = "flex";
label.style.alignItems = "center";
label.style.gap = "8px";
label.style.fontWeight = "700";
label.style.color = "#1f2329";
label.style.marginBottom = "8px";
}
function applyFieldListStyles(list: HTMLElement): void {
list.style.display = "grid";
list.style.gridTemplateColumns = "repeat(auto-fit, minmax(220px, 1fr))";
list.style.gap = "8px";
}
function applyFieldLabelStyles(label: HTMLElement): void {
label.style.display = "flex";
label.style.alignItems = "center";
label.style.gap = "6px";
label.style.fontSize = "13px";
label.style.lineHeight = "18px";
label.style.color = "#374151";
}
function applyActionsStyles(actions: HTMLElement): void {
actions.style.display = "flex";
actions.style.justifyContent = "flex-end";
actions.style.columnGap = "8px";
actions.style.marginTop = "14px";
}
function applyPrimaryButtonStyles(button: HTMLButtonElement): void {
button.style.height = "32px";
button.style.padding = "0 15px";
button.style.border = "1px solid #7f1d2d";
button.style.borderRadius = "8px";
button.style.background = "#7f1d2d";
button.style.color = "#ffffff";
button.style.fontWeight = "600";
}
function applySecondaryButtonStyles(button: HTMLButtonElement): void {
button.style.height = "32px";
button.style.padding = "0 15px";
button.style.border = "1px solid #d0d7de";
button.style.borderRadius = "8px";
button.style.background = "#ffffff";
button.style.color = "#1f2329";
button.style.fontWeight = "600";
}