296 lines
9.5 KiB
TypeScript
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";
|
|
}
|