Refactor MatchProperties component for better maintainability and performance
This commit is contained in:
@@ -1,147 +1,214 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
Button
|
||||
} from "@fluentui/react-components";
|
||||
import {
|
||||
ColorRegular
|
||||
} from "@fluentui/react-icons";
|
||||
import { ColorRegular } from "@fluentui/react-icons";
|
||||
import { useStatusContext } from "./App";
|
||||
import { useCommonStyles } from "./commonStyles";
|
||||
import ActionButton from "./ActionButton";
|
||||
import { getErrorMessage } from "../types/office-types";
|
||||
|
||||
export const MatchProperties: React.FC = () => {
|
||||
const styles = useCommonStyles();
|
||||
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;
|
||||
// Property types that can be matched
|
||||
enum PropertyType {
|
||||
Line = "line",
|
||||
Fill = "fill",
|
||||
Text = "text"
|
||||
}
|
||||
|
||||
// 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([
|
||||
// Shape property loading configuration
|
||||
const SHAPE_PROPERTIES = {
|
||||
line: [
|
||||
"lineFormat/weight",
|
||||
"lineFormat/dashStyle",
|
||||
"lineFormat/color",
|
||||
"lineFormat/color"
|
||||
],
|
||||
fill: [
|
||||
"fill/transparency",
|
||||
"fill/foregroundColor",
|
||||
"fill/foregroundColor"
|
||||
],
|
||||
text: [
|
||||
"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 property loading configuration
|
||||
const FONT_PROPERTIES = [
|
||||
"font/name",
|
||||
"font/size",
|
||||
"font/bold",
|
||||
"font/italic",
|
||||
"font/underline",
|
||||
"font/color"
|
||||
]);
|
||||
];
|
||||
|
||||
export const MatchProperties: React.FC = () => {
|
||||
const styles = useCommonStyles();
|
||||
const { setStatusMessage, setStatusType } = useStatusContext();
|
||||
|
||||
/**
|
||||
* Validates that multiple shapes are selected
|
||||
* @param shapes The collection of selected shapes
|
||||
* @returns True if validation passes, false otherwise
|
||||
*/
|
||||
const validateShapeSelection = (shapes: PowerPoint.ShapeScopedCollection): boolean => {
|
||||
if (shapes.items.length === 0) {
|
||||
setStatusMessage("No shapes are selected. Please select shapes first.");
|
||||
setStatusType("warning");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (shapes.items.length === 1) {
|
||||
setStatusMessage("Please select multiple shapes to copy properties.");
|
||||
setStatusType("warning");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads all necessary properties from the source shape
|
||||
* @param sourceShape The shape to copy properties from
|
||||
* @param context The PowerPoint request context
|
||||
* @returns Object containing loaded property information
|
||||
*/
|
||||
const loadSourceProperties = async (
|
||||
sourceShape: PowerPoint.Shape,
|
||||
context: PowerPoint.RequestContext
|
||||
): Promise<{ hasText: boolean }> => {
|
||||
// Load basic shape properties
|
||||
const propertiesToLoad = [
|
||||
...SHAPE_PROPERTIES.line,
|
||||
...SHAPE_PROPERTIES.fill,
|
||||
...SHAPE_PROPERTIES.text
|
||||
];
|
||||
|
||||
sourceShape.load(propertiesToLoad);
|
||||
await context.sync();
|
||||
|
||||
// Check if the shape has text and load text properties if it does
|
||||
let hasText = false;
|
||||
if (sourceShape.textFrame) {
|
||||
sourceShape.textFrame.load("textRange");
|
||||
await context.sync();
|
||||
|
||||
if (sourceShape.textFrame.textRange) {
|
||||
sourceShape.textFrame.textRange.load(FONT_PROPERTIES);
|
||||
await context.sync();
|
||||
hasText = true;
|
||||
}
|
||||
}
|
||||
|
||||
// First, load all text frames in a batch
|
||||
if (hasText) {
|
||||
// Pre-load textFrame for all target shapes
|
||||
return { hasText };
|
||||
};
|
||||
|
||||
/**
|
||||
* Preloads text properties for all target shapes in batches
|
||||
* @param shapes The collection of shapes
|
||||
* @param hasText Whether the source shape has text
|
||||
* @param context The PowerPoint request context
|
||||
*/
|
||||
const preloadTargetTextProperties = async (
|
||||
shapes: PowerPoint.ShapeScopedCollection,
|
||||
hasText: boolean,
|
||||
context: PowerPoint.RequestContext
|
||||
): Promise<void> => {
|
||||
if (!hasText) return;
|
||||
|
||||
// Batch 1: Load textFrame for all target shapes
|
||||
for (let i = 1; i < shapes.items.length; i++) {
|
||||
shapes.items[i].load("textFrame");
|
||||
}
|
||||
await context.sync();
|
||||
|
||||
// For shapes that have textFrames, load their textRanges
|
||||
// Batch 2: For shapes that have textFrames, load their textRanges
|
||||
for (let i = 1; i < shapes.items.length; i++) {
|
||||
if (shapes.items[i].textFrame) {
|
||||
shapes.items[i].textFrame.load("textRange");
|
||||
}
|
||||
}
|
||||
await context.sync();
|
||||
}
|
||||
};
|
||||
|
||||
// Now apply properties to all shapes
|
||||
let successCount = 0;
|
||||
let textStyleCount = 0;
|
||||
|
||||
for (let i = 1; i < shapes.items.length; i++) {
|
||||
try {
|
||||
const targetShape = shapes.items[i];
|
||||
/**
|
||||
* Applies line properties from source to target shape
|
||||
* @param sourceShape The shape to copy properties from
|
||||
* @param targetShape The shape to apply properties to
|
||||
* @returns True if any properties were applied
|
||||
*/
|
||||
const applyLineProperties = (
|
||||
sourceShape: PowerPoint.Shape,
|
||||
targetShape: PowerPoint.Shape
|
||||
): boolean => {
|
||||
let propertiesApplied = false;
|
||||
|
||||
// Apply line formatting properties
|
||||
try {
|
||||
// Line weight
|
||||
if (firstShape.lineFormat.weight !== undefined) {
|
||||
targetShape.lineFormat.weight = firstShape.lineFormat.weight;
|
||||
if (sourceShape.lineFormat.weight !== undefined) {
|
||||
targetShape.lineFormat.weight = sourceShape.lineFormat.weight;
|
||||
propertiesApplied = true;
|
||||
}
|
||||
|
||||
// Line style
|
||||
if (firstShape.lineFormat.dashStyle !== undefined) {
|
||||
targetShape.lineFormat.dashStyle = firstShape.lineFormat.dashStyle;
|
||||
if (sourceShape.lineFormat.dashStyle !== undefined) {
|
||||
targetShape.lineFormat.dashStyle = sourceShape.lineFormat.dashStyle;
|
||||
propertiesApplied = true;
|
||||
}
|
||||
|
||||
// Line color
|
||||
if (firstShape.lineFormat.color !== undefined) {
|
||||
targetShape.lineFormat.color = firstShape.lineFormat.color;
|
||||
}
|
||||
|
||||
if (sourceShape.lineFormat.color !== undefined) {
|
||||
targetShape.lineFormat.color = sourceShape.lineFormat.color;
|
||||
propertiesApplied = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error applying line format to shape ${i}:`, err);
|
||||
console.error("Error applying line format:", getErrorMessage(err));
|
||||
}
|
||||
|
||||
// Apply fill properties
|
||||
return propertiesApplied;
|
||||
};
|
||||
|
||||
/**
|
||||
* Applies fill properties from source to target shape
|
||||
* @param sourceShape The shape to copy properties from
|
||||
* @param targetShape The shape to apply properties to
|
||||
* @returns True if any properties were applied
|
||||
*/
|
||||
const applyFillProperties = (
|
||||
sourceShape: PowerPoint.Shape,
|
||||
targetShape: PowerPoint.Shape
|
||||
): boolean => {
|
||||
let propertiesApplied = false;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (sourceShape.fill.transparency !== undefined) {
|
||||
targetShape.fill.transparency = sourceShape.fill.transparency;
|
||||
propertiesApplied = true;
|
||||
} catch (err) {
|
||||
console.error(`Error applying fill format to shape ${i}:`, err);
|
||||
}
|
||||
|
||||
// Apply text properties if the source has text
|
||||
if (hasText && targetShape.textFrame && targetShape.textFrame.textRange) {
|
||||
// Fill color
|
||||
if (sourceShape.fill.foregroundColor !== undefined) {
|
||||
targetShape.fill.foregroundColor = sourceShape.fill.foregroundColor;
|
||||
propertiesApplied = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error applying fill format:", getErrorMessage(err));
|
||||
}
|
||||
|
||||
return propertiesApplied;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Applies text properties from source to target shape
|
||||
* @param sourceShape The shape to copy properties from
|
||||
* @param targetShape The shape to apply properties to
|
||||
* @returns True if text properties were applied
|
||||
*/
|
||||
const applyTextProperties = (
|
||||
sourceShape: PowerPoint.Shape,
|
||||
targetShape: PowerPoint.Shape
|
||||
): boolean => {
|
||||
try {
|
||||
const sourceFont = firstShape.textFrame.textRange.font;
|
||||
if (sourceShape.textFrame?.textRange && targetShape.textFrame?.textRange) {
|
||||
const sourceFont = sourceShape.textFrame.textRange.font;
|
||||
const targetFont = targetShape.textFrame.textRange.font;
|
||||
|
||||
// Apply font properties in batch
|
||||
@@ -169,60 +236,102 @@ export const MatchProperties: React.FC = () => {
|
||||
targetFont.color = sourceFont.color;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error applying text format:", getErrorMessage(err));
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Main function to match properties from the first selected shape to others
|
||||
*/
|
||||
const matchPropertiesToFirstSelected = async (): Promise<void> => {
|
||||
await PowerPoint.run(async (context) => {
|
||||
// Get the selected shapes
|
||||
const shapes = context.presentation.getSelectedShapes();
|
||||
shapes.load("items");
|
||||
await context.sync();
|
||||
|
||||
// Validate shape selection
|
||||
if (!validateShapeSelection(shapes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the first shape to use as template
|
||||
const sourceShape = shapes.items[0];
|
||||
|
||||
// Load all necessary properties from the source shape
|
||||
const { hasText } = await loadSourceProperties(sourceShape, context);
|
||||
|
||||
// Preload text properties for target shapes if needed
|
||||
await preloadTargetTextProperties(shapes, hasText, context);
|
||||
|
||||
// Apply properties to all target shapes
|
||||
let successCount = 0;
|
||||
let textStyleCount = 0;
|
||||
|
||||
for (let i = 1; i < shapes.items.length; i++) {
|
||||
const targetShape = shapes.items[i];
|
||||
let shapeUpdated = false;
|
||||
|
||||
// Apply different property types
|
||||
const lineApplied = applyLineProperties(sourceShape, targetShape);
|
||||
const fillApplied = applyFillProperties(sourceShape, targetShape);
|
||||
|
||||
// Apply text properties if available
|
||||
if (hasText) {
|
||||
const textApplied = applyTextProperties(sourceShape, targetShape);
|
||||
if (textApplied) {
|
||||
textStyleCount++;
|
||||
} catch (err) {
|
||||
console.error(`Error applying text format to shape ${i}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
if (propertiesApplied) {
|
||||
// Count successful shape updates
|
||||
if (lineApplied || fillApplied) {
|
||||
shapeUpdated = true;
|
||||
}
|
||||
|
||||
if (shapeUpdated) {
|
||||
successCount++;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(`Error updating shape ${i}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// Single sync after all property changes
|
||||
await context.sync();
|
||||
|
||||
// Final status message based on what was applied
|
||||
// Generate appropriate status message
|
||||
if (successCount > 0) {
|
||||
let message = `Applied properties to ${successCount} shapes`;
|
||||
const propertyTypes = [];
|
||||
|
||||
// Build a detailed status message
|
||||
if (sourceShape.lineFormat) propertyTypes.push("line");
|
||||
if (sourceShape.fill) propertyTypes.push("fill");
|
||||
|
||||
let message = `Applied ${propertyTypes.join(", ")} properties to ${successCount} shapes`;
|
||||
if (textStyleCount > 0) {
|
||||
message += ` (including text styling on ${textStyleCount})`;
|
||||
}
|
||||
|
||||
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}
|
||||
<ActionButton
|
||||
title="Match Properties"
|
||||
icon={<ColorRegular />}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
Match Properties
|
||||
</Button>
|
||||
onClick={matchPropertiesToFirstSelected}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user