Initial version of MatchProperties and border ajustment
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import MatchSizes from "./MatchSizes";
|
import MatchSizes from "./MatchSizes";
|
||||||
|
import MatchProperties from "./MatchProperties";
|
||||||
import { makeStyles, Text, Subtitle1, tokens, Theme } from "@fluentui/react-components";
|
import { makeStyles, Text, Subtitle1, tokens, Theme } from "@fluentui/react-components";
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
@@ -60,6 +61,8 @@ const App: React.FC<AppProps> = () => {
|
|||||||
Shape Tools
|
Shape Tools
|
||||||
</Subtitle1>
|
</Subtitle1>
|
||||||
<MatchSizes />
|
<MatchSizes />
|
||||||
|
<div style={{ marginTop: "8px" }}></div>
|
||||||
|
<MatchProperties />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.footer}>
|
<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",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: "8px",
|
gap: "8px",
|
||||||
marginBottom: "12px",
|
marginBottom: "2px",
|
||||||
},
|
},
|
||||||
actionButton: {
|
actionButton: {
|
||||||
justifyContent: "flex-start",
|
justifyContent: "flex-start",
|
||||||
@@ -37,7 +37,7 @@ const useStyles = makeStyles({
|
|||||||
transitionTimingFunction: "cubic-bezier(0.33, 0, 0.67, 1)",
|
transitionTimingFunction: "cubic-bezier(0.33, 0, 0.67, 1)",
|
||||||
},
|
},
|
||||||
statusContainer: {
|
statusContainer: {
|
||||||
marginTop: "12px",
|
marginTop: "4px",
|
||||||
padding: "8px 12px",
|
padding: "8px 12px",
|
||||||
fontSize: "13px",
|
fontSize: "13px",
|
||||||
borderRadius: "6px",
|
borderRadius: "6px",
|
||||||
|
|||||||
Reference in New Issue
Block a user