Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ App Config UI Location allows you to manage all the app settings centrally. Once

### RTE Location

The RTE Location allows you to create custom plugins to expand the functionality of your JSON Rich Text Editor. Using the Audience and Variables plugin, you can tailor your content as per your requirements.
The RTE Location allows you to create custom plugins to expand the functionality of your JSON Rich Text Editor. Using the Audience and Variables plugin, you can tailor your content as per your requirements. [RTE PLUGIN](docs/rte-plugin.md)

### Sidebar Location

Expand Down Expand Up @@ -118,4 +118,4 @@ This guide provides instructions for migrating your application to App SDK versi

## License

Licensed under [MIT](https://opensource.org/licenses/MIT).
Licensed under [MIT](https://opensource.org/licenses/MIT).
2 changes: 1 addition & 1 deletion __test__/uiLocation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ describe("UI Location", () => {
const config = await uiLocation.getConfig();
expect(config).toEqual({});
expect(postRobotSendToParentMock).toHaveBeenLastCalledWith(
"getConfig"
"getConfig", {"context": {"extensionUID": "extension_uid", "installationUID": "installation_uid"}}
);
});
});
Expand Down
8 changes: 1 addition & 7 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2299,12 +2299,6 @@ Following are a list of helpful functions and properties for a JSON RTE instance
| `title` | Title of the field | string |
| `uid` | Unique ID for the field | string |

### `rte.getConfig: () => Object`

Provides configuration which are defined while creating the plugin or while selecting a plugin in the content type builder page.

For example, if your plugin requires API Key or any other config parameters then, you can specify these configurations while creating a new plugin or you can specify field specific configurations from the content type builder page while selecting the plugin. These configurations can be accessed through the `getConfig() `method.

### Methods:

