You can use this plugin to create and later edit our email document JSON objects.
This plugin provides lots of configuration options so if you find the next example a bit overwhelming then you can check out our Examples section where you can play around with them.
We suggest that you initialize the editor with all of the settings that you won't change in the user session, and update only the settings that are different when the editor is actually shown to the end user.
You can initialize this plugin with the following function call:
const editorConfig = {
plugin: "editor",
data: { document },
settings: {
hideHeader: false, // hides the header
user: { // or false to turn off the user avatar
id: "5c6bfaba1f848b135dc46c41", // Id of the current user
name: "John Doe", // the display name of your user
avatar: "https://example.com/avatar.png" // the avatar of your user
},
staticAssetsBaseUrl: "https://yourdomain.com/path/to/static/assets/", // there are a few image assets that should be hosted on your side
videoElementBaseUrl: "https://yourdomain.com/path/to/video/backend/", // the video element requires a backend that should be hosted on your side
autoSaveInterval: 15000,
buttons: {
header: [], // an array of objects describing the content of the dropdown in the top right corner of the editor
textInsert: [],
},
maxBodyWith: 600,
elements: { // or false, to turn off the elements tab (so only the blocks will be visible)
content: { // or false, to hide the whole element group
title: false,
paragraph: false,
list: false,
text: true,
image: true,
button: true,
divider: true,
social: false,
video: false,
},
structure: { // or false, to hide the whole element group
fullWidth: false,
box: true,
multiColumn: true,
},
advanced: { // or false, to hide the whole element group
code: false,
loop: true,
conditional: true,
dynamicImage: true,
blockLevelConditional: true,
blockLevelLoop: true,
},
},
variables: {
text: {
canAdd: true,
canEdit: true,
canDelete: true,
canReplaceAll: true,
canRemoveAll: true,
},
image: {
canAdd: true,
canEdit: true,
canDelete: true,
canReplaceAll: true,
canRemoveAll: true,
},
color: {
canAdd: true,
canEdit: true,
canDelete: true,
canReplaceAll: true,
canRemoveAll: true,
},
fontStack: {
canAdd: true,
canEdit: true,
canDelete: true,
canReplaceAll: true,
canRemoveAll: true,
},
link: {
canAdd: true,
canEdit: true,
canDelete: true,
canReplaceAll: true,
canRemoveAll: true,
},
}, // customization options for the variable modal
components: {
typedText: {
canAdd: true,
canDelete: true,
canSave: true,
canEdit: true,
canReset: true,
canDetach: true,
canDetachAll: true,
canRestore: true,
},
image: {
canAdd: true,
canDelete: true,
canSave: true,
canEdit: true,
canReset: true,
canDetach: true,
canDetachAll: true,
canRestore: true,
},
button: {
canAdd: true,
canDelete: true,
canSave: true,
canEdit: true,
canReset: true,
canDetach: true,
canDetachAll: true,
canRestore: true,
},
divider: {
canAdd: true,
canDelete: true,
canSave: true,
canEdit: true,
canReset: true,
canDetach: true,
canDetachAll: true,
canRestore: true,
},
video: {
canAdd: true,
canDelete: true,
canSave: true,
canEdit: true,
canReset: true,
canDetach: true,
canDetachAll: true,
canRestore: true,
},
}, // customization options for the components items
actionMenu: {
block: { // if false the Block Menu is turned off (floating menu right side each block)
drag: true,
save: true, // the save option is automatically hidden when no blockLibraries are added to the editor instance
duplicate: true,
delete: true,
},
}, // customization for the action menu
toolboxes: {
body: true,
fullwidth: true,
text: true,
typedText: true,
button: true,
box: true,
multicolumn: true,
image: true,
divider: true,
code: true,
social: true,
column: true,
loop: true,
conditional: true,
dynamicImage: true,
video: true,
blockLevelConditional :true,
blockLevelLoop: true,
}, // // if a toolbox is set to false then the element is not customizable in the Editor
dropzones: {
block: true,
}, // show/hide toggle for drop zones
elementDefaults: {
attrs: {
text: {
text: "<p>Double click to edit text!</p>",
},
typedText: {
text: "Double click to edit typed text!",
},
button: {
text: "<p>Click here to edit me</p>",
},
},
},
blockLibraries: [], // an array of block library descriptors
fontFiles: { // an object, keys are the names of the font families, the values are the URLs to the font files.
"Poppins": "https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,700;1,400;1,700&display=swap",
"Zen Tokyo Zoo": "https://fonts.googleapis.com/css2?family=Zen+Tokyo+Zoo&display=swap",
},
fontStacks: [ // an array of arrays, the inner array contains the font families as strings in the font stack
["Poppins", "Helvetica Neue", "Helvetica", "Arial", "sans-serif"],
],
hideDefaultFonts: true, // if true, the default built-in font stacks are hidden, if false, the custom font stacks are added to the default ones.
addons: {
blockLock: { // or false to fully turn off (hide) the addon
enabled: false, // enable / disable the addon
disabledReason: "This addon is disabled",
},
variableSystem: { // or false to fully turn off (hide) the addon
enabled: false, // enable / disable the addon
disabledReason: "This addon is disabled",
},
components: { // or false to fully turn off (hide) the addon
enabled: false, // enable / disable the addon
behavior: "both", // change the new component behavior
disabledReason: "This addon is disabled",
behaviorDescription: "Description of current behavior"
},
},
panels: {
details: true // or false if you want the panel to be in a hidden state
},
title: {
canEdit: true // or false to disable editing of the document title
},
},
hooks: {}, // an object of available hooks (for example onSave)
};
// Fullscreen
const editorInstance = await chamaileonPlugins.createFullscreenPlugin(editorConfig);
// Inline
const editorInstance = await chamaileonPlugins.createInlinePlugin(
editorConfig,
{
container: "#email-editor", /* HTML Element */
dimensions: {
width: 1000, // default 100%
height: 720, // default 100%
scale: 1, // default 1
}
}
);
These will be returned after a successful plugin initialization.
These are methods provided by the plugin instance.
Updates the settings inside the plugin instance on the fly.
const newSettings = {
addons: {
blockLock: false,
variableSystem: {
enabled: true,
},
},
autoSaveInterval: 20000,
};
await editorInstance.methods.updateSettings(newSettings);
Updates the data inside the plugin instance on the fly.
await editorInstance.methods.updateData({ document });
Updates the hooks inside the plugin instance on the fly.
You can read more about the updateHooks
method here.
await editorInstance.methods.updateHooks({ hooks, resetHooks });
This function will resolve the current state of the document as a JSON object. You should save this and pass it to our other plugins. You can also invoke our generator with this JSON object from your backend.
const document = await editorInstance.methods.getDocument();
You can generate the email HTML version of the document with this function. You can do it any time, but we suggest that you refrain from doing it on each and every change, because you would hit our rate limits pretty soon.
We suggest the usage of the
editorInstance.methods.getDocument()
method and that you should save this document on your backend. After that you can call our API directly and generate the HTML from there.
const emailHtml = await editorInstance.methods.getEmailHtml();
Shows the preview instance iframe. Fullscreen mode only.
You can read more about the optionalParams
object here.
await editorInstance.show(optionalParams);
Hides the preview instance iframe. Fullscreen mode only.
await editorInstance.hide();
Shows the splash screen inside the preview instance. Fullscreen mode only.
await editorInstance.showSplashScreen();
Hides the splash screen inside the preview instance. Fullscreen mode only.
editorInstance.hideSplashScreen();
Destroys the preview instance.
await editorInstance.destroy();
Property | Type | Description |
---|---|---|
data | object | The initial data of the plugin instance |
settings | object | The initial settings of the plugin instance |
hooks | object | You can define functions that will be called on different events that occur inside the plugin. For more, please check out the editorConfig.hooks section. |
The data object has the following properties:
Property | Type | Description |
---|---|---|
document | object | The email document. |
The settings object has the following properties:
Property | Type | Description |
---|---|---|
user | object | The editor user that will be displayed |
staticAssetsBaseUrl | string | This parameter sets the base URL of the default image and the social icon images. You can find these images in the following repository and they should be hosted on your domain: https://github.com/chamaileon-sdk/static-content If you are not hosting these images on your domain, then that can negatively impact the deliverability of your emails. |
videoElementBaseUrl | string | This parameter sets the base URL of the video element. This should point to the video backend hosted on your side. If you are not hosting this backend on your domain, then the video element will not work. Read more below. |
autoSaveInterval | number | This optional parameter sets the frequency of the automatic save in milliseconds. The onAutoSave hook will be called when the interval is reached. If the value is not set, then the onAutoSave hook will not be triggered. |
buttons | object | The configuration of the header buttons and textInsert buttons. |
elements | object | Object that configures the editor elements sidebar. |
variables | object | Object that configures the variable modal and it's items. |
components | object | Object that configures the components items. |
actionMenu | object | Object that configures our action menus. |
toolboxes | object | Object that configures the toolbox visibility for each element type |
dropzones | object | Object that configures the drop zone visibility |
panels | object | Object that configures panel default state (hidden or visible) |
title | object | Object that configures document title state |
blockLibraries | array | You can provide multiple block libraries for your users, with different access-levels. See the editorConfig.settings.blockLibraries section for more. |
fontFiles | object | An object, containing the custom font resources. Keys are the font names, values are the URLs. |
fontStacks | array | An array of arrays, containing the font stacks. |
hideDefaultFonts | boolean | If it's true, the default built-in font stacks are hidden, if it's false, the custom font stacks are added to the default ones. |
addons | object | Addon configurations |
elementDefaults | object | Object that configures the element defaults |
hideHeader | boolean | Hides the plugin header. In inline mode you may want to hide it. |
maxBodyWidth | number | You can change the maximum body width of the email. |
The Video Backend is designed to effortlessly fetch YouTube cover images, then stamp them with a play button for email embedding. To maintain optimum performance, it's essential to use a Content Delivery Network (CDN). This ensures speedy delivery by caching the generated images, preventing potential server overloads.
Quick Setup:
docker load --input ./docker/video-backend.tar
in your terminal.run -d --name "video-backend" -p $OUTSIDE_PORT:80 "video-backend:3.0.0"
, replacing $OUTSIDE_PORT with your desired external port number.editorConfig.settings.videoElementBaseUrl
to your video backend (CDN).Remember, omitting a CDN can cause slow image regenerations, leading to delayed responses.
You can configure the buttons in the top right corner of the editor. You can set up their icons, labels and ids. If a user clicks on them, then the onHeaderButtonClicked
hook will be called with the id
of the button. With this option, you can add custom functionality to the editor. For example, you can create custom dialogs. You can also configure dropdowns and add badges to the buttons as well.
Example:
editorConfig.settings.buttons.header = [
{
id: "preview",
type: "button",
icon: "eye", // icons from https://pictogrammers.github.io/@mdi/font/6.7.96/ without the mdi- prefix
label: "Preview",
color: "#aaaaaa",
style: "text", // filled, depressed (no shadow filled), outlined, text, plain
badge: {
color: "#bbbbbb",
icon: "lock",
},
},
{
type: "dropdown",
icon: "dog",
label: "Dropdown",
color: "secondary",
style: "outlined",
items: [ // if the button has an items array defined it will generate a dropdown button and only the items inside it will trigger the hook
{
id: "share-email",
label: "Get shareable link",
icon: "share",
},
{
id: "send-test-email",
label: "Send test email",
icon: "email",
},
{
id: "request-review",
label: "Request a review",
icon: "comment-eye",
},
]
}
];
You can add clickable buttons into the CKEditor, to insert your custom text. The onTextInsertPluginButtonClicked
hook will be called, when you click on one of these buttons. This is a great way to provide merge tags to your users.
The textInsertPluginButtons
array consists of objects with the following properties:
Property | Type | Description |
---|---|---|
id | string | The id of the button. The onTextInsertPluginButtonClicked hook will get this value as a parameter. |
label | string | This will be displayed in the CKEditor header. |
icon | string (optional) | You can set the CKEditor icon with an image src. If this is blank or undefined a text button will be created with the given label. |
Example:
editorConfig.settings.buttons.textInsert = [
{
id: "insert-tag",
label: "Custom tags",
icon: "https://raw.githubusercontent.com/ckeditor/ckeditor4/major/skins/kama/icons/paste.png",
}
]
You can change the pre-defined inline buttons with this config. This only applies to the save
, undo
, redo
and zoom
buttons.
Property | Type | Description |
---|---|---|
title | string (optional) | You can set the hover title. |
visible | string (optional) | You can set if you want the button visible or not |
Example:
editorConfig.settings.buttons.inlineHeader: {
undo: {
title: "",
visible: "true",
},
redo: {
title: "",
visible: "true",
},
save: {
title: "",
visible: "true",
},
zoom: {
title: "",
visible: "true",
},
},
You can modify the inline text insert buttons with this config.
This is only applied if you have one or more textInsert button configured.
Property | Type | Description |
---|---|---|
id | string (immutable) | This showcases the id corresponding to the button config |
title | string (optional) | You can set the hover title. |
icon | string (optional) | You can set if you want the button visible or not |
visible | string (optional) | mdi icon without the mdi- prefix |
Example:
editorConfig.settings.buttons.inlineTextInsert: {
videoAlt: {
id: "video-alt",
title: "",
icon: "code-braces", // icons from https://pictogrammers.github.io/@mdi/font/6.7.96/ without the mdi- prefix
visible: "true",
},
imageAlt: {
id: "image-alt",
title: "",
icon: "code-braces",
visible: "true",
},
imageLink: {
id: "image-link",
title: "",
icon: "code-braces",
visible: "true",
},
imageLinkTitle: {
id: "image-link-title",
title: "",
icon: "code-braces",
visible: "true",
},
dynamicImageSrc: {
id: "dynamic-image-alt",
title: "",
icon: "code-braces",
visible: "true",
},
dynamicImageAlt: {
id: "dynamic-image-src",
title: "",
icon: "code-braces",
visible: "true",
},
dynamicImageLink: {
id: "dynamic-image-link",
title: "",
icon: "code-braces",
visible: "true",
},
dynamicImageLinkTitle: {
id: "dynamic-image-link-title",
title: "",
icon: "code-braces",
visible: "true",
},
buttonLink: {
id: "button-link",
title: "",
icon: "code-braces",
visible: "true",
},
buttonLinkTitle: {
id: "button-link-title",
title: "",
icon: "code-braces",
visible: "true",
},
},
You can modify the inline text insert buttons inside the cKEditor with this config.
This is only applied if you have one or more textInsert button configured.
Property | Type | Description |
---|---|---|
id | string (immutable) | This showcases the id corresponding to the button config |
title | string (optional) | You can set the hover title. |
label | string (optional) | The text that will show up inside the button |
visible | string (optional) | mdi icon without the mdi- prefix |
Example:
editorConfig.settings.buttons.inlineTextInsert: {
textLink: {
id: "text-link",
title: "",
label: "{}",
visible: "true",
},
textVariableLink: {
id: "text-variable-link",
title: "",
label: "{}",
visible: "true",
},
},
This is an object that gives you the option to turn off some (or all) of the elements draggable from the left-hand-side.
Example:
editorConfig.settings.elements = { // or false, then the elements tab will not be visible
content: { // or false, then the content elements section will not be visible
title: true, // true: visible, false: hidden
paragraph: true,
list: true,
text: false,
image: true,
button: true,
social: true,
divider: true,
},
structure: { // or false, then the structure elements section will not be visible
box: true,
multiColumn: true,
},
advanced: { // or false, then the advanced personalization elements section will not be visible
code: true,
loop: true,
conditional: true,
dynamicImage: true,
blockLevelConditional: true,
blockLevelLoop: true,
},
};
This is an object that gives you the option to set the defaults for the elements. Currently only the text, typed-text and button default values can be modified.
Example:
editorConfig.settings.elementDefaults = {
attrs: {
text: {
text: "<p>Double click to edit text!</p>",
},
typedText: {
text: "Double click to edit typed text!",
},
button: {
text: "<p>Click here to edit me</p>",
},
},
};
This is an object that can modify each variable type separately. You can toggle which action buttons you want to enable or disable with these options. Currently we have these variable types: text
, image
, color
, fontStack
and link
.
Property | Type | Description |
---|---|---|
canAdd | boolean | Inside this type the user can add a new variable. |
canEdit | boolean | Inside this type the user can edit the variable. |
canDelete | boolean | Inside this type the user can delete the variable. |
canReplaceAll | boolean | Inside this type the user can replace every occurrence of the selected variable value with the variable reference. |
canRemoveAll | boolean | Inside this type the user can remove every occurrence of the selected variable value from the email document. |
Example:
editorConfig.settings.variables = {
text: {
canAdd: true,
canEdit: true,
canDelete: true,
canReplaceAll: true,
canRemoveAll: true,
},
image: {
canAdd: true,
canEdit: true,
canDelete: true,
canReplaceAll: true,
canRemoveAll: true,
},
color: {
canAdd: true,
canEdit: true,
canDelete: true,
canReplaceAll: true,
canRemoveAll: true,
},
fontStack: {
canAdd: true,
canEdit: true,
canDelete: true,
canReplaceAll: true,
canRemoveAll: true,
},
link: {
canAdd: true,
canEdit: true,
canDelete: true,
canReplaceAll: true,
canRemoveAll: true,
},
};
This is an object that can modify each component type separately. You can toggle which action buttons you want to enable or disable with these options. Currently we have these component types: typed-text
, image
, button
, divider
and video
.
Property | Type | Description |
---|---|---|
canAdd | boolean | Inside this type the user can add a new component. |
canDelete | boolean | Inside this type the user can delete the component. |
canSave | boolean | Inside this type the user can save modifications made to the component. |
canEdit | boolean | Inside this type the user can toggle the edit mode for the component. |
canReset | boolean | Inside this type the user can remove every unsaved modification made to the component. |
canDetach | boolean | Inside this type the user can remove the component from an element. |
canDetachAll | boolean | Inside this type the user can remove all the references to the component. |
canRestore | boolean | Inside this type the user can restore a non-existent component that is referenced on an element. |
Example:
editorConfig.settings.components = {
typedText: {
canAdd: true,
canDelete: true,
canSave: true,
canEdit: true,
canReset: true,
canDetach: true,
canDetachAll: true,
canRestore: true,
},
image: {
canAdd: true,
canDelete: true,
canSave: true,
canEdit: true,
canReset: true,
canDetach: true,
canDetachAll: true,
canRestore: true,
},
button: {
canAdd: true,
canDelete: true,
canSave: true,
canEdit: true,
canReset: true,
canDetach: true,
canDetachAll: true,
canRestore: true,
},
divider: {
canAdd: true,
canDelete: true,
canSave: true,
canEdit: true,
canReset: true,
canDetach: true,
canDetachAll: true,
canRestore: true,
},
video: {
canAdd: true,
canDelete: true,
canSave: true,
canEdit: true,
canReset: true,
canDetach: true,
canDetachAll: true,
canRestore: true,
},
};
You can control the action menus and their menu items inside the editor with this. If the whole menu is set to false
then it won't be displayed but otherwise you should send an object with some or all of the parameters inside it.
Currently we only have one action menu which is the right-hand side floating block menu.
Example:
editorConfig.settings.actionMenu = {
block: {
drag: true,
save: false,
duplicate: true,
delete: true,
},
};
You can control our toolboxes with this setting. If a toolbox is set to false
that means that that type of element wont't be editable and the toolbox will be hidden for it.
Example:
editorConfig.settings.toolboxes: {
body: false,
fullwidth: true,
text: true,
typedText: true,
button: true,
box: true,
multicolumn: true,
image: true,
divider: true,
code: true,
social: true,
column: true,
loop: true,
conditional: true,
dynamicImage: true,
video: true,
blockLevelConditional :true,
blockLevelLoop: true,
};
You can control the visibility of our drop zones with this setting.
Currently only the block drop zone can be changed.
Example:
editorConfig.settings.dropzones = {
block: false,
};
You can control the default visibility state of our panels.
Currently only the Details
panel can be changed.
Example:
editorConfig.settings.panels = {
details: false,
};
You can control the state of the document title.
Currently only the canEdit
option is available. If it's set to false, the title will be visible but not clickable or editable.
Example:
editorConfig.settings.title = {
canEdit: false,
};
You can configure block libraries, with the settings.blockLibraries
array. They will appear in two parts of the editor:
This way, you can provide multiple block libraries to your users and can allow them to save their blocks into different libraries.
When a user selects a block library on the left hand side, the onLoadBlocks
hook will be called with the id
of the library. This way, you can provide as many block libraries to your clients as you want.
The blockLibraries
array consists of objects with the following properties:
Property | Type | Description |
---|---|---|
id | string | The id of the block library. The onLoadBlocks hook will get this value as a parameter. |
label | string | This will be displayed in the aforementioned dropdowns. |
canDeleteBlock | boolean | If true, the current user will be able to delete from this block library. If false, the delete button is not shown. |
canRenameBlock | boolean | If true, the current user will be able to rename blocks in this block library. If false, the rename functionality is disabled. |
canSaveBlock | boolean | If true, the current user will be able to save blocks to this this block library. If false, then this block library will not be shown to the user when they are trying to save a block. |
useBlockTitleAsMarker | boolean | If true, then the block that is dropped in will use the block title as a marker. If false, then it will use the marker that was saved onto the block originally (which can be empty if nothing was added when the block was saved). |
Example:
editorConfig.settings.blockLibraries = [
{
id: "email-blocks",
label: "Email's blocks",
canDeleteBlock: false,
canRenameBlock: false,
canSaveBlock: false,
useBlockTitleAsMarker: true,
},
{
id: "john-doe-favs",
label: "John Doe's Favorite Blocks",
canDeleteBlock: true,
canRenameBlock: true,
canSaveBlock: true,
useBlockTitleAsMarker: false,
}
]
To display a new font in the email editor and your emails (depending on email clients), you need to provide a font name (as a key) and a font URL (as a value) inside this object.
Every font URL should include the following font styles:
As an example, the URL for Poppins would look like this:
https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,700;1,400;1,700&display=swap
Example:
editorConfig.settings.fontFiles = {
"Poppins": "https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,700;1,400;1,700&display=swap"
};
Once you added the required Font URL, you need to define the Font stack to ensure that email clients (like Gmail) that don't support custom fonts will display your text in a similar web-safe font.
This property is an array of arrays, the inner array contains the font families as strings in the font stack. The font stacks are displayed in the order as they are in the outer array.
Example:
editorConfig.settings.fontStacks: [
["Poppins", "Helvetica Neue", "Helvetica", "Arial", "sans-serif"]
];
In the HTML code, it will look like this:
"Poppins, Helvetica Neue, Helvetica, Arial, sans-serif"
Property | Type | Description |
---|---|---|
blockLock | object or false | Enabling or disabling the block lock addon. |
variableSystem | object or false | Enabling or disabling the variable system addon. |
components | object or false | Enabling or disabling the components addon. |
Each of the addons can be an object
or false
. If you set it as false
, the addon will not be visible. If you use an object, then there should be an enabled
property on the addon. If it is true, then the addon is enabled, if its value is false, the addon will be visible, but disabled.
Example:
editorConfig.settings.addons = {
blockLock: { // or false, then the addon will not be visible
enabled: false, // if it is false, then the addon will be disabled
disabledReason: "This addon is disabled", // the tooltip that will be visible when the addon is disabled
},
variableSystem: { // or false, then the addon will not be visible
enabled: true, // if it is false, then the addon will be disabled
disabledReason: "This addon is disabled", // the tooltip that will be visible when the addon is disabled
},
components: { // or false to fully turn off (hide) the addon
enabled: false, // if it is false, then the addon will be disabled
behavior: "both", // change the new component behavior
disabledReason: "This addon is disabled", // the tooltip that will be visible when the addon is disabled
behaviorDescription: "Description of current behavior" // the information text that will be visible when creating a component
},
};
The block lock addon allows users to lock the design or design & content of a block. If the block is locked, you won't be able to edit the layout or even the content of that block. You can find the toggle button for this feature in the "Block toolbox".
The variable system addon enables your users to define variables, and refer those variables in the email. For example, you can define a primaryColor
variable, that you can use it at multiple places. Whenever you update the value of the primaryColor
variable, it will be automatically updated everywhere in the email where you referenced its value.
The components addon enables your users to define components and use them in the email. For example, you can create a button and then use this button as a component, that way you can use it in different places. Whenever you update the component, it will be automatically updated everywhere in the email where you referenced it. When you enable this addon, the editor automatically disables the old text
element and converts the email's text elements into separate paragraph, title or list elements. This is a non-reversible conversion so make sure you only save the JSONs on your side when you verified that the conversion is correct.
See detailed explanation in the behavior section for that parameter.
Each and every hook should be an asynchronous process, so all of the hook handler functions have to return Promise
s.
In most cases you just have to resolve the promise when the async operation is done without any params, but in some cases, you will have to resolve certain objects with properties that the plugin can use. Similarly to the parameters, we always expect an object to be resolved, even if it only has one property. (This way it will be easier to add new properties later on if needed.)
If any errors occurred on your side, you can reject the promise with an instance of Error
. In this case, the error message will be shown in a snackbar inside the plugin instance.
function handler(params) {
return new Promise((resolve, reject) => {
// You can put the logic here.
// Resolve the promise when everything is okay
// Reject the promise on error
if (!error) {
resolve(dataToResolve) // In some cases, you don't have to resolve any data. You can resolve the promise without a parameter.
} else {
reject(new Error("Your error message"))
}
})
}
Note that, with the async syntax, the unexpected errors will be also displayed:
async function handler(params) {
// Unexpected errors will also cause promise rejections in this case
// For example, if you get a timeout error, that will also be displayed in a snackbar in the editor.
// Any exception will be caught by the SDK and the message property of the error object will be shown in a snackbar.
return dataToResolve;
}
Below are the list of hooks that you can use. Read more about them in the following sections.
editorConfig.hooks = {
close,
onSave,
onAutoSave,
onChange,
onEditTitle,
onEditImage,
onEditBackgroundImage,
onBlockSave,
onLoadBlocks,
onBlockRename,
onBlockDelete,
onHeaderButtonClicked,
onTextInsertPluginButtonClicked,
onExpressionEditClicked,
onUserEvent,
};
This hook is called when the top left back button is clicked. You should hide the plugin with this and you can add any custom logic that runs after the plugin is hidden.
/*
Params: nothing.
Has to resolve: nothing.
*/
editorConfig.hooks.close = () => {
return new Promise(resolve => {
editorInstance.hide();
// You should get the document here and implement a
// logic that saves it on your backend as well
const document = await editorInstance.methods.getDocument();
resolve();
});
};
This function is called when the user clicks on the save button. The "save in progress" indicator will be spinning, until the returned promise is not resolved or rejected.
/*
Params:
- document: The object representation of the document.
Has to resolve: nothing.
*/
editorConfig.hooks.onSave = ({ document }) => {
return new Promise(resolve => {
// you can put here the logic that saves the document object
// and when it's done, you can resolve the promise
resolve();
});
};
This hook is very similar to the previous one. It is triggered when an automatic save happens, and it gets the same params as the previous one. The progress indicator will also be spinning until the promise is not resolved.
/*
Params:
- emailJson: The object representation of the document.
Has to resolve: nothing.
*/
editorConfig.hooks.onAutoSave = ({ document }) => {
return new Promise(resolve => {
// you can put the logic here that saves the document object
// and when it's done, you can resolve the promise
resolve();
});
};
This hook is invoked whenever something happened in the editor. It might be useful if you want to know if there were any changes since the last time you saved the document. For example, you can set a variable that shows that there were changes.
/*
Params:
- mutation: Returns every change as a mutation. This will be useful later when real-time editing functionality is fully introduced.
Has to resolve: nothing.
*/
editorConfig.hooks.onChange = ({ mutation }) => {
return new Promise(resolve => {
// you can put the logic here that saves the email mutation
resolve();
});
};
Invoked when a user changes the title of the document. The progress indicator will be spinning until the promise is resolved.
/*
Params:
- title: The title of the document, you previously set up in the editorConfig.data.document object
Has to resolve: nothing.
*/
editorConfig.hooks.onEditTitle = ({ title }) => {
return new Promise(resolve => {
resolve();
});
};
This function is called when the user wants to edit an image. You can use this hook to pop up a gallery. This function has to be resolved with an object that has an src property. That will be the new src of the image.
This function has two optional parameters:
The first is the originalImage
. If you get a string value in this parameter, that means that the user wants to edit that image, so you should initialize an image editor and resolve the promise with the modified image. If this value is undefined, that means that the user wants to change the image, so you might want to pop up an image selector.
The second is the lockDimensions
. When defined, it means that the user wants to change an image in a block, which has a locked design. In this case, you will have to resolve an image with the exact same aspect ratio as defined by the width
and height
property of the lockDimensions
object. (Also, the dimensions has to be at least as big as these values.) Usually it means, that you will have to use a crop tool if you don't have images with the proper aspect ratio.
When you resolve the promise with an src
property on an object, the editor will set up that value as the new src
of the image. This might be a long-running promise, since you will probably resolve it, when your user selects one of their images from your image library.
/*
Params:
- originalImage: optional, string, shows that a user wants to edit an image
- lockDimensions: optional, object, you have to crop the image to this aspect ratio
- width
- height
Has to resolve:
- src
*/
editorConfig.hooks.onEditImage = ({ originalImage, lockDimensions }) => {
return new Promise(resolve => {
// Eventually, you will have to resolve an object with an src prop
resolve({ src });
});
};
This hook is invoked when a user wants to change the background image of an element.
/*
Params:
- originalImage: optional, string, shows that a user wants to edit an image
Has to resolve:
- src
*/
editorConfig.hooks.onEditBackgroundImage = ({ originalImage }) => {
return new Promise(resolve => {
// Eventually, you will have to resolve an object with an src prop
resolve({ src });
});
};
This will be called when the user selects a block library. The editor will call this hook with one of the pre-configured block library ids. This way, you may provide multiple sets of blocks for your clients.
The resolved blocks
array has to contain the block objects. You can get those block objects in the onSaveBlock
hook.
/*
Params:
- libId: string id of the library from the editorConfig.settings.blockLibraries array
Has to resolve:
- blocks: array of block objects
*/
editorConfig.hooks.onLoadBlocks = ({ libId }) => {
return new Promise(resolve => {
// Eventually, you will have to resolve an object with an src prop
const blocks = [];
// Just put the block objects into the array
resolve({ blocks });
});
};
Called when a user saves a new block. After you saved it to your backend, you have to resolve the promise with a block object in it. The block object has to have an _id
property, that is the id of your block.
/*
Params:
- libId: string id of the library from the editorConfig.settings.blockLibraries array
- block: the object representing a block
Has to resolve:
- block: the final block object with an _id field
*/
editorConfig.hooks.onBlockSave = ({ libId, block }) => {
return new Promise(resolve => {
block._id = "something"; // You have to set up the _id of the block
resolve({ block });
});
};
When the user renames a block, this hook will be invoked. You can save the changes in your db.
/*
Params:
- libId: string id of the library from the editorConfig.settings.blockLibraries array
- block: the object representing a block
- _id: the id of the block, you can update your db entry based on this
- title: the new value of the block's title
Has to resolve: nothing.
*/
editorConfig.hooks.onBlockRename = ({ libId, block: { _id, title } }) => {
return new Promise(resolve => {
// Here, you can save the new title of the block -> resolve when done.
resolve();
});
};
Invoked when a user deletes a block from a block library.
/*
Params:
- libId: string id of the library from the editorConfig.settings.blockLibraries array
- block: the object representing a block
- _id: the id of the block, you can delete your db entry based on this
Has to resolve: nothing.
*/
editorConfig.hooks.onBlockDelete = ({ libId, block: { _id } }) => {
return new Promise(resolve => {
// Here, you can delete your entry and resolve the promise
resolve();
});
};
If you have set up header buttons in the config, you can use this hook to implement anything you like if that button is clicked.
/*
Params:
- buttonId: string id of the button from the editorConfig.settings.buttons.header array
Has to resolve: nothing.
*/
editorConfig.hooks.onHeaderButtonClicked = ({ buttonId }) => {
return new Promise(resolve => {
// Here, you can implement your custom dialog
resolve();
});
};
When a user clicks on any of the configured buttons in the CKEditor header, you will be able to handle the actions with this hook, and insert your own custom text into the editor surface. This is a very handy feature for implementing merge tags.
The editor also has some pre-defined id's which correspond to different text input fields in the editor.
id | Description |
---|---|
video-alt |
Video element alt text |
image-alt |
Image element alt text |
image-link |
Image element link address |
image-link-title |
Image element link title |
dynamic-image-src |
Dynamic image element src address |
dynamic-image-alt |
Dynamic image element alt text |
dynamic-image-link |
Dynamic image element link address |
dynamic-image-link-title |
Dynamic image element link title |
button-link |
Button element link address |
button-link-title |
Button element link title |
text-link |
Text element cKEditor link |
text-variable-link |
Text element cKEditor link in variable editor |
See detailed explanation in the logicalParentTree section for the parameter.
If you don't want to insert anything, you can resolve the Promise with either an empty object or an empty response
/*
Params:
- buttonId: string id of the button from the editorConfig.textInsertPluginButtons array
- logicalParentTree: Every element that logically affected the current element
- blockMarker: The marker string that is applied to the parent block element
- elementType: The type of the element that is currently edited
Has to resolve:
- value: The string you want to insert.
Can resolve:
- isHTML: true if the value contains a HTML snippet. Defaults to false.
*/
editorConfig.hooks.onTextInsertPluginButtonClicked = ({ buttonId }) => {
return new Promise(resolve => {
// Here, you can implement your custom dialog
resolve({ value: "Your inserted text.", isHTML: false });
});
};
Loop, conditional and dynamic image elements have an expression option. If you have a list of expressions, you can pop up a dialog, and resolve the selected expression, and it will be added to the editor's expression input field.
See detailed explanation in the logicalParentTree section for the parameter.
/*
Params:
- expression: The current selected expression in the input field.
- logicalParentTree: Every element that logically affected the current element
- blockMarker: The marker string that is applied to the parent block element
- elementType: The type of the element that is currently edited
Has to resolve:
- expression: Your expression to insert into the editor's toolbox input field.
*/
editorConfig.hooks.onExpressionEditClicked = ({ expression }) => {
return new Promise(resolve => {
// Here, you can implement your custom dialog with a list of expressions
resolve({ expression: "<Your inserted expression>" });
});
};
You can get various information about user interactions on this hook. For example if the zoom was changed or a new element was dropped onto the canvas.
/*
Params:
- userEvent: The current event information
Has to resolve: nothing.
*/
editorConfig.hooks.onUserEvent = ({ userEvent }) => {
return new Promise(resolve => {
// Here, you can save the event information
resolve();
});
};
The logicalParentTree is an array of objects that contains every logical parent or parent sibling element that logically affects the current element. The structure contains the elements from the top down to the direct parent of the current element. The current element is not included in it.
Property | Type | Description |
---|---|---|
expression | string or null | The expression string that is added to the element |
type | string | conditional or loop |
subType | string or undefined | only present on the conditional type, can be if else-if or else |
parent | boolean | true if it's a direct parent. false if it's the parent's sibling but affects the logical structure of the current element |
if condition1
else-if condition2
if condition3
loop expression3
else
loop expression4
else
loop expression1
if condition4
else-if condition5
loop expression2
else
loop expression5
block
button
If we click the Select your expression
button on the if
branch that has the condition4
applied to it we will get the following array:
const logicalParentTree = [
{ expression: "condition1", type: "conditional", subType: "if", parent: false },
{ expression: "condition2", type: "conditional", subType: "else-if", parent: false },
{ expression: null, type: "conditional", subType: "else", parent: true },
{ expression: "expression1", type: "loop", parent: true }
]
If we click the Select your expression
button on the loop
that has the expression4
applied to it we will get the following array:
const logicalParentTree = [
{ expression: "condition1", type: "conditional", subType: "if", parent: false },
{ expression: "condition2", type: "conditional", subType: "else-if", parent: true },
{ expression: "condition3", type: "conditional", subType: "if", parent: false },
{ expression: null, type: "conditional", subType: "else", parent: true }
]
If we click the Add merge tag
button on the button element's link toolbox we will get the following array:
const logicalParentTree = [
{ expression: "condition1", type: "conditional", subType: "if", parent: false },
{ expression: "condition2", type: "conditional", subType: "else-if", parent: false },
{ expression: null, type: "conditional", subType: "else", parent: true },
{ expression: "expression1", type: "loop", parent: true },
{ expression: "condition4", type: "conditional", subType: "if", parent: false },
{ expression: "condition5", type: "conditional", subType: "else-if", parent: false },
{ expression: null, type: "conditional", subType: "else", parent: true },
{ expression: "expression5", type: "loop", parent: true },
]
The behavior
property can have one of these 3 values: nested
, unique
or both
.
If it's nested
, the component will be linked to the original component and will only store the differences between them. With this, if you update a style that is only present in the original component, it will be used for the new component as well. And if the style is present in both, the new component style will take precedence.
If it's unique
, the new component won't be linked to the component that it was created from.
If it's both
, there will be a toggle switch for your users to select from the two options.
Example for the nested
behavior:
const firstComponent = {
name: "base",
reference: null,
type: "text",
style: {
backgroundColor: "#C4C4C4",
backgroundRepeat: "no-repeat",
backgroundPosition: "center center",
backgroundImage: "",
backgroundSize: "300px 100px"
}
}
// and when we created a new component from this
const secondComponent = {
name: "second",
reference: "base",
type: "text",
style: {
color: "#F0F0F0"
}
}
// the new component only has the styles that differ from its reference
And for the unique
behavior:
const firstComponent = {
name: "base",
reference: null,
type: "text",
style: {
backgroundColor: "#C4C4C4",
backgroundRepeat: "no-repeat",
backgroundPosition: "center center",
backgroundImage: "",
backgroundSize: "300px 100px"
}
}
// and when we created a component based on this
const secondComponent = {
name: "second",
reference: null,
type: "text",
style: {
backgroundColor: "#C4C4C4",
backgroundRepeat: "no-repeat",
backgroundPosition: "center center",
backgroundImage: "",
backgroundSize: "300px 100px",
color: "#F0F0F0"
}
}
// the new component has all the styles and no reference to another component
We put together a few demos and you can check out the code here.
You can also check out the email variable editor plugin on the Chamaileon SDK Playground.