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 * as React from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
makeStyles,
|
makeStyles,
|
||||||
Label,
|
Label,
|
||||||
Slider,
|
|
||||||
SpinButton,
|
|
||||||
Divider,
|
|
||||||
ToggleButton,
|
ToggleButton,
|
||||||
tokens,
|
tokens
|
||||||
Input
|
|
||||||
} from "@fluentui/react-components";
|
} from "@fluentui/react-components";
|
||||||
import {
|
import {
|
||||||
GridRegular,
|
|
||||||
DismissRegular,
|
DismissRegular,
|
||||||
AddRegular,
|
AddRegular,
|
||||||
LineHorizontal3Regular,
|
LineHorizontal3Regular,
|
||||||
SplitVerticalRegular,
|
SplitVerticalRegular,
|
||||||
GridDotsRegular,
|
GridDotsRegular
|
||||||
ArrowResetRegular
|
|
||||||
} from "@fluentui/react-icons";
|
} from "@fluentui/react-icons";
|
||||||
import { useStatusContext } from "./App";
|
import { useStatusContext } from "./App";
|
||||||
import { useCommonStyles } from "./commonStyles";
|
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({
|
const useStyles = makeStyles({
|
||||||
buttonGrid: {
|
buttonGrid: {
|
||||||
display: "grid",
|
display: "grid",
|
||||||
@@ -81,18 +130,55 @@ const useStyles = makeStyles({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
type GridColor = "blue" | "red" | "yellow" | "green";
|
/**
|
||||||
|
* ColorButton component - Renders a color selection button
|
||||||
const GRID_PREFIX = "edison_grid_";
|
*
|
||||||
const GUIDE_PREFIX = "edison_guide_";
|
* @param props - Component props
|
||||||
|
* @returns React component
|
||||||
const colorValues = {
|
*/
|
||||||
blue: "#4472C4",
|
const ColorButton: React.FC<ColorButtonProps> = ({ color, selectedColor, onClick }) => {
|
||||||
red: "#C00000",
|
const styles = useStyles();
|
||||||
yellow: "#FFC000",
|
return (
|
||||||
green: "#70AD47"
|
<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 = () => {
|
export const GridGuidelineManager: React.FC = () => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const commonStyles = useCommonStyles();
|
const commonStyles = useCommonStyles();
|
||||||
@@ -103,17 +189,26 @@ export const GridGuidelineManager: React.FC = () => {
|
|||||||
} = useStatusContext();
|
} = useStatusContext();
|
||||||
|
|
||||||
// Grid state
|
// Grid state
|
||||||
const [gridSpacing, setGridSpacing] = React.useState(50);
|
const [gridSpacing, handleGridSpacingChange] = useNumericInputHandler(50);
|
||||||
const [gridColor, setGridColor] = React.useState<GridColor>("blue");
|
const [gridColor, setGridColor] = React.useState<GridColor>("blue");
|
||||||
const [gridOpacity, setGridOpacity] = React.useState(30);
|
const [gridOpacity, handleGridOpacityChange] = useNumericInputHandler(30);
|
||||||
|
|
||||||
// Guidelines state
|
// Guidelines state
|
||||||
const [guidePosition, setGuidePosition] = React.useState(240);
|
const [guidePosition, handleGuidePositionChange] = useNumericInputHandler(240);
|
||||||
const [guideDirection, setGuideDirection] = React.useState<"horizontal" | "vertical">("horizontal");
|
const [guideDirection, setGuideDirection] = React.useState<GuideDirection>("horizontal");
|
||||||
const [guideColor, setGuideColor] = React.useState<GridColor>("red");
|
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,
|
slide: PowerPoint.Slide,
|
||||||
startX: number,
|
startX: number,
|
||||||
startY: number,
|
startY: number,
|
||||||
@@ -138,9 +233,12 @@ export const GridGuidelineManager: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return line;
|
return line;
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const createGrid = async () => {
|
/**
|
||||||
|
* Creates a grid on the current slide based on the configured settings
|
||||||
|
*/
|
||||||
|
const createGrid = React.useCallback(async () => {
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
try {
|
try {
|
||||||
await PowerPoint.run(async (context) => {
|
await PowerPoint.run(async (context) => {
|
||||||
@@ -158,23 +256,19 @@ export const GridGuidelineManager: React.FC = () => {
|
|||||||
|
|
||||||
const currentSlide = selectedSlides.items[0];
|
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
|
// Calculate number of lines needed
|
||||||
const numHorizontalLines = Math.floor(slideHeight / gridSpacing);
|
const numHorizontalLines = Math.floor(SLIDE_HEIGHT / gridSpacing);
|
||||||
const numVerticalLines = Math.floor(slideWidth / gridSpacing);
|
const numVerticalLines = Math.floor(SLIDE_WIDTH / gridSpacing);
|
||||||
|
|
||||||
// Create horizontal grid lines
|
// Create horizontal grid lines
|
||||||
for (let i = 1; i <= numHorizontalLines; i++) {
|
for (let i = 1; i <= numHorizontalLines; i++) {
|
||||||
const yPosition = i * gridSpacing;
|
const yPosition = i * gridSpacing;
|
||||||
|
|
||||||
// Skip if we're at the edge of the slide
|
// Skip if we're at the edge of the slide
|
||||||
if (yPosition >= slideHeight) continue;
|
if (yPosition >= SLIDE_HEIGHT) continue;
|
||||||
|
|
||||||
// Create horizontal line
|
// 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}`;
|
line.name = `${GRID_PREFIX}h_${i}`;
|
||||||
|
|
||||||
// Set line properties
|
// Set line properties
|
||||||
@@ -191,10 +285,10 @@ export const GridGuidelineManager: React.FC = () => {
|
|||||||
const xPosition = i * gridSpacing;
|
const xPosition = i * gridSpacing;
|
||||||
|
|
||||||
// Skip if we're at the edge of the slide
|
// Skip if we're at the edge of the slide
|
||||||
if (xPosition >= slideWidth) continue;
|
if (xPosition >= SLIDE_WIDTH) continue;
|
||||||
|
|
||||||
// Create vertical line
|
// 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}`;
|
line.name = `${GRID_PREFIX}v_${i}`;
|
||||||
|
|
||||||
// Set line properties
|
// Set line properties
|
||||||
@@ -216,9 +310,14 @@ export const GridGuidelineManager: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsProcessing(false);
|
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) {
|
if (showStatus) {
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
}
|
}
|
||||||
@@ -282,9 +381,12 @@ export const GridGuidelineManager: React.FC = () => {
|
|||||||
setIsProcessing(false);
|
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);
|
setIsProcessing(true);
|
||||||
try {
|
try {
|
||||||
await PowerPoint.run(async (context) => {
|
await PowerPoint.run(async (context) => {
|
||||||
@@ -299,9 +401,6 @@ export const GridGuidelineManager: React.FC = () => {
|
|||||||
|
|
||||||
const currentSlide = selectedSlides.items[0];
|
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
|
// Create a unique identifier for this guide
|
||||||
const timestamp = new Date().getTime();
|
const timestamp = new Date().getTime();
|
||||||
@@ -311,10 +410,10 @@ export const GridGuidelineManager: React.FC = () => {
|
|||||||
let line;
|
let line;
|
||||||
if (guideDirection === "horizontal") {
|
if (guideDirection === "horizontal") {
|
||||||
// Create horizontal guideline
|
// Create horizontal guideline
|
||||||
line = createLineShape(currentSlide, 0, guidePosition, slideWidth, guidePosition);
|
line = createLineShape(currentSlide, 0, guidePosition, SLIDE_WIDTH, guidePosition);
|
||||||
} else {
|
} else {
|
||||||
// Create vertical guideline
|
// Create vertical guideline
|
||||||
line = createLineShape(currentSlide, guidePosition, 0, guidePosition, slideHeight);
|
line = createLineShape(currentSlide, guidePosition, 0, guidePosition, SLIDE_HEIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
line.name = guideId;
|
line.name = guideId;
|
||||||
@@ -336,9 +435,12 @@ export const GridGuidelineManager: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsProcessing(false);
|
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);
|
setIsProcessing(true);
|
||||||
try {
|
try {
|
||||||
await PowerPoint.run(async (context) => {
|
await PowerPoint.run(async (context) => {
|
||||||
@@ -358,13 +460,17 @@ export const GridGuidelineManager: React.FC = () => {
|
|||||||
shapes.load("items");
|
shapes.load("items");
|
||||||
await context.sync();
|
await context.sync();
|
||||||
|
|
||||||
// Find shapes that are guidelines
|
// Load all shape names at once
|
||||||
let removedCount = 0;
|
const allShapes = shapes.items;
|
||||||
for (let i = 0; i < shapes.items.length; i++) {
|
for (let i = 0; i < allShapes.length; i++) {
|
||||||
const shape = shapes.items[i];
|
allShapes[i].load("name");
|
||||||
shape.load("name");
|
}
|
||||||
await context.sync();
|
await context.sync();
|
||||||
|
|
||||||
|
// Find shapes that are guidelines and mark them for deletion
|
||||||
|
let removedCount = 0;
|
||||||
|
for (let i = 0; i < allShapes.length; i++) {
|
||||||
|
const shape = allShapes[i];
|
||||||
// Check if this shape is a guideline
|
// Check if this shape is a guideline
|
||||||
if (shape.name && shape.name.startsWith(GUIDE_PREFIX)) {
|
if (shape.name && shape.name.startsWith(GUIDE_PREFIX)) {
|
||||||
shape.delete();
|
shape.delete();
|
||||||
@@ -372,6 +478,7 @@ export const GridGuidelineManager: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Single sync after all deletions
|
||||||
await context.sync();
|
await context.sync();
|
||||||
|
|
||||||
if (removedCount > 0) {
|
if (removedCount > 0) {
|
||||||
@@ -388,20 +495,7 @@ export const GridGuidelineManager: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
}
|
}
|
||||||
};
|
}, [setIsProcessing, setStatusMessage, setStatusType]);
|
||||||
|
|
||||||
// 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)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={commonStyles.container} style={{ maxHeight: "400px", overflowY: "auto" }}>
|
<div className={commonStyles.container} style={{ maxHeight: "400px", overflowY: "auto" }}>
|
||||||
@@ -420,16 +514,11 @@ export const GridGuidelineManager: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={gridSpacing}
|
value={gridSpacing}
|
||||||
onChange={(e) => {
|
onChange={handleGridSpacingChange}
|
||||||
const newValue = e.target.value;
|
|
||||||
const parsed = parseInt(newValue);
|
|
||||||
if (newValue === '') {
|
|
||||||
setGridSpacing(0);
|
|
||||||
} else if (!isNaN(parsed)) {
|
|
||||||
setGridSpacing(parsed);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onClick={(e) => (e.target as HTMLInputElement).select()}
|
onClick={(e) => (e.target as HTMLInputElement).select()}
|
||||||
|
title="Grid Spacing"
|
||||||
|
placeholder="Grid Spacing"
|
||||||
|
aria-label="Grid Spacing"
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
padding: "5px 8px",
|
padding: "5px 8px",
|
||||||
@@ -447,16 +536,11 @@ export const GridGuidelineManager: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={gridOpacity}
|
value={gridOpacity}
|
||||||
onChange={(e) => {
|
onChange={handleGridOpacityChange}
|
||||||
const newValue = e.target.value;
|
|
||||||
const parsed = parseInt(newValue);
|
|
||||||
if (newValue === '') {
|
|
||||||
setGridOpacity(0);
|
|
||||||
} else if (!isNaN(parsed)) {
|
|
||||||
setGridOpacity(parsed);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onClick={(e) => (e.target as HTMLInputElement).select()}
|
onClick={(e) => (e.target as HTMLInputElement).select()}
|
||||||
|
title="Grid Opacity"
|
||||||
|
placeholder="Grid Opacity"
|
||||||
|
aria-label="Grid Opacity"
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
padding: "5px 8px",
|
padding: "5px 8px",
|
||||||
@@ -517,16 +601,11 @@ export const GridGuidelineManager: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={guidePosition}
|
value={guidePosition}
|
||||||
onChange={(e) => {
|
onChange={handleGuidePositionChange}
|
||||||
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()}
|
onClick={(e) => (e.target as HTMLInputElement).select()}
|
||||||
|
title="Guide Position"
|
||||||
|
placeholder="Guide Position"
|
||||||
|
aria-label="Guide Position"
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
padding: "5px 8px",
|
padding: "5px 8px",
|
||||||
|
|||||||
Reference in New Issue
Block a user