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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ App Config UI Location allows you to manage all the app settings centrally. Once

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.

New RTE plugin examples [RTE PLUGIN](/docs/rte-plugin.md)

### Sidebar Location

The Sidebar Location provides powerful tools for analyzing and recommending ideas for your entry. Use the [Smartling](https://help.smartling.com/hc/en-us/articles/4865477629083) sidebar location to help translate your content.
Expand Down
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 Structure Guide](https://www.contentstack.com/docs/developers/apis/content-management-api/#json-rich-text-editor)
- [App SDK API Reference](https://github.com/contentstack/app-sdk-docs)

---

**Happy plugin building! 🚀**
2 changes: 1 addition & 1 deletion src/RTE/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {
IConfig,
IConfigCallback,
IContainerMetaData,
IOnFunction,
IPluginMetaData,
IRteParam,
IConfig,
} from "./types";

export class RTEPlugin {
Expand Down
12 changes: 6 additions & 6 deletions src/RTE/types.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import React, { ReactElement } from "react";
import {
Editor,
ElementEntry,
Location,
Node,
NodeEntry,
NodeMatch,
Path,
Point,
Node,
ElementEntry,
Transforms,
Editor,
Span,
NodeMatch,
Transforms,
} from "slate";

import { RTEPlugin } from "./index";
Expand Down Expand Up @@ -199,7 +199,7 @@ export declare interface IRteElementType {
children: Array<IRteElementType | IRteTextType>;
}

type IDynamicFunction = (
export type IDynamicFunction = (
element: IRteElementType
) =>
| Exclude<IElementTypeOptions, "text">
Expand Down
Loading
Loading