feat: redesign market propagation toolbar
This commit is contained in:
@@ -16,17 +16,35 @@ export interface PluginToolbarHandlers {
|
||||
}
|
||||
|
||||
interface SpreadMetricRuleDom {
|
||||
detailsButton: HTMLButtonElement;
|
||||
enabledInput: HTMLInputElement;
|
||||
flowTypeSelect: HTMLSelectElement;
|
||||
onlyAssignSelect: HTMLSelectElement;
|
||||
rangeSelect: HTMLSelectElement;
|
||||
removeButton: HTMLButtonElement;
|
||||
root: HTMLElement;
|
||||
secondaryControls: HTMLElement;
|
||||
thresholdInput: HTMLInputElement;
|
||||
typeSelect: HTMLSelectElement;
|
||||
}
|
||||
|
||||
type SpreadMetricRuleDomMap = Record<SpreadFilterMetric, SpreadMetricRuleDom>;
|
||||
|
||||
interface MetricCatalogDom {
|
||||
addButton: HTMLButtonElement;
|
||||
closeButton: HTMLButtonElement;
|
||||
metricActions: Record<SpreadFilterMetric, HTMLButtonElement>;
|
||||
panel: HTMLElement;
|
||||
root: HTMLElement;
|
||||
searchInput: HTMLInputElement;
|
||||
selectedCount: HTMLElement;
|
||||
}
|
||||
|
||||
interface ToolbarResizeListener {
|
||||
handler: EventListener;
|
||||
window: Window;
|
||||
}
|
||||
|
||||
const SPREAD_FILTER_DEFINITIONS: ReadonlyArray<{
|
||||
label: string;
|
||||
metric: SpreadFilterMetric;
|
||||
@@ -35,6 +53,14 @@ const SPREAD_FILTER_DEFINITIONS: ReadonlyArray<{
|
||||
{ label: "互动率", metric: "interactionRate" }
|
||||
];
|
||||
|
||||
const DEFAULT_ENABLED_SPREAD_METRIC: SpreadFilterMetric = "finishRate";
|
||||
const DEFAULT_SPREAD_METRIC_CONFIG: Readonly<SpreadInfoConfig> = {
|
||||
flowType: 0,
|
||||
onlyAssign: true,
|
||||
range: 2,
|
||||
type: 2
|
||||
};
|
||||
|
||||
export interface PluginToolbarDom {
|
||||
audienceProfileByIdExportButton: HTMLButtonElement;
|
||||
audienceProfileExportButton: HTMLButtonElement;
|
||||
@@ -44,18 +70,30 @@ export interface PluginToolbarDom {
|
||||
exportCustomPagesInput: HTMLInputElement;
|
||||
exportRangeSelect: HTMLSelectElement;
|
||||
exportStatusText: HTMLElement;
|
||||
metricCatalog: MetricCatalogDom;
|
||||
spreadMetricRules: SpreadMetricRuleDomMap;
|
||||
root: HTMLElement;
|
||||
}
|
||||
|
||||
const PLUGIN_ACTION_BUTTON_STYLE_ID = "sces-plugin-action-button-style";
|
||||
const TOOLBAR_RESIZE_LISTENER = Symbol("sces-toolbar-resize-listener");
|
||||
|
||||
type ToolbarRootWithResizeListener = HTMLElement & {
|
||||
[TOOLBAR_RESIZE_LISTENER]?: ToolbarResizeListener;
|
||||
};
|
||||
|
||||
export function isPluginToolbarMounted(
|
||||
root: HTMLElement,
|
||||
document: Document
|
||||
): boolean {
|
||||
const actionRow = findNativeActionRow(document);
|
||||
return Boolean(actionRow && root.parentElement === actionRow && !root.hidden);
|
||||
const isMounted = Boolean(
|
||||
actionRow && root.parentElement === actionRow && !root.hidden
|
||||
);
|
||||
if (!isMounted) {
|
||||
cleanupToolbarResizeListener(root);
|
||||
}
|
||||
return isMounted;
|
||||
}
|
||||
|
||||
export function ensurePluginToolbar(
|
||||
@@ -72,12 +110,18 @@ export function ensurePluginToolbar(
|
||||
existingRoot.querySelector(
|
||||
'[data-plugin-export-audience-profile-by-id="button"]'
|
||||
) &&
|
||||
existingRoot.querySelector('[data-plugin-spread-metric="finishRate"]')
|
||||
existingRoot.querySelector(
|
||||
'[data-plugin-toolbar-action-group="csv-export"]'
|
||||
) &&
|
||||
existingRoot.querySelector('[data-plugin-spread-metric-catalog-trigger="button"]')
|
||||
) {
|
||||
ensureToolbarMounted(existingRoot, document);
|
||||
return readToolbarDom(existingRoot);
|
||||
const toolbarDom = readToolbarDom(existingRoot);
|
||||
ensureToolbarResizeListener(toolbarDom);
|
||||
return toolbarDom;
|
||||
}
|
||||
|
||||
cleanupToolbarResizeListener(existingRoot);
|
||||
existingRoot.remove();
|
||||
}
|
||||
|
||||
@@ -140,6 +184,11 @@ export function ensurePluginToolbar(
|
||||
applyStatusStyles(exportStatusText);
|
||||
|
||||
const spreadMetricRules = createSpreadMetricRuleDoms(document);
|
||||
const defaultSpreadMetricRule =
|
||||
spreadMetricRules[DEFAULT_ENABLED_SPREAD_METRIC];
|
||||
defaultSpreadMetricRule.enabledInput.checked = true;
|
||||
applyDefaultSpreadMetricRuleState(defaultSpreadMetricRule);
|
||||
const metricCatalog = createMetricCatalog(document);
|
||||
|
||||
const panel = document.createElement("div");
|
||||
panel.dataset.pluginToolbarPanel = "root";
|
||||
@@ -156,18 +205,33 @@ export function ensurePluginToolbar(
|
||||
const dataGroup = document.createElement("div");
|
||||
dataGroup.dataset.pluginToolbarGroup = "data";
|
||||
applyToolbarGroupStyles(dataGroup);
|
||||
dataGroup.append(
|
||||
const csvExportGroup = createToolbarActionGroup(document, "csv-export", [
|
||||
exportButton,
|
||||
exportRangeSelect,
|
||||
exportCustomPagesInput,
|
||||
audienceProfileExportButton,
|
||||
audienceProfileByIdExportButton,
|
||||
audienceProfileFieldButton,
|
||||
exportCustomPagesInput
|
||||
]);
|
||||
const selectedAudienceExportGroup = createToolbarActionGroup(
|
||||
document,
|
||||
"selected-audience-export",
|
||||
[audienceProfileExportButton, audienceProfileFieldButton]
|
||||
);
|
||||
const idExportGroup = createToolbarActionGroup(document, "id-export", [
|
||||
audienceProfileByIdExportButton
|
||||
]);
|
||||
const batchSubmitGroup = createToolbarActionGroup(document, "batch-submit", [
|
||||
batchSubmitButton
|
||||
]);
|
||||
dataGroup.append(
|
||||
csvExportGroup,
|
||||
createToolbarActionDivider(document),
|
||||
selectedAudienceExportGroup,
|
||||
createToolbarActionDivider(document),
|
||||
idExportGroup,
|
||||
createToolbarActionDivider(document),
|
||||
batchSubmitGroup
|
||||
);
|
||||
|
||||
const thresholdTitle = createToolbarGroupTitle(document, "传播指标筛选");
|
||||
const metricSelector = createSpreadMetricSelector(document, spreadMetricRules);
|
||||
const rulesGroup = document.createElement("div");
|
||||
rulesGroup.dataset.pluginSpreadRules = "root";
|
||||
applySpreadRulesGroupStyles(rulesGroup);
|
||||
@@ -179,7 +243,7 @@ export function ensurePluginToolbar(
|
||||
const filterNote = createSpreadFilterNote(document);
|
||||
|
||||
firstRow.append(dataGroup, exportStatusText);
|
||||
secondRow.append(thresholdTitle, metricSelector, rulesGroup, filterNote);
|
||||
secondRow.append(thresholdTitle, metricCatalog.root, rulesGroup, filterNote);
|
||||
panel.append(firstRow, secondRow);
|
||||
|
||||
root.append(panel);
|
||||
@@ -222,6 +286,7 @@ export function ensurePluginToolbar(
|
||||
exportCustomPagesInput,
|
||||
exportRangeSelect,
|
||||
exportStatusText,
|
||||
metricCatalog,
|
||||
spreadMetricRules,
|
||||
root
|
||||
} satisfies PluginToolbarDom;
|
||||
@@ -230,6 +295,16 @@ export function ensurePluginToolbar(
|
||||
syncCustomPagesInputVisibility(toolbarDom);
|
||||
});
|
||||
|
||||
metricCatalog.addButton.addEventListener("click", () => {
|
||||
setMetricCatalogOpen(metricCatalog, metricCatalog.panel.hidden);
|
||||
});
|
||||
metricCatalog.closeButton.addEventListener("click", () => {
|
||||
setMetricCatalogOpen(metricCatalog, false);
|
||||
});
|
||||
metricCatalog.searchInput.addEventListener("input", () => {
|
||||
syncMetricCatalog(toolbarDom);
|
||||
});
|
||||
|
||||
SPREAD_FILTER_DEFINITIONS.forEach(({ metric }) => {
|
||||
const rule = spreadMetricRules[metric];
|
||||
rule.enabledInput.addEventListener("change", () => {
|
||||
@@ -239,8 +314,27 @@ export function ensurePluginToolbar(
|
||||
rule.typeSelect.addEventListener("change", () => {
|
||||
syncSpreadMetricVideoConstraints(rule);
|
||||
});
|
||||
rule.removeButton.addEventListener("click", () => {
|
||||
rule.enabledInput.checked = false;
|
||||
syncAllSpreadMetricRules(toolbarDom);
|
||||
});
|
||||
rule.detailsButton.addEventListener("click", () => {
|
||||
rule.root.dataset.pluginSpreadRuleDetails =
|
||||
rule.root.dataset.pluginSpreadRuleDetails === "open" ? "closed" : "open";
|
||||
syncSpreadMetricRuleSecondaryControls(rule);
|
||||
});
|
||||
metricCatalog.metricActions[metric].addEventListener("click", () => {
|
||||
if (rule.enabledInput.checked) {
|
||||
return;
|
||||
}
|
||||
rule.enabledInput.checked = true;
|
||||
syncAllSpreadMetricRules(toolbarDom);
|
||||
setMetricCatalogOpen(metricCatalog, false);
|
||||
});
|
||||
});
|
||||
|
||||
ensureToolbarResizeListener(toolbarDom);
|
||||
|
||||
syncCustomPagesInputVisibility(toolbarDom);
|
||||
syncAllSpreadMetricRules(toolbarDom);
|
||||
|
||||
@@ -258,6 +352,30 @@ function appendOption(
|
||||
select.appendChild(option);
|
||||
}
|
||||
|
||||
function createToolbarActionGroup(
|
||||
document: Document,
|
||||
groupName:
|
||||
| "batch-submit"
|
||||
| "csv-export"
|
||||
| "id-export"
|
||||
| "selected-audience-export",
|
||||
controls: HTMLElement[]
|
||||
): HTMLElement {
|
||||
const group = document.createElement("div");
|
||||
group.dataset.pluginToolbarActionGroup = groupName;
|
||||
applyToolbarActionGroupStyles(group);
|
||||
group.append(...controls);
|
||||
return group;
|
||||
}
|
||||
|
||||
function createToolbarActionDivider(document: Document): HTMLElement {
|
||||
const divider = document.createElement("span");
|
||||
divider.dataset.pluginToolbarActionDivider = "divider";
|
||||
divider.setAttribute("aria-hidden", "true");
|
||||
applyToolbarActionDividerStyles(divider);
|
||||
return divider;
|
||||
}
|
||||
|
||||
function createSpreadMetricRuleDoms(
|
||||
document: Document
|
||||
): SpreadMetricRuleDomMap {
|
||||
@@ -276,6 +394,7 @@ function createSpreadMetricRuleDom(
|
||||
): SpreadMetricRuleDom {
|
||||
const enabledInput = document.createElement("input");
|
||||
enabledInput.type = "checkbox";
|
||||
enabledInput.hidden = true;
|
||||
enabledInput.dataset.pluginSpreadMetric = metric;
|
||||
enabledInput.setAttribute("aria-label", `启用${label}筛选`);
|
||||
|
||||
@@ -332,44 +451,131 @@ function createSpreadMetricRuleDom(
|
||||
unitText.textContent = "%";
|
||||
thresholdControl.append(operator, thresholdInput, unitText);
|
||||
|
||||
root.append(
|
||||
metricLabel,
|
||||
thresholdControl,
|
||||
typeSelect,
|
||||
const primaryControls = document.createElement("div");
|
||||
primaryControls.dataset.pluginSpreadRulePrimary = metric;
|
||||
applySpreadMetricRulePrimaryStyles(primaryControls);
|
||||
|
||||
const detailsButton = document.createElement("button");
|
||||
detailsButton.type = "button";
|
||||
detailsButton.dataset.pluginSpreadRuleDetails = metric;
|
||||
detailsButton.textContent = "更多选项";
|
||||
detailsButton.setAttribute("aria-expanded", "false");
|
||||
applySpreadMetricDetailsButtonStyles(detailsButton);
|
||||
primaryControls.append(metricLabel, thresholdControl, typeSelect, detailsButton);
|
||||
|
||||
const secondaryControls = document.createElement("div");
|
||||
secondaryControls.dataset.pluginSpreadRuleSecondary = metric;
|
||||
applySpreadMetricRuleSecondaryStyles(secondaryControls);
|
||||
|
||||
const removeButton = document.createElement("button");
|
||||
removeButton.type = "button";
|
||||
removeButton.dataset.pluginSpreadRuleRemove = metric;
|
||||
removeButton.textContent = "×";
|
||||
removeButton.title = `删除${label}筛选`;
|
||||
removeButton.setAttribute("aria-label", `删除${label}筛选`);
|
||||
applySpreadMetricRemoveButtonStyles(removeButton);
|
||||
secondaryControls.append(
|
||||
onlyAssignSelect,
|
||||
flowTypeSelect,
|
||||
rangeSelect
|
||||
rangeSelect,
|
||||
removeButton
|
||||
);
|
||||
|
||||
root.append(enabledInput, primaryControls, secondaryControls);
|
||||
|
||||
return {
|
||||
detailsButton,
|
||||
enabledInput,
|
||||
flowTypeSelect,
|
||||
onlyAssignSelect,
|
||||
rangeSelect,
|
||||
removeButton,
|
||||
root,
|
||||
secondaryControls,
|
||||
thresholdInput,
|
||||
typeSelect
|
||||
};
|
||||
}
|
||||
|
||||
function createSpreadMetricSelector(
|
||||
document: Document,
|
||||
rules: SpreadMetricRuleDomMap
|
||||
): HTMLElement {
|
||||
const selector = document.createElement("div");
|
||||
selector.dataset.pluginSpreadMetrics = "root";
|
||||
applySpreadMetricSelectorStyles(selector);
|
||||
function createMetricCatalog(document: Document): MetricCatalogDom {
|
||||
const root = document.createElement("div");
|
||||
root.dataset.pluginSpreadMetricCatalog = "root";
|
||||
applyMetricCatalogStyles(root);
|
||||
|
||||
SPREAD_FILTER_DEFINITIONS.forEach(({ label, metric }) => {
|
||||
const option = document.createElement("label");
|
||||
applySpreadMetricOptionStyles(option);
|
||||
const labelText = document.createElement("span");
|
||||
labelText.textContent = label;
|
||||
option.append(rules[metric].enabledInput, labelText);
|
||||
selector.appendChild(option);
|
||||
});
|
||||
const addButton = document.createElement("button");
|
||||
addButton.type = "button";
|
||||
addButton.dataset.pluginSpreadMetricCatalogTrigger = "button";
|
||||
addButton.textContent = "添加筛选指标";
|
||||
addButton.setAttribute("aria-expanded", "false");
|
||||
applyMetricCatalogTriggerStyles(addButton);
|
||||
|
||||
return selector;
|
||||
const selectedCount = document.createElement("span");
|
||||
selectedCount.dataset.pluginSpreadMetricSelectedCount = "text";
|
||||
selectedCount.setAttribute("aria-live", "polite");
|
||||
applyMetricCatalogCountStyles(selectedCount);
|
||||
|
||||
const panel = document.createElement("div");
|
||||
panel.dataset.pluginSpreadMetricCatalogPanel = "root";
|
||||
panel.hidden = true;
|
||||
applyMetricCatalogPanelStyles(panel);
|
||||
|
||||
const searchInput = document.createElement("input");
|
||||
searchInput.type = "search";
|
||||
searchInput.placeholder = "搜索指标";
|
||||
searchInput.dataset.pluginSpreadMetricCatalogSearch = "input";
|
||||
searchInput.setAttribute("aria-label", "搜索筛选指标");
|
||||
applyMetricCatalogSearchStyles(searchInput);
|
||||
|
||||
const closeButton = document.createElement("button");
|
||||
closeButton.type = "button";
|
||||
closeButton.dataset.pluginSpreadMetricCatalogClose = "button";
|
||||
closeButton.textContent = "×";
|
||||
closeButton.title = "关闭指标目录";
|
||||
closeButton.setAttribute("aria-label", "关闭指标目录");
|
||||
applyMetricCatalogCloseButtonStyles(closeButton);
|
||||
|
||||
const panelHeader = document.createElement("div");
|
||||
panelHeader.dataset.pluginSpreadMetricCatalogHeader = "root";
|
||||
applyMetricCatalogHeaderStyles(panelHeader);
|
||||
panelHeader.append(searchInput, closeButton);
|
||||
|
||||
const group = document.createElement("div");
|
||||
group.dataset.pluginSpreadMetricCatalogGroup = "传播表现";
|
||||
applyMetricCatalogGroupStyles(group);
|
||||
const groupTitle = document.createElement("strong");
|
||||
groupTitle.textContent = "传播表现";
|
||||
applyMetricCatalogGroupTitleStyles(groupTitle);
|
||||
group.appendChild(groupTitle);
|
||||
|
||||
const metricActions = Object.fromEntries(
|
||||
SPREAD_FILTER_DEFINITIONS.map(({ label, metric }) => {
|
||||
const item = document.createElement("div");
|
||||
item.dataset.pluginSpreadMetricCatalogItem = metric;
|
||||
applyMetricCatalogItemStyles(item);
|
||||
const name = document.createElement("span");
|
||||
name.textContent = label;
|
||||
const action = document.createElement("button");
|
||||
action.type = "button";
|
||||
action.dataset.pluginSpreadMetricCatalogAction = metric;
|
||||
applyMetricCatalogActionStyles(action);
|
||||
item.append(name, action);
|
||||
group.appendChild(item);
|
||||
return [metric, action];
|
||||
})
|
||||
) as unknown as Record<SpreadFilterMetric, HTMLButtonElement>;
|
||||
|
||||
panel.append(panelHeader, group);
|
||||
root.append(addButton, selectedCount, panel);
|
||||
|
||||
return {
|
||||
addButton,
|
||||
closeButton,
|
||||
metricActions,
|
||||
panel,
|
||||
root,
|
||||
searchInput,
|
||||
selectedCount
|
||||
};
|
||||
}
|
||||
|
||||
function createSpreadFilterNote(document: Document): HTMLElement {
|
||||
@@ -390,6 +596,9 @@ function readSpreadMetricRuleDoms(root: HTMLElement): SpreadMetricRuleDomMap {
|
||||
return [
|
||||
metric,
|
||||
{
|
||||
detailsButton: ruleRoot.querySelector(
|
||||
`[data-plugin-spread-rule-details="${metric}"]`
|
||||
) as HTMLButtonElement,
|
||||
enabledInput: root.querySelector(
|
||||
`[data-plugin-spread-metric="${metric}"]`
|
||||
) as HTMLInputElement,
|
||||
@@ -402,7 +611,13 @@ function readSpreadMetricRuleDoms(root: HTMLElement): SpreadMetricRuleDomMap {
|
||||
rangeSelect: ruleRoot.querySelector(
|
||||
'[data-plugin-spread-filter="range"]'
|
||||
) as HTMLSelectElement,
|
||||
removeButton: ruleRoot.querySelector(
|
||||
`[data-plugin-spread-rule-remove="${metric}"]`
|
||||
) as HTMLButtonElement,
|
||||
root: ruleRoot,
|
||||
secondaryControls: ruleRoot.querySelector(
|
||||
`[data-plugin-spread-rule-secondary="${metric}"]`
|
||||
) as HTMLElement,
|
||||
thresholdInput: ruleRoot.querySelector(
|
||||
`[data-plugin-spread-threshold="${metric}"]`
|
||||
) as HTMLInputElement,
|
||||
@@ -415,6 +630,37 @@ function readSpreadMetricRuleDoms(root: HTMLElement): SpreadMetricRuleDomMap {
|
||||
) as unknown as SpreadMetricRuleDomMap;
|
||||
}
|
||||
|
||||
function readMetricCatalogDom(root: HTMLElement): MetricCatalogDom {
|
||||
return {
|
||||
addButton: root.querySelector(
|
||||
'[data-plugin-spread-metric-catalog-trigger="button"]'
|
||||
) as HTMLButtonElement,
|
||||
closeButton: root.querySelector(
|
||||
'[data-plugin-spread-metric-catalog-close="button"]'
|
||||
) as HTMLButtonElement,
|
||||
metricActions: Object.fromEntries(
|
||||
SPREAD_FILTER_DEFINITIONS.map(({ metric }) => [
|
||||
metric,
|
||||
root.querySelector(
|
||||
`[data-plugin-spread-metric-catalog-action="${metric}"]`
|
||||
) as HTMLButtonElement
|
||||
])
|
||||
) as unknown as Record<SpreadFilterMetric, HTMLButtonElement>,
|
||||
panel: root.querySelector(
|
||||
'[data-plugin-spread-metric-catalog-panel="root"]'
|
||||
) as HTMLElement,
|
||||
root: root.querySelector(
|
||||
'[data-plugin-spread-metric-catalog="root"]'
|
||||
) as HTMLElement,
|
||||
searchInput: root.querySelector(
|
||||
'[data-plugin-spread-metric-catalog-search="input"]'
|
||||
) as HTMLInputElement,
|
||||
selectedCount: root.querySelector(
|
||||
'[data-plugin-spread-metric-selected-count="text"]'
|
||||
) as HTMLElement
|
||||
};
|
||||
}
|
||||
|
||||
function readToolbarDom(root: HTMLElement): PluginToolbarDom {
|
||||
const toolbarDom = {
|
||||
audienceProfileByIdExportButton: root.querySelector(
|
||||
@@ -441,6 +687,7 @@ function readToolbarDom(root: HTMLElement): PluginToolbarDom {
|
||||
exportStatusText: root.querySelector(
|
||||
'[data-plugin-export-status="text"]'
|
||||
) as HTMLElement,
|
||||
metricCatalog: readMetricCatalogDom(root),
|
||||
spreadMetricRules: readSpreadMetricRuleDoms(root),
|
||||
root
|
||||
} satisfies PluginToolbarDom;
|
||||
@@ -551,13 +798,19 @@ export function setToolbarBusyState(
|
||||
toolbar.exportButton,
|
||||
toolbar.exportRangeSelect,
|
||||
toolbar.exportCustomPagesInput,
|
||||
toolbar.metricCatalog.addButton,
|
||||
toolbar.metricCatalog.closeButton,
|
||||
toolbar.metricCatalog.searchInput,
|
||||
...Object.values(toolbar.metricCatalog.metricActions),
|
||||
...Object.values(toolbar.spreadMetricRules).flatMap((rule) => [
|
||||
rule.detailsButton,
|
||||
rule.enabledInput,
|
||||
rule.thresholdInput,
|
||||
rule.typeSelect,
|
||||
rule.onlyAssignSelect,
|
||||
rule.flowTypeSelect,
|
||||
rule.rangeSelect
|
||||
rule.rangeSelect,
|
||||
rule.removeButton
|
||||
])
|
||||
].forEach((element) => {
|
||||
element.disabled = isBusy;
|
||||
@@ -579,22 +832,67 @@ function syncCustomPagesInputVisibility(toolbar: PluginToolbarDom): void {
|
||||
toolbar.exportCustomPagesInput.hidden = toolbar.exportRangeSelect.value !== "custom";
|
||||
}
|
||||
|
||||
function ensureToolbarResizeListener(toolbar: PluginToolbarDom): void {
|
||||
const root = toolbar.root as ToolbarRootWithResizeListener;
|
||||
const currentWindow = root.ownerDocument.defaultView;
|
||||
const existingListener = root[TOOLBAR_RESIZE_LISTENER];
|
||||
if (!currentWindow) {
|
||||
cleanupToolbarResizeListener(root);
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingListener?.window === currentWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingListener) {
|
||||
existingListener.window.removeEventListener(
|
||||
"resize",
|
||||
existingListener.handler
|
||||
);
|
||||
delete root[TOOLBAR_RESIZE_LISTENER];
|
||||
}
|
||||
|
||||
const handler = existingListener?.handler ?? (() => {
|
||||
syncAllSpreadMetricRules(toolbar);
|
||||
});
|
||||
root[TOOLBAR_RESIZE_LISTENER] = { handler, window: currentWindow };
|
||||
currentWindow.addEventListener("resize", handler);
|
||||
}
|
||||
|
||||
function cleanupToolbarResizeListener(root: HTMLElement): void {
|
||||
const toolbarRoot = root as ToolbarRootWithResizeListener;
|
||||
const listener = toolbarRoot[TOOLBAR_RESIZE_LISTENER];
|
||||
if (!listener) {
|
||||
return;
|
||||
}
|
||||
|
||||
listener.window.removeEventListener("resize", listener.handler);
|
||||
delete toolbarRoot[TOOLBAR_RESIZE_LISTENER];
|
||||
}
|
||||
|
||||
function syncAllSpreadMetricRules(toolbar: PluginToolbarDom): void {
|
||||
Object.values(toolbar.spreadMetricRules).forEach(syncSpreadMetricRuleState);
|
||||
syncMetricCatalog(toolbar);
|
||||
syncSpreadFilterNote(toolbar);
|
||||
}
|
||||
|
||||
function syncSpreadMetricRuleState(rule: SpreadMetricRuleDom): void {
|
||||
rule.root.hidden = !rule.enabledInput.checked;
|
||||
if (!rule.enabledInput.checked) {
|
||||
rule.thresholdInput.value = "";
|
||||
rule.typeSelect.value = "1";
|
||||
rule.onlyAssignSelect.value = "false";
|
||||
rule.flowTypeSelect.value = "0";
|
||||
rule.rangeSelect.value = "2";
|
||||
applyDefaultSpreadMetricRuleState(rule);
|
||||
clearSpreadMetricRuleValidation(rule);
|
||||
}
|
||||
syncSpreadMetricVideoConstraints(rule);
|
||||
syncSpreadMetricRuleSecondaryControls(rule);
|
||||
}
|
||||
|
||||
function applyDefaultSpreadMetricRuleState(rule: SpreadMetricRuleDom): void {
|
||||
rule.thresholdInput.value = "";
|
||||
rule.typeSelect.value = String(DEFAULT_SPREAD_METRIC_CONFIG.type);
|
||||
rule.onlyAssignSelect.value = String(DEFAULT_SPREAD_METRIC_CONFIG.onlyAssign);
|
||||
rule.flowTypeSelect.value = String(DEFAULT_SPREAD_METRIC_CONFIG.flowType);
|
||||
rule.rangeSelect.value = String(DEFAULT_SPREAD_METRIC_CONFIG.range);
|
||||
}
|
||||
|
||||
function syncSpreadMetricVideoConstraints(rule: SpreadMetricRuleDom): void {
|
||||
@@ -621,6 +919,52 @@ function syncSpreadFilterNote(toolbar: PluginToolbarDom): void {
|
||||
note.hidden = enabledCount < 2;
|
||||
}
|
||||
|
||||
function syncMetricCatalog(toolbar: PluginToolbarDom): void {
|
||||
const searchTerm = toolbar.metricCatalog.searchInput.value.trim().toLowerCase();
|
||||
const enabledCount = Object.values(toolbar.spreadMetricRules).filter(
|
||||
(rule) => rule.enabledInput.checked
|
||||
).length;
|
||||
toolbar.metricCatalog.selectedCount.textContent = `已选 ${enabledCount} 项`;
|
||||
|
||||
SPREAD_FILTER_DEFINITIONS.forEach(({ label, metric }) => {
|
||||
const action = toolbar.metricCatalog.metricActions[metric];
|
||||
const isSelected = toolbar.spreadMetricRules[metric].enabledInput.checked;
|
||||
const item = action.closest(
|
||||
`[data-plugin-spread-metric-catalog-item="${metric}"]`
|
||||
) as HTMLElement | null;
|
||||
action.disabled = isSelected;
|
||||
action.textContent = isSelected ? "已添加" : "添加";
|
||||
if (item) {
|
||||
item.hidden = Boolean(searchTerm) && !label.toLowerCase().includes(searchTerm);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setMetricCatalogOpen(catalog: MetricCatalogDom, isOpen: boolean): void {
|
||||
catalog.panel.hidden = !isOpen;
|
||||
catalog.addButton.setAttribute("aria-expanded", String(isOpen));
|
||||
if (!isOpen) {
|
||||
catalog.searchInput.value = "";
|
||||
Object.values(catalog.metricActions).forEach((action) => {
|
||||
const item = action.closest("[data-plugin-spread-metric-catalog-item]") as
|
||||
| HTMLElement
|
||||
| null;
|
||||
if (item) {
|
||||
item.hidden = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function syncSpreadMetricRuleSecondaryControls(rule: SpreadMetricRuleDom): void {
|
||||
const isNarrowViewport =
|
||||
(rule.root.ownerDocument.defaultView?.innerWidth ?? Number.POSITIVE_INFINITY) <= 720;
|
||||
const isOpen = rule.root.dataset.pluginSpreadRuleDetails === "open";
|
||||
rule.secondaryControls.hidden = isNarrowViewport && !isOpen;
|
||||
rule.detailsButton.hidden = !isNarrowViewport;
|
||||
rule.detailsButton.setAttribute("aria-expanded", String(isOpen));
|
||||
}
|
||||
|
||||
function readSpreadMetricConfig(rule: SpreadMetricRuleDom): SpreadInfoConfig {
|
||||
const type = rule.typeSelect.value === "2" ? 2 : 1;
|
||||
return {
|
||||
@@ -850,23 +1194,115 @@ function applyToolbarGroupStyles(group: HTMLElement): void {
|
||||
group.style.flexWrap = "nowrap";
|
||||
}
|
||||
|
||||
function applySpreadMetricSelectorStyles(selector: HTMLElement): void {
|
||||
selector.style.display = "flex";
|
||||
selector.style.alignItems = "center";
|
||||
selector.style.gap = "12px";
|
||||
selector.style.flex = "0 0 auto";
|
||||
selector.style.whiteSpace = "nowrap";
|
||||
function applyToolbarActionGroupStyles(group: HTMLElement): void {
|
||||
group.style.display = "flex";
|
||||
group.style.alignItems = "center";
|
||||
group.style.gap = "8px";
|
||||
group.style.flex = "0 0 auto";
|
||||
group.style.flexWrap = "nowrap";
|
||||
}
|
||||
|
||||
function applySpreadMetricOptionStyles(option: HTMLElement): void {
|
||||
option.style.display = "inline-flex";
|
||||
option.style.alignItems = "center";
|
||||
option.style.gap = "5px";
|
||||
option.style.height = "32px";
|
||||
option.style.color = "#344054";
|
||||
option.style.fontSize = "12px";
|
||||
option.style.fontWeight = "700";
|
||||
option.style.whiteSpace = "nowrap";
|
||||
function applyToolbarActionDividerStyles(divider: HTMLElement): void {
|
||||
divider.style.width = "1px";
|
||||
divider.style.height = "20px";
|
||||
divider.style.background = "#d0d5dd";
|
||||
divider.style.flex = "0 0 auto";
|
||||
}
|
||||
|
||||
function applyMetricCatalogStyles(catalog: HTMLElement): void {
|
||||
catalog.style.position = "relative";
|
||||
catalog.style.display = "inline-flex";
|
||||
catalog.style.alignItems = "center";
|
||||
catalog.style.gap = "8px";
|
||||
catalog.style.flex = "0 0 auto";
|
||||
}
|
||||
|
||||
function applyMetricCatalogTriggerStyles(button: HTMLButtonElement): void {
|
||||
button.style.height = "32px";
|
||||
button.style.padding = "0 10px";
|
||||
button.style.border = "1px solid #94a3b8";
|
||||
button.style.borderRadius = "6px";
|
||||
button.style.background = "#ffffff";
|
||||
button.style.color = "#334155";
|
||||
button.style.fontSize = "12px";
|
||||
button.style.fontWeight = "700";
|
||||
button.style.whiteSpace = "nowrap";
|
||||
}
|
||||
|
||||
function applyMetricCatalogCountStyles(count: HTMLElement): void {
|
||||
count.style.color = "#64748b";
|
||||
count.style.fontSize = "12px";
|
||||
count.style.fontWeight = "700";
|
||||
count.style.whiteSpace = "nowrap";
|
||||
}
|
||||
|
||||
function applyMetricCatalogPanelStyles(panel: HTMLElement): void {
|
||||
panel.style.position = "absolute";
|
||||
panel.style.top = "calc(100% + 6px)";
|
||||
panel.style.left = "0";
|
||||
panel.style.zIndex = "4";
|
||||
panel.style.width = "260px";
|
||||
panel.style.padding = "10px";
|
||||
panel.style.border = "1px solid #cbd5e1";
|
||||
panel.style.borderRadius = "6px";
|
||||
panel.style.background = "#ffffff";
|
||||
panel.style.boxShadow = "0 10px 20px rgba(15, 23, 42, 0.14)";
|
||||
}
|
||||
|
||||
function applyMetricCatalogHeaderStyles(header: HTMLElement): void {
|
||||
header.style.display = "flex";
|
||||
header.style.alignItems = "center";
|
||||
header.style.gap = "6px";
|
||||
}
|
||||
|
||||
function applyMetricCatalogSearchStyles(input: HTMLInputElement): void {
|
||||
input.style.width = "100%";
|
||||
input.style.height = "30px";
|
||||
input.style.padding = "0 8px";
|
||||
input.style.border = "1px solid #cbd5e1";
|
||||
input.style.borderRadius = "5px";
|
||||
input.style.boxSizing = "border-box";
|
||||
}
|
||||
|
||||
function applyMetricCatalogCloseButtonStyles(button: HTMLButtonElement): void {
|
||||
button.style.width = "30px";
|
||||
button.style.height = "30px";
|
||||
button.style.padding = "0";
|
||||
button.style.border = "0";
|
||||
button.style.background = "transparent";
|
||||
button.style.color = "#64748b";
|
||||
button.style.fontSize = "20px";
|
||||
}
|
||||
|
||||
function applyMetricCatalogGroupStyles(group: HTMLElement): void {
|
||||
group.style.display = "flex";
|
||||
group.style.flexDirection = "column";
|
||||
group.style.gap = "4px";
|
||||
group.style.marginTop = "10px";
|
||||
}
|
||||
|
||||
function applyMetricCatalogGroupTitleStyles(title: HTMLElement): void {
|
||||
title.style.color = "#64748b";
|
||||
title.style.fontSize = "12px";
|
||||
}
|
||||
|
||||
function applyMetricCatalogItemStyles(item: HTMLElement): void {
|
||||
item.style.display = "flex";
|
||||
item.style.alignItems = "center";
|
||||
item.style.justifyContent = "space-between";
|
||||
item.style.minHeight = "32px";
|
||||
item.style.color = "#334155";
|
||||
item.style.fontSize = "13px";
|
||||
}
|
||||
|
||||
function applyMetricCatalogActionStyles(button: HTMLButtonElement): void {
|
||||
button.style.minWidth = "56px";
|
||||
button.style.height = "28px";
|
||||
button.style.border = "1px solid #94a3b8";
|
||||
button.style.borderRadius = "5px";
|
||||
button.style.background = "#ffffff";
|
||||
button.style.color = "#334155";
|
||||
button.style.fontSize = "12px";
|
||||
}
|
||||
|
||||
function applySpreadRulesGroupStyles(group: HTMLElement): void {
|
||||
@@ -884,9 +1320,46 @@ function applySpreadMetricRuleStyles(rule: HTMLElement): void {
|
||||
rule.style.display = "flex";
|
||||
rule.style.alignItems = "center";
|
||||
rule.style.gap = "7px";
|
||||
rule.style.minWidth = "max-content";
|
||||
rule.style.flexWrap = "wrap";
|
||||
rule.style.minWidth = "0";
|
||||
rule.style.minHeight = "32px";
|
||||
rule.style.whiteSpace = "nowrap";
|
||||
}
|
||||
|
||||
function applySpreadMetricRulePrimaryStyles(primary: HTMLElement): void {
|
||||
primary.style.display = "flex";
|
||||
primary.style.alignItems = "center";
|
||||
primary.style.gap = "7px";
|
||||
primary.style.minWidth = "0";
|
||||
primary.style.whiteSpace = "nowrap";
|
||||
}
|
||||
|
||||
function applySpreadMetricRuleSecondaryStyles(secondary: HTMLElement): void {
|
||||
secondary.style.display = "flex";
|
||||
secondary.style.alignItems = "center";
|
||||
secondary.style.gap = "7px";
|
||||
secondary.style.minWidth = "0";
|
||||
secondary.style.whiteSpace = "nowrap";
|
||||
}
|
||||
|
||||
function applySpreadMetricDetailsButtonStyles(button: HTMLButtonElement): void {
|
||||
button.style.height = "28px";
|
||||
button.style.padding = "0 8px";
|
||||
button.style.border = "1px solid #cbd5e1";
|
||||
button.style.borderRadius = "5px";
|
||||
button.style.background = "#ffffff";
|
||||
button.style.color = "#475569";
|
||||
button.style.fontSize = "12px";
|
||||
}
|
||||
|
||||
function applySpreadMetricRemoveButtonStyles(button: HTMLButtonElement): void {
|
||||
button.style.width = "28px";
|
||||
button.style.height = "28px";
|
||||
button.style.padding = "0";
|
||||
button.style.border = "0";
|
||||
button.style.borderRadius = "4px";
|
||||
button.style.background = "transparent";
|
||||
button.style.color = "#b91c1c";
|
||||
button.style.fontSize = "20px";
|
||||
}
|
||||
|
||||
function applySpreadMetricRuleLabelStyles(label: HTMLElement): void {
|
||||
@@ -953,16 +1426,18 @@ function applyNativeControlStyles(
|
||||
controls.batchSubmitButton.className = nativeButton.className;
|
||||
}
|
||||
|
||||
[
|
||||
const secondaryButtons = [
|
||||
controls.exportButton,
|
||||
controls.audienceProfileExportButton,
|
||||
controls.audienceProfileByIdExportButton,
|
||||
controls.audienceProfileFieldButton,
|
||||
controls.batchSubmitButton
|
||||
].forEach((button) => {
|
||||
applyPrimaryButtonStyles(button);
|
||||
controls.audienceProfileFieldButton
|
||||
];
|
||||
secondaryButtons.forEach((button) => {
|
||||
applySecondaryButtonStyles(button);
|
||||
button.style.whiteSpace = "nowrap";
|
||||
});
|
||||
applyPrimaryButtonStyles(controls.batchSubmitButton);
|
||||
controls.batchSubmitButton.style.whiteSpace = "nowrap";
|
||||
|
||||
const ruleControls = Object.values(controls.spreadMetricRules).flatMap(
|
||||
(rule) => [
|
||||
@@ -1037,6 +1512,19 @@ function applyPrimaryButtonStyles(
|
||||
"background-color 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease";
|
||||
}
|
||||
|
||||
function applySecondaryButtonStyles(button: HTMLButtonElement): void {
|
||||
button.style.backgroundColor = "#ffffff";
|
||||
button.style.border = "1px solid #cbd5e1";
|
||||
button.style.borderRadius = "8px";
|
||||
button.style.color = "#344054";
|
||||
button.style.height = "32px";
|
||||
button.style.padding = "0 15px";
|
||||
button.style.boxSizing = "border-box";
|
||||
button.style.fontWeight = "600";
|
||||
button.style.transition =
|
||||
"background-color 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease";
|
||||
}
|
||||
|
||||
function applySpreadThresholdControlStyles(control: HTMLElement): void {
|
||||
control.style.display = "grid";
|
||||
control.style.gridTemplateColumns = "auto auto 1fr auto";
|
||||
@@ -1072,7 +1560,11 @@ function ensurePluginActionButtonTheme(document: Document): void {
|
||||
[data-plugin-export="button"]:hover:not(:disabled),
|
||||
[data-plugin-export-audience-profile="button"]:hover:not(:disabled),
|
||||
[data-plugin-export-audience-profile-by-id="button"]:hover:not(:disabled),
|
||||
[data-plugin-audience-profile-fields="button"]:hover:not(:disabled),
|
||||
[data-plugin-audience-profile-fields="button"]:hover:not(:disabled) {
|
||||
background-color: #f8fafc !important;
|
||||
border-color: #94a3b8 !important;
|
||||
}
|
||||
|
||||
[data-plugin-batch-submit="button"]:hover:not(:disabled) {
|
||||
background-color: #6d1627 !important;
|
||||
border-color: #6d1627 !important;
|
||||
@@ -1081,7 +1573,12 @@ function ensurePluginActionButtonTheme(document: Document): void {
|
||||
[data-plugin-export="button"]:active:not(:disabled),
|
||||
[data-plugin-export-audience-profile="button"]:active:not(:disabled),
|
||||
[data-plugin-export-audience-profile-by-id="button"]:active:not(:disabled),
|
||||
[data-plugin-audience-profile-fields="button"]:active:not(:disabled),
|
||||
[data-plugin-audience-profile-fields="button"]:active:not(:disabled) {
|
||||
background-color: #f1f5f9 !important;
|
||||
border-color: #94a3b8 !important;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
[data-plugin-batch-submit="button"]:active:not(:disabled) {
|
||||
background-color: #58111f !important;
|
||||
border-color: #58111f !important;
|
||||
@@ -1091,7 +1588,11 @@ function ensurePluginActionButtonTheme(document: Document): void {
|
||||
[data-plugin-export="button"]:focus-visible,
|
||||
[data-plugin-export-audience-profile="button"]:focus-visible,
|
||||
[data-plugin-export-audience-profile-by-id="button"]:focus-visible,
|
||||
[data-plugin-audience-profile-fields="button"]:focus-visible,
|
||||
[data-plugin-audience-profile-fields="button"]:focus-visible {
|
||||
outline: none !important;
|
||||
box-shadow: 0 0 0 3px rgba(52, 64, 84, 0.18) !important;
|
||||
}
|
||||
|
||||
[data-plugin-batch-submit="button"]:focus-visible {
|
||||
outline: none !important;
|
||||
box-shadow: 0 0 0 3px rgba(127, 29, 45, 0.2) !important;
|
||||
@@ -1100,7 +1601,16 @@ function ensurePluginActionButtonTheme(document: Document): void {
|
||||
[data-plugin-export="button"]:disabled,
|
||||
[data-plugin-export-audience-profile="button"]:disabled,
|
||||
[data-plugin-export-audience-profile-by-id="button"]:disabled,
|
||||
[data-plugin-audience-profile-fields="button"]:disabled,
|
||||
[data-plugin-audience-profile-fields="button"]:disabled {
|
||||
background-color: #f8fafc !important;
|
||||
border-color: #cbd5e1 !important;
|
||||
color: #98a2b3 !important;
|
||||
cursor: not-allowed !important;
|
||||
opacity: 1 !important;
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
[data-plugin-batch-submit="button"]:disabled {
|
||||
background-color: #c89ca4 !important;
|
||||
border-color: #c89ca4 !important;
|
||||
@@ -1128,6 +1638,54 @@ function ensurePluginActionButtonTheme(document: Document): void {
|
||||
border-color: #dc2626 !important;
|
||||
box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.12) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
[data-plugin-toolbar='root'],
|
||||
[data-plugin-toolbar-row='thresholds'] {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
[data-plugin-toolbar-row='thresholds'] {
|
||||
flex-wrap: wrap !important;
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
|
||||
[data-plugin-toolbar-group='data'],
|
||||
[data-plugin-toolbar-action-group] {
|
||||
flex-wrap: wrap !important;
|
||||
}
|
||||
|
||||
[data-plugin-toolbar-action-divider] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
[data-plugin-spread-metric-catalog='root'] {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
[data-plugin-spread-metric-catalog-panel='root'] {
|
||||
position: static !important;
|
||||
width: 100% !important;
|
||||
margin-top: 6px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
[data-plugin-spread-rules='root'] {
|
||||
width: 100% !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
[data-plugin-spread-rule] {
|
||||
align-items: stretch !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
[data-plugin-spread-rule-primary],
|
||||
[data-plugin-spread-rule-secondary] {
|
||||
width: 100% !important;
|
||||
flex-wrap: wrap !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user