0.2.3 • Published 4 years ago

@dewen_li/richtext v0.2.3

Weekly downloads
-
License
Apache-2.0
Repository
github
Last release
4 years ago

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

  1. Exports

  2. Plugin Factories

  3. APIs

  1. Guides

  2. Roadmap

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:

NametypeDefault valueDescription
initialValueobject{document: {...}}Initial value of the editor that will be used on editor mount.
styleobject{}Inline styles to be applied to the wrapping <div />
classNamestring""Class name to be applied to the wrapping <div />
pluginsPlugin[][]An array of slate editor native plugins to be applied to the editor instance on initialization.
valueValueGenerated 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.
onChangeonChange(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.
childrenanyrequiredComponents 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

NametypeDefault valueDescription
wrapperStyleobject{}Inline styles to be applied to the wrapping <div />
classNamestring""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:

NameTypeDescription
editorContentRead official reference. Initial value of the editor that will be used on editor mount.
valueValueRead 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 require href, 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:

NameTypeDescription
dataobjectCurrent slate node data
setDatasetData(newData: object): voidSets the data for current slate node
unwrapunwrap(): voidRemove current slate node from the editor

Example:

function InlineComponent({data, setData, unwrap}) {
  return <span>...</span>
}

NodeForm Properties:

NameTypeDescription
dataobjectCurrent slate node data
setDatasetData(newData: object): voidSets the data for current slate node
closeFormcloseForm(): voidUpon calling closes the form
unwrapunwrap(): voidRemove current slate node from the editor

Example:

function FormComponent({data, setData, closeForm, unwrap}) {
  return <form>...</form>
}

See Using Form API Guide

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,
};

Roadmap

- Create block factory - Write Unit Tests