react-mobiledoc-editor
A toolkit for Mobiledoc editors written using React and Mobiledoc Kit.
react-mobiledoc-editor
supports the creation of
Mobiledoc Cards
as React components. For existing React projects, this makes it possible to
build React components once and share them between Mobiledoc and other contexts.
Installation
npm install react-mobiledoc-editor
Please note: MobiledocKit and React are specified as peer dependencies,
and will not be automatically installed. If you haven't already, please
add mobiledoc-kit
, react
, and react-dom
to your package.json
.
Usage
This package contains a number of React components suitable for building your own editor UI.
The most basic usage with standard toolbar and empty editor is:
<Container>
<Toolbar />
<Editor />
</Container>
Read on for how to provide more typical configurations to each component.
<Container>
This is the top-level component, which must be present and wrap the rest of your editor UI. It accepts the configuration for your mobiledoc editor instance and is responsible for establishing the React context which enables the other editor components to work together.
Please note that by itself, Container
only renders an empty root-level
component. At a minimum, you'll need to include an Editor
component inside
it. In addition to the Mobiledoc-specific properties listed below, any known
React props (like className
) will be passed to the root-level component.
The Container
component accepts these Mobiledoc-specific props:
mobiledoc
: A Mobiledoc to be edited.cards
: An array of available cards for use by the editor. Jump to the section on Card-based components for more detail on how to create cards as React components.atoms
: An array of available atoms for use by the editor.spellcheck
: Boolean.autofocus
: Boolean.placeholder
: A string to use as the placeholder text when the mobiledoc is blank.options
: A hash of additional options that will be passed through to the Mobiledoc editor constructor.serializeVersion
: A string representing the mobiledoc version to serialize to when firing theonChange
action. Defaults to0.3.1
.onChange
: A callback that will fire whenever the underlying document changes. Use this to persist and/or serialize your mobiledoc to another format as it's being edited. Will be called with the serialized mobiledoc.willCreateEditor
: A callback that fires when the Mobiledoc editor instance is about to be created. Takes no arguments.didCreateEditor
: A callback that fires once the Mobiledoc editor instance has been created. Will be called with the editor instance and may be used to configure it further.
<Editor>
The Editor
component is the actual editor interface. In its most basic form
it renders an empty editor with no toolbar. It accepts no Mobiledoc-specific
props, but will respect any known React props like className
or onDrop
.
(See the How To
page in the wiki for more information on drag & drop.)
<Toolbar>
Creates a toolbar with a basic set of editing controls. While this may be suitable for very limited implementations, the expectation is that most people will prefer to customize the toolbar and this component is primarily presented as a reference implementation. Please see the How To page in the wiki.
<AttributeSelect>
Accepts an attribute name and an array of possible attribute values, in
addition to any known React props such as className
. When changed, sets the
attribute on the section under the editor cursor to the selected value.
If the attribute under the editor cursor matches one of the supplied
attribute values, the <select>
component's value will be set to match. If
multiple sections with different attribute values are selected, the component
shows an indeterminate state.
<AttributeSelect attribute="text-align" values={["left", "center", "right"]} />
By default, the first value in the values
array is considered the "default"
value. Selecting this value will remove the specified attribute, rather than
setting its value. A custom "default" attribute value can be specified with
the defaultValue
prop:
<AttributeSelect attribute="text-align" values={["left", "center", "right"]} defaultValue="right" />
(Does not support customization of the child <option>
elements; primarily
meant as a sample implementation.)
<SectionButton>
Creates a button that, when clicked, toggles the supplied tag on the section under the editor cursor.
Takes one required property: tag
, the name of the section tag. Accepts any
known React props, like className
or title
. The returned <button>
component will have a class of active
when the corresponding tag is active
under the editor cursor. The active class name can be changed by setting the
activeClassName
prop.
<SectionButton tag="h2" />
Alternately, custom child node(s) may be yielded to render something other than the tag name within the button:
<SectionButton tag="ul">
List
</SectionButton>
<SectionSelect>
An alternative to <SectionButton>
. Accepts an array of valid MobileDoc
section-level tags (p
, h1
, h2
, h3
, h4
, h5
, h6
, blockquote
,
aside
) in addition to any known React props such as className
. If the
section under the editor cursor matches one of the supplied tags, the
<select>
component's value will be set to match. When changed, toggles
the selected tag on the section under the editor cursor.
<SectionSelect tags={["h1", "h2", "h3"]} />
(Does not support customization of the child <option>
elements; primarily
meant as a sample implementation.)
<MarkupButton>
Creates a button that, when clicked, toggles the supplied tag on the selected range in the editor.
Takes one required property: tag
, the name of the markup tag. Accepts any
known React props, like className
or title
. The returned <button>
component will have a class of active
when the corresponding tag is active
under the editor cursor. The active class name can be changed by setting the
activeClassName
prop.
<MarkupButton tag="em" />
Alternately, custom child node(s) may be yielded to render something other than the tag name within the button:
<MarkupButton tag="strong">
Bold
</MarkupButton>
<LinkButton>
Creates a button that, when clicked, toggles the presence of a link on the selected range in the editor. User will be prompted for a URL if necessary.
Accepts any known React props, like className
or title
. The returned
<button>
component will have a class of active
when an anchor tag is
active under the editor cursor.
<LinkButton />
Alternately, custom child node(s) may be yielded to render something other than the default label on the button:
<LinkButton>
<span className="icon icon-link" />
</LinkButton>
If you need to customize the link dialogue or use something other than
window.prompt
, you may supply your own handler. This is a function that
should take three arguments:
message
: This is the default text prompt ("Enter a URL".)defaultUrl
: If the currently selected text appears to be a URL, it will be passed in this parameter. Useful for auto-linking.promptCallback
: Once you've processed any user input (or thedefaultUrl
param) you must pass the final URL to this callback in order to actually link the selected text.
function myPrompt(message, defaultURL, promptCallback) {
let url = window.prompt(message, defaultURL);
if (url.indexOf('file://') > -1) {
console.warn('Unable to create local link.');
} else {
promptCallback(url);
}
}
<LinkButton handler={myPrompt} />
Component-based Cards
Mobiledoc supports "cards", blocks of rich content that are embedded in a post. For specifics of the underlying card API, please see the Mobiledoc Card documentation.
react-mobiledoc-editor
comes with a helper for using your own React
components as the display and edit modes of a card.
To wrap your own component in the Card interface, simply call classToDOMCard
on it:
import { Component } from 'react';
import { classToDOMCard } from 'react-mobiledoc-editor';
class MyComponent extends Component {
static displayName = 'MyComponent'
render() {
let { isInEditor } = this.props;
let text = isInEditor ? "This is the editable interface"
: "This is the display version";
return <p>{text}</p>;
}
}
const MyComponentCard = classToDOMCard(MyComponent);
Please note that your component MUST implement displayName
. This is so the
editor and other mobiledoc consumers can identify your custom cards.
Once your components have been wrapped in the card interface, they can be
passed to a <Container>
component via the cards
prop, like any other card.
Card-based components will be instantiated with the following mobiledoc-specific props:
payload
: The payload for this card. Please note the payload object is disconnected from the card's representation in the serialized mobiledoc; to update the payload as it exists in the mobiledoc, use thesave
callback.edit
: A callback for toggling this card into edit mode (no-op if the card is already in edit mode).save
: A callback which accepts a new payload for the card, then saves that payload to the underlying mobiledoc and toggles the card back into display mode. Can optionally be passedfalse
as an extra argument to avoid toggling to display mode.cancel
: A callback for toggling this card to display mode without saving (a no-op if the card is already in display mode).remove
: A callback for removing this card entirely.name
: The name of this card.postModel
: A reference to this card's model in the editor's abstract tree. This may be necessary to do programmatic editing.isInEditor
: A bool indicating if the card is displayed inside an editor interface or not.isEditing
: A bool indicating if the card is in Edit mode or not.
Component-based Atoms
As stated in the Mobiledoc Atom Documentation, "Atoms are effectively read-only inline cards." They are sections of rich content that only spans the space of a word or a sentence within a paragraph. The common example is an @
mention within a block of text.
react-mobiledoc-editor
comes with a helper for using your own React
components as the display and update the content of an Atom.
To wrap your own component in the Atom interface, simply call classToDOMAtom
on it. This example illustrates an Atom component which renders a button and saves the click count to the underlying mobiledoc:
import { Component } from 'react';
import { classToDOMAtom } from 'react-mobiledoc-editor';
class MyComponent extends React.Component<Props> {
static displayName = 'MyComponent';
render() {
let { value } = this.props;
return (
@{value}
);
}
}
const MyComponentAtom = classToDOMAtom(MyComponent);
As with Cards, note that your component MUST implement displayName
. This is so the
editor and other mobiledoc consumers can identify your custom atoms.
Once your components have been wrapped in the atom interface, they should be
passed to the Mobiledoc <Container>
component via the atoms
prop.
Atom-based components will be instantiated with the following mobiledoc-specific props:
value
: The textual representation to for this atom.payload
: The payload for this atom. Please note the payload object is disconnected from the atom's representation in the serialized mobiledoc; to update the payload as it exists in the mobiledoc, use thesave
callback.save
: A callback which accepts a new payload for the card, then saves that value and payload to the underlying mobiledoc.name
: The name of this card.onTeardown
: A callback that can be called when the rendered content is torn down.
React 18 Support
To use custom card & atom components with React 18 without warnings, you can pass an instance of createRoot
from react-dom v18 as a prop on the Container
. Internally, components will render with createRoot
if available and fallback to the legacy render
:
import { createRoot } from 'react-dom/client';
<Container createRoot={createRoot}>...</Container>;
Development
Testing
Run tests with npm test
, or npm run test:watch
to start Karma in continuous
watch mode. The test script will automatically apply linting according to our
house style, but the linter can be run independently with npm run lint
.
Running the Demo
A small demo of basic usage and simple card and atom integration is available under the
/demo
directory. To start the demo server, run npm start
from the project
root.