@dewen_li/richtext v0.2.3
Rich Text Editor
Note this document describes the low-level API of Bodiless's rich text editing support. A new high level API is under active development.
Bodiless RichText provides a Rich Text Component that allows content uses to edit and manipulate text. This package also includes a set of tools for elegant scaffolding and extending Slate editor.
Before using this module it is essential to understand how Slate editor works: Read Slate Walkthroughs and Guides sections.
Contents
Exports
Plugin Factories
APIs
Guides
- How to create mark plugin
- How to creat inline plugin.
- TBD Creating a block plugin
Exports
Slate Editor Component - <SlateEditor />
<SlateEditor>
- Main Content controller component that provides react context with editor related data and callbacks to nested components. Expects <SlateEditor>
as one of its direct children to obtain editor reference.
Basic usage:
import { SlateEditor, Content } from '@bodiless/richtext/core';
function MyEditor() {
return (<SlateEditor>
<Content />
</SlateEditor>);
}
Properties:
Name | type | Default value | Description |
---|---|---|---|
initialValue | object | {document: {...}} | Initial value of the editor that will be used on editor mount. |
style | object | {} | Inline styles to be applied to the wrapping <div /> |
className | string | "" | Class name to be applied to the wrapping <div /> |
plugins | Plugin[] | [] | An array of slate editor native plugins to be applied to the editor instance on initialization. |
value | Value | Generated from Value.fromJSON( initialValue ) | Read official reference. A Value object representing the current value of the editor. Prop value takes priority over initialValue prop and internal value state. |
onChange | onChange(editor) | (editor) => {} | Read official reference. A change handler that will be called with the change that applied the change. This hook allows you to add persistence logic to your editor. |
children | any | required | Components listed as children will have access to the Content Context. <SlateEditor> requires <Content> to be an immediate children. |
Content Component - <Content />
<Content>
- Wrapper around <Content />
which is the main editorial area. <Content>
wraps Content with a <div />
and supplies values from Content Context to <Content />
.
Basic usage:
import { SlateEditor, Content } from '@bodiless/richtext/core';
function MyEditor() {
return (<SlateEditor>
<Content
className='editor'
wrapperStyle={{ opacity: '0.5'}}
spellCheck
placeholder='Type here...'
/>
</SlateEditor>);
}
Properties:
See original reference for <Content />
props
Name | type | Default value | Description |
---|---|---|---|
wrapperStyle | object | {} | Inline styles to be applied to the wrapping <div /> |
className | string | "" | Class name to be applied to the wrapping <div /> |
SlateContext Component
SlateContext
- an object with editor related data to be used in nested components.
Basic usage:
import React, { useContext } from 'react';
import { SlateEditor, Content, SlateContext } from '@bodiless/richtext/core';
function Toolbar() {
const {value, editor } = useContext(SlateContext);
// obtain value properties to manipulate Toolbar look...
const { fragment, selection } = value;
if (selection.isBlurred || selection.isCollapsed || fragment.text === '') {
console.log('No text is selected');
}
// perform updates using editor...
function moveFocusForward(chars) {
editor.command((editor) => {
editor.moveFocusForward(chars);
})
}
return (<div>
<button onMouseDown={() => moveFocusForward(5)}>Move focus forward by 5</button>
</div>)
}
function MyEditor() {
return (<SlateEditor>
<Toolbar />
<Content />
</SlateEditor>);
}
Properties:
Name | Type | Description |
---|---|---|
editor | Content | Read official reference. Initial value of the editor that will be used on editor mount. |
value | Value | Read official reference. A Value object representing the current value of the editor. Prop value takes priority over initialValue prop and internal value state. |
Hover Menu Component - <HoverMenu />
<HoverMenu />
- a hover menu that appears on any selection within editor. <HoverMenu />
passes values of SlateContext
to all its children as props.
Basic usage:
import React from "react";
import { SlateEditor, Content, HoverMenu } from "@bodiless/richtext/core";
const Content = (props) => {
return (
<SlateEditor {...props} plugins={plugins} >
<HoverMenu>
<BoldButton />
<ItalicButton />
</HoverMenu>
<Content />
</SlateEditor>
);
};
export default Content;
RichText Component
The RichText Component is built on the SlateJS framework. It takes design object (see @bodiless/Design System) that contain HOC to build out the componet that are avaiable in the RichText Editor. Those are then presented to the user using both a contextual hover menu as well as the standard menu. These items can be used by using a set of HOC's.
starWith(Component)
lets us know which component should be part of the item
asMark
, asInline
and asBlock
are used to say how the slate editor should use the component.
marks
are used for basic character-level formatting (eg bold, italic, underline, text=color, etc).inlines
may also be used for character formatting, but should generally be reserved for cases where the component requires additional configuration besides the text (for example, a link, which may requirehref
,target
or other attributes).blocks
should be used for block-level formatting (eg headers, lists, etc).
withKey("k")
can be used to add a shortcut key to the component.
withButton("icon")
can be used to add a button that will set the text to a component. If the item is asBlock then it will be added to the global menu if not then it will be added to the local hover menu.
There are a set of keys that have defaults that are often used they are the following:
- SuperScript
- Bold
- Italic
- Underline
- Link
- AlignLeft
- AlignRight
- AlignCenter
- AlignJustify
- H1
- H2
- H3
With these one only need to include the key.
Each of this helper return a function that we pass in as items. we can use flow to combine them as such:
const design = {
Bold: asBold,
Link: asLink,
Strikethrough: flow(
startWith(Span),
withButton('format_strikethrough'),
withKey('s'),
asMark,
),
};
const EditorFullFeatured = <P extends object> (props:P) => (
<RichText design={items2} initialValue={demoValue} {...props} />
);
Plugin Factories
In order to minify boilerplate creating a slate plugin @bodiless/richtext
provides factories. Plugin types are mimicking Slate data model. There are 3 types of plugins that can be created:
- Mark plugin - renders provided component wrapping a piece of text. Doesn't have any data and component is togglable.
- Inline plugin - acts as a mark, but has data and a way to control it. You can provide a Form component that implements Form API along with the component, and plugin with handle the data updates and rendering of both Component and Form.
- Block plugin - TBD
Mark Plugin Factory
Mark plugin factory reduces boilerplate required to create a plugin to render custom marks in Slate editor. Also, you can generate a button that is going to trigger the mark on and off.
Exports:
createMarkButton(markType: string, materialIcon: string): React.ComponentType
- markType: string - a unique string to identify mark component. Should match markType value of the corresponding mark plugin.
- materialIcon: string - a string that is converted into a Material Icon glyph
createMarkPlugin(MarkComponent: Mark, markType: string, options: MarkPluginOptions): Hooks
- MarkComponent: MarkComponent - mark component that is going to be rendered to represent mark occurrences. Should match markType value of the corresponding mark button.
- markType: string - a unique string to identify mark component. Should match markType value of the corresponding mark button.
- options: MarkPluginOptions - options object to provide additional plugin configuration. Acceptable options:
- keyboardKey: string - a keyboard key to trigger the mark. Make sure the provided key is not used in any other plugins.
Inline Plugin Factory
Inline plugin factory reduces boilerplate required to create a plugin to render custom inline nodes in Slate editor. You can generate a button for an inline node like for marks. Also, in addition, you can provide a custom form for each inline node to manage its data.
Exports:
- createInlineButton(inlineType: string, materialIcon: string): React.ComponentType
- inlineType: string - a unique string to identify inline node component. Should match inlineType value of the corresponding inline plugin.
- materialIcon: string - a string that is converted into a Material Icon glyph
Block Plugin Factory
Block plugin factory reduces boilerplate required to create a plugin to render custom block nodes in Slate editor. You can generate a button for block node like for marks.
Exports:
- createBlockButton(inlineType: string, materialIcon: string): React.ComponentType
- inlineType: string - a unique string to identify inline node component. Should match inlineType value of the corresponding block plugin.
- materialIcon: string - a string that is converted into a Material Icon glyph
APIs
Node Component API
Inline plugin factory generates a wrapper around provided Component.
NodeComponent Properties:
Name | Type | Description |
---|---|---|
data | object | Current slate node data |
setData | setData(newData: object): void | Sets the data for current slate node |
unwrap | unwrap(): void | Remove current slate node from the editor |
Example:
function InlineComponent({data, setData, unwrap}) {
return <span>...</span>
}
NodeForm Properties:
Name | Type | Description |
---|---|---|
data | object | Current slate node data |
setData | setData(newData: object): void | Sets the data for current slate node |
closeForm | closeForm(): void | Upon calling closes the form |
unwrap | unwrap(): void | Remove current slate node from the editor |
Example:
function FormComponent({data, setData, closeForm, unwrap}) {
return <form>...</form>
}
Guides
Creating mark plugin
Mark is a simple wrapper for a specific piece of text in the editor. Marks are the simplest entities of Slate Content and can be only triggered on and off and stack with other marks.
Mark Plugin Factory usage:
import React from 'react';
import { createMarkButton, createMarkPlugin } from '@bodiless/richtext/plugin-factory';
function CustomMark({children}) {
return (<span className="custom-mark">{children}</span>);
}
const MARK_TYPE = 'custom_mark';
// trigger mark by pressing ctrl+'m' or alt+'m'
const KEYBOARD_KEY = 'm';
const CustomMarkPlugin = createMarkPlugin(CustomMark, MARK_TYPE, { keyboardKey: KEYBOARD_KEY });
const CustomMarkButton = createMarkButton(MARK_TYPE, 'format_underline');
export {
CustomMarkButton,
CustomMarkPlugin
};
Creating Inline Plugin
Inline nodes are more complex entities of the slate editor and can contain data. That's why inline can have an optional form and inline component has a way of updating itself data using setData()
prop. Inline Form API can be used to render edit form in a tooltip around the inline node, however, it's not mandatory.
Using Form API:
import React, { useState } from 'react';
import { createInlineButton, createInlinePlugin } from '@bodiless/richtext/plugin-factory';
function LinkForm({setData, closeForm, unwrap, data}) {
const [href, setHref] = useState(data.href);
return <form onSubmit={(newData) => {
setData(newData);
closeForm();
}}>
<input value={href} onChange={event => setHref(event.target.value)}/>
<button type="submit">Save</button>
<button type="button" onMouseDown={() => {
if (href) {
closeForm();
} else {
unwrap();
}
}}>Cancel</button>
<button type="button" onMouseDown={unwrap}>Remove</button>
</form>
}
function Link({data, children, setData, unwrap}) {
return <a {...data}>{children}</a>;
}
//
// External
//
const INLINE_TYPE = 'link';
const ImagePlugin = (options = {}) => {
return createInlinePlugin(Link, INLINE_TYPE, {Form: LinkForm});
};
const LinkButton = createInlineButton(INLINE_TYPE, 'link');
export {
ImagePlugin,
LinkButton,
};
Create Link Plugin using Inline Plugin Factory without Form API:
import React, { useState } from 'react';
import { createInlineButton } from '@bodiless/richtext/plugin-factory';
import Tooltip from 'rc-tooltip';
function Link({data, children, setData}) {
const [formActive, setFormActive] = useState(false);
return <Tooltip
visible={formActive}
onClick={() => setFormActive(true)}
overlay={() => {
const [href, setHref] = useState(data.href);
return <form onSubmit={(newData) => {
setData(newData);
setFormActive(false);
}}>
<input value={href} onChange={event => setHref(event.target.value)}/>
<button type="submit">Save</button>
<button type="button" onMouseDown={() => {
setFormActive(false);
}}>Cancel</button>
</form>
}}
>
<a {...data}>{children}</a>
</Tooltip>;
}
//
// External
//
const INLINE_TYPE = 'link';
const ImagePlugin = (options = {}) => {
return createInlinePlugin(Link, INLINE_TYPE, options);
};
const LinkButton = createInlineButton(INLINE_TYPE, 'link');
export {
ImagePlugin,
LinkButton,
};