Refactor MatchProperties component for better maintainability and performance

This commit is contained in:
2025-03-22 15:50:19 +01:00
parent 3dc9afea50
commit 04c86c6162
+225 -116
View File
@@ -1,147 +1,214 @@
import * as React from "react"; import * as React from "react";
import { import { ColorRegular } from "@fluentui/react-icons";
Button
} from "@fluentui/react-components";
import {
ColorRegular
} from "@fluentui/react-icons";
import { useStatusContext } from "./App"; import { useStatusContext } from "./App";
import { useCommonStyles } from "./commonStyles"; import { useCommonStyles } from "./commonStyles";
import ActionButton from "./ActionButton";
import { getErrorMessage } from "../types/office-types";
export const MatchProperties: React.FC = () => { // Property types that can be matched
const styles = useCommonStyles(); enum PropertyType {
const { Line = "line",
statusMessage, setStatusMessage, Fill = "fill",
statusType, setStatusType, Text = "text"
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 // Shape property loading configuration
if (shapes.items.length === 1) { const SHAPE_PROPERTIES = {
setStatusMessage("Please select multiple shapes to copy properties."); line: [
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/weight",
"lineFormat/dashStyle", "lineFormat/dashStyle",
"lineFormat/color", "lineFormat/color"
],
fill: [
"fill/transparency", "fill/transparency",
"fill/foregroundColor", "fill/foregroundColor"
],
text: [
"textFrame" "textFrame"
]); ]
};
await context.sync(); // Font property loading configuration
const FONT_PROPERTIES = [
// 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/name",
"font/size", "font/size",
"font/bold", "font/bold",
"font/italic", "font/italic",
"font/underline", "font/underline",
"font/color" "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(); await context.sync();
hasText = true; hasText = true;
} }
} }
// First, load all text frames in a batch return { hasText };
if (hasText) { };
// Pre-load textFrame for all target shapes
/**
* 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++) { for (let i = 1; i < shapes.items.length; i++) {
shapes.items[i].load("textFrame"); shapes.items[i].load("textFrame");
} }
await context.sync(); 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++) { for (let i = 1; i < shapes.items.length; i++) {
if (shapes.items[i].textFrame) { if (shapes.items[i].textFrame) {
shapes.items[i].textFrame.load("textRange"); shapes.items[i].textFrame.load("textRange");
} }
} }
await context.sync(); await context.sync();
} };
// Now apply properties to all shapes /**
let successCount = 0; * Applies line properties from source to target shape
let textStyleCount = 0; * @param sourceShape The shape to copy properties from
* @param targetShape The shape to apply properties to
for (let i = 1; i < shapes.items.length; i++) { * @returns True if any properties were applied
try { */
const targetShape = shapes.items[i]; const applyLineProperties = (
sourceShape: PowerPoint.Shape,
targetShape: PowerPoint.Shape
): boolean => {
let propertiesApplied = false; let propertiesApplied = false;
// Apply line formatting properties
try { try {
// Line weight // Line weight
if (firstShape.lineFormat.weight !== undefined) { if (sourceShape.lineFormat.weight !== undefined) {
targetShape.lineFormat.weight = firstShape.lineFormat.weight; targetShape.lineFormat.weight = sourceShape.lineFormat.weight;
propertiesApplied = true;
} }
// Line style // Line style
if (firstShape.lineFormat.dashStyle !== undefined) { if (sourceShape.lineFormat.dashStyle !== undefined) {
targetShape.lineFormat.dashStyle = firstShape.lineFormat.dashStyle; targetShape.lineFormat.dashStyle = sourceShape.lineFormat.dashStyle;
propertiesApplied = true;
} }
// Line color // Line color
if (firstShape.lineFormat.color !== undefined) { if (sourceShape.lineFormat.color !== undefined) {
targetShape.lineFormat.color = firstShape.lineFormat.color; targetShape.lineFormat.color = sourceShape.lineFormat.color;
}
propertiesApplied = true; propertiesApplied = true;
}
} catch (err) { } 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 { try {
// Fill transparency // Fill transparency
if (firstShape.fill.transparency !== undefined) { if (sourceShape.fill.transparency !== undefined) {
targetShape.fill.transparency = firstShape.fill.transparency; targetShape.fill.transparency = sourceShape.fill.transparency;
}
if (firstShape.fill.foregroundColor !== undefined) {
targetShape.fill.foregroundColor = firstShape.fill.foregroundColor;
}
propertiesApplied = true; propertiesApplied = true;
} catch (err) {
console.error(`Error applying fill format to shape ${i}:`, err);
} }
// Apply text properties if the source has text // Fill color
if (hasText && targetShape.textFrame && targetShape.textFrame.textRange) { 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 { 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; const targetFont = targetShape.textFrame.textRange.font;
// Apply font properties in batch // Apply font properties in batch
@@ -169,60 +236,102 @@ export const MatchProperties: React.FC = () => {
targetFont.color = sourceFont.color; 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++; 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++; successCount++;
} }
} catch (err) {
console.error(`Error updating shape ${i}:`, err);
}
} }
// Single sync after all property changes // Single sync after all property changes
await context.sync(); await context.sync();
// Final status message based on what was applied // Generate appropriate status message
if (successCount > 0) { 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) { if (textStyleCount > 0) {
message += ` (including text styling on ${textStyleCount})`; message += ` (including text styling on ${textStyleCount})`;
} }
setStatusMessage(message); setStatusMessage(message);
setStatusType("success"); setStatusType("success");
} else { } else {
setStatusMessage("Couldn't apply properties. Try selecting different shapes."); setStatusMessage("Couldn't apply properties. Try selecting different shapes.");
setStatusType("error"); 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 ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.buttonGroup}> <div className={styles.buttonGroup}>
<Button <ActionButton
appearance="primary" title="Match Properties"
className={styles.actionButton}
onClick={matchPropertiesToFirstSelected}
icon={<ColorRegular />} icon={<ColorRegular />}
disabled={isProcessing} onClick={matchPropertiesToFirstSelected}
> />
Match Properties
</Button>
</div> </div>
</div> </div>
); );