Migrating
On this page
@better-rich-text/migrate moves an existing TinyMCE or Froala integration across. It does three jobs:
Translates an editor configuration object into
createRichTextEditoroptions, with a warning for everything it could not map.Cleans HTML saved by those editors (bookmarks, helper attributes, vendor classes) so it loads as plain semantic content.
Offers a small
tinymce-shaped facade, so an application can switch editors before rewriting every call site.
The package reads public option names and stored markup only. It does not import or bundle either editor.
Quick start
import { createRichTextEditor, standardPreset } from '@better-rich-text/core';
import { fromTinyMceConfig } from '@better-rich-text/migrate';
const result = fromTinyMceConfig({
selector: '#body',
plugins: 'lists link image table code',
toolbar: 'undo redo | blocks | bold italic | bullist numlist | link image table | code',
height: 400,
menubar: false,
});
for (const warning of result.warnings) console.warn(warning.option, warning.message);
const editor = createRichTextEditor('#body', result.options);fromFroalaConfig(config) works the same way. Both return:
| Field | Contents |
|---|---|
options |
Options for createRichTextEditor: toolbar, mode, sizes, placeholder, read-only, locale, plugin config, and plugins (always including migrationPlugin()). |
requires |
Package exports the configuration needs, with inStandardPreset telling you whether an import is needed at all. |
config |
Plugin configuration keyed by plugin id (fonts, sizes, line heights, palette, character limit). Also copied into options.config. |
css |
content_style or iframeStyle, scoped to .brt-content. Add it to the page. |
imageUpload |
An upload provider that calls your existing upload endpoint or handler. |
contentPolicy |
Element and attribute rules from valid_elements, htmlRemoveTags and similar options. |
warnings |
{ option, severity, message, suggestion? } for every option that changed meaning or was left out. |
unsupported |
Toolbar buttons and plugins with no equivalent. |
code |
TypeScript that builds the same editor, with the right imports. |
The translator imports no plugin packages, so it does not grow your bundle. To get ready-to-use plugins and features lists, pass the objects you import:
import { documentsFeature, documentsPlugin } from '@better-rich-text/plugin-documents';
import { createImagePlugin } from '@better-rich-text/plugin-image';
const result = fromTinyMceConfig(config, {
plugins: { documentsPlugin, createImagePlugin },
features: { documentsFeature },
preset: standardPreset,
});
// result.options.preset now uses an image plugin wired to your upload endpoint.TinyMCE options
| TinyMCE | Better Rich Text | Notes |
|---|---|---|
selector, target |
First argument of createRichTextEditor |
One editor per element. |
plugins |
Plugins and features | See TinyMCE plugins. |
toolbar, toolbar1…toolbarN |
toolbar |
Strings, arrays of rows and { name, items } groups are all read. false hides the toolbar. |
toolbar_mode |
toolbarMode |
wrap stays wrap; floating, sliding and scrolling become overflow. |
toolbar_sticky |
None | Give .brt-toolbar position: sticky in your CSS. |
fixed_toolbar_container |
toolbarContainer |
Pass the element. |
menubar, menu |
menubar, menu |
Needs menubarFeature. Layout strings and { title, items } menus are kept; TinyMCE item names work. false hides the bar. |
contextmenu |
contextmenu |
Needs contextMenuFeature. Section names are kept (editimage joins image); false keeps the browser menu. |
height, min_height, max_height |
height, minHeight, maxHeight |
Pixels only. With the autoresize plugin and no height, height: 'auto'. |
width |
None | Size the container with CSS. |
statusbar |
statusBar |
false hides it. Word counts are in the status bar, so wordcount needs nothing. |
inline |
mode: 'inline' |
|
readonly, disabled |
readOnly |
|
placeholder |
placeholder |
|
browser_spellcheck |
spellcheck |
|
auto_focus |
autofocus |
|
language |
locale |
pt_BR becomes pt-BR. Load a built-in language from @better-rich-text/locales (Localisation) or your own messages with registerMessages. |
skin, skin_url |
skin |
Pick one of the built-in skins. |
content_css |
None | URLs are listed in a warning. Scope their rules to .brt-content (scopeCss helps). |
content_style |
css |
Scoped to .brt-content; body and html selectors map to the content element. @import and @font-face are removed and reported. |
font_family_formats, font_formats |
config.textStyle.fontFamilies |
Label=stack; pairs. Invalid stacks are reported. |
font_size_formats, fontsize_formats |
config.textStyle.fontSizes |
Points; px values are multiplied by 0.75 and em by 12. |
line_height_formats |
config.textStyle.lineHeights |
|
color_map, color_map_foreground, color_map_background |
config.textStyle.palette |
Flat [hex, label, …] lists and [hex, label] pairs. One palette serves text and highlight. |
images_upload_url, images_upload_base_path, images_upload_credentials |
imageUpload |
Posts the file as file and reads location, as TinyMCE does. |
images_upload_handler |
imageUpload |
Your handler is called with the same blobInfo object. Both the promise form and the older callback form work. |
file_picker_callback |
None | Insert from your picker with editor.execute('image.insert', { src }). |
valid_elements |
contentPolicy.allowedTags |
Markup is defined by the installed plugins; the policy only narrows it. |
invalid_elements |
contentPolicy.deniedTags |
|
extended_valid_elements, valid_styles, valid_classes, custom_elements |
None | Write a plugin with node or mark specs for anything extra you need to keep. |
paste_as_text |
None | There is no plain-text paste mode; use the browser's paste-as-plain-text command. |
paste_preprocess, paste_postprocess |
clipboard.pasteHtml filter |
|
forced_root_block |
None | Blocks are always paragraphs; Shift+Enter inserts a line break. |
style_formats |
createStylesPlugin({ formats }) |
Reported with the formats to paste in. block, inline: 'span', classes, attributes (lang, dir) and groups (items) keep their meaning. |
templates |
createTemplatesPlugin({ templates }) |
Rename title to name; {{date}}, {{user.name}} and host variables resolve on insert. |
block_formats, formats |
None | The blocks menu offers paragraph, headings and code. |
quickbars_selection_toolbar |
selectionToolbarFeature(layout) |
The warning includes the mapped layout. |
setup, init_instance_callback |
onReady, editor.on(...), plugins |
The facade still runs them. |
save_onsavecallback |
createSavePlugin({ provider }) |
|
license_key, api_key, branding, promotion |
None | Not needed. |
Options not listed produce a warning with the message "Option not migrated".
TinyMCE toolbar
| TinyMCE | Better Rich Text | TinyMCE | Better Rich Text | |
|---|---|---|---|---|
undo, redo |
undo, redo |
bullist, numlist |
bulletList, orderedList |
|
bold, italic, underline |
same | checklist |
taskList |
|
strikethrough |
strike |
outdent, indent |
same | |
superscript, subscript |
same | link, unlink |
same | |
blocks, formatselect, styles |
blocks |
image |
image |
|
fontfamily, fontselect |
fontFamily |
media |
media (plugin-media) |
|
fontsize, fontsizeselect |
fontSize |
table, tableprops… |
table, tableMenu |
|
lineheight |
lineHeight |
hr |
rule |
|
forecolor |
textColor |
blockquote |
quote |
|
backcolor, hilitecolor |
highlight |
removeformat |
clearFormat |
|
alignleft…alignjustify |
alignLeft…alignJustify |
searchreplace |
search |
|
align |
align |
charmap, emoticons |
characters |
|
code |
source |
fullscreen |
fullscreen |
|
codesample |
codeBlock |
ltr, rtl |
same | |
casechange |
changeCase |
formatpainter |
formatPainter |
|
exportword, importword |
exportWord, importWord |
print, exportpdf |
exportPdf |
|
fullpage |
documentProperties |
addcomment |
comment |
|
anchor |
anchor |
pagebreak |
pageBreak |
|
insertdatetime |
dateTime |
template, inserttemplate |
templates |
|
addtemplate |
saveTemplate |
visualblocks, visualchars |
visualBlocks, visualChars |
|
preview |
preview |
help |
help |
|
editimage |
editImage |
a11ycheck |
a11yCheck |
|
spellchecker |
proofing |
accordion |
details |
|
footnotes |
footnote |
mergetags |
mergeTags |
|
tableofcontents |
toc |
permanentpen |
formatPainter |
|
math |
math |
newdocument, copy, cut, paste, selectall, openlink and other unlisted buttons go into unsupported; the first five are menu bar items instead. Group separators (|) are kept.
TinyMCE plugins
| TinyMCE | Better Rich Text |
|---|---|
lists, advlist, checklist, link, autolink, image, table, advtable, code, codesample, searchreplace, charmap, emoticons, nonbreaking, hr, casechange, formatpainter, quickbars, paste, powerpaste, directionality, wordcount, fullscreen, textpattern |
In the standard preset |
media, pageembed |
mediaPlugin, mediaFeature from @better-rich-text/plugin-media |
exportword, importword, exportpdf, print, fullpage, fullpagehtml, inlinecss |
documentsPlugin, documentsFeature from @better-rich-text/plugin-documents |
preview, help, contextmenu |
previewFeature, helpFeature, contextMenuFeature from @better-rich-text/ui |
visualblocks, visualchars |
visualAidsFeature() from @better-rich-text/ui |
anchor |
anchorPlugin, anchorFeature from @better-rich-text/plugin-anchor |
pagebreak |
pageBreakPlugin, pageBreakFeature from @better-rich-text/plugin-page-break |
template, advtemplate |
createTemplatesPlugin, templatesFeature from @better-rich-text/plugin-templates |
insertdatetime |
dateTimePlugin, dateTimeFeature from @better-rich-text/plugin-datetime |
importcss |
importCssFormats, createStylesPlugin, stylesFeature from @better-rich-text/plugin-styles |
editimage, imagetools |
imageEditorPlugin, imageEditorFeature from @better-rich-text/plugin-image-editor |
a11ychecker |
createA11yCheckPlugin from @better-rich-text/plugin-a11y-check |
footnotes |
footnotesPlugin, footnotesFeature from @better-rich-text/plugin-footnotes |
mergetags |
mergeTagsPlugin, mergeTagsFeature from @better-rich-text/plugin-merge-tags |
accordion |
detailsPlugin, detailsFeature from @better-rich-text/plugin-details |
linkchecker |
createLinkCheckPlugin from @better-rich-text/plugin-link-check |
tableofcontents |
tocPlugin, tocFeature from @better-rich-text/plugin-toc |
mentions |
createMentionsPlugin, mentionsFeature from @better-rich-text/plugin-mentions |
permanentpen |
Sticky formatting (format.togglePen) in formatPainterPlugin |
autocorrect |
createAutocorrectPlugin from @better-rich-text/plugin-typography |
suggestededits |
suggestionsPlugin, suggestionsFeature from @better-rich-text/plugin-suggestions |
tinydrive |
createAssetsPlugin from @better-rich-text/plugin-assets (connect an AssetSource) |
math |
mathPlugin, mathFeature from @better-rich-text/plugin-math |
autosave, save |
createSavePlugin from @better-rich-text/plugin-save |
tinymcespellchecker |
proofingPlugin from @better-rich-text/plugin-proofing |
comments, tinycomments |
commentsPlugin, commentsFeature from @better-rich-text/plugin-comments |
revisionhistory |
createRevisionsPlugin from @better-rich-text/plugin-revisions |
ai |
createAiPlugin and aiFeature from @better-rich-text/plugin-ai (needs a model endpoint on your server) |
markdown |
parseMarkdown and serializeMarkdown from @better-rich-text/format-markdown |
Froala options
| Froala | Better Rich Text | Notes |
|---|---|---|
toolbarButtons |
toolbar |
Flat arrays (` |
toolbarButtonsMD, SM, XS |
None | The toolbar overflows into a menu on narrow screens. |
pluginsEnabled |
Plugins and features | See Froala plugins. |
height, heightMin, heightMax |
height, minHeight, maxHeight |
Pixels only. |
placeholderText |
placeholder |
|
toolbarInline |
mode: 'inline' |
Add toolbarContainer for a fixed toolbar, or rely on the selection toolbar. |
toolbarSticky, toolbarBottom |
None | Use CSS or toolbarContainer. |
iframe |
None | The editor does not use an iframe. |
iframeStyle |
css |
Scoped to .brt-content. |
iframeStyleFiles |
None | Scope the stylesheet rules to .brt-content. |
fullPage |
Documents plugin | Full-page HTML keeps <head> metadata through the documents plugin. |
language |
locale |
|
direction |
None | Direction is stored per paragraph; set dir on the container. |
charCounterMax |
config.limits.maxCharacters |
Adds limitsPlugin to requires. |
spellcheck, autofocus |
spellcheck, autofocus |
|
enter |
None | Enter always makes paragraphs; a warning is raised for ENTER_BR and ENTER_DIV. |
fontFamily |
config.textStyle.fontFamilies |
Keys are stacks, values are labels. |
fontSize with fontSizeUnit |
config.textStyle.fontSizes |
Pixel sizes become points (×0.75). |
lineHeights |
config.textStyle.lineHeights |
|
colorsText, colorsBackground |
config.textStyle.palette |
REMOVE entries are skipped. |
imageUploadURL, imageUploadParam, imageUploadParams, imageUploadMethod, imageMaxSize, imageAllowedTypes |
imageUpload |
Same request; the address is read from link. |
imageUploadToS3, fileUploadURL, videoUploadURL |
None | Write an UploadProvider; embed videos by URL with the media plugin. |
htmlAllowedTags |
contentPolicy.allowedTags |
|
htmlRemoveTags, pasteDeniedTags |
contentPolicy.deniedTags |
|
pasteDeniedAttrs |
contentPolicy.deniedAttributes |
|
htmlAllowedAttrs, htmlAllowedStyleProps, pasteAllowedStyleProps, htmlUntouched |
None | Markup is always parsed through the schema. |
pastePlain, wordPasteModal, wordPasteKeepFormatting |
None | Word cleanup is automatic. There is no plain-text paste mode. |
events |
onChange, onReady, editor.on(...) |
See Events. |
key, attribution |
None | Not needed. |
Froala toolbar
| Froala | Better Rich Text | Froala | Better Rich Text | |
|---|---|---|---|---|
bold, italic, underline |
same | formatOL, formatOLSimple |
orderedList |
|
strikeThrough |
strike |
formatUL |
bulletList |
|
subscript, superscript |
same | paragraphFormat |
blocks |
|
fontFamily, fontSize, lineHeight |
same | quote |
quote |
|
textColor |
textColor |
insertLink |
link |
|
backgroundColor |
highlight |
insertImage |
image |
|
clearFormatting |
clearFormat |
insertVideo, insertEmbed |
media |
|
alignLeft…alignJustify, align |
same | insertTable |
table |
|
outdent, indent |
same | insertHR |
rule |
|
emoticons, specialCharacters |
characters |
html |
source |
|
undo, redo, fullscreen |
same | print, getPDF |
exportPdf |
|
findReplaceButton |
search |
export_to_word |
exportWord |
|
codeSnippet |
codeBlock |
import_from_word |
importWord |
| inlineClass, inlineStyle, paragraphStyle | styles | | help | help | | insertFile, insertFiles, imageManager | assets | | insertAnchor | anchor | | pageBreak | pageBreak | | spellChecker | proofing | | trackChanges | suggest | | | |
selectAll, markdown, save and fontAwesome go into unsupported.
Froala plugins
| Froala | Better Rich Text |
|---|---|
align, colors, fontFamily, fontSize, lineHeight, link, url, lists, paragraphFormat, quote, table, image, codeView, codeSnippet, emoticons, specialCharacters, findReplace, wordPaste, fullscreen, help, quickInsert, charCounter, draggable, lineBreaker, entities |
In the standard preset |
video, embedly |
mediaPlugin, mediaFeature |
print, exportToWord, importFromWord |
documentsPlugin, documentsFeature |
save |
createSavePlugin |
spellChecker |
proofingPlugin |
markdown |
@better-rich-text/format-markdown |
filesManager, imageManager |
createAssetsPlugin (connect an AssetSource) |
imageTUI |
imageEditorPlugin, imageEditorFeature |
inlineClass, inlineStyle, paragraphStyle |
createStylesPlugin, stylesFeature |
trackChanges |
suggestionsPlugin, suggestionsFeature |
pageBreak |
pageBreakPlugin, pageBreakFeature |
file, fontAwesome, forms, filestack, trimVideo, editInPopup |
Not supported |
Events
| TinyMCE | Froala | Better Rich Text |
|---|---|---|
init |
initialized |
onReady option |
change, input |
contentChanged |
onChange option or editor.on('change', …) |
NodeChange, SelectionChange |
click, keyup |
editor.on('selectionChange', …) |
focus, blur |
focus, blur |
DOM focusin and focusout on instance.view.dom |
remove |
destroy |
editor.on('destroy', …) |
PastePreProcess |
paste.beforeCleanup |
clipboard.pasteHtml filter hook |
Content
HTML saved by either editor can carry editor-only markup. cleanEditorHtml removes it and leaves the content:
import { cleanEditorHtml } from '@better-rich-text/migrate';
const { html, source, changes } = cleanEditorHtml(storedHtml);
// source: 'tinymce' | 'froala' | 'unknown'; changes: what was removed or convertedIt is a string-to-string function with no DOM, so it can run in a batch job over stored records. HTML without vendor markup comes back unchanged.
| Source | Cleanup |
|---|---|
| TinyMCE | Removes selection bookmarks (data-mce-type="bookmark", mce_SELRES_*, mce_marker), data-mce-bogus elements and helper <br>s. Restores src, href and style from data-mce-src, data-mce-href and data-mce-style, then drops every data-mce-* attribute. Removes mce-* classes and unwraps mce-nbsp-wrap spans. Turns <!-- pagebreak --> and img.mce-pagebreak into <hr>, or into page breaks when plugin-page-break is installed. Keeps tox-checklist lists, which load as task lists. |
| Froala | Removes fr-marker spans and the "Powered by Froala" paragraph. Restores fr-original-style and fr-original-class. Converts image classes fr-dib and fr-fil/fr-fir to centred, left and right alignment. Replaces fr-video embeds with a link to the video. Turns fr-alternate-rows into banded rows. Unwraps fr-view and fr-element wrappers and emoticon spans. Removes the remaining fr-* classes and reports visual-only ones (rounded, bordered, shadow, dashed borders). |
| Both | Removes contenteditable attributes, zero-width marker characters, scripts and empty or attribute-less spans. |
To clean content as it is loaded and pasted, add the plugin. It hooks the parser: html.parse.input cleans string input, and html.parse.element handles HTML the browser has already parsed (as setHTML and paste do). After a parse that changed something it fires the migrate.cleaned action.
import { migrationPlugin } from '@better-rich-text/migrate';
const editor = createRichTextEditor('#body', { plugins: [migrationPlugin()], content: storedHtml });
editor.editor.hooks.addAction('migrate.cleaned', ({ source, changes }) => console.info(source, changes));contentPolicyPlugin({ allowedTags, deniedTags, deniedAttributes }) applies element rules before the schema parser. Denied tags are removed with their content; tags outside the allow list are flattened so their text stays.
Uploads
The translators wrap existing upload endpoints so the server does not change:
import { createImagePlugin } from '@better-rich-text/plugin-image';
import { tinyMceHandlerProvider, urlUploadProvider } from '@better-rich-text/migrate';
// TinyMCE images_upload_handler, unchanged.
const fromHandler = createImagePlugin({ provider: tinyMceHandlerProvider(images_upload_handler) });
// A Froala endpoint that answers { link }.
const fromUrl = createImagePlugin({
provider: urlUploadProvider({ url: '/upload_image', field: 'file', params: { folder: 'posts' }, responseKey: 'link' }),
});Replace the image plugin in your preset with the new one (the generated code shows how).
Facade
createTinyMceShim gives a tinymce-shaped object for an incremental switch:
import { createRichTextEditor } from '@better-rich-text/core';
import { createTinyMceShim } from '@better-rich-text/migrate';
const tinymce = createTinyMceShim({ create: createRichTextEditor });
const [editor] = await tinymce.init({ selector: '#body', plugins: 'lists link', toolbar: 'bold italic | bullist' });
editor.setContent('<p>Hello</p>');
editor.execCommand('Bold');
editor.on('change', () => save(editor.getContent()));| Facade | Behaviour |
|---|---|
tinymce.init(config) |
Translates the config and creates one editor per matching element. Runs setup and init_instance_callback, then fires init. |
tinymce.get(), get(id), get(index), activeEditor, remove() |
As in TinyMCE. |
getContent(), getContent({ format: 'text' }), setContent, insertContent |
HTML in and out through the schema. |
execCommand |
Bold, Italic, Underline, Strikethrough, Superscript, Subscript, InsertUnorderedList, InsertOrderedList, JustifyLeft/Center/Right/Full, Indent, Outdent, Undo, Redo, RemoveFormat, Unlink, CreateLink, ForeColor, HiliteColor, FormatBlock, InsertHorizontalRule, mceBlockQuote, mceInsertContent, mceSetContent, SelectAll. Unknown commands return false. |
on, off, fire |
change and dirty come from the editor; focus, blur, input, key, mouse and clipboard events come from the editable element. |
isDirty, setDirty, save |
save writes the HTML back to a textarea target. |
mode.set('readonly' | 'design'), getBody, getElement, focus, remove |
As in TinyMCE. |
The facade covers common integration code. TinyMCE plugins, UI registry calls (editor.ui.registry), editor.dom, editor.selection and editor.formatter are not emulated; use facade.instance to reach the Better Rich Text editor.
Limitations
Custom formats (
style_formats,paragraphStyle,inlineClass) are reported for you to pass tocreateStylesPlugin; they are not applied automatically. Custom toolbar buttons and menu items are not converted. Rewrite them as toolbar items and custom plugins as Better Rich Text plugins.Option values given as functions are only used for upload handlers,
setupandinit_instance_callback(the last two by the facade).Allowed-element rules can only narrow what the installed plugins accept; they cannot add new elements.
In the browser parse path, Froala video embeds are dropped rather than turned into links. Run
cleanEditorHtmlon stored content first to keep the links.Comments, revisions and suggestions stored inside a vendor service are not recovered from HTML. Export them from the service and import them as records.