-
Notifications
You must be signed in to change notification settings - Fork 15
Home
This wiki goes over some of the concepts in the Rich Text Editor Toolkit.
Rich Text Editor Toolkit is an Elm package that provides a set of tools for building rich text editors with contenteditable.
The editor's model is not straight HTML, but a custom data structure that contains elements that you define. All updates go through a validation and reduction process to make sure that it's always as you expect.
This package is not a drop in rich text editor. It's a package you use to write your own rich text editors. It is inspired by frameworks like Trix, DraftJS, and most of all, ProseMirror. If you're familiar with that library, then some of these concepts should look familiar.
The main module of the package is RichText.Editor, which has the model, update, and view methods for an editor.
Here's a code snippet that shows the main entry points for the editor. Modules used in the model can be found in in RichText.Model. Modules used in the config can be found in RichText.Config.
-- Imports the main methods required by the editor
import RichText.Editor exposing (config, init, update, view, Config, Editor)
import RichText.Commands exposing (defaultCommandMap)
import RichText.Config.Decorations exposing (emptyDecorations)
import RichText.Definitions exposing (markdown)
import RichText.Model.Element as Element
import RichText.Model.Node
exposing
( Block
, Children(..)
, Inline(..)
, block
, blockChildren
, inlineChildren
, plainText
)
...
type MyMsg
= InternalMsg Message | ...
myConfig : Config
myConfig =
config
{ decorations = emptyDecorations
, commandMap = defaultCommandMap
, spec = markdown
, toMsg = InternalMsg
}
docNode : Block
docNode =
block
(Element.element doc [])
(blockChildren <|
Array.fromList
[ block
(Element.element paragraph [])
(inlineChildren <| Array.fromList [ plainText "Hello world" ])
]
)
myModel : Model
myModel = { editor = init <| State.state docNode Nothing }
myView : Editor -> Html Msg
myView model = Editor.view myConfig model.editor
myUpdate : Msg -> Model -> ( Model, Cmd Msg )
myUpdate msg model =
case msg of
InternalMsg internalEditorMsg ->
( { model | editor = update myConfig internalEditorMsg model.editor }, Cmd.none )For a real example, see the demo editor.
Rich Text Editor Toolkit has its own data structure and types to represent content in your editor.
An editor consists of two different groups of nodes, Block and Inline. They can be found in the RichText.Model.Node module. Block nodes represent heirarchical content like tables, nested lists, and block quotes. Inline nodes represent flat contents with metadata for styles attached in the form of marks.
Block nodes can be split into three different types: "block nodes", which are Block nodes with Block children like blockquotes, lists, and tables, "text block nodes", which are Block nodes with Inline children like paragraphs and headers, and "block leaves" which are blocks with no children like horizontal rules or charts/figures.
There are two types of Inline nodes: inline elements and text. An inline element is a thing like an inline image or line break. A text node is a piece of editable text. All inline nodes can have marks attached, which is metadata that adds markup to a node.
Here's a diagram of what the editor state looks like of a sample document with block, textblock, and text nodes with marks. It also shows what the rendered document looks like:

