Initial version of MatchProperties and border ajustment
This commit is contained in:
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user