Rich text editor based on Slate developed by The Concord Consortium. It is published to npm as @concord-consortium/slate-editor. Storybook examples can be viewed at https://slate-editor.concord.org.
CC's use of Slate began with the CLUE project in the fall of 2018 when Slate was at version 0.40.0. In CLUE, the editor was student-facing and quite limited in functionality. In early 2020, to meet the needs of the CODAP project for a user-facing rich text editor, the slate-editor
library project began as a means to share editor functionality across CC projects. At this point it is also being used for authoring in the question-interactives project and efforts are underway to use it in LARA project authoring as well.
In late 2019 Slate released its 0.50 version, which was a major backwards-incompatible release with substantial architectural changes. In the process, Android support was dropped (presumably temporarily) along with weakening of support for some browser versions. Ideally, this library will adopt the later Slate versions once these support issues are ironed out. In the meantime, we (and others) continue to use the Slate 0.47 release.
The setup of this project was patterned after https://blog.harveydelaney.com/creating-your-own-react-component-library/, which contains additional details on working with the project.
There are npm scripts to handle the basics of development:
$ npm run lint # run linter
$ npm run test # run unit tests
$ npm run build # perform build
For local library development we use Storybook to organize and run test cases. To run the Storybook examples:
$ npm start # launch storybook
With the storybook server running, examples can be viewed in a browser at http://localhost:6006.
For development in the context of a client application (such as CODAP) it's useful to link the library under development directly.
In the slate-editor
project directory:
$ npm link # to make the module linkable
$ npm run link:react # to avoid duplicate react warnings when testing in CODAP
If testing in another application besides CODAP, adjust the path to the client application in the script appropriately. See Understanding npm-link and Duplicate React when using npm link for details.
In client project directory:
$ npm link @concord-consortium/slate-editor # to link to the `slate-editor` library directly
Note that these links can be inadvertently undone when performing other npm tasks like npm install, so if things stop working you may need to refresh the links.
yalc provides an alternative to npm link
. It acts as a very simple local repository for locally developed packages that can be shared across a local environment. It provides a better workflow than npm | yarn link
for package authors. There are scripts in package.json to make this easier.
To publish an in-development version of the slate-editor library, in the root directory of the slate-editor project:
$ npm run yalc:publish
To consume an in-development version of the slate-editor library, in the root directory of the client project:
$ npx yalc add @concord-consortium/slate-editor
To update all clients that are using the in-development version of slate-editor, in the slate-editor project:
$ npm run yalc:publish
yalc
modifies the package.json
of the client project with a link to the local yalc
repository. This is a good thing! as it makes it obvious when you're using an in-development version of a library and serves as a reminder to install a fully published version before pushing to GitHub, etc. It also means that running npm install
in the client project will not break the setup.
The slate-editor
library makes use of a local fork of the slate GitHub repository at @concord-consortium/slate. As of this writing the only package that has been modified is slate-react
which has been published as @concord-consortium/slate-react.
The storybook examples for the slate-editor library are automatically deployed by Travis CI. The main production deploy URL is https://slate-editor.concord.org. Branch builds are deployed to https://slate-editor.concord.org/branch/{branch-name}/ (the trailing /
is required), e.g. the most recent contents of the master
branch can be tested at https://slate-editor.concord.org/branch/master/.
- Update the
CHANGELOG.md
with a description of the new version - Update the version number in
package.json
andpackage-lock.json
npm version --no-git-tag-version [patch|minor|major]
- Verify that everything builds correctly
npm run lint && npm run test && npm run build
- Commit and push the changes either directly or via GitHub pull request
- Create/push a tag for the new version (e.g. v0.5.0) and a description (e.g. Release 0.5.0)
- This can be done in a local git client or on the releases page of the GitHub repository
- Publish new release on releases page of GitHub repository
- Test a dry-run of publishing the package to the npm repository
npm publish --access public --dry-run
- Publish the package to the npm repository
npm publish --access public
Editor content can be serialized to/from JSON, HTML, or plain text (lossy).
deserializeValue(value: SlateExchangeValue): EditorValue
serializeValue(value: EditorValue): SlateExchangeValue
deserializeDocument(document: SlateDocument): DocumentJSON
serializeDocument(document: DocumentJSON): SlateDocument
htmlToSlate(html: string): EditorValue
slateToHtml(value: EditorValue): string
textToSlate(text: string): EditorValue
slateToText(value: EditorValue): string
The best way to understand the JSON serialization format is to play with the serialization storybook example which provides an active editor session on the left and a live-updating view of the serialized JSON on the right. See Slate's Data Model documentation for a high-level description of the organization of the editor content. Slate doesn't proscribe the set of block/inline nodes or marks that are supported by a particular editor built with Slate, however. One final wrinkle is that one of the changes introduced by the 0.50 version of Slate is a simpler, less deeply-nested JSON serialization format. With an eye toward easing an eventual migration to the latest Slate version, this library adopts aspects of that simpler, less deeply-nested format for its own JSON serialization.
Marks are how Slate represents formatting data that is attached to the characters in the text itself.
All of the characters in a text node must have the same marks (character-level formatting), so as formatting is applied the text is broken up into nodes with common formatting. A TypeScript definition of a text node would look like:
interface TextNode {
text: string; // the text of the node
bold?: boolean; // <strong>
italic?: boolean; // <em>
underlined?: boolean; // <u>
deleted?: boolean; // <del> (strikethrough)
color?: string; // <span color="#rrggbb">
code?: boolean; // <code>
subscript?: boolean; // <sub>
superscript?: boolean; // <sup>
}
Inlines can only contain inline or text nodes.
Inline nodes are rendered inline with text nodes (as their name would suggest), but can contain more elaborate representations and can (in theory) contain other nodes. There are currently two types of inlines supported by the slate-editor
library:
interface LinkNode {
type: "link"; // <a>
children: TextNode[]; // link text
href: string; // link url
}
interface ImageNode {
type: "image"; // <img>
src: string; // image url
alt?: string; // alt/title text
width?: number;
height?: number;
constrain?: boolean; // whether to constrain to original aspect ratio (default true)
float?: "left" | "right" // whether/how to float the image (defaults to inline, i.e. not floated)
children: TextNode[]; // not used, but an empty text node is required for normalization
}
type InlineNode = ImageNode | LinkNode;
Blocks may contain either only block nodes, or a combination of inline and text nodes.
Essentially, each block corresponds to a block-level HTML tag:
interface ParagraphNode {
type: "paragraph"; // <p>
children: (InlineNode | TextNode)[];
}
interface HeadingNode {
type: "heading1" | ... | "heading6"; // <h1> ... <h6>
children: (InlineNode | TextNode)[];
}
interface BlockQuoteNode {
type: "block-quote"; // <blockquote>
children: (InlineNode | TextNode)[];
}
interface BulletedListNode {
type: "bulleted-list"; // <ul>
children: ListItemNode[];
}
interface OrderedListNode {
type: "ordered-list"; // <ol>
children: ListItemNode[];
}
interface ListItemNode {
type: "list-item"; // <li>
children: (InlineNode | TextNode)[];
}
type BlockNode = ParagraphNode | HeadingNode | BlockQuoteNode |
BulletedListNode | OrderedListNode | ListItemNode;
The document contains a list of block nodes:
interface EditorDocument {
children: BlockNode[];
// mapping from node type to block/inline; the need for this may be removed at some point
objTypes: Record<string, "block" | "inline">
}
The editor value also includes document-wide settings, e.g. fontSize
(which is really a font scale factor):
interface DocumentSettings {
fontSize: number; // scale factor, e.g. 1.1 = +10%
}
interface EditorValue {
object: "value";
data: DocumentSettings;
document: EditorDocument;
}
- slate-editor storybook examples: https://slate-editor.concord.org
- Slate: https://slatejs.org
- Slate Documentation (0.47): https://docs.slatejs.org/v/v0.47/
- Slate GitHub (0.47 branch): https://github.com/ianstormtaylor/slate/tree/v0.47
- @CC/slate GitHub (cc-master branch): https://github.com/concord-consortium/slate/tree/cc-master
- Project Configuration: https://blog.harveydelaney.com/creating-your-own-react-component-library/
- ESLint/TypeScript Configuration: https://michelenasti.com/2019/06/27/typescript-babel-webpack-eslint-my-configuration.html
- FontAwesome Icons: https://www.iconfinder.com/iconsets/font-awesome
- SVG Conversion: https://react-svgr.com/playground/