Files
powerpoint-toolbox/src/taskpane/components/MatchProperties.tsx
T
2025-03-08 00:30:43 +01:00

274 lines
8.4 KiB
TypeScript

import * as React from "react";
import {
Button,
makeStyles,
tokens
} from "@fluentui/react-components";
import {
ColorRegular
} from "@fluentui/react-icons";
import { useStatusContext } from "./App";
const useStyles = makeStyles({
container: {
display: "flex",
flexDirection: "column",
width: "100%",
},
buttonGroup: {
display: "flex",
flexDirection: "column",
gap: "8px",
marginBottom: "2px",
},
actionButton: {
justifyContent: "flex-start",
transitionProperty: "all",
transitionDuration: "200ms",
transitionTimingFunction: "cubic-bezier(0.33, 0, 0.67, 1)",
},
statusContainer: {
marginTop: "4px",
padding: "8px 12px",
fontSize: "13px",
borderRadius: "6px",
backgroundColor: "#f3f2f1", // Light gray background
transition: "all 0.3s ease",
},
successStatus: {
backgroundColor: "#DFF6DD", // Light green background
color: "#107C10", // Green text
},
warningStatus: {
backgroundColor: "#FFF4CE", // Light yellow background
color: "#797673", // Dark gray text
},
errorStatus: {
backgroundColor: "#FDE7E9", // Light red background
color: "#A80000", // Red text
},
statusIcon: {
marginRight: "8px",
},
statusText: {
display: "flex",
alignItems: "center",
},
});
export const MatchProperties: React.FC = () => {
const styles = useStyles();
const {
statusMessage, setStatusMessage,
statusType, setStatusType,
isProcessing, setIsProcessing
} = useStatusContext();
const matchPropertiesToFirstSelected = async () => {
setIsProcessing(true);
try {
await PowerPoint.run(async (context) => {
// Get the selected shapes
const shapes = context.presentation.getSelectedShapes();
shapes.load("items");
await context.sync();
// Check if shapes are selected
if (shapes.items.length === 0) {
setStatusMessage("No shapes are selected. Please select shapes first.");
setStatusType("warning");
return;
}
// Check if there is more than one shape selected
if (shapes.items.length === 1) {
setStatusMessage("Please select multiple shapes to copy properties.");
setStatusType("warning");
return;
}
// Get the first shape to use as template
const firstShape = shapes.items[0];
// Load all necessary properties from the first shape
firstShape.load([
"lineFormat/weight",
"lineFormat/dashStyle",
"lineFormat/color",
"fill/transparency",
"fill/foregroundColor",
"textFrame"
]);
await context.sync();
// Check if the shape has text and load text properties
let hasText = false;
if (firstShape.textFrame) {
firstShape.textFrame.load("textRange");
await context.sync();
if (firstShape.textFrame.textRange) {
firstShape.textFrame.textRange.load([
"font/name",
"font/size",
"font/bold",
"font/italic",
"font/underline",
"font/color"
]);
await context.sync();
hasText = true;
}
}
let successCount = 0;
let textStyleCount = 0;
// Loop through remaining shapes and apply properties
for (let i = 1; i < shapes.items.length; i++) {
try {
const targetShape = shapes.items[i];
let propertiesApplied = false;
// Apply line formatting properties
try {
// Line weight
if (firstShape.lineFormat.weight !== undefined) {
targetShape.lineFormat.weight = firstShape.lineFormat.weight;
}
// Line style
if (firstShape.lineFormat.dashStyle !== undefined) {
targetShape.lineFormat.dashStyle = firstShape.lineFormat.dashStyle;
}
// Line color
if (firstShape.lineFormat.color !== undefined) {
targetShape.lineFormat.color = firstShape.lineFormat.color;
}
await context.sync();
propertiesApplied = true;
} catch (err) {
console.error(`Error applying line format to shape ${i}:`, err);
}
// Apply fill properties
try {
// Fill transparency
if (firstShape.fill.transparency !== undefined) {
targetShape.fill.transparency = firstShape.fill.transparency;
}
if (firstShape.fill.foregroundColor !== undefined) {
targetShape.fill.foregroundColor = firstShape.fill.foregroundColor;
}
await context.sync();
propertiesApplied = true;
} catch (err) {
console.error(`Error applying fill format to shape ${i}:`, err);
}
// Apply text properties if the source has text
if (hasText) {
try {
// First check if target shape has text
targetShape.load("textFrame");
await context.sync();
if (targetShape.textFrame) {
targetShape.textFrame.load("textRange");
await context.sync();
if (targetShape.textFrame.textRange) {
const sourceFont = firstShape.textFrame.textRange.font;
const targetFont = targetShape.textFrame.textRange.font;
// Apply font properties
if (sourceFont.name !== undefined) {
targetFont.name = sourceFont.name;
}
if (sourceFont.size !== undefined) {
targetFont.size = sourceFont.size;
}
if (sourceFont.bold !== undefined) {
targetFont.bold = sourceFont.bold;
}
if (sourceFont.italic !== undefined) {
targetFont.italic = sourceFont.italic;
}
if (sourceFont.underline !== undefined) {
targetFont.underline = sourceFont.underline;
}
if (sourceFont.color !== undefined) {
targetFont.color = sourceFont.color;
}
await context.sync();
textStyleCount++;
}
}
} catch (err) {
console.error(`Error applying text format to shape ${i}:`, err);
}
}
if (propertiesApplied) {
successCount++;
}
} catch (err) {
console.error(`Error updating shape ${i}:`, err);
}
}
// Final status message based on what was applied
if (successCount > 0) {
let message = `Applied properties to ${successCount} shapes`;
if (textStyleCount > 0) {
message += ` (including text styling on ${textStyleCount})`;
}
message += ". Note: Fill colors may not be fully supported in the Office JS API.";
setStatusMessage(message);
setStatusType("success");
} else {
setStatusMessage("Couldn't apply properties. Try selecting different shapes.");
setStatusType("error");
}
// Timeout is handled in App.tsx now
});
} catch (error) {
setStatusMessage(`Error: ${error.message}`);
setStatusType("error");
console.error("Main error:", error);
} finally {
setIsProcessing(false);
}
};
return (
<div className={styles.container}>
<div className={styles.buttonGroup}>
<Button
appearance="primary"
className={styles.actionButton}
onClick={matchPropertiesToFirstSelected}
icon={<ColorRegular />}
disabled={isProcessing}
>
Match Properties
</Button>
</div>
</div>
);
};
export default MatchProperties;