These methods are part of the RTE instance and can be accessed as rte.methodName().
Expand Down Expand Up @@ -2409,4 +2403,4 @@ const Asset = RTE("asset-picker", () => {
Asset.addPlugins(ChooseAsset, UploadAsset);
```

<img src="./images/Dropdown.jpg" width='350' style="text-align:center" />
<img src="./images/Dropdown.jpg" width='350' style="text-align:center" />
296 changes: 296 additions & 0 deletions docs/rte-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
# JSON RTE Plugin Development Guide

Quick reference for creating JSON Rich Text Editor plugins using the new simplified approach.

## 🚀 Quick Start

```typescript
import ContentstackAppSDK, { PluginBuilder } from '@contentstack/app-sdk';

// Create a simple plugin
const boldPlugin = new PluginBuilder('bold-plugin')
.title('Bold')
.elementType('inline')
.on('exec', (rte) => {
rte.addMark('bold', true);
})
.build();

// Register the plugin
ContentstackAppSDK.registerRTEPlugins(boldPlugin);
```

## 📋 Plugin Types

### Inline Plugin
For text formatting (bold, italic, etc.)

```typescript
const italicPlugin = new PluginBuilder('italic')
.title('Italic')
.elementType('inline')
.display(['toolbar', 'hoveringToolbar'])
.on('exec', (rte) => {
rte.addMark('italic', true);
})
.build();
```

### Block Plugin
For block-level elements (headings, paragraphs, etc.)

```typescript
const headingPlugin = new PluginBuilder('heading')
.title('Heading')
.elementType('block')
.render(({ children, attrs }) => (
<h2 style={{ color: attrs.color || 'black' }}>
{children}
</h2>
))
.on('exec', (rte) => {
rte.insertNode({
type: 'heading',
attrs: { level: 2 },
children: [{ text: 'New Heading' }]
});
})
.build();
```

### Void Plugin
For self-closing elements (images, embeds, etc.)

```typescript
const imagePlugin = new PluginBuilder('image')
.title('Image')
.elementType('void')
.render(({ attrs }) => (
<img
src={attrs.src}
alt={attrs.alt || 'Image'}
style={{ maxWidth: '100%' }}
/>
))
.on('exec', (rte) => {
const src = prompt('Enter image URL:');
if (src) {
rte.insertNode({
type: 'image',
attrs: { src },
children: [{ text: '' }]
});
}
})
.build();
```

## 🎛️ Builder Methods

### Basic Configuration
```typescript
new PluginBuilder('plugin-id')
.title('Plugin Name') // Toolbar button text
.icon(<CustomIcon />) // Button icon (React element)
.elementType('block') // 'inline' | 'block' | 'void'
```

### Display Options
```typescript
.display(['toolbar']) // Show in main toolbar only
.display(['hoveringToolbar']) // Show in hover toolbar only
.display(['toolbar', 'hoveringToolbar']) // Show in both
```

### Event Handlers
```typescript
.on('exec', (rte) => {}) // Button click
.on('keydown', ({ event, rte }) => {}) // Key press
.on('paste', ({ rte, preventDefault }) => {}) // Paste event
```

### Advanced Options
```typescript
.render(ComponentFunction) // Custom render component
.shouldOverride((element) => boolean) // Override existing elements
.configure(async (sdk) => {}) // Dynamic configuration
```

## 🔧 Event Handling

### Click Handler
```typescript
.on('exec', (rte) => {
// Insert text
rte.insertText('Hello World');

// Add formatting
rte.addMark('bold', true);

// Insert node
rte.insertNode({
type: 'custom-element',
attrs: { id: 'unique-id' },
children: [{ text: 'Content' }]
});
})
```

### Keyboard Handler
```typescript
.on('keydown', ({ event, rte }) => {
if (event.key === 'Enter' && event.ctrlKey) {
event.preventDefault();
// Custom enter behavior
rte.insertBreak();
}
})
```

## 📦 Container Plugins (Dropdowns)

Create grouped plugins in a dropdown menu:

```typescript
const mediaContainer = new PluginBuilder('media-dropdown')
.title('Media')
.icon(<MediaIcon />)
.addPlugins(
imagePlugin,
videoPlugin,
audioPlugin
)
.build();
```

## 🔄 Plugin Registration

### Single Plugin
```typescript
ContentstackAppSDK.registerRTEPlugins(myPlugin);
```

### Multiple Plugins
```typescript
ContentstackAppSDK.registerRTEPlugins(
boldPlugin,
italicPlugin,
headingPlugin,
imagePlugin
);
```

### With Enhanced SDK Context
```typescript
// Register plugins first (captures RTE context)
await ContentstackAppSDK.registerRTEPlugins(myPlugin);

// Then initialize SDK (gets enhanced context)
const sdk = await ContentstackAppSDK.init();
```

## 💡 Real-World Examples

### YouTube Embed Plugin
```typescript
const youtubePlugin = new PluginBuilder('youtube')
.title('YouTube')
.elementType('void')
.render(({ attrs }) => (
<iframe
width="560"
height="315"
src={`https://www.youtube.com/embed/${attrs.videoId}`}
frameBorder="0"
allowFullScreen
/>
))
.on('exec', (rte) => {
const url = prompt('Enter YouTube URL:');
const videoId = extractVideoId(url);
if (videoId) {
rte.insertNode({
type: 'youtube',
attrs: { videoId },
children: [{ text: '' }]
});
}
})
.build();
```

### Smart Quote Plugin
```typescript
const smartQuotePlugin = new PluginBuilder('smart-quote')
.title('Smart Quotes')
.elementType('inline')
.on('keydown', ({ event, rte }) => {
if (event.key === '"') {
event.preventDefault();
const isStart = rte.selection.isAtStart();
rte.insertText(isStart ? '"' : '"');
}
})
.build();
```

### Dynamic Configuration Plugin
```typescript
const configurablePlugin = new PluginBuilder('configurable')
.title('Dynamic Plugin')
.configure(async (sdk) => {
const config = await sdk.getConfig();
return {
title: config.customTitle || 'Default Title',
icon: config.customIcon || <DefaultIcon />
};
})
.on('exec', (rte) => {
// Plugin logic using dynamic config
})
.build();
```

## 🎯 Best Practices

1. **Use semantic IDs**: `'heading-h2'` instead of `'plugin1'`
2. **Provide clear titles**: Users see these in the toolbar
3. **Handle edge cases**: Check for selection, validate inputs
4. **Use TypeScript**: Better development experience
5. **Test thoroughly**: Different content structures, browser compatibility

## 📚 Migration from Legacy

### Old Way (Legacy RTEPlugin)
```typescript
const oldPlugin = new RTEPlugin('my-plugin', (rte) => ({
title: 'My Plugin',
icon: <Icon />,
display: ['toolbar'],
elementType: ['block'],
render: MyComponent
}));
oldPlugin.on('exec', handler);
```

### New Way (PluginBuilder)
```typescript
const newPlugin = new PluginBuilder('my-plugin')
.title('My Plugin')
.icon(<Icon />)
.display(['toolbar'])
.elementType('block')
.render(MyComponent)
.on('exec', handler)
.build();
```

## 🔗 Resources

- [Contentstack RTE Documentation](https://www.contentstack.com/docs/developers/developer-hub/rte-location)
- [JSON RTE API Structure Guide](https://www.contentstack.com/docs/developers/apis/content-management-api/##json-rte-plugins)
- [App SDK API Reference](https://github.com/contentstack/app-sdk-docs)

---

**Happy plugin building! 🚀**
9 changes: 5 additions & 4 deletions src/RTE/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "slate";

import { RTEPlugin } from "./index";
import UiLocation from "../uiLocation";

declare interface TransformOptions {
at?: Location;
Expand Down Expand Up @@ -48,7 +49,7 @@ export declare interface IRteParam {
voids?: boolean;
}
) => Point | undefined;

sdk: UiLocation;
isPointEqual: (point: Point, another: Point) => boolean;
};

Expand Down Expand Up @@ -139,7 +140,7 @@ export declare interface IRteParam {
getEmbeddedItems: () => { [key: string]: any };
getVariable: <T = unknown>(name: string, defaultValue: any) => T;
setVariable: <T = unknown>(name: string, value: T) => void;
getConfig: <T>() => { [key: string]: T };
sdk: UiLocation;
}

export declare type IRteParamWithPreventDefault = {
Expand Down Expand Up @@ -199,7 +200,7 @@ export declare interface IRteElementType {
children: Array<IRteElementType | IRteTextType>;
}

type IDynamicFunction = (
export type IDynamicFunction = (
element: IRteElementType
) =>
| Exclude<IElementTypeOptions, "text">
Expand Down Expand Up @@ -294,4 +295,4 @@ export declare interface IContainerMetaData {
export declare type IRTEPluginInitializer = (
id: string,
config: IConfigCallback
) => RTEPlugin;
) => RTEPlugin;
Loading
Loading