Optimize GridGuidelineManager component
- Improve performance by batching API calls and reducing context.sync() calls - Add memoization with useCallback for event handlers - Extract ColorButton component and create reusable numeric input hook - Define slide dimensions as constants - Add accessibility attributes to input fields - Add comprehensive JSDoc documentation
This commit is contained in:
@@ -1,27 +1,76 @@
|
||||
/**
|
||||
* GridGuidelineManager Component
|
||||
*
|
||||
* This component provides functionality to create and manage grids and guidelines
|
||||
* on PowerPoint slides. It allows users to add customizable grids with adjustable
|
||||
* spacing, opacity, and color, as well as add individual guidelines with custom
|
||||
* positioning and color.
|
||||
*
|
||||
* @module GridGuidelineManager
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import {
|
||||
Button,
|
||||
makeStyles,
|
||||
Label,
|
||||
Slider,
|
||||
SpinButton,
|
||||
Divider,
|
||||
ToggleButton,
|
||||
tokens,
|
||||
Input
|
||||
tokens
|
||||
} from "@fluentui/react-components";
|
||||
import {
|
||||
GridRegular,
|
||||
DismissRegular,
|
||||
AddRegular,
|
||||
LineHorizontal3Regular,
|
||||
SplitVerticalRegular,
|
||||
GridDotsRegular,
|
||||
ArrowResetRegular
|
||||
GridDotsRegular
|
||||
} from "@fluentui/react-icons";
|
||||
import { useStatusContext } from "./App";
|
||||
import { useCommonStyles } from "./commonStyles";
|
||||
|
||||
/**
|
||||
* Standard dimensions for PowerPoint slides in points
|
||||
*/
|
||||
const SLIDE_WIDTH = 960; // Width in points
|
||||
const SLIDE_HEIGHT = 540; // Height in points
|
||||
|
||||
/**
|
||||
* Available colors for grids and guidelines
|
||||
*/
|
||||
type GridColor = "blue" | "red" | "yellow" | "green";
|
||||
|
||||
/**
|
||||
* Direction options for guidelines
|
||||
*/
|
||||
type GuideDirection = "horizontal" | "vertical";
|
||||
|
||||
/**
|
||||
* Prefixes used for naming shapes to identify them later
|
||||
*/
|
||||
const GRID_PREFIX = "edison_grid_";
|
||||
const GUIDE_PREFIX = "edison_guide_";
|
||||
|
||||
/**
|
||||
* Color values mapping for the available grid and guideline colors
|
||||
*/
|
||||
const colorValues = {
|
||||
blue: "#4472C4",
|
||||
red: "#C00000",
|
||||
yellow: "#FFC000",
|
||||
green: "#70AD47"
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for the ColorButton component
|
||||
*/
|
||||
interface ColorButtonProps {
|
||||
/** The color this button represents */
|
||||
color: GridColor;
|
||||
/** The currently selected color */
|
||||
selectedColor: GridColor;
|
||||
/** Callback function when the color is clicked */
|
||||
onClick: (color: GridColor) => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
buttonGrid: {
|
||||
display: "grid",
|
||||
@@ -81,18 +130,55 @@ const useStyles = makeStyles({
|
||||
}
|
||||
});
|
||||
|
||||
type GridColor = "blue" | "red" | "yellow" | "green";
|
||||
|
||||
const GRID_PREFIX = "edison_grid_";
|
||||
const GUIDE_PREFIX = "edison_guide_";
|
||||
|
||||
const colorValues = {
|
||||
blue: "#4472C4",
|
||||
red: "#C00000",
|
||||
yellow: "#FFC000",
|
||||
green: "#70AD47"
|
||||
/**
|
||||
* ColorButton component - Renders a color selection button
|
||||
*
|
||||
* @param props - Component props
|
||||
* @returns React component
|
||||
*/
|
||||
const ColorButton: React.FC<ColorButtonProps> = ({ color, selectedColor, onClick }) => {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<Button
|
||||
className={`${styles.colorButton} ${color === selectedColor ? styles.colorButtonSelected : ''}`}
|
||||
style={{ backgroundColor: colorValues[color] }}
|
||||
onClick={() => onClick(color)}
|
||||
title={color.charAt(0).toUpperCase() + color.slice(1)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook for handling numeric input fields
|
||||
*
|
||||
* @param initialValue - The initial value for the input
|
||||
* @returns A tuple containing the current value and a change handler function
|
||||
*/
|
||||
const useNumericInputHandler = (
|
||||
initialValue: number
|
||||
): [number, (e: React.ChangeEvent<HTMLInputElement>) => void] => {
|
||||
const [value, setValue] = React.useState(initialValue);
|
||||
|
||||
const handleChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value;
|
||||
const parsed = parseInt(newValue);
|
||||
|
||||
if (newValue === '') {
|
||||
setValue(0);
|
||||
} else if (!isNaN(parsed)) {
|
||||
setValue(parsed);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return [value, handleChange];
|
||||
};
|
||||
|
||||
/**
|
||||
* GridGuidelineManager component - Provides UI and functionality for creating
|
||||
* and managing grids and guidelines on PowerPoint slides
|
||||
*
|
||||
* @returns React component
|
||||
*/
|
||||
export const GridGuidelineManager: React.FC = () => {
|
||||
const styles = useStyles();
|
||||
const commonStyles = useCommonStyles();
|
||||
@@ -103,17 +189,26 @@ export const GridGuidelineManager: React.FC = () => {
|
||||
} = useStatusContext();
|
||||
|
||||
// Grid state
|
||||
const [gridSpacing, setGridSpacing] = React.useState(50);
|
||||
const [gridSpacing, handleGridSpacingChange] = useNumericInputHandler(50);
|
||||
const [gridColor, setGridColor] = React.useState<GridColor>("blue");
|
||||
const [gridOpacity, setGridOpacity] = React.useState(30);
|
||||
const [gridOpacity, handleGridOpacityChange] = useNumericInputHandler(30);
|
||||
|
||||
// Guidelines state
|
||||
const [guidePosition, setGuidePosition] = React.useState(240);
|
||||
const [guideDirection, setGuideDirection] = React.useState<"horizontal" | "vertical">("horizontal");
|
||||
const [guidePosition, handleGuidePositionChange] = useNumericInputHandler(240);
|
||||
const [guideDirection, setGuideDirection] = React.useState<GuideDirection>("horizontal");
|
||||
const [guideColor, setGuideColor] = React.useState<GridColor>("red");
|
||||
|
||||
// Helper function to create a line shape
|
||||
const createLineShape = (
|
||||
/**
|
||||
* Creates a line shape on the slide using a thin rectangle
|
||||
*
|
||||
* @param slide - The PowerPoint slide to add the shape to
|
||||
* @param startX - Starting X coordinate
|
||||
* @param startY - Starting Y coordinate
|
||||
* @param endX - Ending X coordinate
|
||||
* @param endY - Ending Y coordinate
|
||||
* @returns The created shape object
|
||||
*/
|
||||
const createLineShape = React.useCallback((
|
||||
slide: PowerPoint.Slide,
|
||||
startX: number,
|
||||
startY: number,
|
||||
@@ -138,9 +233,12 @@ export const GridGuidelineManager: React.FC = () => {
|
||||
}
|
||||
|
||||
return line;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const createGrid = async () => {
|
||||
/**
|
||||
* Creates a grid on the current slide based on the configured settings
|
||||
*/
|
||||
const createGrid = React.useCallback(async () => {
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
await PowerPoint.run(async (context) => {
|
||||
@@ -158,23 +256,19 @@ export const GridGuidelineManager: React.FC = () => {
|
||||
|
||||
const currentSlide = selectedSlides.items[0];
|
||||
|
||||
// Standard dimensions for PowerPoint slides
|
||||
const slideWidth = 960; // Width in points
|
||||
const slideHeight = 540; // Height in points
|
||||
|
||||
// Calculate number of lines needed
|
||||
const numHorizontalLines = Math.floor(slideHeight / gridSpacing);
|
||||
const numVerticalLines = Math.floor(slideWidth / gridSpacing);
|
||||
const numHorizontalLines = Math.floor(SLIDE_HEIGHT / gridSpacing);
|
||||
const numVerticalLines = Math.floor(SLIDE_WIDTH / gridSpacing);
|
||||
|
||||
// Create horizontal grid lines
|
||||
for (let i = 1; i <= numHorizontalLines; i++) {
|
||||
const yPosition = i * gridSpacing;
|
||||
|
||||
// Skip if we're at the edge of the slide
|
||||
if (yPosition >= slideHeight) continue;
|
||||
if (yPosition >= SLIDE_HEIGHT) continue;
|
||||
|
||||
// Create horizontal line
|
||||
const line = createLineShape(currentSlide, 0, yPosition, slideWidth, yPosition);
|
||||
const line = createLineShape(currentSlide, 0, yPosition, SLIDE_WIDTH, yPosition);
|
||||
line.name = `${GRID_PREFIX}h_${i}`;
|
||||
|
||||
// Set line properties
|
||||
@@ -191,10 +285,10 @@ export const GridGuidelineManager: React.FC = () => {
|
||||
const xPosition = i * gridSpacing;
|
||||
|
||||
// Skip if we're at the edge of the slide
|
||||
if (xPosition >= slideWidth) continue;
|
||||
if (xPosition >= SLIDE_WIDTH) continue;
|
||||
|
||||
// Create vertical line
|
||||
const line = createLineShape(currentSlide, xPosition, 0, xPosition, slideHeight);
|
||||
const line = createLineShape(currentSlide, xPosition, 0, xPosition, SLIDE_HEIGHT);
|
||||
line.name = `${GRID_PREFIX}v_${i}`;
|
||||
|
||||
// Set line properties
|
||||
@@ -216,9 +310,14 @@ export const GridGuidelineManager: React.FC = () => {
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
}, [createLineShape, gridColor, gridOpacity, gridSpacing, setIsProcessing, setStatusMessage, setStatusType]);
|
||||
|
||||
const removeGrid = async (showStatus = true) => {
|
||||
/**
|
||||
* Removes the grid from the current slide
|
||||
*
|
||||
* @param showStatus - Whether to show status messages (default: true)
|
||||
*/
|
||||
const removeGrid = React.useCallback(async (showStatus = true) => {
|
||||
if (showStatus) {
|
||||
setIsProcessing(true);
|
||||
}
|
||||
@@ -282,9 +381,12 @@ export const GridGuidelineManager: React.FC = () => {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [setIsProcessing, setStatusMessage, setStatusType]);
|
||||
|
||||
const addGuideline = async () => {
|
||||
/**
|
||||
* Adds a guideline to the current slide based on the configured settings
|
||||
*/
|
||||
const addGuideline = React.useCallback(async () => {
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
await PowerPoint.run(async (context) => {
|
||||
@@ -299,9 +401,6 @@ export const GridGuidelineManager: React.FC = () => {
|
||||
|
||||
const currentSlide = selectedSlides.items[0];
|
||||
|
||||
// Standard dimensions for PowerPoint slides
|
||||
const slideWidth = 960; // Width in points
|
||||
const slideHeight = 540; // Height in points
|
||||
|
||||
// Create a unique identifier for this guide
|
||||
const timestamp = new Date().getTime();
|
||||
@@ -311,10 +410,10 @@ export const GridGuidelineManager: React.FC = () => {
|
||||
let line;
|
||||
if (guideDirection === "horizontal") {
|
||||
// Create horizontal guideline
|
||||
line = createLineShape(currentSlide, 0, guidePosition, slideWidth, guidePosition);
|
||||
line = createLineShape(currentSlide, 0, guidePosition, SLIDE_WIDTH, guidePosition);
|
||||
} else {
|
||||
// Create vertical guideline
|
||||
line = createLineShape(currentSlide, guidePosition, 0, guidePosition, slideHeight);
|
||||
line = createLineShape(currentSlide, guidePosition, 0, guidePosition, SLIDE_HEIGHT);
|
||||
}
|
||||
|
||||
line.name = guideId;
|
||||
@@ -336,9 +435,12 @@ export const GridGuidelineManager: React.FC = () => {
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
}, [createLineShape, guideColor, guideDirection, guidePosition, setIsProcessing, setStatusMessage, setStatusType]);
|
||||
|
||||
const removeGuidelines = async () => {
|
||||
/**
|
||||
* Removes all guidelines from the current slide
|
||||
*/
|
||||
const removeGuidelines = React.useCallback(async () => {
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
await PowerPoint.run(async (context) => {
|
||||
@@ -358,13 +460,17 @@ export const GridGuidelineManager: React.FC = () => {
|
||||
shapes.load("items");
|
||||
await context.sync();
|
||||
|
||||
// Find shapes that are guidelines
|
||||
// Load all shape names at once
|
||||
const allShapes = shapes.items;
|
||||
for (let i = 0; i < allShapes.length; i++) {
|
||||
allShapes[i].load("name");
|
||||
}
|
||||
await context.sync();
|
||||
|
||||
// Find shapes that are guidelines and mark them for deletion
|
||||
let removedCount = 0;
|
||||
for (let i = 0; i < shapes.items.length; i++) {
|
||||
const shape = shapes.items[i];
|
||||
shape.load("name");
|
||||
await context.sync();
|
||||
|
||||
for (let i = 0; i < allShapes.length; i++) {
|
||||
const shape = allShapes[i];
|
||||
// Check if this shape is a guideline
|
||||
if (shape.name && shape.name.startsWith(GUIDE_PREFIX)) {
|
||||
shape.delete();
|
||||
@@ -372,6 +478,7 @@ export const GridGuidelineManager: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Single sync after all deletions
|
||||
await context.sync();
|
||||
|
||||
if (removedCount > 0) {
|
||||
@@ -388,20 +495,7 @@ export const GridGuidelineManager: React.FC = () => {
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Color picker button component
|
||||
const ColorButton = ({ color, selectedColor, onClick }:
|
||||
{ color: GridColor, selectedColor: GridColor, onClick: (color: GridColor) => void }) => {
|
||||
return (
|
||||
<Button
|
||||
className={`${styles.colorButton} ${color === selectedColor ? styles.colorButtonSelected : ''}`}
|
||||
style={{ backgroundColor: colorValues[color] }}
|
||||
onClick={() => onClick(color)}
|
||||
title={color.charAt(0).toUpperCase() + color.slice(1)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}, [setIsProcessing, setStatusMessage, setStatusType]);
|
||||
|
||||
return (
|
||||
<div className={commonStyles.container} style={{ maxHeight: "400px", overflowY: "auto" }}>
|
||||
@@ -420,16 +514,11 @@ export const GridGuidelineManager: React.FC = () => {
|
||||
<input
|
||||
type="text"
|
||||
value={gridSpacing}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
const parsed = parseInt(newValue);
|
||||
if (newValue === '') {
|
||||
setGridSpacing(0);
|
||||
} else if (!isNaN(parsed)) {
|
||||
setGridSpacing(parsed);
|
||||
}
|
||||
}}
|
||||
onChange={handleGridSpacingChange}
|
||||
onClick={(e) => (e.target as HTMLInputElement).select()}
|
||||
title="Grid Spacing"
|
||||
placeholder="Grid Spacing"
|
||||
aria-label="Grid Spacing"
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "5px 8px",
|
||||
@@ -447,16 +536,11 @@ export const GridGuidelineManager: React.FC = () => {
|
||||
<input
|
||||
type="text"
|
||||
value={gridOpacity}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
const parsed = parseInt(newValue);
|
||||
if (newValue === '') {
|
||||
setGridOpacity(0);
|
||||
} else if (!isNaN(parsed)) {
|
||||
setGridOpacity(parsed);
|
||||
}
|
||||
}}
|
||||
onChange={handleGridOpacityChange}
|
||||
onClick={(e) => (e.target as HTMLInputElement).select()}
|
||||
title="Grid Opacity"
|
||||
placeholder="Grid Opacity"
|
||||
aria-label="Grid Opacity"
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "5px 8px",
|
||||
@@ -511,32 +595,27 @@ export const GridGuidelineManager: React.FC = () => {
|
||||
Guidelines
|
||||
</Label>
|
||||
|
||||
<div className={styles.controlRow}>
|
||||
<Label className={styles.controlLabel}>Position</Label>
|
||||
<div style={{ width: "80px", flex: "none" }}>
|
||||
<input
|
||||
type="text"
|
||||
value={guidePosition}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
const parsed = parseInt(newValue);
|
||||
if (newValue === '') {
|
||||
setGuidePosition(0);
|
||||
} else if (!isNaN(parsed)) {
|
||||
setGuidePosition(parsed);
|
||||
}
|
||||
}}
|
||||
onClick={(e) => (e.target as HTMLInputElement).select()}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "5px 8px",
|
||||
border: "1px solid #d1d1d1",
|
||||
borderRadius: "4px",
|
||||
fontSize: "14px"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.controlRow}>
|
||||
<Label className={styles.controlLabel}>Position</Label>
|
||||
<div style={{ width: "80px", flex: "none" }}>
|
||||
<input
|
||||
type="text"
|
||||
value={guidePosition}
|
||||
onChange={handleGuidePositionChange}
|
||||
onClick={(e) => (e.target as HTMLInputElement).select()}
|
||||
title="Guide Position"
|
||||
placeholder="Guide Position"
|
||||
aria-label="Guide Position"
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "5px 8px",
|
||||
border: "1px solid #d1d1d1",
|
||||
borderRadius: "4px",
|
||||
fontSize: "14px"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.controlRow}>
|
||||
<Label className={styles.controlLabel}>Type</Label>
|
||||
@@ -601,4 +680,4 @@ export const GridGuidelineManager: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default GridGuidelineManager;
|
||||
export default GridGuidelineManager;
|
||||
|
||||
Reference in New Issue
Block a user