Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3dc9afea50 | |||
| 338810ef60 | |||
| 50966867b2 | |||
| 07b0232726 | |||
| d09dec4706 | |||
| 0cbb9c948e |
@@ -159,5 +159,6 @@ This project is licensed under the MIT License - see the LICENSE file for detail
|
||||
|
||||
## Version History
|
||||
|
||||
- v1.1.0 - Performance improvements, code quality, and UI enhancements
|
||||
- v1.0.0 - First stable release
|
||||
- v0.1.0 - Initial release with core functionality
|
||||
+92
-6
@@ -24,18 +24,24 @@ The application follows a component-based architecture where each tool is encaps
|
||||
│ │ └── commands.ts # Command implementations
|
||||
│ └── taskpane/ # Main add-in UI
|
||||
│ ├── components/ # React components
|
||||
│ │ ├── ActionButton.tsx # Reusable button with error handling
|
||||
│ │ ├── AlignmentButtons.tsx
|
||||
│ │ ├── App.tsx # Main application component
|
||||
│ │ ├── App.tsx # Main application component
|
||||
│ │ ├── ConfidentialButtons.tsx
|
||||
│ │ ├── DraftButtons.tsx
|
||||
│ │ ├── GridGuidelineManager.tsx
|
||||
│ │ ├── Header.tsx
|
||||
│ │ ├── InsertTitles.tsx
|
||||
│ │ ├── MatchProperties.tsx
|
||||
│ │ ├── MatchSizes.tsx
|
||||
│ │ ├── ProgressBarButtons.tsx
|
||||
│ │ ├── RoundImage.tsx
|
||||
│ │ ├── Section.tsx # Reusable section component
|
||||
│ │ ├── SmartTextFormatter.tsx
|
||||
│ │ ├── SwapPositions.tsx
|
||||
│ │ └── commonStyles.tsx
|
||||
│ ├── types/ # TypeScript type definitions
|
||||
│ │ └── office-types.ts # Office API type utilities
|
||||
│ ├── index.tsx # Entry point
|
||||
│ └── taskpane.html # Main HTML container
|
||||
├── babel.config.json # Babel configuration
|
||||
@@ -92,6 +98,34 @@ npx office-addin-dev-certs install
|
||||
|
||||
## Component Development Guidelines
|
||||
|
||||
### Reusable Components
|
||||
|
||||
#### ActionButton
|
||||
A reusable button component that handles error states, loading states, and consistent styling:
|
||||
|
||||
```tsx
|
||||
import ActionButton from "./ActionButton";
|
||||
|
||||
// In your component:
|
||||
<ActionButton
|
||||
title="Match Sizes"
|
||||
icon={<ArrowFitInRegular />}
|
||||
onClick={myAsyncFunction}
|
||||
/>
|
||||
```
|
||||
|
||||
#### Section
|
||||
A consistent container for organizing the UI:
|
||||
|
||||
```tsx
|
||||
import Section from "./Section";
|
||||
|
||||
// In your component:
|
||||
<Section title="My Feature Group">
|
||||
<MyFeatureComponent />
|
||||
</Section>
|
||||
```
|
||||
|
||||
### Creating a New Tool
|
||||
|
||||
1. Create a new component file in `src/taskpane/components/`
|
||||
@@ -142,6 +176,25 @@ export default MyNewTool;
|
||||
|
||||
### Common PowerPoint.js Operations
|
||||
|
||||
#### Type-Safe Utilities
|
||||
The application provides type-safe utility functions in `src/taskpane/types/office-types.ts`:
|
||||
|
||||
```typescript
|
||||
// Get error message safely from any error type
|
||||
const errorMsg = getErrorMessage(error);
|
||||
|
||||
// Check if a shape is an image
|
||||
if (isPictureShape(shape)) {
|
||||
// It's a picture shape
|
||||
}
|
||||
|
||||
// Get the first selected slide
|
||||
const slide = getFirstSelectedSlide(context);
|
||||
|
||||
// Select shapes by ID
|
||||
selectShapesById(slide, [shape1.id, shape2.id]);
|
||||
```
|
||||
|
||||
#### Selecting Shapes
|
||||
```typescript
|
||||
const shapes = context.presentation.getSelectedShapes();
|
||||
@@ -217,10 +270,40 @@ Recommended testing approach:
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- Minimize `context.sync()` calls, especially in loops
|
||||
- Batch operations when possible
|
||||
### Optimizing context.sync() calls
|
||||
|
||||
A key performance optimization is to batch context.sync() calls:
|
||||
|
||||
```typescript
|
||||
// BAD: Calling context.sync() in a loop
|
||||
for (let i = 0; i < shapes.items.length; i++) {
|
||||
shapes.items[i].load("name");
|
||||
await context.sync(); // Slow! One network roundtrip per shape
|
||||
if (shapes.items[i].name === "Target") {
|
||||
// Do something
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Batching context.sync() calls
|
||||
// Load all properties at once
|
||||
for (let i = 0; i < shapes.items.length; i++) {
|
||||
shapes.items[i].load("name");
|
||||
}
|
||||
await context.sync(); // Single network roundtrip for all shapes
|
||||
|
||||
// Process the data locally
|
||||
for (let i = 0; i < shapes.items.length; i++) {
|
||||
if (shapes.items[i].name === "Target") {
|
||||
// Do something
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Additional performance tips:
|
||||
- Use memoization for expensive calculations
|
||||
- Ensure proper cleanup of event listeners
|
||||
- Use the ActionButton component for consistent loading states
|
||||
- Consider using Web Workers for heavy computations
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
@@ -236,11 +319,14 @@ Recommended testing approach:
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Error Handling**: Always use try/catch blocks with PowerPoint API calls
|
||||
2. **User Feedback**: Provide clear status messages for all operations
|
||||
3. **Accessibility**: Ensure all UI elements have proper labels and keyboard access
|
||||
1. **Error Handling**: Always use try/catch blocks with PowerPoint API calls and use proper types (see `office-types.ts`)
|
||||
2. **User Feedback**: Provide clear status messages for all operations using the StatusContext
|
||||
3. **Accessibility**: Ensure all UI elements have proper ARIA attributes and support keyboard navigation
|
||||
4. **Theme Support**: Use Fluent UI theme tokens for consistent theming
|
||||
5. **Code Organization**: Keep component files focused on a single responsibility
|
||||
6. **Performance**: Batch context.sync() calls and avoid calling them inside loops
|
||||
7. **Reusability**: Use custom hooks for common operations and shared components like ActionButton and Section
|
||||
8. **Type Safety**: Avoid using "any" type and leverage TypeScript's static typing
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
+9
-1
@@ -212,4 +212,12 @@ If you encounter issues not covered in this guide, contact your IT department or
|
||||
|
||||
## Version Information
|
||||
|
||||
This user guide is for Edison PowerPoint Add-in v1.0.0.
|
||||
This user guide is for Edison PowerPoint Add-in v1.1.0.
|
||||
|
||||
### What's New in v1.1.0
|
||||
|
||||
* **Performance Improvements** - Faster operations especially when working with multiple shapes
|
||||
* **Enhanced Stability** - Improved error handling and recovery
|
||||
* **Better UI Experience** - More responsive interface with improved scrolling
|
||||
* **Accessibility Enhancements** - Better support for screen readers and keyboard navigation
|
||||
* **Code Quality** - Improved TypeScript typing for increased reliability
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="TaskPaneApp">
|
||||
<Id>be5c6f61-4bbf-4bd2-a8da-5031abf096a5</Id>
|
||||
<Version>1.0.0.0</Version>
|
||||
<Version>1.1.0.0</Version>
|
||||
<ProviderName>Contoso</ProviderName>
|
||||
<DefaultLocale>en-US</DefaultLocale>
|
||||
<DisplayName DefaultValue="Edison"/>
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "edison-powerpoint-addin",
|
||||
"version": "0.0.1",
|
||||
"version": "1.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "edison-powerpoint-addin",
|
||||
"version": "0.0.1",
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fluentui/react-components": "^9.55.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "edison-powerpoint-addin",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/OfficeDev/Office-Addin-TaskPane-React.git"
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import * as React from "react";
|
||||
import { Button, ButtonProps } from "@fluentui/react-components";
|
||||
import { useStatusContext } from "./App";
|
||||
import { useCommonStyles } from "./commonStyles";
|
||||
import { getErrorMessage } from "../types/office-types";
|
||||
|
||||
export interface ActionButtonProps {
|
||||
icon: ButtonProps["icon"];
|
||||
onClick: () => Promise<void>;
|
||||
disabled?: boolean;
|
||||
title: string;
|
||||
appearance?: "primary" | "secondary" | "outline" | "subtle";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable action button component with consistent styling and error handling
|
||||
*/
|
||||
export const ActionButton: React.FC<ActionButtonProps> = ({
|
||||
icon,
|
||||
onClick,
|
||||
disabled = false,
|
||||
title,
|
||||
appearance = "primary",
|
||||
className = "",
|
||||
}) => {
|
||||
const styles = useCommonStyles();
|
||||
const {
|
||||
setStatusMessage,
|
||||
setStatusType,
|
||||
isProcessing,
|
||||
setIsProcessing
|
||||
} = useStatusContext();
|
||||
|
||||
const handleClick = async () => {
|
||||
if (isProcessing) return;
|
||||
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
await onClick();
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
setStatusMessage(`Error: ${errorMessage}`);
|
||||
setStatusType("error");
|
||||
console.error(`Error in ${title} action:`, error);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
appearance={appearance}
|
||||
className={`${styles.actionButton} ${className}`}
|
||||
onClick={handleClick}
|
||||
icon={icon}
|
||||
disabled={disabled || isProcessing}
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionButton;
|
||||
@@ -56,10 +56,11 @@ export const AlignmentButtons: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Loop through all select shapes and align them
|
||||
for (let i = 0; i < shapes.items.length; i++) {
|
||||
shapes.items[i].left = 81.1;
|
||||
}
|
||||
// Set alignment for all shapes at once
|
||||
shapes.items.forEach(shape => {
|
||||
shape.left = 81.1;
|
||||
});
|
||||
// Single sync after all updates
|
||||
await context.sync();
|
||||
|
||||
setStatusMessage("Objects aligned to left.");
|
||||
@@ -90,10 +91,11 @@ export const AlignmentButtons: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Loop through all select shapes and align them
|
||||
for (let i = 0; i < shapes.items.length; i++) {
|
||||
shapes.items[i].left = 480 - (shapes.items[i].width / 2);
|
||||
}
|
||||
// Set alignment for all shapes at once
|
||||
shapes.items.forEach(shape => {
|
||||
shape.left = 480 - (shape.width / 2);
|
||||
});
|
||||
// Single sync after all updates
|
||||
await context.sync();
|
||||
|
||||
setStatusMessage("Objects aligned to center.");
|
||||
@@ -124,10 +126,11 @@ export const AlignmentButtons: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Loop through all select shapes and align them
|
||||
for (let i = 0; i < shapes.items.length; i++) {
|
||||
shapes.items[i].left = 879.75 - shapes.items[i].width;
|
||||
}
|
||||
// Set alignment for all shapes at once
|
||||
shapes.items.forEach(shape => {
|
||||
shape.left = 879.75 - shape.width;
|
||||
});
|
||||
// Single sync after all updates
|
||||
await context.sync();
|
||||
|
||||
setStatusMessage("Objects aligned to right.");
|
||||
@@ -158,10 +161,11 @@ export const AlignmentButtons: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Loop through all select shapes and align them
|
||||
for (let i = 0; i < shapes.items.length; i++) {
|
||||
shapes.items[i].top = 136.75;
|
||||
}
|
||||
// Set alignment for all shapes at once
|
||||
shapes.items.forEach(shape => {
|
||||
shape.top = 136.75;
|
||||
});
|
||||
// Single sync after all updates
|
||||
await context.sync();
|
||||
|
||||
setStatusMessage("Objects aligned to top.");
|
||||
@@ -192,10 +196,11 @@ export const AlignmentButtons: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Loop through all select shapes and align them
|
||||
for (let i = 0; i < shapes.items.length; i++) {
|
||||
shapes.items[i].top = 321 - (shapes.items[i].height / 2);
|
||||
}
|
||||
// Set alignment for all shapes at once
|
||||
shapes.items.forEach(shape => {
|
||||
shape.top = 321 - (shape.height / 2);
|
||||
});
|
||||
// Single sync after all updates
|
||||
await context.sync();
|
||||
|
||||
setStatusMessage("Objects aligned to middle.");
|
||||
@@ -226,10 +231,11 @@ export const AlignmentButtons: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Loop through all select shapes and align them
|
||||
for (let i = 0; i < shapes.items.length; i++) {
|
||||
shapes.items[i].top = 505.25 - shapes.items[i].height;
|
||||
}
|
||||
// Set alignment for all shapes at once
|
||||
shapes.items.forEach(shape => {
|
||||
shape.top = 505.25 - shape.height;
|
||||
});
|
||||
// Single sync after all updates
|
||||
await context.sync();
|
||||
|
||||
setStatusMessage("Objects aligned to bottom.");
|
||||
|
||||
@@ -10,7 +10,8 @@ import DraftButtons from "./DraftButtons";
|
||||
import ProgressBarButtons from "./ProgressBarButtons";
|
||||
import AlignmentButtons from "./AlignmentButtons";
|
||||
import GridGuidelineManager from "./GridGuidelineManager";
|
||||
import { makeStyles, Text, Subtitle1, tokens, Theme, Spinner } from "@fluentui/react-components";
|
||||
import Section from "./Section";
|
||||
import { makeStyles, Text, tokens, Theme, Spinner } from "@fluentui/react-components";
|
||||
import { ShapeUnionRegular, SquareRegular, InfoRegular } from "@fluentui/react-icons";
|
||||
|
||||
interface AppProps {
|
||||
@@ -42,12 +43,15 @@ const useStyles = makeStyles({
|
||||
root: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
padding: "16px",
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
|
||||
backgroundColor: tokens.colorNeutralBackground1,
|
||||
overflow: "auto",
|
||||
maxHeight: "100vh",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
statusContainer: {
|
||||
marginTop: "4px",
|
||||
@@ -76,22 +80,6 @@ const useStyles = makeStyles({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
section: {
|
||||
marginBottom: "6px",
|
||||
padding: "12px",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: tokens.colorNeutralBackground2,
|
||||
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.06)",
|
||||
transition: "all 0.2s ease",
|
||||
":hover": {
|
||||
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
|
||||
},
|
||||
},
|
||||
sectionTitle: {
|
||||
marginBottom: "12px",
|
||||
fontWeight: tokens.fontWeightSemibold,
|
||||
color: tokens.colorBrandForeground1,
|
||||
},
|
||||
footer: {
|
||||
fontSize: "12px",
|
||||
color: tokens.colorNeutralForeground3,
|
||||
@@ -153,10 +141,7 @@ const App: React.FC<AppProps> = () => {
|
||||
setIsProcessing
|
||||
}}>
|
||||
<div className={styles.root} data-theme={theme}>
|
||||
<div className={styles.section}>
|
||||
<Subtitle1 block className={styles.sectionTitle}>
|
||||
Content
|
||||
</Subtitle1>
|
||||
<Section title="Content">
|
||||
<MatchProperties />
|
||||
<div style={{ marginTop: "8px" }}></div>
|
||||
<MatchSizes />
|
||||
@@ -166,42 +151,27 @@ const App: React.FC<AppProps> = () => {
|
||||
<InsertTitles />
|
||||
<div style={{ marginTop: "8px" }}></div>
|
||||
<RoundImage />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className={styles.section}>
|
||||
<Subtitle1 block className={styles.sectionTitle}>
|
||||
Progress Bar
|
||||
</Subtitle1>
|
||||
<Section title="Progress Bar">
|
||||
<ProgressBarButtons />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className={styles.section}>
|
||||
<Subtitle1 block className={styles.sectionTitle}>
|
||||
Confidential Marking
|
||||
</Subtitle1>
|
||||
<Section title="Confidential Marking">
|
||||
<ConfidentialButtons />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className={styles.section}>
|
||||
<Subtitle1 block className={styles.sectionTitle}>
|
||||
Draft Watermark
|
||||
</Subtitle1>
|
||||
<Section title="Draft Watermark">
|
||||
<DraftButtons />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className={styles.section}>
|
||||
<Subtitle1 block className={styles.sectionTitle}>
|
||||
Alignment
|
||||
</Subtitle1>
|
||||
<Section title="Alignment">
|
||||
<AlignmentButtons />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className={styles.section}>
|
||||
<Subtitle1 block className={styles.sectionTitle}>
|
||||
Layout
|
||||
</Subtitle1>
|
||||
<Section title="Layout">
|
||||
<GridGuidelineManager />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Status message area at the bottom */}
|
||||
{isProcessing && (
|
||||
@@ -223,7 +193,7 @@ const App: React.FC<AppProps> = () => {
|
||||
)}
|
||||
|
||||
<div className={styles.footer}>
|
||||
<Text size={100}>Edison • v1.0.0</Text>
|
||||
<Text size={100}>Edison • v1.1.0</Text>
|
||||
</div>
|
||||
</div>
|
||||
</StatusContext.Provider>
|
||||
|
||||
@@ -241,13 +241,17 @@ export const GridGuidelineManager: React.FC = () => {
|
||||
shapes.load("items");
|
||||
await context.sync();
|
||||
|
||||
// Find shapes that are part of our grid
|
||||
// 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 part of our grid 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 part of our grid
|
||||
if (shape.name && shape.name.startsWith(GRID_PREFIX)) {
|
||||
shape.delete();
|
||||
@@ -255,6 +259,7 @@ export const GridGuidelineManager: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Single sync after all deletions
|
||||
await context.sync();
|
||||
|
||||
if (showStatus) {
|
||||
|
||||
@@ -74,10 +74,27 @@ export const MatchProperties: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// First, load all text frames in a batch
|
||||
if (hasText) {
|
||||
// Pre-load textFrame for all target shapes
|
||||
for (let i = 1; i < shapes.items.length; i++) {
|
||||
shapes.items[i].load("textFrame");
|
||||
}
|
||||
await context.sync();
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
// Now apply properties to all shapes
|
||||
let successCount = 0;
|
||||
let textStyleCount = 0;
|
||||
|
||||
// Loop through remaining shapes and apply properties
|
||||
|
||||
for (let i = 1; i < shapes.items.length; i++) {
|
||||
try {
|
||||
const targetShape = shapes.items[i];
|
||||
@@ -100,7 +117,6 @@ export const MatchProperties: React.FC = () => {
|
||||
targetShape.lineFormat.color = firstShape.lineFormat.color;
|
||||
}
|
||||
|
||||
await context.sync();
|
||||
propertiesApplied = true;
|
||||
} catch (err) {
|
||||
console.error(`Error applying line format to shape ${i}:`, err);
|
||||
@@ -117,56 +133,43 @@ export const MatchProperties: React.FC = () => {
|
||||
targetShape.fill.foregroundColor = firstShape.fill.foregroundColor;
|
||||
}
|
||||
|
||||
await context.sync();
|
||||
propertiesApplied = true;
|
||||
} catch (err) {
|
||||
console.error(`Error applying fill format to shape ${i}:`, err);
|
||||
}
|
||||
|
||||
// Apply text properties if the source has text
|
||||
if (hasText) {
|
||||
if (hasText && targetShape.textFrame && targetShape.textFrame.textRange) {
|
||||
try {
|
||||
// First check if target shape has text
|
||||
targetShape.load("textFrame");
|
||||
await context.sync();
|
||||
const sourceFont = firstShape.textFrame.textRange.font;
|
||||
const targetFont = targetShape.textFrame.textRange.font;
|
||||
|
||||
if (targetShape.textFrame) {
|
||||
targetShape.textFrame.load("textRange");
|
||||
await context.sync();
|
||||
|
||||
if (targetShape.textFrame.textRange) {
|
||||
const sourceFont = firstShape.textFrame.textRange.font;
|
||||
const targetFont = targetShape.textFrame.textRange.font;
|
||||
|
||||
// Apply font properties
|
||||
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;
|
||||
}
|
||||
|
||||
await context.sync();
|
||||
textStyleCount++;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
textStyleCount++;
|
||||
} catch (err) {
|
||||
console.error(`Error applying text format to shape ${i}:`, err);
|
||||
}
|
||||
@@ -180,6 +183,9 @@ export const MatchProperties: React.FC = () => {
|
||||
console.error(`Error updating shape ${i}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// Single sync after all property changes
|
||||
await context.sync();
|
||||
|
||||
// Final status message based on what was applied
|
||||
if (successCount > 0) {
|
||||
|
||||
@@ -1,89 +1,59 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
Button,
|
||||
Text,
|
||||
Tooltip,
|
||||
InfoLabel,
|
||||
Card
|
||||
} from "@fluentui/react-components";
|
||||
import {
|
||||
ArrowFitInRegular,
|
||||
ArrowSortDownRegular,
|
||||
ArrowSortUpRegular
|
||||
} from "@fluentui/react-icons";
|
||||
import { ArrowFitInRegular } from "@fluentui/react-icons";
|
||||
import { useStatusContext } from "./App";
|
||||
import { useCommonStyles } from "./commonStyles";
|
||||
import ActionButton from "./ActionButton";
|
||||
|
||||
export const MatchSizes: React.FC = () => {
|
||||
const styles = useCommonStyles();
|
||||
const {
|
||||
statusMessage, setStatusMessage,
|
||||
statusType, setStatusType,
|
||||
isProcessing, setIsProcessing
|
||||
} = useStatusContext();
|
||||
const { setStatusMessage, setStatusType } = useStatusContext();
|
||||
|
||||
const matchSizeToFirstSelected = async () => {
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
await PowerPoint.run(async (context) => {
|
||||
// Get the selected shapes
|
||||
const shapes = context.presentation.getSelectedShapes();
|
||||
shapes.load("items");
|
||||
await context.sync();
|
||||
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 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
|
||||
if (shapes.items.length === 1) {
|
||||
setStatusMessage("Please select multiple shapes to resize.");
|
||||
setStatusType("warning");
|
||||
return;
|
||||
}
|
||||
// Check if there is more than one shape selected
|
||||
if (shapes.items.length === 1) {
|
||||
setStatusMessage("Please select multiple shapes to resize.");
|
||||
setStatusType("warning");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the first shape's dimensions
|
||||
const firstShape = shapes.items[0];
|
||||
firstShape.load("width,height");
|
||||
await context.sync();
|
||||
// Get the first shape's dimensions
|
||||
const firstShape = shapes.items[0];
|
||||
firstShape.load("width,height");
|
||||
await context.sync();
|
||||
|
||||
// Loop through the remaining shapes and resize them
|
||||
for (let i = 1; i < shapes.items.length; i++) {
|
||||
shapes.items[i].width = firstShape.width;
|
||||
shapes.items[i].height = firstShape.height;
|
||||
}
|
||||
// Loop through the remaining shapes and resize them
|
||||
for (let i = 1; i < shapes.items.length; i++) {
|
||||
shapes.items[i].width = firstShape.width;
|
||||
shapes.items[i].height = firstShape.height;
|
||||
}
|
||||
|
||||
await context.sync();
|
||||
setStatusMessage(`Resized ${shapes.items.length - 1} shapes to match the first selected shape.`);
|
||||
setStatusType("success");
|
||||
|
||||
// Timeout is handled in App.tsx now
|
||||
});
|
||||
} catch (error) {
|
||||
setStatusMessage(`Error: ${error.message}`);
|
||||
setStatusType("error");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
await context.sync();
|
||||
setStatusMessage(`Resized ${shapes.items.length - 1} shapes to match the first selected shape.`);
|
||||
setStatusType("success");
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.buttonGroup}>
|
||||
<Button
|
||||
appearance="primary"
|
||||
className={styles.actionButton}
|
||||
onClick={matchSizeToFirstSelected}
|
||||
icon={<ArrowFitInRegular />}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
Match Sizes
|
||||
</Button>
|
||||
<ActionButton
|
||||
title="Match Sizes"
|
||||
icon={<ArrowFitInRegular />}
|
||||
onClick={matchSizeToFirstSelected}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "@fluentui/react-icons";
|
||||
import { useStatusContext } from "./App";
|
||||
import { useCommonStyles } from "./commonStyles";
|
||||
import { getErrorMessage, isPictureShape, getFirstSelectedSlide, selectShapesById } from "../types/office-types";
|
||||
|
||||
export const RoundImage: React.FC = () => {
|
||||
const styles = useCommonStyles();
|
||||
@@ -38,8 +39,8 @@ export const RoundImage: React.FC = () => {
|
||||
shape.load(["type"]);
|
||||
await context.sync();
|
||||
|
||||
// Ensure the shape is a picture
|
||||
if (shape.type !== PowerPoint.ShapeType.image) {
|
||||
// Ensure the shape is a picture using our type-safe utility
|
||||
if (!isPictureShape(shape)) {
|
||||
setStatusMessage("Please select an image.");
|
||||
setStatusType("warning");
|
||||
return;
|
||||
@@ -53,8 +54,10 @@ export const RoundImage: React.FC = () => {
|
||||
const width = shape.width;
|
||||
const height = shape.height;
|
||||
|
||||
const slide = context.presentation.getSelectedSlides().getItemAt(0);
|
||||
const maskShape = slide.shapes.addGeometricShape("Ellipse" as any);
|
||||
// Get the current slide using our type-safe utility
|
||||
const slide = getFirstSelectedSlide(context);
|
||||
// Create elliptical mask with proper type
|
||||
const maskShape = slide.shapes.addGeometricShape(PowerPoint.GeometricShapeType.ellipse);
|
||||
|
||||
maskShape.load(["width", "height", "left", "top", "id"]);
|
||||
await context.sync();
|
||||
@@ -73,17 +76,19 @@ export const RoundImage: React.FC = () => {
|
||||
setStatusMessage("Created mask shape. Please select both the image and the oval, then use the 'Shape Forma > Merge Shapes > Intersect' command in PowerPoint.");
|
||||
setStatusType("warning");
|
||||
|
||||
slide.setSelectedShapes([shape.id, maskShape.id]);
|
||||
// Use our type-safe utility for selecting shapes
|
||||
selectShapesById(slide, [shape.id, maskShape.id]);
|
||||
// Ensure we maintain the same size
|
||||
shape.width = width;
|
||||
shape.height = height;
|
||||
|
||||
await context.sync();
|
||||
});
|
||||
} catch (error) {
|
||||
setStatusMessage(`Error: ${error.message}`);
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
setStatusMessage(`Error: ${errorMessage}`);
|
||||
setStatusType("error");
|
||||
console.error("Main error:", error);
|
||||
console.error("Round image error:", error);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import * as React from "react";
|
||||
import { Subtitle1, makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
interface SectionProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
section: {
|
||||
marginBottom: "6px",
|
||||
padding: "12px",
|
||||
borderRadius: "8px",
|
||||
backgroundColor: tokens.colorNeutralBackground2,
|
||||
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.06)",
|
||||
transition: "all 0.2s ease",
|
||||
":hover": {
|
||||
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
|
||||
},
|
||||
},
|
||||
sectionTitle: {
|
||||
marginBottom: "12px",
|
||||
fontWeight: tokens.fontWeightSemibold,
|
||||
color: tokens.colorBrandForeground1,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Reusable section component with consistent styling
|
||||
*/
|
||||
export const Section: React.FC<SectionProps> = ({ title, children }) => {
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<div className={styles.section}>
|
||||
<Subtitle1 block className={styles.sectionTitle}>
|
||||
{title}
|
||||
</Subtitle1>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Section;
|
||||
@@ -1,82 +1,61 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
Button
|
||||
} from "@fluentui/react-components";
|
||||
import {
|
||||
ArrowSwapRegular
|
||||
} from "@fluentui/react-icons";
|
||||
import { ArrowSwapRegular } from "@fluentui/react-icons";
|
||||
import { useStatusContext } from "./App";
|
||||
import { useCommonStyles } from "./commonStyles";
|
||||
import ActionButton from "./ActionButton";
|
||||
|
||||
export const SwapPositions: React.FC = () => {
|
||||
const styles = useCommonStyles();
|
||||
const {
|
||||
statusMessage, setStatusMessage,
|
||||
statusType, setStatusType,
|
||||
isProcessing, setIsProcessing
|
||||
} = useStatusContext();
|
||||
const { setStatusMessage, setStatusType } = useStatusContext();
|
||||
|
||||
const swapPositionsOfTwoSelectedObjects = async () => {
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
await PowerPoint.run(async (context) => {
|
||||
// Get the selected shapes
|
||||
const shapes = context.presentation.getSelectedShapes();
|
||||
shapes.load("items/count");
|
||||
await context.sync();
|
||||
await PowerPoint.run(async (context) => {
|
||||
// Get the selected shapes
|
||||
const shapes = context.presentation.getSelectedShapes();
|
||||
shapes.load("items/count");
|
||||
await context.sync();
|
||||
|
||||
// Check if exactly two shapes are selected
|
||||
if (shapes.items.length !== 2) {
|
||||
setStatusMessage("Please select exactly two shapes to swap their positions.");
|
||||
setStatusType("warning");
|
||||
return;
|
||||
}
|
||||
// Check if exactly two shapes are selected
|
||||
if (shapes.items.length !== 2) {
|
||||
setStatusMessage("Please select exactly two shapes to swap their positions.");
|
||||
setStatusType("warning");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the two shapes
|
||||
const shapeObj1 = shapes.items[0];
|
||||
const shapeObj2 = shapes.items[1];
|
||||
|
||||
// Load position properties
|
||||
shapeObj1.load("left,top");
|
||||
shapeObj2.load("left,top");
|
||||
await context.sync();
|
||||
|
||||
// Store the position of the first shape
|
||||
const tempLeft = shapeObj1.left;
|
||||
const tempTop = shapeObj1.top;
|
||||
|
||||
// Swap positions
|
||||
shapeObj1.left = shapeObj2.left;
|
||||
shapeObj1.top = shapeObj2.top;
|
||||
shapeObj2.left = tempLeft;
|
||||
shapeObj2.top = tempTop;
|
||||
|
||||
await context.sync();
|
||||
|
||||
setStatusMessage("Positions of the two shapes have been swapped successfully.");
|
||||
setStatusType("success");
|
||||
});
|
||||
} catch (error) {
|
||||
setStatusMessage(`Error: ${error.message}`);
|
||||
setStatusType("error");
|
||||
console.error("Swap positions error:", error);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
// Get the two shapes
|
||||
const shapeObj1 = shapes.items[0];
|
||||
const shapeObj2 = shapes.items[1];
|
||||
|
||||
// Load position properties
|
||||
shapeObj1.load("left,top");
|
||||
shapeObj2.load("left,top");
|
||||
await context.sync();
|
||||
|
||||
// Store the position of the first shape
|
||||
const tempLeft = shapeObj1.left;
|
||||
const tempTop = shapeObj1.top;
|
||||
|
||||
// Swap positions
|
||||
shapeObj1.left = shapeObj2.left;
|
||||
shapeObj1.top = shapeObj2.top;
|
||||
shapeObj2.left = tempLeft;
|
||||
shapeObj2.top = tempTop;
|
||||
|
||||
await context.sync();
|
||||
|
||||
setStatusMessage("Positions of the two shapes have been swapped successfully.");
|
||||
setStatusType("success");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.buttonGroup}>
|
||||
<Button
|
||||
appearance="primary"
|
||||
className={styles.actionButton}
|
||||
onClick={swapPositionsOfTwoSelectedObjects}
|
||||
<ActionButton
|
||||
title="Swap Positions"
|
||||
icon={<ArrowSwapRegular />}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
Swap Positions
|
||||
</Button>
|
||||
onClick={swapPositionsOfTwoSelectedObjects}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+15
-3
@@ -19,9 +19,21 @@ Office.onReady(() => {
|
||||
);
|
||||
});
|
||||
|
||||
if ((module as any).hot) {
|
||||
(module as any).hot.accept("./components/App", () => {
|
||||
// Define proper module hot interface for webpack hot module replacement
|
||||
interface HotModule extends NodeModule {
|
||||
hot?: {
|
||||
accept(path: string, callback: () => void): void;
|
||||
};
|
||||
}
|
||||
|
||||
// Use the proper type for module with HMR
|
||||
if ((module as HotModule).hot) {
|
||||
(module as HotModule).hot?.accept("./components/App", () => {
|
||||
const NextApp = require("./components/App").default;
|
||||
root?.render(NextApp);
|
||||
root?.render(
|
||||
<FluentProvider theme={webLightTheme}>
|
||||
<NextApp title={title} />
|
||||
</FluentProvider>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Type definitions for the Office JS API
|
||||
*/
|
||||
|
||||
/**
|
||||
* Extended Error interface for Office/PowerPoint API errors
|
||||
*/
|
||||
export interface OfficeApiError extends Error {
|
||||
code?: string;
|
||||
debugInfo?: {
|
||||
code?: string;
|
||||
message?: string;
|
||||
errorLocation?: string;
|
||||
statement?: string;
|
||||
innerError?: any;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to safely extract error message from any error object
|
||||
*/
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe wrapper for PowerPoint API operations on shapes
|
||||
*/
|
||||
export function isPictureShape(shape: PowerPoint.Shape): boolean {
|
||||
return shape.type === PowerPoint.ShapeType.image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe wrapper for working with PowerPoint slides
|
||||
*/
|
||||
export function getFirstSelectedSlide(context: PowerPoint.RequestContext): PowerPoint.Slide {
|
||||
return context.presentation.getSelectedSlides().getItemAt(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe utility for selecting shapes
|
||||
*/
|
||||
export function selectShapesById(slide: PowerPoint.Slide, shapeIds: string[]): void {
|
||||
slide.setSelectedShapes(shapeIds);
|
||||
}
|
||||
Reference in New Issue
Block a user