Initial version of MatchProperties and border ajustment

This commit is contained in:
2025-03-07 23:54:15 +01:00
parent e4589ee252
commit a9c568a20d
3 changed files with 227 additions and 2 deletions
+3
View File
@@ -1,6 +1,7 @@
import * as React from "react";
import { useEffect, useState } from "react";
import MatchSizes from "./MatchSizes";
import MatchProperties from "./MatchProperties";
import { makeStyles, Text, Subtitle1, tokens, Theme } from "@fluentui/react-components";
interface AppProps {
@@ -60,6 +61,8 @@ const App: React.FC<AppProps> = () => {
Shape Tools
</Subtitle1>
<MatchSizes />
<div style={{ marginTop: "8px" }}></div>
<MatchProperties />
</div>
<div className={styles.footer}>
+222
View File
@@ -0,0 +1,222 @@
import * as React from "react";
import {
Button,
makeStyles,
Text,
Spinner,
tokens
} from "@fluentui/react-components";
import {
ColorRegular,
SquareRegular,
ShapeUnionRegular,
InfoRegular
} from "@fluentui/react-icons";
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] = React.useState("");
const [statusType, setStatusType] = React.useState<"info" | "success" | "warning" | "error">("info");
const [isProcessing, setIsProcessing] = React.useState(false);
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 properties we need from the first shape
firstShape.load([
"lineFormat/weight",
"lineFormat/dashStyle",
"fill/transparency"
]);
await context.sync();
// Loop through remaining shapes and apply properties
for (let i = 1; i < shapes.items.length; i++) {
const targetShape = shapes.items[i];
// Copy line properties that are available in the Office JS API
try {
// Line weight (width)
targetShape.lineFormat.weight = firstShape.lineFormat.weight;
// Line style (dash type)
targetShape.lineFormat.dashStyle = firstShape.lineFormat.dashStyle;
// Fill transparency
targetShape.fill.transparency = firstShape.fill.transparency;
} catch (err) {
console.error("Error copying basic properties:", err);
}
// Check if first shape has text frame
try {
// Get text ranges to copy font properties
const sourceTextRange = firstShape.textFrame.textRange;
const targetTextRange = targetShape.textFrame.textRange;
// Load font properties from source
sourceTextRange.load([
"font/name",
"font/size",
"font/bold",
"font/italic",
"font/underline"
]);
await context.sync();
// Copy font properties
targetTextRange.font.name = sourceTextRange.font.name;
targetTextRange.font.size = sourceTextRange.font.size;
targetTextRange.font.bold = sourceTextRange.font.bold;
targetTextRange.font.italic = sourceTextRange.font.italic;
targetTextRange.font.underline = sourceTextRange.font.underline;
} catch (err) {
// Silently fail if text properties can't be copied
// This could happen if shape doesn't have text frame
console.error("Error copying text properties:", err);
}
}
await context.sync();
setStatusMessage(`Copied properties from the first shape to ${shapes.items.length - 1} other shapes.`);
setStatusType("success");
// Auto-clear success message after 5 seconds
setTimeout(() => {
if (statusType === "success") {
setStatusMessage("");
}
}, 5000);
});
} catch (error) {
setStatusMessage(`Error: ${error.message}`);
setStatusType("error");
console.error(error);
} finally {
setIsProcessing(false);
}
};
const getStatusIcon = () => {
switch (statusType) {
case "success":
return <ShapeUnionRegular className={styles.statusIcon} />;
case "warning":
return <SquareRegular className={styles.statusIcon} />;
case "error":
return <InfoRegular className={styles.statusIcon} />;
default:
return null;
}
};
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>
{isProcessing && (
<div className={styles.statusContainer}>
<div className={styles.statusText}>
<Spinner size="tiny" style={{ marginRight: "8px" }} />
Applying properties...
</div>
</div>
)}
{!isProcessing && statusMessage && (
<div className={`${styles.statusContainer} ${styles[`${statusType}Status`]}`}>
<div className={styles.statusText}>
{getStatusIcon()}
{statusMessage}
</div>
</div>
)}
</div>
);
};
export default MatchProperties;
+2 -2
View File
@@ -28,7 +28,7 @@ const useStyles = makeStyles({
display: "flex",
flexDirection: "column",
gap: "8px",
marginBottom: "12px",
marginBottom: "2px",
},
actionButton: {
justifyContent: "flex-start",
@@ -37,7 +37,7 @@ const useStyles = makeStyles({
transitionTimingFunction: "cubic-bezier(0.33, 0, 0.67, 1)",
},
statusContainer: {
marginTop: "12px",
marginTop: "4px",
padding: "8px 12px",
fontSize: "13px",
borderRadius: "6px",