Preserve QPowerPoint text run styles in PPTX

This commit is contained in:
Курнат Андрей
2026-06-02 07:41:11 +03:00
parent b8c347142a
commit c4532902c8
7 changed files with 228 additions and 9 deletions
+88 -5
View File
@@ -1,6 +1,6 @@
import { useRef, useState } from "react";
import type { CSSProperties } from "react";
import { Copy, Download, EyeOff, FileText, FileUp, Image as ImageIcon, Link, List, ListOrdered, ListX, Palette, Plus, Save, Trash2 } from "lucide-react";
import { Bold, Copy, Download, EyeOff, FileText, FileUp, Image as ImageIcon, Italic, Link, List, ListOrdered, ListX, Palette, Plus, Save, Trash2, Underline } from "lucide-react";
import type { Slide, SlideDeck, SlideImage, SlideListStyle, SlideTransition } from "../../types";
import { downloadBlobFile, downloadTextFile } from "../../utils/download";
import { exportPptxBlob, importPptxFile } from "../../io/powerPointOffice";
@@ -14,6 +14,8 @@ interface QPowerPointProps {
onChange: (deck: SlideDeck) => void;
}
type SlideTextFormatField = "titleBold" | "titleItalics" | "titleUnderline" | "subtitleBold" | "subtitleItalics" | "subtitleUnderline";
const themeLabels: Record<Slide["theme"], string> = {
classic: "Классическая",
ocean: "Океан",
@@ -102,14 +104,23 @@ function slideCanvasStyle(slide: Slide) {
return backgroundColor ? { background: `#${backgroundColor}` } : undefined;
}
function slideTextStyle(color?: string, fontSize?: number, fontFamily?: string): CSSProperties | undefined {
function slideTextStyle(
color?: string,
fontSize?: number,
fontFamily?: string,
textFormat: { bold?: boolean; italics?: boolean; underline?: boolean; defaultBold?: boolean } = {}
): CSSProperties | undefined {
const textColor = normalizedHexColor(color);
const normalizedFontSize = normalizeFontSize(fontSize);
const normalizedFamily = normalizedFontFamily(fontFamily);
const effectiveBold = textFormat.bold ?? textFormat.defaultBold;
const style: CSSProperties = {
...(textColor ? { color: `#${textColor}` } : {}),
...(normalizedFontSize ? { fontSize: `${normalizedFontSize}pt` } : {}),
...(normalizedFamily ? { fontFamily: normalizedFamily } : {})
...(normalizedFamily ? { fontFamily: normalizedFamily } : {}),
...(effectiveBold !== undefined ? { fontWeight: effectiveBold ? 780 : 400 } : {}),
...(textFormat.italics ? { fontStyle: "italic" } : {}),
...(textFormat.underline ? { textDecoration: "underline" } : {})
};
return Object.keys(style).length > 0 ? style : undefined;
@@ -153,6 +164,11 @@ export function QPowerPoint({ deck, onChange }: QPowerPointProps) {
});
}
function toggleSlideTextFormat(field: SlideTextFormatField, defaultValue = false) {
const enabled = Boolean(activeSlide[field] ?? defaultValue);
updateSlide({ ...activeSlide, [field]: !enabled });
}
function updateImages(images: SlideImage[]) {
updateSlide({ ...activeSlide, images });
}
@@ -497,6 +513,35 @@ export function QPowerPoint({ deck, onChange }: QPowerPointProps) {
/>
))}
</div>
<div className="cell-format-tools slide-format-tools" aria-label="Начертание заголовка">
<button
className={`icon-button format-toggle ${(activeSlide.titleBold ?? true) ? "is-active" : ""}`.trim()}
type="button"
onClick={() => toggleSlideTextFormat("titleBold", true)}
title="Полужирный заголовок"
aria-label="Полужирный заголовок"
>
<Bold size={16} />
</button>
<button
className={`icon-button format-toggle ${activeSlide.titleItalics ? "is-active" : ""}`.trim()}
type="button"
onClick={() => toggleSlideTextFormat("titleItalics")}
title="Курсив заголовка"
aria-label="Курсив заголовка"
>
<Italic size={16} />
</button>
<button
className={`icon-button format-toggle ${activeSlide.titleUnderline ? "is-active" : ""}`.trim()}
type="button"
onClick={() => toggleSlideTextFormat("titleUnderline")}
title="Подчеркнутый заголовок"
aria-label="Подчеркнутый заголовок"
>
<Underline size={16} />
</button>
</div>
<label className="office-field office-field-wide">
<span>
<Link size={14} />
@@ -553,6 +598,35 @@ export function QPowerPoint({ deck, onChange }: QPowerPointProps) {
/>
))}
</div>
<div className="cell-format-tools slide-format-tools" aria-label="Начертание подзаголовка">
<button
className={`icon-button format-toggle ${activeSlide.subtitleBold ? "is-active" : ""}`.trim()}
type="button"
onClick={() => toggleSlideTextFormat("subtitleBold")}
title="Полужирный подзаголовок"
aria-label="Полужирный подзаголовок"
>
<Bold size={16} />
</button>
<button
className={`icon-button format-toggle ${activeSlide.subtitleItalics ? "is-active" : ""}`.trim()}
type="button"
onClick={() => toggleSlideTextFormat("subtitleItalics")}
title="Курсив подзаголовка"
aria-label="Курсив подзаголовка"
>
<Italic size={16} />
</button>
<button
className={`icon-button format-toggle ${activeSlide.subtitleUnderline ? "is-active" : ""}`.trim()}
type="button"
onClick={() => toggleSlideTextFormat("subtitleUnderline")}
title="Подчеркнутый подзаголовок"
aria-label="Подчеркнутый подзаголовок"
>
<Underline size={16} />
</button>
</div>
<label className="office-field office-field-wide">
<span>
<Link size={14} />
@@ -707,14 +781,23 @@ export function QPowerPoint({ deck, onChange }: QPowerPointProps) {
className="slide-title-input"
value={activeSlide.title}
aria-label="Заголовок слайда"
style={slideTextStyle(activeSlide.titleColor, activeSlide.titleFontSize, activeSlide.titleFontFamily)}
style={slideTextStyle(activeSlide.titleColor, activeSlide.titleFontSize, activeSlide.titleFontFamily, {
bold: activeSlide.titleBold,
italics: activeSlide.titleItalics,
underline: activeSlide.titleUnderline,
defaultBold: true
})}
onChange={(event) => updateSlide({ ...activeSlide, title: event.target.value })}
/>
<textarea
className={`slide-subtitle-input ${activeSlide.subtitleListStyle ? `is-${activeSlide.subtitleListStyle}-list` : ""}`.trim()}
value={activeSlide.subtitle}
aria-label="Подзаголовок слайда"
style={slideTextStyle(activeSlide.subtitleColor, activeSlide.subtitleFontSize, activeSlide.subtitleFontFamily)}
style={slideTextStyle(activeSlide.subtitleColor, activeSlide.subtitleFontSize, activeSlide.subtitleFontFamily, {
bold: activeSlide.subtitleBold,
italics: activeSlide.subtitleItalics,
underline: activeSlide.subtitleUnderline
})}
onChange={(event) => updateSlide({ ...activeSlide, subtitle: event.target.value })}
/>
<div className="slide-accent-line" />