Reactivepad
IntroductionPluginsDraftJSProseMirrorCKEditor4

Plugins for rich text editors

DraftJS

https://draftjs.org

Demo

New FormulaNew TableH1H2H3H4H5H6ULOLCode Block
BoldItalicUnderlineMonospace

Installation

npm i @reactivepad/draftjs
# or
yarn add @reactivepad/draftjs
import React, { Component, Fragment } from "react";
import {
convertFromRaw,
CompositeDecorator,
Editor,
EditorState,
Modifier,
AtomicBlockUtils
} from "draft-js";
import { sample } from "./samples";
import {
buildPlugin,
ENTITY_TYPES,
syncStoreWithDoc,
formulaDecoratorStrategy
} from "@reactivepad/draftjs";
class DraftEditor extends Component {
constructor(props) {
super(props);
this.reactivepadPlugin = buildPlugin({
toggleReadOnly: this.toggleReadOnly
});
const {
components: { Formula }
} = this.reactivepadPlugin;
const decorator = new CompositeDecorator([
{
strategy: formulaDecoratorStrategy,
component: props => (
<Formula {...props} updateContentState={this.updateContentState} />
)
}
]);
const blocks = convertFromRaw(sample1);
this.state = {
editorState: EditorState.createWithContent(blocks, decorator),
readOnly: false
};
}
componentWillUnmount() {
clearTimeout(this.formulaTimeoutId);
this.reactivepadPlugin.destroy();
}
toggleReadOnly = (readOnly = !this.state.readOnly) => {
this.setState({ readOnly });
};
onChange = (editorState, skipSyncWithStore) => {
const prevContentState = this.state.editorState.getCurrentContent();
this.setState({ editorState }, () => {
if (
!skipSyncWithStore &&
prevContentState !== this.state.editorState.getCurrentContent()
// don't sync when only selection has changed
) {
syncStoreWithDoc(this.reactivepadPlugin.store, this.state.editorState);
}
});
};
updateContentState = (
contentState,
skipSyncWithStore, // sometimes sync is unnecessary
editorState = this.state.editorState
) => {
const newEditorState = EditorState.push(
editorState,
contentState,
"insert-characters"
);
this.onChange(newEditorState, skipSyncWithStore);
};
insertFormula = e => {
e.preventDefault();
const { editorState } = this.state;
const formula = this.reactivepadPlugin.store.createAndAddFormula();
const whitespace = " ";
let contentState = editorState.getCurrentContent();
contentState = Modifier.replaceText(
contentState,
editorState.getSelection(),
whitespace
);
const contentStateWithEntity = contentState.createEntity(
ENTITY_TYPES.FORMULA,
"IMMUTABLE",
formula.serialize()
);
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
const selectionState = editorState.getSelection();
const contentStateWithFormula = Modifier.applyEntity(
contentStateWithEntity,
selectionState.merge({
focusOffset: selectionState.getAnchorOffset() + whitespace.length
}),
entityKey
);
this.updateContentState(contentStateWithFormula, true);
this.formulaTimeoutId = setTimeout(() => {
formula.toggleEditing(true);
}, 5);
};
insertTable = e => {
e.preventDefault();
const { editorState } = this.state;
const table = this.reactivepadPlugin.store.createAndAddTable();
const whitespace = " ";
const contentStateWithEntity = editorState
.getCurrentContent()
.createEntity(ENTITY_TYPES.TABLE, "IMMUTABLE", table.serialize());
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
let newEditorState = EditorState.set(editorState, {
currentContent: contentStateWithEntity
});
newEditorState = AtomicBlockUtils.insertAtomicBlock(
newEditorState,
entityKey,
whitespace
);
this.onChange(newEditorState);
};
blockRenderer = block => {
const entityKey = block.getEntityAt(0);
if (!entityKey) {
return null;
}
const entity = this.state.editorState
.getCurrentContent()
.getEntity(entityKey);
const type = entity.getType();
if (type !== ENTITY_TYPES.TABLE) {
return null;
}
const { Table } = this.reactivepadPlugin.components;
return {
component: props => (
<Table {...props} updateContentState={this.updateContentState} />
),
editable: false
};
};
render() {
const { editorState, readOnly } = this.state;
return (
<Fragment>
<button onClick={this.insertFormula}>Formula</button>
<button onClick={this.insertTable}>Table</button>
<div style={{ minHeight: "6rem" }}>
<Editor
editorState={editorState}
onChange={this.onChange}
blockRendererFn={this.blockRenderer}
readOnly={readOnly}
/>
</div>
</Fragment>
);
}
}

ProseMirror

https://prosemirror.net

Demo

loading...

Installation

npm i @reactivepad/prosemirror
# or
yarn add @reactivepad/prosemirror

Below is slightly a modified version of a basic ProseMirror sample setup from official documentation https://prosemirror.net/examples/basic.

const {
buildPlugin,
nodes,
menuItems,
buildMenuDropdown
} = "@reactivepad/prosemirror";
// create a plugin
const reactivepadPlugin = buildPlugin();
// extend default editor schema with nodes from Reactivepad
const editorSchema = new Schema({
nodes: addListNodes(schema.spec.nodes, "paragraph block*", "block").append(
nodes
),
marks: schema.spec.marks
});
// add Reactivepad's dropdown to menubar
const menu = buildMenuItems(editorSchema);
menu.fullMenu.unshift([buildMenuDropdown(menuItems)]);
// create ProseMirror editor instance with Reactivepad plugin
new EditorView(this.editorRef, {
state: EditorState.create({
schema: editorSchema,
plugins: [
...exampleSetup({
schema: editorSchema,
menuContent: menu.fullMenu
}),
reactivepadPlugin.plugin
]
})
});
}

CKEditor4

https://ckeditor.com/ckeditor-4

Demo

Installation

  • Extract the contents of this .zip file to <CKEditor folder>/plugins/reactivepad. Download link: https://github.com/reactivepad/reactivepad-ckeditor4/archive/master.zip
  • Add reactivepad to the list of extra plugins:
CKEDITOR.replace("editorId", {
extraPlugins: "reactivepad"
});

This plugin requires "widget" and "dialog" plugins, please make sure to have them installed.

For more information on CKEditor plugins installation, please refer to the official manual: https://ckeditor.com/docs/ckeditor4/latest/guide/dev_plugins.html#manual-installation