Elements and Marks in your document are defined by ElementDefintion and MarkDefinition instances that live in a Spec. These types can be found in RichText.Config.ElementDefintion, RichText.Config.MarkDefinition, and RichText.Config.Spec.
An ElementDefintion defines how a block or inline element is encoded to html or decoded from html. It also defines what type of children it can have, and in the case of text blocks, what kinds of marks are allowed for its inline content.
Here is an example element definition for a heading:
{-| A heading element. It can have inline children. It supports one integer attribute `level`,
which defaults to 1 if not set.
-}
heading : ElementDefinition
heading =
elementDefinition
{ name = "heading"
, group = "block"
, contentType = textBlock { allowedGroups = [ "inline" ], allowedMarks = [] }
, toHtmlNode = headingToHtml
, fromHtmlNode = htmlToHeading
, selectable = False
}
headingToHtml : ElementToHtml
headingToHtml parameters children =
let
level =
Maybe.withDefault 1 <| findIntegerAttribute "level" (attributes parameters)
in
ElementNode ("h" ++ String.fromInt level) [] children
htmlToHeading : HtmlToElement
htmlToHeading def node =
case node of
ElementNode name _ children ->
let
maybeLevel =
case name of
"h1" ->
Just 1
"h2" ->
Just 2
"h3" ->
Just 3
"h4" ->
Just 4
"h5" ->
Just 5
"h6" ->
Just 6
_ ->
Nothing
in
case maybeLevel of
Nothing ->
Nothing
Just level ->
Just <|
( element def
[ IntegerAttribute "level" level ]
, children
)
_ ->
Nothing
A MarkDefinition defines how a mark is encoded to html when rendered and decoded from html for things like copy/paste. Here is an example definition for a bold mark:
import RichText.Config.MarkDefinition
exposing
( HtmlToMark
, MarkDefinition
, MarkToHtml
, defaultHtmlToMark
, markDefinition
)
bold : MarkDefinition
bold =
markDefinition { name = "bold", toHtmlNode = boldToHtmlNode, fromHtmlNode = htmlNodeToBold }
boldToHtmlNode : MarkToHtml
boldToHtmlNode _ children =
ElementNode "b" [] children
htmlNodeToBold : HtmlToMark
htmlNodeToBold =
defaultHtmlToMark "b"A Spec contains all the mark and element definitions that are allowed to be in your document. It is used to encode and decode a document to html, as well as validate editor state. You can see an example of a markdown spec with several element and mark definitions in the RichText.Definitions module.
An editor's model is defined fully by RichText.Editor.Editor type, but the object that defines its current state is RichText.Model.State.State. A state consists of a root Block and a Maybe Selection. An editor's state is initialized by RichText.Editor.init, and modified by commands applied via functions like RichText.Editor.apply, or by commands triggered by key down or input events defined in a CommandMap.
As mentioned previous, the core way an editor's state changes is from a Command. With the exception of internal actions (specifically Undo and Redo), commands are defined by transforms, which are simply functions with the following signature:
-- A transform takes a state and return either a state or an error why the transform couldn't be done
type alias Transform =
State -> Result String StateHere is an example transform that clears the selection from a state
removeSelection : Transform
removeSelection state =
Ok (state |> withSelection Nothing)Transforms are turned into commands with the transform function found in RichText.Config.Command. Commands, or more speficially a (String, Command) tuple called a NamedCommand, can be applied directly with the apply and applyList functions. apply calls the command and if it receives a new state, validates that state against the editor's spec. If the new state is valid, it is reduced (see the RichText.State module for details about the reduction rules), the history is updated, and the editor's state changes to the new state. Otherwise, an error is returned. applyList tries each command in the list until one is successful and valid, then reduces, updates the history, and changes the state. Like apply, if no command is successful or valid, an error is returned.
You can also have commands triggered by key down events and before input events. These are defined by a CommandMap, which is also in RichText.Config.Command. A CommandMap maps a set of keys on a key down event or a input event type triggered on before input to a list of named commands that get applied via applyList. They also define default keyboard and input event functions where you can create your own more customized keydown or input event logic. Note that if a command is successful, preventDefault is called for that event to prevent whatever the default behavior was going to be. Actually, because of browser inconsistency, most of the default contenteditable behavior is reverted by the editor with the exception text node changes. So it's important that you define the behavior you want with your own commands.
Here is an example of an insert line break command being added to an empty command map.
emptyCommandMap
|> set
[ inputEvent "insertLineBreak", key [ shift, enter ], key [ shift, return ] ]
[ ( "insertLineBreak", transform insertLineBreak ) ]In addition to elements and marks being defined by a spec, you can also add arbitrary Html.Attributes to an element or mark with Decorations, which can be found in RichText.Config.Decorations. Decoration functions are useful for adding event listeners and attributes that would otherwise be superfluous in an element or mark definition. A very useful decoration is the predefined selectableDecoration in RichText.Config.Decorations, which will add a class to an element that has the selection annotation, as well as adds an onclick event listener that will select that element if it is clicked. Here is an example of a Decorations type being initialized with some selection decorations:
decorations : Decorations
decorations =
emptyDecorations
|> addElementDecoration image (selectableDecoration InternalMsg)
|> addElementDecoration horizontalRule (selectableDecoration InternalMsg)