274 lines
8.4 KiB
TypeScript
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; |