From 577d5b15c83653099ff878dac63a0fcb5ed7d5d8 Mon Sep 17 00:00:00 2001 From: Heiko Joerg Schick Date: Sat, 22 Mar 2025 15:54:13 +0100 Subject: [PATCH] Refactor MatchSizes component for better maintainability and performance --- src/taskpane/components/MatchSizes.tsx | 187 ++++++++++++++++++++----- 1 file changed, 155 insertions(+), 32 deletions(-) diff --git a/src/taskpane/components/MatchSizes.tsx b/src/taskpane/components/MatchSizes.tsx index 5f687f97..15cd326c 100644 --- a/src/taskpane/components/MatchSizes.tsx +++ b/src/taskpane/components/MatchSizes.tsx @@ -1,49 +1,172 @@ import * as React from "react"; -import { ArrowFitInRegular } from "@fluentui/react-icons"; +import { ArrowFitInRegular, ArrowMaximizeRegular, ArrowMinimizeRegular } from "@fluentui/react-icons"; import { useStatusContext } from "./App"; import { useCommonStyles } from "./commonStyles"; import ActionButton from "./ActionButton"; +import { getErrorMessage } from "../types/office-types"; + +// Size matching options +enum SizeMatchType { + Both = "both", + Width = "width", + Height = "height" +} + +// Dimension properties to load and match +const DIMENSION_PROPERTIES = { + [SizeMatchType.Both]: ["width", "height"], + [SizeMatchType.Width]: ["width"], + [SizeMatchType.Height]: ["height"] +}; export const MatchSizes: React.FC = () => { const styles = useCommonStyles(); const { setStatusMessage, setStatusType } = useStatusContext(); - const matchSizeToFirstSelected = async () => { - await PowerPoint.run(async (context) => { - // Get the selected shapes - const shapes = context.presentation.getSelectedShapes(); - shapes.load("items"); - await context.sync(); + /** + * 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; + } - // Check if shapes are selected - if (shapes.items.length === 0) { - setStatusMessage("No shapes are selected. Please select shapes first."); - setStatusType("warning"); - return; - } + if (shapes.items.length === 1) { + setStatusMessage("Please select multiple shapes to resize."); + setStatusType("warning"); + return false; + } - // Check if there is more than one shape selected - if (shapes.items.length === 1) { - setStatusMessage("Please select multiple shapes to resize."); - setStatusType("warning"); - return; - } + return true; + }; - // Get the first shape's dimensions - const firstShape = shapes.items[0]; - firstShape.load("width,height"); - await context.sync(); + /** + * Loads dimension properties from the source shape + * @param sourceShape The shape to copy dimensions from + * @param context The PowerPoint request context + * @param matchType The type of dimension matching to perform + * @returns The loaded source shape + */ + const loadSourceShape = async ( + sourceShape: PowerPoint.Shape, + context: PowerPoint.RequestContext, + matchType: SizeMatchType = SizeMatchType.Both + ): Promise => { + // Load the appropriate dimension properties + sourceShape.load(DIMENSION_PROPERTIES[matchType]); + await context.sync(); + return sourceShape; + }; - // Loop through the remaining shapes and resize them + /** + * Applies size changes to target shapes + * @param sourceShape The shape to copy dimensions from + * @param shapes The collection of shapes to resize + * @param matchType The type of dimension matching to perform + * @returns The number of shapes that were resized + */ + const applySizeChanges = ( + sourceShape: PowerPoint.Shape, + shapes: PowerPoint.ShapeScopedCollection, + matchType: SizeMatchType = SizeMatchType.Both + ): number => { + let resizedCount = 0; + + try { + // Apply dimensions to all target shapes for (let i = 1; i < shapes.items.length; i++) { - shapes.items[i].width = firstShape.width; - shapes.items[i].height = firstShape.height; + const targetShape = shapes.items[i]; + + // Apply width if needed + if (matchType === SizeMatchType.Both || matchType === SizeMatchType.Width) { + targetShape.width = sourceShape.width; + } + + // Apply height if needed + if (matchType === SizeMatchType.Both || matchType === SizeMatchType.Height) { + targetShape.height = sourceShape.height; + } + + resizedCount++; } + } catch (err) { + console.error("Error applying size changes:", getErrorMessage(err)); + } + + return resizedCount; + }; - await context.sync(); - setStatusMessage(`Resized ${shapes.items.length - 1} shapes to match the first selected shape.`); - setStatusType("success"); - }); + /** + * Generates an appropriate status message based on the results + * @param resizedCount The number of shapes that were resized + * @param matchType The type of dimension matching performed + * @returns The formatted status message + */ + const generateStatusMessage = ( + resizedCount: number, + matchType: SizeMatchType = SizeMatchType.Both + ): string => { + if (resizedCount === 0) { + return "No shapes were resized. Please try again with different shapes."; + } + + let dimensionText = ""; + switch (matchType) { + case SizeMatchType.Width: + dimensionText = "width"; + break; + case SizeMatchType.Height: + dimensionText = "height"; + break; + default: + dimensionText = "dimensions"; + } + + return `Resized ${resizedCount} shapes to match the ${dimensionText} of the first selected shape.`; + }; + + /** + * Main function to match sizes from the first selected shape to others + * @param matchType The type of dimension matching to perform + */ + const matchSizeToFirstSelected = async ( + matchType: SizeMatchType = SizeMatchType.Both + ): Promise => { + try { + 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 = await loadSourceShape(shapes.items[0], context, matchType); + + // Apply size changes to all target shapes + const resizedCount = applySizeChanges(sourceShape, shapes, matchType); + + // Sync changes to PowerPoint + await context.sync(); + + // Update status message + const statusMessage = generateStatusMessage(resizedCount, matchType); + setStatusMessage(statusMessage); + setStatusType(resizedCount > 0 ? "success" : "error"); + }); + } catch (error) { + console.error("Error in matchSizeToFirstSelected:", getErrorMessage(error)); + setStatusMessage(`Error: ${getErrorMessage(error)}`); + setStatusType("error"); + } }; return ( @@ -52,11 +175,11 @@ export const MatchSizes: React.FC = () => { } - onClick={matchSizeToFirstSelected} + onClick={() => matchSizeToFirstSelected(SizeMatchType.Both)} /> ); }; -export default MatchSizes; \ No newline at end of file +export default MatchSizes;