/** * @file MatchProperties.tsx * @description Component that provides functionality to match visual properties between shapes * in PowerPoint presentations. This allows users to copy line, fill, and text properties from * one shape to multiple other shapes, ensuring consistent styling across elements. */ import * as React from "react"; import { ColorRegular } from "@fluentui/react-icons"; import { useStatusContext } from "./App"; import { useCommonStyles } from "./commonStyles"; import ActionButton from "./ActionButton"; import { getErrorMessage } from "../types/office-types"; // Property types that can be matched enum PropertyType { Line = "line", Fill = "fill", Text = "text" } // Shape property loading configuration const SHAPE_PROPERTIES = { line: [ "lineFormat/weight", "lineFormat/dashStyle", "lineFormat/color" ], fill: [ "fill/transparency", "fill/foregroundColor" ], text: [ "textFrame" ] }; // 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; } } 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 => { 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(); // 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(); }; /** * 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; try { // Line weight if (sourceShape.lineFormat.weight !== undefined) { targetShape.lineFormat.weight = sourceShape.lineFormat.weight; propertiesApplied = true; } // Line style if (sourceShape.lineFormat.dashStyle !== undefined) { targetShape.lineFormat.dashStyle = sourceShape.lineFormat.dashStyle; propertiesApplied = true; } // Line color if (sourceShape.lineFormat.color !== undefined) { targetShape.lineFormat.color = sourceShape.lineFormat.color; propertiesApplied = true; } } catch (err) { console.error("Error applying line format:", getErrorMessage(err)); } 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 (sourceShape.fill.transparency !== undefined) { targetShape.fill.transparency = sourceShape.fill.transparency; propertiesApplied = true; } // 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 { if (sourceShape.textFrame?.textRange && targetShape.textFrame?.textRange) { const sourceFont = sourceShape.textFrame.textRange.font; const targetFont = targetShape.textFrame.textRange.font; // Apply font properties in batch if (sourceFont.name !== undefined) { targetFont.name = sourceFont.name; } if (sourceFont.size !== undefined) { targetFont.size = sourceFont.size; } if (sourceFont.bold !== undefined) { targetFont.bold = sourceFont.bold; } if (sourceFont.italic !== undefined) { targetFont.italic = sourceFont.italic; } if (sourceFont.underline !== undefined) { targetFont.underline = sourceFont.underline; } if (sourceFont.color !== undefined) { 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 => { 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++; } } // Count successful shape updates if (lineApplied || fillApplied) { shapeUpdated = true; } if (shapeUpdated) { successCount++; } } // Single sync after all property changes await context.sync(); // Generate appropriate status message if (successCount > 0) { 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"); } }); }; return (
} onClick={matchPropertiesToFirstSelected} />
); }; export default MatchProperties;