Email Editor Plugin

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
		}
	}
);

Editor instance methods

These will be returned after a successful plugin initialization.

editorInstance.methods

These are methods provided by the plugin instance.

editorInstance.methods.updateSettings

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);

editorInstance.methods.updateData

Updates the data inside the plugin instance on the fly.

await editorInstance.methods.updateData({ document });

editorInstance.methods.updateHooks

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 });

editorInstance.methods.getDocument

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();

editorInstance.methods.getEmailHtml

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();

editorInstance.show

Shows the preview instance iframe. Fullscreen mode only.

You can read more about the optionalParams object here.

await editorInstance.show(optionalParams);

editorInstance.hide

Hides the preview instance iframe. Fullscreen mode only.

await editorInstance.hide();

editorInstance.showSplashScreen

Shows the splash screen inside the preview instance. Fullscreen mode only.

await editorInstance.showSplashScreen();

editorInstance.hideSplashScreen

Hides the splash screen inside the preview instance. Fullscreen mode only.

editorInstance.hideSplashScreen();

editorInstance.destroy

Destroys the preview instance.

await editorInstance.destroy();

Editor configuration

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.

editorConfig.data

The data object has the following properties:

Property Type Description
document object The email document.

editorConfig.settings

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.

editorConfig.settings.videoElementBaseUrl

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:

  • Download: Grab the Docker Image here.
  • Load: Execute docker load --input ./docker/video-backend.tar in your terminal.
  • Run: Use docker run -d --name "video-backend" -p $OUTSIDE_PORT:80 "video-backend:3.0.0", replacing $OUTSIDE_PORT with your desired external port number.
  • Point editorConfig.settings.videoElementBaseUrl to your video backend (CDN).

Remember, omitting a CDN can cause slow image regenerations, leading to delayed responses.

editorConfig.settings.buttons.header

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",
			},
		]
	}
];

editorConfig.settings.buttons.textInsert

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",
	}
]

editorConfig.settings.buttons.inlineHeader

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",
	},
},

editorConfig.settings.buttons.inlineTextInsert

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",
	},
},

editorConfig.settings.buttons.cKEditorTextInsert

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",
	},
},

editorConfig.settings.elements

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,
	},
};

editorConfig.settings.elementDefaults

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>",
		},
	},
};

editorConfig.settings.variables

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,
	},
};

editorConfig.settings.components

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,
	},
};

editorConfig.settings.actionMenu

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,
	},
};

editorConfig.settings.toolboxes

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,
};

editorConfig.settings.dropzones

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,
};

editorConfig.settings.panels

You can control the default visibility state of our panels.

Currently only the Details panel can be changed.

Example:

editorConfig.settings.panels = {
	details: false,
};

editorConfig.settings.title

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,
};

editorConfig.settings.blockLibraries

You can configure block libraries, with the settings.blockLibraries array. They will appear in two parts of the editor:

  • on the left hand side in a dropdown when you selected the blocks tab
  • and when a user wants to save a block, also in a dropdown.

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,
	}
]

editorConfig.settings.fontFiles

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:

  • Regular 400
  • Regular 400 italic
  • Bold 700
  • Bold 700 italic

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"
};

editorConfig.settings.fontStacks

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"

editorConfig.settings.addons

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.

editorConfig.hooks

Each and every hook should be an asynchronous process, so all of the hook handler functions have to return Promises.

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,
};

editorConfig.hooks.close

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();
	});
};

editorConfig.hooks.onSave

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();
	});
};

editorConfig.hooks.onAutoSave

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();
	});
};

editorConfig.hooks.onChange

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();
	});
};

editorConfig.hooks.onEditTitle

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();
	});
};

editorConfig.hooks.onEditImage

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 });
	});
};

editorConfig.hooks.onEditBackgroundImage

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 });
	});
};

editorConfig.hooks.onLoadBlocks

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 });
	});
};

editorConfig.hooks.onBlockSave

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 });
	});
};

editorConfig.hooks.onBlockRename

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();
	});
};

editorConfig.hooks.onBlockDelete

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();
	});
};

editorConfig.hooks.onHeaderButtonClicked

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();
	});
};

editorConfig.hooks.onTextInsertPluginButtonClicked

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 });
	});
};

editorConfig.hooks.onExpressionEditClicked

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>" });
	});
};

editorConfig.hooks.onUserEvent

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();
	});
};

logicalParentTree

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

logicalParentTree output

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 },
]

behavior

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

Examples

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.