diff --git a/README.md b/README.md
index 8812daf..c9470df 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -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).
\ No newline at end of file
diff --git a/__test__/uiLocation.test.ts b/__test__/uiLocation.test.ts
index 2017748..1c743a5 100644
--- a/__test__/uiLocation.test.ts
+++ b/__test__/uiLocation.test.ts
@@ -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"}}
);
});
});
diff --git a/docs/api-reference.md b/docs/api-reference.md
index 058809d..af202ee 100644
--- a/docs/api-reference.md
+++ b/docs/api-reference.md
@@ -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().
@@ -2409,4 +2403,4 @@ const Asset = RTE("asset-picker", () => {
Asset.addPlugins(ChooseAsset, UploadAsset);
```
-
+
\ No newline at end of file
diff --git a/docs/rte-plugin.md b/docs/rte-plugin.md
new file mode 100644
index 0000000..80a6e79
--- /dev/null
+++ b/docs/rte-plugin.md
@@ -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 }) => (
+
+ {children}
+
+ ))
+ .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 }) => (
+
+ ))
+ .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() // 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()
+ .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 }) => (
+
+ ))
+ .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 ||
+ };
+ })
+ .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: ,
+ display: ['toolbar'],
+ elementType: ['block'],
+ render: MyComponent
+}));
+oldPlugin.on('exec', handler);
+```
+
+### New Way (PluginBuilder)
+```typescript
+const newPlugin = new PluginBuilder('my-plugin')
+ .title('My Plugin')
+ .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! 🚀**
\ No newline at end of file
diff --git a/src/RTE/types.tsx b/src/RTE/types.tsx
index d119c09..8793029 100644
--- a/src/RTE/types.tsx
+++ b/src/RTE/types.tsx
@@ -13,6 +13,7 @@ import {
} from "slate";
import { RTEPlugin } from "./index";
+import UiLocation from "../uiLocation";
declare interface TransformOptions {
at?: Location;
@@ -48,7 +49,7 @@ export declare interface IRteParam {
voids?: boolean;
}
) => Point | undefined;
-
+ sdk: UiLocation;
isPointEqual: (point: Point, another: Point) => boolean;
};
@@ -139,7 +140,7 @@ export declare interface IRteParam {
getEmbeddedItems: () => { [key: string]: any };
getVariable: (name: string, defaultValue: any) => T;
setVariable: (name: string, value: T) => void;
- getConfig: () => { [key: string]: T };
+ sdk: UiLocation;
}
export declare type IRteParamWithPreventDefault = {
@@ -199,7 +200,7 @@ export declare interface IRteElementType {
children: Array;
}
-type IDynamicFunction = (
+export type IDynamicFunction = (
element: IRteElementType
) =>
| Exclude
@@ -294,4 +295,4 @@ export declare interface IContainerMetaData {
export declare type IRTEPluginInitializer = (
id: string,
config: IConfigCallback
-) => RTEPlugin;
+) => RTEPlugin;
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
index b36ddec..3b4b706 100755
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,8 +1,10 @@
import postRobot from "post-robot";
+import { InitializationData } from "./types";
+import { IRteParam } from "./RTE/types";
+import { PluginDefinition, PluginBuilder, registerPlugins } from "./rtePlugin";
import UiLocation from "./uiLocation";
import { version } from "../package.json";
-import { InitializationData } from "./types";
postRobot.CONFIG.LOG_LEVEL = "error";
@@ -43,6 +45,55 @@ class ContentstackAppSDK {
.catch((e: Error) => Promise.reject(e));
}
+ /**
+ * Registers RTE plugins with the Contentstack platform.
+ * This method is the primary entry point for defining and registering custom RTE plugins
+ * built using the PluginBuilder pattern. It returns a function that the Contentstack
+ * platform will invoke at runtime, providing the necessary context.
+ *
+ * @example
+ * // In your plugin's entry file (e.g., src/index.ts):
+ * import ContentstackAppSDK, { PluginBuilder } from '@contentstack/app-sdk';
+ *
+ * const MyCustomPlugin = new PluginBuilder("my-plugin-id")
+ * .title("My Plugin")
+ * .icon()
+ * .elementType("block")
+ * .display("toolbar")
+ * .render(()=>{return })
+ * .on("exec", (rte: IRteParam) => {
+ * // Access SDK via rte.sdk if needed:
+ * const sdk = rte.sdk;
+ * // ... plugin execution logic ...
+ * })
+ * .build();
+ *
+ * export default ContentstackAppSDK.registerRTEPlugins(
+ * MyCustomPlugin
+ * );
+ *
+ * @param {...PluginDefinition} pluginDefinitions - One or more plugin definitions created using the `PluginBuilder`.
+ * Each `PluginDefinition` describes the plugin's configuration, callbacks, and any child plugins.
+ * @returns {Promise<{ __isPluginBuilder__: boolean; version: string; plugins: (context: RTEContext, rte: IRteParam) => Promise<{ [key: string]: RTEPlugin; }>; }>}
+ * A Promise that resolves to an object containing:
+ * - `__isPluginBuilder__`: A boolean flag indicating this is a builder-based plugin export.
+ * - `version`: The version of the SDK that registered the plugins.
+ * - `plugins`: An asynchronous function. This function is designed to be invoked by the
+ * Contentstack platform loader, providing the `context` (initialization data) and
+ * the `rte` instance. When called, it materializes and returns a map of the
+ * registered `RTEPlugin` instances, keyed by their IDs.
+ */
+
+ static async registerRTEPlugins(...pluginDefinitions: PluginDefinition[]) {
+ return {
+ __isPluginBuilder__: true,
+ version,
+ plugins: (context: InitializationData, rte: IRteParam) => {
+ return registerPlugins(...pluginDefinitions)(context, rte);
+ }
+ };
+ }
+
/**
* Version of Contentstack App SDK.
*/
@@ -52,4 +103,11 @@ class ContentstackAppSDK {
}
export default ContentstackAppSDK;
-module.exports = ContentstackAppSDK;
+export { PluginBuilder };
+
+// CommonJS compatibility
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = ContentstackAppSDK;
+ module.exports.default = ContentstackAppSDK;
+ module.exports.PluginBuilder = PluginBuilder;
+}
\ No newline at end of file
diff --git a/src/rtePlugin.ts b/src/rtePlugin.ts
new file mode 100644
index 0000000..3253557
--- /dev/null
+++ b/src/rtePlugin.ts
@@ -0,0 +1,181 @@
+import { RTEPlugin as Plugin, rtePluginInitializer } from "./RTE";
+import {
+ IConfig,
+ IDisplayOnOptions,
+ IDynamicFunction,
+ IElementTypeOptions,
+ IOnFunction,
+ IRteElementType,
+ IRteParam,
+} from "./RTE/types";
+import { InitializationData } from "./types";
+import UiLocation from "./uiLocation";
+
+type PluginConfigCallback = (sdk: UiLocation) => Promise | IConfig;
+
+interface PluginDefinition {
+ id: string;
+ config: Partial;
+ callbacks: Partial;
+ asyncConfigCallback?: PluginConfigCallback;
+ childBuilders: PluginBuilder[];
+}
+
+class PluginBuilder {
+ private id: string;
+ private _config: Partial = {};
+ private _callbacks: Partial = {};
+ private _asyncConfigCallback?: PluginConfigCallback;
+ private _childBuilders: PluginBuilder[] = [];
+
+ constructor(id: string) {
+ this.id = id;
+ this._config.title = id;
+ }
+
+ title(title: string): PluginBuilder {
+ this._config.title = title;
+ return this;
+ }
+ icon(icon: React.ReactElement | null): PluginBuilder {
+ this._config.icon = icon;
+ return this;
+ }
+ display(display: IDisplayOnOptions | IDisplayOnOptions[]): PluginBuilder {
+ this._config.display = display;
+ return this;
+ }
+ elementType(
+ elementType:
+ | IElementTypeOptions
+ | IElementTypeOptions[]
+ | IDynamicFunction
+ ): PluginBuilder {
+ this._config.elementType = elementType;
+ return this;
+ }
+ render(renderFn: (element: React.ReactElement, attrs: { [key: string]: any }, path: number[], rte: IRteParam) => React.ReactElement): PluginBuilder {
+ this._config.render = renderFn;
+ return this;
+ }
+ shouldOverride(
+ shouldOverrideFn: (element: IRteElementType) => boolean
+ ): PluginBuilder {
+ this._config.shouldOverride = shouldOverrideFn;
+ return this;
+ }
+ on(
+ type: T,
+ callback: IOnFunction[T]
+ ): PluginBuilder {
+ this._callbacks[type] = callback;
+ return this;
+ }
+ configure(callback: PluginConfigCallback): PluginBuilder {
+ this._asyncConfigCallback = callback;
+ return this;
+ }
+ addPlugins(...builders: PluginBuilder[]): PluginBuilder {
+ this._childBuilders.push(...builders);
+ return this;
+ }
+
+ /**
+ * Builds and returns a definition of the RTE Plugin, ready to be materialized
+ * into a concrete RTEPlugin instance later when the SDK and Plugin Factory are available.
+ * This method no longer performs the actual creation of RTEPlugin instances.
+ */
+ build(): PluginDefinition {
+ return {
+ id: this.id,
+ config: this._config,
+ callbacks: this._callbacks,
+ asyncConfigCallback: this._asyncConfigCallback,
+ childBuilders: this._childBuilders,
+ };
+ }
+}
+
+async function materializePlugin(
+ pluginDef: PluginDefinition,
+ sdk: UiLocation
+): Promise {
+ let finalConfig: Partial = { ...pluginDef.config };
+ if (pluginDef.asyncConfigCallback) {
+ const dynamicConfig = await Promise.resolve(
+ pluginDef.asyncConfigCallback(sdk)
+ );
+ finalConfig = { ...finalConfig, ...dynamicConfig };
+ }
+
+ // CRITICAL FIX: Match the old RTE implementation exactly
+ // In the old implementation, the plugin was created and then callbacks were set
+ // But the key difference is that the old implementation used direct RTEPlugin instantiation
+ const plugin = rtePluginInitializer(
+ pluginDef.id,
+ (rte: IRteParam | void) => {
+ return finalConfig;
+ }
+ );
+
+ // IMPORTANT: Set callbacks immediately after plugin creation
+ // This must happen BEFORE the RTE system calls plugin.get()
+ Object.entries(pluginDef.callbacks).forEach(([type, callback]) => {
+ plugin.on(type as keyof IOnFunction, callback);
+ });
+
+ if (pluginDef.childBuilders.length > 0) {
+ const childPlugins = await Promise.all(
+ pluginDef.childBuilders.map((childBuilder) =>
+ materializePlugin(childBuilder.build(), sdk)
+ )
+ );
+ plugin.addPlugins(...childPlugins);
+ }
+
+ return plugin;
+}
+
+function registerPlugins(
+ ...pluginDefinitions: PluginDefinition[]
+): (
+ context: InitializationData,
+ rte: IRteParam
+) => Promise<{ [key: string]: Plugin }> {
+ const definitionsToProcess = [...pluginDefinitions];
+ const plugins = async (context: InitializationData, rte: IRteParam) => {
+ try {
+ // Import ContentstackAppSDK to access the shared UiLocation instance
+ const { default: ContentstackAppSDK } = await import('./index');
+
+ // Use the existing UiLocation instance if available, otherwise create new one
+ const sdk = ContentstackAppSDK._uiLocation || new UiLocation(context);
+
+ const materializedPlugins: { [key: string]: Plugin } = {};
+ for (const def of definitionsToProcess) {
+ const pluginInstance = await materializePlugin(def, sdk);
+ materializedPlugins[def.id] = pluginInstance;
+ }
+ rte.sdk = sdk;
+ return materializedPlugins;
+ } catch (err) {
+ console.error("Error during plugin registration:", err);
+ throw err;
+ }
+ };
+ return plugins;
+}
+
+export {
+ IConfig,
+ IDisplayOnOptions,
+ IDynamicFunction,
+ IElementTypeOptions,
+ IOnFunction,
+ IRteElementType,
+ IRteParam,
+ Plugin,
+ PluginBuilder,
+ PluginDefinition,
+ registerPlugins
+};
\ No newline at end of file
diff --git a/src/uiLocation.ts b/src/uiLocation.ts
index 5507b49..351a677 100755
--- a/src/uiLocation.ts
+++ b/src/uiLocation.ts
@@ -431,7 +431,7 @@ class UiLocation {
return Promise.resolve(this.config);
}
return this.postRobot
- .sendToParent("getConfig")
+ .sendToParent("getConfig", {context:{installationUID:this.installationUID, extensionUID:this.locationUID}})
.then(onData)
.catch(onError);
};
@@ -484,7 +484,7 @@ class UiLocation {
*/
api = (url: string, option?: RequestInit): Promise =>
- dispatchApiRequest(url, option) as Promise;
+ dispatchApiRequest(url, option, {installationUID:this.installationUID, extensionUID:this.locationUID}) as Promise;
/**
* Method used to create an adapter for management sdk.
@@ -519,4 +519,4 @@ class UiLocation {
}
}
-export default UiLocation;
+export default UiLocation;
\ No newline at end of file
diff --git a/src/utils/adapter.ts b/src/utils/adapter.ts
index b96fb4c..0ae0461 100644
--- a/src/utils/adapter.ts
+++ b/src/utils/adapter.ts
@@ -7,17 +7,19 @@ import {
} from "axios";
import { axiosToFetchResponse, fetchToAxiosConfig } from "./utils";
-
/**
* Dispatches a request using PostRobot.
* @param postRobot - The PostRobot instance.
* @returns A function that takes AxiosRequestConfig and returns a promise.
*/
export const dispatchAdapter =
- (postRobot: typeof PostRobot) => (config: AxiosRequestConfig) => {
+ (postRobot: typeof PostRobot) => (config: AxiosRequestConfig, context?: {installationUID: string, extensionUID: string}) => {
return new Promise((resolve, reject) => {
postRobot
- .sendToParent("apiAdapter", config)
+ .sendToParent("apiAdapter", {
+ data:config,
+ extension:context
+ })
.then((event: unknown) => {
const { data: response } = event as { data: AxiosResponse };
@@ -56,12 +58,14 @@ export const dispatchAdapter =
*/
export const dispatchApiRequest = async (
url: string,
- options?: RequestInit
+ options?: RequestInit,
+ context?: {installationUID: string, extensionUID: string}
): Promise => {
try {
const config = fetchToAxiosConfig(url, options);
const axiosResponse = (await dispatchAdapter(PostRobot)(
- config
+ config,
+ context
)) as AxiosResponse;
return axiosToFetchResponse(axiosResponse);
@@ -80,4 +84,4 @@ export const dispatchApiRequest = async (
headers: err.config.headers,
});
}
-};
+};
\ No newline at end of file