From f6f6c126aee7edfd0815ffdcef4ebd68b63705f6 Mon Sep 17 00:00:00 2001 From: "Rijil T. R" Date: Thu, 5 Jun 2025 18:54:36 +0530 Subject: [PATCH 1/8] feat(rte): Introduce PluginBuilder for declarative RTE plugin definition --- src/RTE/index.tsx | 2 +- src/RTE/types.tsx | 12 ++-- src/index.ts | 59 +++++++++++++++- src/rtePlugin.ts | 167 +++++++++++++++++++++++++++++++++++++++++++++ src/types/rte.d.ts | 7 ++ 5 files changed, 238 insertions(+), 9 deletions(-) create mode 100644 src/rtePlugin.ts create mode 100644 src/types/rte.d.ts diff --git a/src/RTE/index.tsx b/src/RTE/index.tsx index b4cdd78..deeffe7 100644 --- a/src/RTE/index.tsx +++ b/src/RTE/index.tsx @@ -1,10 +1,10 @@ import { + IConfig, IConfigCallback, IContainerMetaData, IOnFunction, IPluginMetaData, IRteParam, - IConfig, } from "./types"; export class RTEPlugin { diff --git a/src/RTE/types.tsx b/src/RTE/types.tsx index d119c09..e94b263 100644 --- a/src/RTE/types.tsx +++ b/src/RTE/types.tsx @@ -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"; @@ -199,7 +199,7 @@ export declare interface IRteElementType { children: Array; } -type IDynamicFunction = ( +export type IDynamicFunction = ( element: IRteElementType ) => | Exclude diff --git a/src/index.ts b/src/index.ts index b36ddec..bf73559 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,11 @@ import postRobot from "post-robot"; -import UiLocation from "./uiLocation"; import { version } from "../package.json"; -import { InitializationData } from "./types"; +import { RTEPlugin } from "./RTE"; +import { IRteParam } from "./RTE/types"; +import { PluginDefinition, registerPlugins } from "./rtePlugin"; +import { InitializationData, IRTEInitData } from "./types"; +import UiLocation from "./uiLocation"; postRobot.CONFIG.LOG_LEVEL = "error"; @@ -43,6 +46,58 @@ 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 from '@contentstack/app-sdk'; + * import { PluginBuilder, IRteParam } from '@contentstack/app-sdk/rtePlugin'; + * + * const MyCustomPlugin = new PluginBuilder("my-plugin-id") + * .title("My Plugin") + * .icon() + * .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: IRTEInitData, 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[] + ): Promise<{ + __isPluginBuilder__: boolean; + version: string; + plugins: (context: IRTEInitData, rte: IRteParam) => Promise<{ + [key: string]: RTEPlugin; + }>; + }> { + return { + __isPluginBuilder__: true, + version, + plugins: registerPlugins(...pluginDefinitions) + }; + } + /** * Version of Contentstack App SDK. */ diff --git a/src/rtePlugin.ts b/src/rtePlugin.ts new file mode 100644 index 0000000..857a4e3 --- /dev/null +++ b/src/rtePlugin.ts @@ -0,0 +1,167 @@ +import { RTEPlugin as Plugin, rtePluginInitializer } from "./RTE"; +import { + IConfig, + IDisplayOnOptions, + IDynamicFunction, + IElementTypeOptions, + IOnFunction, + IRteElementType, + IRteParam, +} from "./RTE/types"; +import { IRTEInitData } 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: (...params: any) => 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 }; + } + const plugin = rtePluginInitializer( + pluginDef.id, + (rte: IRteParam | void) => finalConfig + ); + 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: IRTEInitData, + rte: IRteParam +) => Promise<{ [key: string]: Plugin }> { + const definitionsToProcess = [...pluginDefinitions]; + const plugins = async (context: IRTEInitData, rte: IRteParam) => { + try { + const sdk = 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 +}; + diff --git a/src/types/rte.d.ts b/src/types/rte.d.ts new file mode 100644 index 0000000..01ce0c9 --- /dev/null +++ b/src/types/rte.d.ts @@ -0,0 +1,7 @@ +import type { UiLocation } from '../uiLocation'; + +declare module "../RTE/types" { + export interface IRteParam { + sdk: UiLocation; + } +} \ No newline at end of file From d405deed03c3fe2def711004cd79027588f1ec8a Mon Sep 17 00:00:00 2001 From: Amitkanswal Date: Wed, 25 Jun 2025 12:25:37 +0530 Subject: [PATCH 2/8] fix:updated exported format Signed-off-by: Amitkanswal --- src/index.ts | 34 ++-------------------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/src/index.ts b/src/index.ts index bf73559..4d1b89b 100755 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import postRobot from "post-robot"; import { version } from "../package.json"; import { RTEPlugin } from "./RTE"; import { IRteParam } from "./RTE/types"; -import { PluginDefinition, registerPlugins } from "./rtePlugin"; +import { PluginDefinition, registerPlugins,PluginBuilder } from "./rtePlugin"; import { InitializationData, IRTEInitData } from "./types"; import UiLocation from "./uiLocation"; @@ -30,7 +30,6 @@ class ContentstackAppSDK { * A static variable that stores the instance of {@link UiLocation} class after initialization */ static _uiLocation: UiLocation; - /** * Initializes the App SDK and returns an instance of {@link UiLocation} class */ @@ -48,36 +47,6 @@ class ContentstackAppSDK { /** * 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 from '@contentstack/app-sdk'; - * import { PluginBuilder, IRteParam } from '@contentstack/app-sdk/rtePlugin'; - * - * const MyCustomPlugin = new PluginBuilder("my-plugin-id") - * .title("My Plugin") - * .icon() - * .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: IRTEInitData, 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. @@ -107,4 +76,5 @@ class ContentstackAppSDK { } export default ContentstackAppSDK; +export { PluginBuilder }; module.exports = ContentstackAppSDK; From 85309861c92c2a6147bd682124db2bbb6f0d8f6c Mon Sep 17 00:00:00 2001 From: Amitkanswal Date: Fri, 27 Jun 2025 00:32:24 +0530 Subject: [PATCH 3/8] fix:added id's support in init --- src/index.ts | 64 ++++++++++++++++++++++++++++++++-------------------- src/types.ts | 21 +++++++++++++++++ 2 files changed, 60 insertions(+), 25 deletions(-) diff --git a/src/index.ts b/src/index.ts index 4d1b89b..d4a4ca5 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,12 @@ import postRobot from "post-robot"; import { version } from "../package.json"; -import { RTEPlugin } from "./RTE"; import { IRteParam } from "./RTE/types"; -import { PluginDefinition, registerPlugins,PluginBuilder } from "./rtePlugin"; -import { InitializationData, IRTEInitData } from "./types"; +import { PluginDefinition, PluginBuilder, registerPlugins } from "./rtePlugin"; +import { Extension, InitializationData, RTEContext } from "./types"; import UiLocation from "./uiLocation"; +// Configure post-robot logging postRobot.CONFIG.LOG_LEVEL = "error"; /** @@ -23,58 +23,72 @@ postRobot.CONFIG.LOG_LEVEL = "error"; * }) * @return {Promise} A promise object which will be resolved with an instance of the {@link UiLocation} class. * @hideconstructor - */ - +*/ class ContentstackAppSDK { /** * A static variable that stores the instance of {@link UiLocation} class after initialization */ static _uiLocation: UiLocation; - /** + private static _rteInitData: Extension | null = null; + + /** * Initializes the App SDK and returns an instance of {@link UiLocation} class */ static init(): Promise { if (this._uiLocation) { - return Promise.resolve(this._uiLocation); + return Promise.resolve(this._uiLocation); } + return UiLocation.initialize(version) .then((initializationData: InitializationData) => { - this._uiLocation = new UiLocation(initializationData); - return Promise.resolve(this._uiLocation); + // Merge with RTE context if available + const mergedInitData = this._rteInitData + ? { + ...initializationData, + app_id: this._rteInitData.app_uid, + installation_uid: this._rteInitData.app_installation_uid, + extension_uid: this._rteInitData.uid, + } + : initializationData; + + this._uiLocation = new UiLocation(mergedInitData); + return this._uiLocation; }) .catch((e: Error) => Promise.reject(e)); } /** - * Registers RTE plugins with the Contentstack platform. - * 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. + * Register RTE plugins with enhanced context capture + * @param pluginDefinitions Plugin definitions to register + * @returns Plugin registration object */ - static async registerRTEPlugins( - ...pluginDefinitions: PluginDefinition[] - ): Promise<{ - __isPluginBuilder__: boolean; - version: string; - plugins: (context: IRTEInitData, rte: IRteParam) => Promise<{ - [key: string]: RTEPlugin; - }>; - }> { + static async registerRTEPlugins(...pluginDefinitions: PluginDefinition[]) { return { __isPluginBuilder__: true, version, - plugins: registerPlugins(...pluginDefinitions) + plugins: (context: RTEContext, rte: IRteParam) => { + // Capture RTE context for SDK enhancement + this._rteInitData = context.extension; + return registerPlugins(...pluginDefinitions)(context, rte); + } }; } /** - * Version of Contentstack App SDK. + * Get SDK version */ static get SDK_VERSION() { return version; } } +// ES6 exports export default ContentstackAppSDK; export { PluginBuilder }; -module.exports = ContentstackAppSDK; + +// CommonJS compatibility +if (typeof module !== 'undefined' && module.exports) { + module.exports = ContentstackAppSDK; + module.exports.default = ContentstackAppSDK; + module.exports.PluginBuilder = PluginBuilder; +} diff --git a/src/types.ts b/src/types.ts index c351baa..b97881e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -268,3 +268,24 @@ export type RegionType = | "AZURE_EU" | "GCP_NA" | string; + +export type Extension = { + app_installation_uid: string + app_uid: string + config: GenericObjectType + created_at: string + created_by: string + signed: boolean + src: string + tags: string[] + title: string + type: string + uid: string + updated_at: string + updated_by: string + _version: number +} + +export type RTEContext = IRTEInitData & { + extension: Extension +} \ No newline at end of file From 3c1ed2fc098d4c624eca0c422064ba94a8b5bd58 Mon Sep 17 00:00:00 2001 From: Amitkanswal Date: Fri, 27 Jun 2025 00:45:28 +0530 Subject: [PATCH 4/8] doc:updated readme file with rte example --- README.md | 2 + docs/rte-plugin.md | 296 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 298 insertions(+) create mode 100644 docs/rte-plugin.md diff --git a/README.md b/README.md index 8812daf..3480043 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/rte-plugin.md b/docs/rte-plugin.md new file mode 100644 index 0000000..4ac6b6e --- /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 }) => ( + {attrs.alt + )) + .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 }) => ( +