193 lines
5.5 KiB
TypeScript
193 lines
5.5 KiB
TypeScript
import type { CSSProperties } from "react";
|
|
import type { ListBlockProps } from "@/features/editor/state/editorStore";
|
|
|
|
export interface ListExportTokens {
|
|
Tag: "ul" | "ol";
|
|
items: string[];
|
|
listStyleParts: string[];
|
|
gapEm: number;
|
|
}
|
|
|
|
export const computeListExportTokens = (props: ListBlockProps): ListExportTokens => {
|
|
const ordered = Boolean(props.ordered);
|
|
const Tag: "ul" | "ol" = ordered ? "ol" : "ul";
|
|
|
|
const items: string[] = [];
|
|
|
|
if (Array.isArray(props.itemsTree) && props.itemsTree.length > 0) {
|
|
const walk = (nodes: { text?: string; children?: any[] }[]) => {
|
|
for (const node of nodes) {
|
|
if (typeof node.text === "string" && node.text.trim() !== "") {
|
|
items.push(node.text);
|
|
}
|
|
if (Array.isArray(node.children) && node.children.length > 0) {
|
|
walk(node.children);
|
|
}
|
|
}
|
|
};
|
|
walk(props.itemsTree as any);
|
|
} else if (Array.isArray(props.items)) {
|
|
for (const raw of props.items) {
|
|
if (typeof raw === "string" && raw.trim() !== "") {
|
|
items.push(raw);
|
|
}
|
|
}
|
|
}
|
|
|
|
const alignToken = props.align;
|
|
const align = alignToken === "center" ? "center" : alignToken === "right" ? "right" : "left";
|
|
|
|
const listStyleParts: string[] = [`text-align:${align}`];
|
|
|
|
const bulletStyleRaw = props.bulletStyle ?? (props.ordered ? "decimal" : "disc");
|
|
const bulletStyle = bulletStyleRaw === "none" ? "none" : bulletStyleRaw;
|
|
listStyleParts.push(`list-style-type:${bulletStyle}`);
|
|
|
|
const gapPx =
|
|
typeof props.gapYPx === "number"
|
|
? props.gapYPx
|
|
: props.gapY === "sm"
|
|
? 4
|
|
: props.gapY === "lg"
|
|
? 16
|
|
: 8;
|
|
const gapEm = gapPx / 16;
|
|
|
|
// 리스트 아이템 간 간격은 CSS 커스텀 프로퍼티 --pb-list-gap 으로 전달한다.
|
|
// 이렇게 하면 margin-bottom 계산은 builder.css 레이어에서만 수행되어,
|
|
// 정적 Export HTML 의 style 속성에 직접적인 margin-bottom 이 포함되지 않는다.
|
|
listStyleParts.push(`--pb-list-gap:${gapEm}em`);
|
|
|
|
if (typeof props.backgroundColorCustom === "string" && props.backgroundColorCustom.trim() !== "") {
|
|
listStyleParts.push(`background-color:${props.backgroundColorCustom.trim()}`);
|
|
}
|
|
|
|
return {
|
|
Tag,
|
|
items,
|
|
listStyleParts,
|
|
gapEm,
|
|
};
|
|
};
|
|
|
|
export interface ListEditorTokens {
|
|
gapPx: number;
|
|
listStyle: CSSProperties;
|
|
bulletStyle: string;
|
|
}
|
|
|
|
export const computeListEditorTokens = (props: ListBlockProps): ListEditorTokens => {
|
|
const bulletStyleRaw = props.bulletStyle ?? (props.ordered ? "decimal" : "disc");
|
|
|
|
const gapPx =
|
|
typeof props.gapYPx === "number"
|
|
? props.gapYPx
|
|
: props.gapY === "sm"
|
|
? 4
|
|
: props.gapY === "lg"
|
|
? 16
|
|
: 8;
|
|
|
|
const listStyle: CSSProperties = {};
|
|
if (props.fontSizeCustom && props.fontSizeCustom.trim() !== "") {
|
|
listStyle.fontSize = props.fontSizeCustom;
|
|
}
|
|
if (props.lineHeightCustom && props.lineHeightCustom.trim() !== "") {
|
|
listStyle.lineHeight = props.lineHeightCustom;
|
|
}
|
|
if (props.textColorCustom && props.textColorCustom.trim() !== "") {
|
|
listStyle.color = props.textColorCustom;
|
|
}
|
|
|
|
const bulletStyle = bulletStyleRaw;
|
|
|
|
return {
|
|
gapPx,
|
|
listStyle,
|
|
bulletStyle,
|
|
};
|
|
};
|
|
|
|
export interface ListPublicTokens {
|
|
alignClass: string;
|
|
gapEm: number;
|
|
listStyle: CSSProperties;
|
|
bulletStyle: string;
|
|
}
|
|
|
|
const pxToEm = (px: number, base = 16) => `${px / base}em`;
|
|
|
|
const convertPxStringToEm = (value?: string | null) => {
|
|
if (!value) return null;
|
|
const match = value.trim().match(/-?\d+(?:\.\d+)?/);
|
|
if (!match) return null;
|
|
const px = parseFloat(match[0]);
|
|
if (!Number.isFinite(px)) return null;
|
|
return pxToEm(px);
|
|
};
|
|
|
|
export const computeListPublicTokens = (props: ListBlockProps): ListPublicTokens => {
|
|
const alignClass =
|
|
props.align === "center"
|
|
? "pb-text-center"
|
|
: props.align === "right"
|
|
? "pb-text-right"
|
|
: "pb-text-left";
|
|
|
|
const bulletStyleRaw = props.bulletStyle ?? (props.ordered ? "decimal" : "disc");
|
|
|
|
const gapPx =
|
|
typeof props.gapYPx === "number"
|
|
? props.gapYPx
|
|
: props.gapY === "sm"
|
|
? 4
|
|
: props.gapY === "lg"
|
|
? 16
|
|
: 8;
|
|
|
|
const gapEm = gapPx / 16;
|
|
|
|
const listStyle: CSSProperties = {};
|
|
if (props.fontSizeCustom && props.fontSizeCustom.trim() !== "") {
|
|
const fontSizeValue = props.fontSizeCustom.trim();
|
|
if (fontSizeValue.endsWith("px")) {
|
|
const fontSizeEm = convertPxStringToEm(fontSizeValue);
|
|
if (fontSizeEm) {
|
|
listStyle.fontSize = fontSizeEm;
|
|
}
|
|
} else {
|
|
listStyle.fontSize = fontSizeValue;
|
|
}
|
|
}
|
|
if (props.lineHeightCustom && props.lineHeightCustom.trim() !== "") {
|
|
const lineHeightValue = props.lineHeightCustom.trim();
|
|
if (lineHeightValue.endsWith("px")) {
|
|
const lineHeightEm = convertPxStringToEm(lineHeightValue);
|
|
if (lineHeightEm) {
|
|
listStyle.lineHeight = lineHeightEm;
|
|
}
|
|
} else {
|
|
listStyle.lineHeight = lineHeightValue;
|
|
}
|
|
}
|
|
if (props.textColorCustom && props.textColorCustom.trim() !== "") {
|
|
listStyle.color = props.textColorCustom;
|
|
}
|
|
if (props.backgroundColorCustom && props.backgroundColorCustom.trim() !== "") {
|
|
listStyle.backgroundColor = props.backgroundColorCustom.trim();
|
|
}
|
|
|
|
// 리스트 아이템 간 여백을 CSS 변수로 노출한다. 실제 margin-bottom 은 builder.css 의
|
|
// .pb-list > li 규칙에서 --pb-list-gap 값을 참조해 계산한다.
|
|
(listStyle as any)["--pb-list-gap"] = `${gapEm}em`;
|
|
|
|
const bulletStyle = bulletStyleRaw;
|
|
|
|
return {
|
|
alignClass,
|
|
gapEm,
|
|
listStyle,
|
|
bulletStyle,
|
|
};
|
|
};
|