Home

Awesome

react-mobiledoc-editor

Build status

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:

<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:

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:

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:

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.