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
|
## Version History
|
||||||
|
|
||||||
|
- v1.1.0 - Performance improvements, code quality, and UI enhancements
|
||||||
- v1.0.0 - First stable release
|
- v1.0.0 - First stable release
|
||||||
- v0.1.0 - Initial release with core functionality
|
- 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
|
│ │ └── commands.ts # Command implementations
|
||||||
│ └── taskpane/ # Main add-in UI
|
│ └── taskpane/ # Main add-in UI
|
||||||
│ ├── components/ # React components
|
│ ├── components/ # React components
|
||||||
|
│ │ ├── ActionButton.tsx # Reusable button with error handling
|
||||||
│ │ ├── AlignmentButtons.tsx
|
│ │ ├── AlignmentButtons.tsx
|
||||||
│ │ ├── App.tsx # Main application component
|
│ │ ├── App.tsx # Main application component
|
||||||
│ │ ├── ConfidentialButtons.tsx
|
│ │ ├── ConfidentialButtons.tsx
|
||||||
│ │ ├── DraftButtons.tsx
|
│ │ ├── DraftButtons.tsx
|
||||||
|
│ │ ├── GridGuidelineManager.tsx
|
||||||
│ │ ├── Header.tsx
|
│ │ ├── Header.tsx
|
||||||
│ │ ├── InsertTitles.tsx
|
│ │ ├── InsertTitles.tsx
|
||||||
│ │ ├── MatchProperties.tsx
|
│ │ ├── MatchProperties.tsx
|
||||||
│ │ ├── MatchSizes.tsx
|
│ │ ├── MatchSizes.tsx
|
||||||
│ │ ├── ProgressBarButtons.tsx
|
│ │ ├── ProgressBarButtons.tsx
|
||||||
│ │ ├── RoundImage.tsx
|
│ │ ├── RoundImage.tsx
|
||||||
|
│ │ ├── Section.tsx # Reusable section component
|
||||||
|
│ │ ├── SmartTextFormatter.tsx
|
||||||
│ │ ├── SwapPositions.tsx
|
│ │ ├── SwapPositions.tsx
|
||||||
│ │ └── commonStyles.tsx
|
│ │ └── commonStyles.tsx
|
||||||
|
│ ├── types/ # TypeScript type definitions
|
||||||
|
│ │ └── office-types.ts # Office API type utilities
|
||||||
│ ├── index.tsx # Entry point
|
│ ├── index.tsx # Entry point
|
||||||
│ └── taskpane.html # Main HTML container
|
│ └── taskpane.html # Main HTML container
|
||||||
├── babel.config.json # Babel configuration
|
├── babel.config.json # Babel configuration
|
||||||
@@ -92,6 +98,34 @@ npx office-addin-dev-certs install
|
|||||||
|
|
||||||
## Component Development Guidelines
|
## 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
|
### Creating a New Tool
|
||||||
|
|
||||||
1. Create a new component file in `src/taskpane/components/`
|
1. Create a new component file in `src/taskpane/components/`
|
||||||
@@ -142,6 +176,25 @@ export default MyNewTool;
|
|||||||
|
|
||||||
### Common PowerPoint.js Operations
|
### 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
|
#### Selecting Shapes
|
||||||
```typescript
|
```typescript
|
||||||
const shapes = context.presentation.getSelectedShapes();
|
const shapes = context.presentation.getSelectedShapes();
|
||||||
@@ -217,10 +270,40 @@ Recommended testing approach:
|
|||||||
|
|
||||||
## Performance Considerations
|
## Performance Considerations
|
||||||
|
|
||||||
- Minimize `context.sync()` calls, especially in loops
|
### Optimizing context.sync() calls
|
||||||
- Batch operations when possible
|
|
||||||
|
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
|
- Use memoization for expensive calculations
|
||||||
- Ensure proper cleanup of event listeners
|
- 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
|
## Common Issues and Solutions
|
||||||
|
|
||||||
@@ -236,11 +319,14 @@ Recommended testing approach:
|
|||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
1. **Error Handling**: Always use try/catch blocks with PowerPoint API calls
|
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
|
2. **User Feedback**: Provide clear status messages for all operations using the StatusContext
|
||||||
3. **Accessibility**: Ensure all UI elements have proper labels and keyboard access
|
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
|
4. **Theme Support**: Use Fluent UI theme tokens for consistent theming
|
||||||
5. **Code Organization**: Keep component files focused on a single responsibility
|
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
|
## Contributing
|
||||||
|
|
||||||
|
|||||||
+9
-1
@@ -212,4 +212,12 @@ If you encounter issues not covered in this guide, contact your IT department or
|
|||||||
|
|
||||||
## Version Information
|
## 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"?>
|
<?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">
|
<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>
|
<Id>be5c6f61-4bbf-4bd2-a8da-5031abf096a5</Id>
|
||||||
<Version>1.0.0.0</Version>
|
<Version>1.1.0.0</Version>
|
||||||
<ProviderName>Contoso</ProviderName>
|
<ProviderName>Contoso</ProviderName>
|
||||||
<DefaultLocale>en-US</DefaultLocale>
|
<DefaultLocale>en-US</DefaultLocale>
|
||||||
<DisplayName DefaultValue="Edison"/>
|
<DisplayName DefaultValue="Edison"/>
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "edison-powerpoint-addin",
|
"name": "edison-powerpoint-addin",
|
||||||
"version": "0.0.1",
|
"version": "1.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "edison-powerpoint-addin",
|
"name": "edison-powerpoint-addin",
|
||||||
"version": "0.0.1",
|
"version": "1.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluentui/react-components": "^9.55.1",
|
"@fluentui/react-components": "^9.55.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "edison-powerpoint-addin",
|
"name": "edison-powerpoint-addin",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/OfficeDev/Office-Addin-TaskPane-React.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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop through all select shapes and align them
|
// Set alignment for all shapes at once
|
||||||
for (let i = 0; i < shapes.items.length; i++) {
|
shapes.items.forEach(shape => {
|
||||||
shapes.items[i].left = 81.1;
|
shape.left = 81.1;
|
||||||
}
|
});
|
||||||
|
// Single sync after all updates
|
||||||
await context.sync();
|
await context.sync();
|
||||||
|
|
||||||
setStatusMessage("Objects aligned to left.");
|
setStatusMessage("Objects aligned to left.");
|
||||||
@@ -90,10 +91,11 @@ export const AlignmentButtons: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop through all select shapes and align them
|
// Set alignment for all shapes at once
|
||||||
for (let i = 0; i < shapes.items.length; i++) {
|
shapes.items.forEach(shape => {
|
||||||
shapes.items[i].left = 480 - (shapes.items[i].width / 2);
|
shape.left = 480 - (shape.width / 2);
|
||||||
}
|
});
|
||||||
|
// Single sync after all updates
|
||||||
await context.sync();
|
await context.sync();
|
||||||
|
|
||||||
setStatusMessage("Objects aligned to center.");
|
setStatusMessage("Objects aligned to center.");
|
||||||
@@ -124,10 +126,11 @@ export const AlignmentButtons: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop through all select shapes and align them
|
// Set alignment for all shapes at once
|
||||||
for (let i = 0; i < shapes.items.length; i++) {
|
shapes.items.forEach(shape => {
|
||||||
shapes.items[i].left = 879.75 - shapes.items[i].width;
|
shape.left = 879.75 - shape.width;
|
||||||
}
|
});
|
||||||
|
// Single sync after all updates
|
||||||
await context.sync();
|
await context.sync();
|
||||||
|
|
||||||
setStatusMessage("Objects aligned to right.");
|
setStatusMessage("Objects aligned to right.");
|
||||||
@@ -158,10 +161,11 @@ export const AlignmentButtons: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop through all select shapes and align them
|
// Set alignment for all shapes at once
|
||||||
for (let i = 0; i < shapes.items.length; i++) {
|
shapes.items.forEach(shape => {
|
||||||
shapes.items[i].top = 136.75;
|
shape.top = 136.75;
|
||||||
}
|
});
|
||||||
|
// Single sync after all updates
|
||||||
await context.sync();
|
await context.sync();
|
||||||
|
|
||||||
setStatusMessage("Objects aligned to top.");
|
setStatusMessage("Objects aligned to top.");
|
||||||
@@ -192,10 +196,11 @@ export const AlignmentButtons: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop through all select shapes and align them
|
// Set alignment for all shapes at once
|
||||||
for (let i = 0; i < shapes.items.length; i++) {
|
shapes.items.forEach(shape => {
|
||||||
shapes.items[i].top = 321 - (shapes.items[i].height / 2);
|
shape.top = 321 - (shape.height / 2);
|
||||||
}
|
});
|
||||||
|
// Single sync after all updates
|
||||||
await context.sync();
|
await context.sync();
|
||||||
|
|
||||||
setStatusMessage("Objects aligned to middle.");
|
setStatusMessage("Objects aligned to middle.");
|
||||||
@@ -226,10 +231,11 @@ export const AlignmentButtons: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop through all select shapes and align them
|
// Set alignment for all shapes at once
|
||||||
for (let i = 0; i < shapes.items.length; i++) {
|
shapes.items.forEach(shape => {
|
||||||
shapes.items[i].top = 505.25 - shapes.items[i].height;
|
shape.top = 505.25 - shape.height;
|
||||||
}
|
});
|
||||||
|
// Single sync after all updates
|
||||||
await context.sync();
|
await context.sync();
|
||||||
|
|
||||||
setStatusMessage("Objects aligned to bottom.");
|
setStatusMessage("Objects aligned to bottom.");
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import DraftButtons from "./DraftButtons";
|
|||||||
import ProgressBarButtons from "./ProgressBarButtons";
|
import ProgressBarButtons from "./ProgressBarButtons";
|
||||||
import AlignmentButtons from "./AlignmentButtons";
|
import AlignmentButtons from "./AlignmentButtons";
|
||||||
import GridGuidelineManager from "./GridGuidelineManager";
|
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";
|
import { ShapeUnionRegular, SquareRegular, InfoRegular } from "@fluentui/react-icons";
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
@@ -42,12 +43,15 @@ const useStyles = makeStyles({
|
|||||||
root: {
|
root: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
height: "100%",
|
|
||||||
padding: "16px",
|
padding: "16px",
|
||||||
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
|
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
|
||||||
backgroundColor: tokens.colorNeutralBackground1,
|
backgroundColor: tokens.colorNeutralBackground1,
|
||||||
overflow: "auto",
|
overflow: "auto",
|
||||||
maxHeight: "100vh",
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
},
|
},
|
||||||
statusContainer: {
|
statusContainer: {
|
||||||
marginTop: "4px",
|
marginTop: "4px",
|
||||||
@@ -76,22 +80,6 @@ const useStyles = makeStyles({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
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: {
|
footer: {
|
||||||
fontSize: "12px",
|
fontSize: "12px",
|
||||||
color: tokens.colorNeutralForeground3,
|
color: tokens.colorNeutralForeground3,
|
||||||
@@ -153,10 +141,7 @@ const App: React.FC<AppProps> = () => {
|
|||||||
setIsProcessing
|
setIsProcessing
|
||||||
}}>
|
}}>
|
||||||
<div className={styles.root} data-theme={theme}>
|
<div className={styles.root} data-theme={theme}>
|
||||||
<div className={styles.section}>
|
<Section title="Content">
|
||||||
<Subtitle1 block className={styles.sectionTitle}>
|
|
||||||
Content
|
|
||||||
</Subtitle1>
|
|
||||||
<MatchProperties />
|
<MatchProperties />
|
||||||
<div style={{ marginTop: "8px" }}></div>
|
<div style={{ marginTop: "8px" }}></div>
|
||||||
<MatchSizes />
|
<MatchSizes />
|
||||||
@@ -166,42 +151,27 @@ const App: React.FC<AppProps> = () => {
|
|||||||
<InsertTitles />
|
<InsertTitles />
|
||||||
<div style={{ marginTop: "8px" }}></div>
|
<div style={{ marginTop: "8px" }}></div>
|
||||||
<RoundImage />
|
<RoundImage />
|
||||||
</div>
|
</Section>
|
||||||
|
|
||||||
<div className={styles.section}>
|
<Section title="Progress Bar">
|
||||||
<Subtitle1 block className={styles.sectionTitle}>
|
|
||||||
Progress Bar
|
|
||||||
</Subtitle1>
|
|
||||||
<ProgressBarButtons />
|
<ProgressBarButtons />
|
||||||
</div>
|
</Section>
|
||||||
|
|
||||||
<div className={styles.section}>
|
<Section title="Confidential Marking">
|
||||||
<Subtitle1 block className={styles.sectionTitle}>
|
|
||||||
Confidential Marking
|
|
||||||
</Subtitle1>
|
|
||||||
<ConfidentialButtons />
|
<ConfidentialButtons />
|
||||||
</div>
|
</Section>
|
||||||
|
|
||||||
<div className={styles.section}>
|
<Section title="Draft Watermark">
|
||||||
<Subtitle1 block className={styles.sectionTitle}>
|
|
||||||
Draft Watermark
|
|
||||||
</Subtitle1>
|
|
||||||
<DraftButtons />
|
<DraftButtons />
|
||||||
</div>
|
</Section>
|
||||||
|
|
||||||
<div className={styles.section}>
|
<Section title="Alignment">
|
||||||
<Subtitle1 block className={styles.sectionTitle}>
|
|
||||||
Alignment
|
|
||||||
</Subtitle1>
|
|
||||||
<AlignmentButtons />
|
<AlignmentButtons />
|
||||||
</div>
|
</Section>
|
||||||
|
|
||||||
<div className={styles.section}>
|
<Section title="Layout">
|
||||||
<Subtitle1 block className={styles.sectionTitle}>
|
|
||||||
Layout
|
|
||||||
</Subtitle1>
|
|
||||||
<GridGuidelineManager />
|
<GridGuidelineManager />
|
||||||
</div>
|
</Section>
|
||||||
|
|
||||||
{/* Status message area at the bottom */}
|
{/* Status message area at the bottom */}
|
||||||
{isProcessing && (
|
{isProcessing && (
|
||||||
@@ -223,7 +193,7 @@ const App: React.FC<AppProps> = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
<Text size={100}>Edison • v1.0.0</Text>
|
<Text size={100}>Edison • v1.1.0</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</StatusContext.Provider>
|
</StatusContext.Provider>
|
||||||
|
|||||||
@@ -241,13 +241,17 @@ export const GridGuidelineManager: React.FC = () => {
|
|||||||
shapes.load("items");
|
shapes.load("items");
|
||||||
await context.sync();
|
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;
|
let removedCount = 0;
|
||||||
for (let i = 0; i < shapes.items.length; i++) {
|
for (let i = 0; i < allShapes.length; i++) {
|
||||||
const shape = shapes.items[i];
|
const shape = allShapes[i];
|
||||||
shape.load("name");
|
|
||||||
await context.sync();
|
|
||||||
|
|
||||||
// Check if this shape is part of our grid
|
// Check if this shape is part of our grid
|
||||||
if (shape.name && shape.name.startsWith(GRID_PREFIX)) {
|
if (shape.name && shape.name.startsWith(GRID_PREFIX)) {
|
||||||
shape.delete();
|
shape.delete();
|
||||||
@@ -255,6 +259,7 @@ export const GridGuidelineManager: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Single sync after all deletions
|
||||||
await context.sync();
|
await context.sync();
|
||||||
|
|
||||||
if (showStatus) {
|
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 successCount = 0;
|
||||||
let textStyleCount = 0;
|
let textStyleCount = 0;
|
||||||
|
|
||||||
// Loop through remaining shapes and apply properties
|
|
||||||
for (let i = 1; i < shapes.items.length; i++) {
|
for (let i = 1; i < shapes.items.length; i++) {
|
||||||
try {
|
try {
|
||||||
const targetShape = shapes.items[i];
|
const targetShape = shapes.items[i];
|
||||||
@@ -100,7 +117,6 @@ export const MatchProperties: React.FC = () => {
|
|||||||
targetShape.lineFormat.color = firstShape.lineFormat.color;
|
targetShape.lineFormat.color = firstShape.lineFormat.color;
|
||||||
}
|
}
|
||||||
|
|
||||||
await context.sync();
|
|
||||||
propertiesApplied = true;
|
propertiesApplied = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error applying line format to shape ${i}:`, 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;
|
targetShape.fill.foregroundColor = firstShape.fill.foregroundColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
await context.sync();
|
|
||||||
propertiesApplied = true;
|
propertiesApplied = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error applying fill format to shape ${i}:`, err);
|
console.error(`Error applying fill format to shape ${i}:`, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply text properties if the source has text
|
// Apply text properties if the source has text
|
||||||
if (hasText) {
|
if (hasText && targetShape.textFrame && targetShape.textFrame.textRange) {
|
||||||
try {
|
try {
|
||||||
// First check if target shape has text
|
const sourceFont = firstShape.textFrame.textRange.font;
|
||||||
targetShape.load("textFrame");
|
const targetFont = targetShape.textFrame.textRange.font;
|
||||||
await context.sync();
|
|
||||||
|
|
||||||
if (targetShape.textFrame) {
|
// Apply font properties in batch
|
||||||
targetShape.textFrame.load("textRange");
|
if (sourceFont.name !== undefined) {
|
||||||
await context.sync();
|
targetFont.name = sourceFont.name;
|
||||||
|
|
||||||
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++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
} catch (err) {
|
||||||
console.error(`Error applying text format to shape ${i}:`, 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);
|
console.error(`Error updating shape ${i}:`, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Single sync after all property changes
|
||||||
|
await context.sync();
|
||||||
|
|
||||||
// Final status message based on what was applied
|
// Final status message based on what was applied
|
||||||
if (successCount > 0) {
|
if (successCount > 0) {
|
||||||
|
|||||||
@@ -1,89 +1,59 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {
|
import { ArrowFitInRegular } from "@fluentui/react-icons";
|
||||||
Button,
|
|
||||||
Text,
|
|
||||||
Tooltip,
|
|
||||||
InfoLabel,
|
|
||||||
Card
|
|
||||||
} from "@fluentui/react-components";
|
|
||||||
import {
|
|
||||||
ArrowFitInRegular,
|
|
||||||
ArrowSortDownRegular,
|
|
||||||
ArrowSortUpRegular
|
|
||||||
} from "@fluentui/react-icons";
|
|
||||||
import { useStatusContext } from "./App";
|
import { useStatusContext } from "./App";
|
||||||
import { useCommonStyles } from "./commonStyles";
|
import { useCommonStyles } from "./commonStyles";
|
||||||
|
import ActionButton from "./ActionButton";
|
||||||
|
|
||||||
export const MatchSizes: React.FC = () => {
|
export const MatchSizes: React.FC = () => {
|
||||||
const styles = useCommonStyles();
|
const styles = useCommonStyles();
|
||||||
const {
|
const { setStatusMessage, setStatusType } = useStatusContext();
|
||||||
statusMessage, setStatusMessage,
|
|
||||||
statusType, setStatusType,
|
|
||||||
isProcessing, setIsProcessing
|
|
||||||
} = useStatusContext();
|
|
||||||
|
|
||||||
const matchSizeToFirstSelected = async () => {
|
const matchSizeToFirstSelected = async () => {
|
||||||
setIsProcessing(true);
|
await PowerPoint.run(async (context) => {
|
||||||
try {
|
// Get the selected shapes
|
||||||
await PowerPoint.run(async (context) => {
|
const shapes = context.presentation.getSelectedShapes();
|
||||||
// Get the selected shapes
|
shapes.load("items");
|
||||||
const shapes = context.presentation.getSelectedShapes();
|
await context.sync();
|
||||||
shapes.load("items");
|
|
||||||
await context.sync();
|
|
||||||
|
|
||||||
// Check if shapes are selected
|
// Check if shapes are selected
|
||||||
if (shapes.items.length === 0) {
|
if (shapes.items.length === 0) {
|
||||||
setStatusMessage("No shapes are selected. Please select shapes first.");
|
setStatusMessage("No shapes are selected. Please select shapes first.");
|
||||||
setStatusType("warning");
|
setStatusType("warning");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there is more than one shape selected
|
// Check if there is more than one shape selected
|
||||||
if (shapes.items.length === 1) {
|
if (shapes.items.length === 1) {
|
||||||
setStatusMessage("Please select multiple shapes to resize.");
|
setStatusMessage("Please select multiple shapes to resize.");
|
||||||
setStatusType("warning");
|
setStatusType("warning");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the first shape's dimensions
|
// Get the first shape's dimensions
|
||||||
const firstShape = shapes.items[0];
|
const firstShape = shapes.items[0];
|
||||||
firstShape.load("width,height");
|
firstShape.load("width,height");
|
||||||
await context.sync();
|
await context.sync();
|
||||||
|
|
||||||
// Loop through the remaining shapes and resize them
|
// Loop through the remaining shapes and resize them
|
||||||
for (let i = 1; i < shapes.items.length; i++) {
|
for (let i = 1; i < shapes.items.length; i++) {
|
||||||
shapes.items[i].width = firstShape.width;
|
shapes.items[i].width = firstShape.width;
|
||||||
shapes.items[i].height = firstShape.height;
|
shapes.items[i].height = firstShape.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
await context.sync();
|
await context.sync();
|
||||||
setStatusMessage(`Resized ${shapes.items.length - 1} shapes to match the first selected shape.`);
|
setStatusMessage(`Resized ${shapes.items.length - 1} shapes to match the first selected shape.`);
|
||||||
setStatusType("success");
|
setStatusType("success");
|
||||||
|
});
|
||||||
// Timeout is handled in App.tsx now
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
setStatusMessage(`Error: ${error.message}`);
|
|
||||||
setStatusType("error");
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
setIsProcessing(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.buttonGroup}>
|
<div className={styles.buttonGroup}>
|
||||||
<Button
|
<ActionButton
|
||||||
appearance="primary"
|
title="Match Sizes"
|
||||||
className={styles.actionButton}
|
icon={<ArrowFitInRegular />}
|
||||||
onClick={matchSizeToFirstSelected}
|
onClick={matchSizeToFirstSelected}
|
||||||
icon={<ArrowFitInRegular />}
|
/>
|
||||||
disabled={isProcessing}
|
|
||||||
>
|
|
||||||
Match Sizes
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from "@fluentui/react-icons";
|
} from "@fluentui/react-icons";
|
||||||
import { useStatusContext } from "./App";
|
import { useStatusContext } from "./App";
|
||||||
import { useCommonStyles } from "./commonStyles";
|
import { useCommonStyles } from "./commonStyles";
|
||||||
|
import { getErrorMessage, isPictureShape, getFirstSelectedSlide, selectShapesById } from "../types/office-types";
|
||||||
|
|
||||||
export const RoundImage: React.FC = () => {
|
export const RoundImage: React.FC = () => {
|
||||||
const styles = useCommonStyles();
|
const styles = useCommonStyles();
|
||||||
@@ -38,8 +39,8 @@ export const RoundImage: React.FC = () => {
|
|||||||
shape.load(["type"]);
|
shape.load(["type"]);
|
||||||
await context.sync();
|
await context.sync();
|
||||||
|
|
||||||
// Ensure the shape is a picture
|
// Ensure the shape is a picture using our type-safe utility
|
||||||
if (shape.type !== PowerPoint.ShapeType.image) {
|
if (!isPictureShape(shape)) {
|
||||||
setStatusMessage("Please select an image.");
|
setStatusMessage("Please select an image.");
|
||||||
setStatusType("warning");
|
setStatusType("warning");
|
||||||
return;
|
return;
|
||||||
@@ -53,8 +54,10 @@ export const RoundImage: React.FC = () => {
|
|||||||
const width = shape.width;
|
const width = shape.width;
|
||||||
const height = shape.height;
|
const height = shape.height;
|
||||||
|
|
||||||
const slide = context.presentation.getSelectedSlides().getItemAt(0);
|
// Get the current slide using our type-safe utility
|
||||||
const maskShape = slide.shapes.addGeometricShape("Ellipse" as any);
|
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"]);
|
maskShape.load(["width", "height", "left", "top", "id"]);
|
||||||
await context.sync();
|
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.");
|
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");
|
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
|
// Ensure we maintain the same size
|
||||||
shape.width = width;
|
shape.width = width;
|
||||||
shape.height = height;
|
shape.height = height;
|
||||||
|
|
||||||
await context.sync();
|
await context.sync();
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
setStatusMessage(`Error: ${error.message}`);
|
const errorMessage = getErrorMessage(error);
|
||||||
|
setStatusMessage(`Error: ${errorMessage}`);
|
||||||
setStatusType("error");
|
setStatusType("error");
|
||||||
console.error("Main error:", error);
|
console.error("Round image error:", error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsProcessing(false);
|
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 * as React from "react";
|
||||||
import {
|
import { ArrowSwapRegular } from "@fluentui/react-icons";
|
||||||
Button
|
|
||||||
} from "@fluentui/react-components";
|
|
||||||
import {
|
|
||||||
ArrowSwapRegular
|
|
||||||
} from "@fluentui/react-icons";
|
|
||||||
import { useStatusContext } from "./App";
|
import { useStatusContext } from "./App";
|
||||||
import { useCommonStyles } from "./commonStyles";
|
import { useCommonStyles } from "./commonStyles";
|
||||||
|
import ActionButton from "./ActionButton";
|
||||||
|
|
||||||
export const SwapPositions: React.FC = () => {
|
export const SwapPositions: React.FC = () => {
|
||||||
const styles = useCommonStyles();
|
const styles = useCommonStyles();
|
||||||
const {
|
const { setStatusMessage, setStatusType } = useStatusContext();
|
||||||
statusMessage, setStatusMessage,
|
|
||||||
statusType, setStatusType,
|
|
||||||
isProcessing, setIsProcessing
|
|
||||||
} = useStatusContext();
|
|
||||||
|
|
||||||
const swapPositionsOfTwoSelectedObjects = async () => {
|
const swapPositionsOfTwoSelectedObjects = async () => {
|
||||||
setIsProcessing(true);
|
await PowerPoint.run(async (context) => {
|
||||||
try {
|
// Get the selected shapes
|
||||||
await PowerPoint.run(async (context) => {
|
const shapes = context.presentation.getSelectedShapes();
|
||||||
// Get the selected shapes
|
shapes.load("items/count");
|
||||||
const shapes = context.presentation.getSelectedShapes();
|
await context.sync();
|
||||||
shapes.load("items/count");
|
|
||||||
await context.sync();
|
|
||||||
|
|
||||||
// Check if exactly two shapes are selected
|
// Check if exactly two shapes are selected
|
||||||
if (shapes.items.length !== 2) {
|
if (shapes.items.length !== 2) {
|
||||||
setStatusMessage("Please select exactly two shapes to swap their positions.");
|
setStatusMessage("Please select exactly two shapes to swap their positions.");
|
||||||
setStatusType("warning");
|
setStatusType("warning");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the two shapes
|
// Get the two shapes
|
||||||
const shapeObj1 = shapes.items[0];
|
const shapeObj1 = shapes.items[0];
|
||||||
const shapeObj2 = shapes.items[1];
|
const shapeObj2 = shapes.items[1];
|
||||||
|
|
||||||
// Load position properties
|
// Load position properties
|
||||||
shapeObj1.load("left,top");
|
shapeObj1.load("left,top");
|
||||||
shapeObj2.load("left,top");
|
shapeObj2.load("left,top");
|
||||||
await context.sync();
|
await context.sync();
|
||||||
|
|
||||||
// Store the position of the first shape
|
// Store the position of the first shape
|
||||||
const tempLeft = shapeObj1.left;
|
const tempLeft = shapeObj1.left;
|
||||||
const tempTop = shapeObj1.top;
|
const tempTop = shapeObj1.top;
|
||||||
|
|
||||||
// Swap positions
|
// Swap positions
|
||||||
shapeObj1.left = shapeObj2.left;
|
shapeObj1.left = shapeObj2.left;
|
||||||
shapeObj1.top = shapeObj2.top;
|
shapeObj1.top = shapeObj2.top;
|
||||||
shapeObj2.left = tempLeft;
|
shapeObj2.left = tempLeft;
|
||||||
shapeObj2.top = tempTop;
|
shapeObj2.top = tempTop;
|
||||||
|
|
||||||
await context.sync();
|
await context.sync();
|
||||||
|
|
||||||
setStatusMessage("Positions of the two shapes have been swapped successfully.");
|
setStatusMessage("Positions of the two shapes have been swapped successfully.");
|
||||||
setStatusType("success");
|
setStatusType("success");
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
setStatusMessage(`Error: ${error.message}`);
|
|
||||||
setStatusType("error");
|
|
||||||
console.error("Swap positions error:", error);
|
|
||||||
} finally {
|
|
||||||
setIsProcessing(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.buttonGroup}>
|
<div className={styles.buttonGroup}>
|
||||||
<Button
|
<ActionButton
|
||||||
appearance="primary"
|
title="Swap Positions"
|
||||||
className={styles.actionButton}
|
|
||||||
onClick={swapPositionsOfTwoSelectedObjects}
|
|
||||||
icon={<ArrowSwapRegular />}
|
icon={<ArrowSwapRegular />}
|
||||||
disabled={isProcessing}
|
onClick={swapPositionsOfTwoSelectedObjects}
|
||||||
>
|
/>
|
||||||
Swap Positions
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+15
-3
@@ -19,9 +19,21 @@ Office.onReady(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if ((module as any).hot) {
|
// Define proper module hot interface for webpack hot module replacement
|
||||||
(module as any).hot.accept("./components/App", () => {
|
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;
|
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