File: Readme.md
Slate lets you build rich, intuitive editors like those in Medium, Dropbox Paper or Google Docsβwhich are becoming table stakes for applications on the webβwithout your codebase getting mired in complexity.
It can do this because all of its logic is implemented with a series of plugins, so you aren't ever constrained by what is or isn't in "core". You can think of it like a pluggable implementation of contenteditable built on top of React. It was inspired by libraries like Draft.js, Prosemirror and Quill.
π€ Slate is currently in beta. Its core API is useable right now, but you might need to pull request improvements for advanced use cases, or fixes for some bugs. Some of its APIs are not "finalized" and will have breaking changes over time as we discover better solutions. There isn't currently a
1.0release schedule, we're still getting the architecture right.
π€ Slate is also contributor-driven. It is not backed by any huge company, which means that all contributions are voluntary and done by the people who need them. If you need something improved, added, or fixed, please contribute it yourself or no one will. And if you want to become a more active maintainer, let us know in the Slack channel.
Why?
Why create Slate? Well... (Beware: this section has a few of my opinions!)
Before creating Slate, I tried a lot of the other rich text libraries out thereβDraft.js, Prosemirror, Quill, etc. What I found was that while getting simple examples to work was easy enough, once you started trying to build something like Medium, Dropbox Paper or Google Docs, you ran into deeper issues...
The editor's "schema" was hardcoded and hard to customize. Things like bold and italic were supported out of the box, but what about comments, or embeds, or even more domain-specific needs?
Transforming the documents programmatically was very convoluted. Writing as a user may have worked, but making programmatic changes, which is critical for building advanced behaviors, was needlessly complex.
Serializing to HTML, Markdown, etc. seemed like an afterthought. Simple things like transforming a document to HTML or Markdown involved writing lots of boilerplate code, for what seemed like very common use cases.
Re-inventing the view layer seemed inefficient and limiting. Most editors rolled their own views, instead of using existing technologies like React, so you have to learn a whole new system with new "gotchas".
Collaborative editing wasn't designed for in advance. Often the editor's internal representation of data made it impossible to use to for a realtime, collaborative editing use case without basically rewriting the editor.
The repositories were monolithic, not small and reusable. The code bases for many of the editors often didn't expose the internal tooling that could have been re-used by developers, leading to having to reinvent the wheel.
Building complex, nested documents was impossible. Many editors were designed around simplistic "flat" documents, making things like tables, embeds and captions difficult to reason about and sometimes impossible.
Of course not every editor exhibits all of these issues, but if you've tried using another editor you might have run into similar problems. To get around the limitations of their API's and achieve the user experience you're after, you have to resort to very hacky things. And some experiences are just plain impossible to achieve.
If that sounds familiar, you might like Slate.
Which brings me to how Slate solves all of that...
Principles
Slate tries to solve the question of "Why?" with a few principles:
First-class plugins. The most important part of Slate is that plugins are first-class entities. That means you can completely customize the editing experience, to build complex editors like Medium's or Dropbox's, without having to fight against the library's assumptions.
Schema-less core. Slate's core logic assumes very little about the schema of the data you'll be editing, which means that there are no assumptions baked into the library that'll trip you up when you need to go beyond the most basic use cases.
Nested document model. The document model used for Slate is a nested, recursive tree, just like the DOM itself. This means that creating complex components like tables or nested block quotes are possible for advanced use cases. But it's also easy to keep it simple by only using a single level of hierarchy.
Parallel to the DOM. Slate's data model is based on the DOMβthe document is a nested tree, it uses selections and ranges, and it exposes all the standard event handlers. This means that advanced behaviors like tables or nested block quotes are possible. Pretty much anything you can do in the DOM, you can do in Slate.
Intuitive commands. Slate documents are edited using "commands", that are designed to be high-level and extremely intuitive to write and read, so that custom functionality is as expressive as possible. This greatly increases your ability to reason about your code.
Collaboration-ready data model. The data model Slate usesβspecifically how operations are applied to the documentβhas been designed to allow for collaborative editing to be layered on top, so you won't need to rethink everything if you decide to make your editor collaborative.
Clear "core" boundaries. With a plugin-first architecture, and a schema-less core, it becomes a lot clearer where the boundary is between "core" and "custom", which means that the core experience doesn't get bogged down in edge cases.
Demo
Check out the live demo of all of the examples!
Examples
To get a sense for how you might use Slate, check out a few of the examples:
- Plain text β showing the most basic case: a glorified
<textarea>. - Rich text β showing the features you'd expect from a basic editor.
- Markdown preview β showing how to add key handlers for Markdown-like shortcuts.
- Inlines β showing how wrap text in inline nodes with associated data.
- Images β showing how to use void (text-less) nodes to add images.
- Hovering toolbar β showing how a hovering toolbar can be implemented.
- Tables β showing how to nest blocks to render more advanced components.
- Paste HTML β showing how to use an HTML serializer to handle pasted HTML.
- Mentions β showing how to use inline void nodes for simple @-mentions.
- See all the examples...
If you have an idea for an example that shows a common use case, pull request it!
Documentation
If you're using Slate for the first time, check out the Getting Started walkthroughs and the Concepts to familiarize yourself with Slate's architecture and mental models.
If even that's not enough, you can always read the source itself, which is heavily commented.
There are also translations of the documentation into other languages:
If you're maintaining a translation, feel free to pull request it here!
Packages
Slate's codebase is monorepo managed with Lerna. It consists of a handful of packagesβalthough you won't always use all of them. They are:
| Package | Version | Size | Description |
|---|---|---|---|
slate |
Slate's core data model logic. | ||
slate-history |
A plugin that adds undo/redo history to Slate. | ||
slate-hyperscript |
A hyperscript tool to write JSX Slate documents! | ||
slate-react |
React components for rendering Slate editors. |
Contributing!
All contributions are super welcome! Check out the Contributing instructions for more info!
Slate is MIT-licensed.
File: docs/general/contributing.md
Contributing
Want to contribute to Slate? That would be awesome!
Reporting Bugs
If you run into any weird behavior while using Slate, feel free to open a new issue in this repository! Please run a search before opening a new issue, to make sure that someone else hasn't already reported or solved the bug you've found.
Any issue you open must include:
- A JSFiddle that reproduces the bug with a minimal setup.
- A GIF showing the issue in action. (Using something like RecordIt.)
- A clear explanation of what the issue is.
Here's a JSFiddle template for Slate to get you started:
Asking Questions
βΉοΈ If you're looking for help with the Slate CRM platform, that's an unrelated project to us.
We've also got a Slate Slack team where you can ask questions and get answers from other people using Slate:
Please use the Slack instead of asking questions in issues, since we want to reserve issues for keeping track of bugs and features. We close questions in issues so that maintaining the project isn't overwhelming.
Submitting Pull Requests
All pull requests are super welcomed and greatly appreciated! Issues in need of a solution are marked with a β₯ help label if you're looking for somewhere to start.
Please include tests and docs with every pull request!
Repository Setup
The Slate repository is a monorepo that is managed with lerna. Unlike more traditional repositories, this means that the repository must be built in order for tests, linting, or other common development activities to function as expected.
To run the build, you need to have the Slate repository cloned to your computer. After that, you need to cd into the directory where you cloned it, and install the dependencies with yarn and build the monorepo:
yarn install
yarn buildRunning Examples
To run the examples, start by building the monorepo as described in the Repository Setup section.
Then you can start the examples server with:
yarn startRunning Tests
To run the tests, start by building the monorepo as described in the Repository Setup section.
Then you can rerun the tests with:
yarn testIf you need to debug something, you can add a debugger line to the source, and then run yarn test:inspect.
If you only want to run a specific test or tests, you can run yarn run test:mocha --fgrep="slate-react rendering" flag which will filter the tests being run by grepping for the string in each test. (This is a Mocha flag that gets passed through.)
In addition to tests you should also run the linter:
yarn lintThis will catch TypeScript, Prettier, and Eslint errors.
yarn fixThis will fix Prettier and Eslint errors.
Running integration tests
To run integrations with Playwright, first run yarn start to run the examples website, then run yarn playwright in a separate session to open the Playwright test suite. Or alternatively, run just yarn test:integration-local.
Running integration tests in Docker
If tests fail on CI but pass locally (often due to OS differences), you can run tests in a Docker container that replicates the same environment as CI.
Prerequisites: The project must be built first (same as running tests locally).
yarn test:integration-dockerThe script will automatically:
- Start the development server (if not already running)
- Run tests inside a Docker container
- Stop the server when tests complete
You can also pass additional arguments to the test runner. For example, to run a specific test file:
yarn test:integration-docker playwright/integration/slate-react/selection.test.tsOr run a specific browser project:
yarn test:integration-docker --project=chromiumYou can combine arguments as well:
yarn test:integration-docker playwright/integration/examples/check-lists.test.ts --project chromiumTesting Input Methods
Here's a helpful page detailing how to test various input scenarios on Windows, Mac and Linux.
Android tests
When making changes that might affect Android compatibility, you can perform the manual Android tests at /examples/android-tests.
Publishing Releases
Important: When creating releases using Lerna with the instructions below, you will be given choices around how to increase version numbers. You should always use a major, minor or patch release and must never use a prerelease. If a prerelease is used, the root package will not link to the packages in the packages directory creating hard to diagnose issues.
Publishing Normal `@latest` Release
Since we use Lerna to manage the Slate packages this is fairly easy, just run:
yarn release:latestAnd follow the prompts Lerna gives you.
Note that this will automatically run the prelease script first that will build, test and lint before attempting to publish.
Publishing `@next` Release
If we are unsure as to the stability of a release because there are significant changes and/or particularly complex changes, release with the @next tag.
yarn release:nextAnd follow the prompts Lerna gives you.
Publishing `@experimental` Release
If you need to create an experimental release to see how a published package will behave during an actual publish, release with the @experimental tag. End users should have no expectation that an @experimental release will be usable.
yarn release:experimentalRunning Prerelease Script
If we want to make sure that Slate code follows the preparations for a release but without actually publishing, run:
yarn prereleaseWhich will build, test and lint Slate code.
File: docs/general/faq.md
FAQ
A series of common questions people have about Slate:
Why is content pasted as plain text?
One of Slate's core principles is that, unlike most other editors, it does not prescribe a specific "schema" to the content you are editing. This means that Slate's core has no concept of "block quotes" or "bold formatting".
For the most part, this leads to increased flexibility without many downsides, but there are certain cases where you have to do a bit more work. Pasting is one of those cases.
Since Slate knows nothing about your domain, it can't know how to parse pasted HTML content (or other content). So, by default whenever a user pastes content into a Slate editor, it will parse it as plain text. If you want it to be smarter about pasted content, you need to override the insert_data command and deserialize the DataTransfer object's text/html data as you wish.
What browsers and devices does Slate support?
Slate's goal is to support all the modern browsers on both desktop and mobile devices.
Slate is in beta and is community-driven and so its support is not as robust as it could be.
On the desktop, it's currently tested against the latest few versions of Chrome, Edge, Firefox and Safari on desktops. And it does not work in Internet Explorer.
On mobile, iOS devices are supported but not regularly tested. Chrome on Android was until recently unsupported except for in older versions of Slate (0.47) but has recently been added. For clarity, due to the differences in Android's support of the beforeInput event, Android input uses compositions and mutations which is different from other browsers. This means that Android support progresses separately from other browsers and due to it being new, may have more bugs.
If you want to add or improve browser or device support, we'd love for you to submit a pull request! Or in the case of incompatible browsers, build a plugin.
For older browsers, such as IE11, a lot of the now standard native APIs aren't available. Slate's position on this is that it is up to the user to bring polyfills (like https://polyfill-fastly.io/) when needed for things like el.closest, etc. Otherwise we'd have to bundle and maintain lots of polyfills that others may not even need in the first place. For clarity, Slate makes no guarantees that it will work with older browsers, even with polyfills and at present, there are still unresolved issues with IE11.
File: docs/general/resources.md
Resources
A few resources that are helpful for building with Slate.
Libraries
These libraries are helpful when developing with Slate:
is-hotkeyis a simple way to check whether anonKeyDownhandler
should fire for a given hotkey, handling cross-platform concerns like cmd vs. ctrl keys for you automatically.
Extensions and Plugins
These extensions and plugins add additional features and capabilities to Slate:
- @liveblocks/yjs A fully-hosted WebSocket infrastructure and
persisted data store for Yjs documents - Plate Rich text editor plugin system for Slate & React
slate-angularAngular-based view layer, which is a useful supplement to
Slate for building a rich text editor using Angular.slate-yjsCollaborative editing utilities for Slate leveraging Yjsslate-collaborativeCollaborative editing utilities for Slate
leveraging Automergeslate-vue3Which is a useful supplement to Slate for building a rich text editor using Vue3, integrated all functions in an npm package- Velt Collaboration SDK adding comments, presence, live cursors and CRDT to Slate
Products
These products use Slate, and can give you an idea of what's possible:
- Aline: Digital contracting for the modern enterprise
- Archbee: Build better product docs with your team
- Campfire: The most versatile writing software [for fiction]
- Chatterbug: Learn languages from real people
- CoCalc: Collaborative calculation and data science
- Coda: Coda brings all your words, data, and teamwork into one powerful doc
- Decap CMS: Open source content management for your Git workflow
- Dropdeck: Magically create beautiful slides β faster than ever
- Easy Email Pro: A powerful, highly customizable email editor
- Eraser: The Whiteboard for Engineering Teams
- GitBook: Plan, centralize and share knowledge
- Discord: Text & voice channels to collaborate & share
- Grafana: Operational dashboards for your data
- Guilded: Better chat, happier communities
- Guru: Organize company information. Access it anywhere. [wiki]
- Hygraph: The federated content platform
- Hot Page: A graphical website builder for writing HTML and CSS from scratch
- Kitemaker: Product development tool built to manage the work, not the workers
- Living Spec: A modern way for product teams to work
- Notestamp: A note-taking web app that synchronizes your notes to audio, video and pdfs.
- Notes Together: Get mundane details out of your head
- Prezly: Turn your audience into fans
- Quill Forms: Open Source Typeform Alternative WordPress Plugin
- Sana: Reimagining learning with AI [LMS]
- Sanity.io: Platform for structured content [data driven content]
- Slite: Where remote teams make decisions and share knowledge
- Static CMS: Git-based CMS for Static Site Generators
- Taskade: Get work done in one unified workspace
- TRPG Engine
- Thoughts
Editors
These pre-packaged editors are built on top of Slate, and can be helpful to see how you might structure your code:
- Accord Project Markdown Editor is a WYSIWYG editor
for CommonMark. - Canner Editor is a rich text editor.
- Chatterslate helps teach language grammar and more
at Chatterbug. - CoCalc Collaborative Calculation editor in the Cloud
- French Press Editor is a customizeable editor with offline
support. - Nossas Editor is a drop-in WYSIWYG editor.
- Notestamp: Synchronize your text entries to media using this rich text editor that inserts clickable stamps alongside text.
- React Force Slate Editor is a
light-weight medium-style editor with no editor chrome. - React Page is a self-contained, customizable inline WYSIWYG editor
library. - Plate (Plugins for Slate) provides an editor with configurable and
extendable plugins. - Tripdocs: It's a modern, production-ready rich text editor.
(Or, if you have their exact use case, can be a drop-in editor for you.)
File: docs/concepts/01-interfaces.md
Interfaces
Slate works with pure JSON objects. All it requires is that those JSON objects conform to certain interfaces. For example, a text node in Slate must obey the Text interface:
interface Text {
text: string
}Which means it must have a text property with a string of content.
But any other custom properties are also allowed, and completely up to you. This lets you tailor your data to your specific domain and use case, adding whatever formatting logic you'd like, without Slate getting in the way.
This interface-based approach separates Slate from most other rich text editors which require you to work with their hand-rolled "model" classes and makes it much easier to reason about. It also means that it avoids startup time penalties related to "initializing" the data model.
Custom Properties
To take another example, the Element node interface in Slate is:
interface Element {
children: Node[]
}This is a very permissive interface. All it requires is that the children property gets defined containing the element's child nodes.
But you can extend elements (or any other interface) with your custom properties that are specific to your domain. For example, you might have "paragraph" and "link" elements:
const paragraph = {
type: 'paragraph',
children: [...],
}
const link = {
type: 'link',
url: 'https://example.com',
children: [...]
}The type and url properties are your custom API. Slate sees that they exist, but doesn't use them. However, when Slate renders a link element, you'll receive an object with the custom properties attached so that you can render it as:
<a href={element.url}>{element.children}</a>When getting started with Slate, it's important to understand all of the interfaces it defines. There are a handful of interfaces that are discussed in each of the guides.
Helper Functions
In addition to the typing information, each interface in Slate also exposes a series of helper functions that make them easier to work with.
For example, when working with nodes:
import { Node } from 'slate'
// Get the string content of an element node.
const string = Node.string(element)
// Get the node at a specific path inside a root node.
const descendant = Node.get(value, path)Or, when working with ranges:
import { Range } from 'slate'
// Get the start and end points of a range in order.
const [start, end] = Range.edges(range)
// Check if a range is collapsed to a single point.
if (Range.isCollapsed(range)) {
// ...
}There are many helper functions available for all common use cases when working with different interfaces. When getting started it helps to read through all of them so you can often simplify complex logic into just a handful of lines of code.
Custom Helpers
In addition to the built-in helper functions, you might want to define your custom helper functions and expose them on your custom namespaces.
For example, if your editor supports images, you might want a helper that determines if an element is an image element:
const isImageElement = element => {
return element.type === 'image' && typeof element.url === 'string'
}You can define these as one-off functions easily. But you might also bundle them up into namespaces, just like the core interfaces do, and use them instead.
For example:
import { Element } from 'slate'
// You can use `MyElement` everywhere to have access to your extensions.
export const MyElement = {
...Element,
isImageElement,
isParagraphElement,
isQuoteElement,
}This makes it easy to reuse domain-specific logic alongside the built-in Slate helpers.
File: docs/concepts/02-nodes.md
Nodes
The most important types are the Node objects:
- A root-level
Editornode that contains the entire document's content. - Container
Elementnodes that have semantic meaning in your domain. - And leaf-level
Textnodes which contain the document's text.
These three interfaces are combined to form a treeβjust like the DOM. For example, here's a simple plaintext value:
const editor = {
children: [
{
type: 'paragraph',
children: [
{
text: 'A line of text!',
},
],
},
],
// ...the editor has other properties too.
}Mirroring the DOM as much as possible is one of Slate's principles. People use the DOM to represent documents with rich text-like structures all the time. Mirroring the DOM helps make the library familiar for new users, and it lets us reuse battle-tested patterns without having to reinvent them ourselves.
π€ The following content on Mozilla's Developer Network may help you learn more about the corresponding DOM concepts:
A Slate document is a nested and recursive structure. In a document, elements can have children nodesβall of which may have children nodes without limit. The nested and recursive structure enables you to model simple behaviors such as user mentions and hashtags or complex behaviors such as tables and figures with captions.
`Editor`
The top-level node in a Slate document is the Editor itself. It encapsulates all of the rich text "content" of the document. Its interface is:
interface Editor {
children: Node[]
...
}We'll cover its functionality later, but the important part as far as nodes are concerned is its children property which contains a tree of Node objects.
`Element`
Elements make up the middle layers of a rich text document. They are the nodes that are custom to your domain. Their interface is:
interface Element {
children: Node[]
}You can define custom elements for any type of content you want. For example, you might have paragraphs and quotes in your data model which are differentiated by a type property:
const paragraph = {
type: 'paragraph',
children: [...],
}
const quote = {
type: 'quote',
children: [...],
}It's important to note that you can use any custom properties you want. The type property in that example isn't something Slate knows or cares about. If you were defining your own "link" nodes, you might have a url property:
const link = {
type: 'link',
url: 'https://example.com',
children: [...],
}Or maybe you want to give all of your nodes an ID property:
const paragraph = {
id: 1,
type: 'paragraph',
children: [...],
}All that matters is that elements always have a children property.
Blocks vs. Inlines
Depending on your use case, you might want to define another behavior for Element nodes which determines their editing "flow".
All elements default to being "block" elements. They each appear separated by vertical space, and they never run into each other.
But in certain cases, like for links, you might want to make them "inline" flowing elements instead. That way they live at the same level as text nodes, and flow.
π€ This is a concept borrowed from the DOM's behavior, see Block Elements and Inline Elements.
You can define which nodes are treated as inline nodes by overriding the editor.isInline function. (By default it always returns false.) Note that inline nodes cannot be the first or last child of a parent block, nor can they be next to another inline node in the children array. Slate will automatically space these with { text: '' } children by default with normalizeNode.
Elements can either contain block elements or inline elements intermingled with text nodes as children. But elements cannot contain some children that are blocks and some that are inlined.
Voids
Similar to blocks and inlines, there is another element-specific behavior you can define depending on your use case: their "void"-ness.
Elements default to being non-void, meaning that their children are fully editable as text. But in some cases, like for images, you want to ensure that Slate doesn't treat their content as editable text, but instead as a black box.
π€ This is a concept borrowed from the HTML spec, see Void Elements.
You can define which elements are treated as void by overriding the editor.isVoid function. (By default it always returns false.) See Rendering Void Elements for implementation details.
`Text`
Text nodes are the lowest-level nodes in the tree, containing the text content of the document, along with any formatting. Their interface is:
interface Text {
text: string
}For example, a string of bold text:
const text = {
text: 'A string of bold text',
bold: true,
}Text nodes too can contain any custom properties you want, and that's how you implement custom formatting like bold, italic, code, etc.
These custom properties are sometimes called marks.
File: docs/concepts/03-locations.md
Locations
Locations are how you refer to specific places in the document when inserting, deleting, or doing anything else with a Slate editor. There are a few different kinds of location interfaces, each for different use cases.
`Path`
Paths are the lowest-level way to refer to a location. Each path is a simple array of numbers that refers to a node in the document tree by its indexes in each of its ancestor nodes down the tree:
type Path = number[]For example, in this document:
const editor = {
children: [
{
type: 'paragraph',
children: [
{
text: 'A line of text!',
},
],
},
],
}The leaf text node would have a path of: [0, 0].
The Editor itself has a path of []. For example, to select the whole contents of the editor, callTransforms.select(editor, [])
`Point`
Points are slightly more specific than paths, and contain an offset into a specific text node. Their interface is:
interface Point {
path: Path
offset: number
}For example, with that same document, if you wanted to refer to the very first place you could put your cursor it would be:
const start = {
path: [0, 0],
offset: 0,
}Or, if you wanted to refer to the end of the sentence:
const end = {
path: [0, 0],
offset: 15,
}It can be helpful to think of points as being "cursors" (or "carets") of a selection.
π€ Points always refer to text nodes! Since they are the only ones with strings that can have cursors.
`Range`
Ranges are a way to refer not just to a single point in the document, but to a wider span of content between two points. (An example of a range is when you make a selection.) Their interface is:
interface Range {
anchor: Point
focus: Point
}π€ The terms "anchor" and "focus" are borrowed from the DOM, see Anchor and Focus.
An anchor and focus are established by a user interaction. The anchor point isn't always before the focus point in the document. Just like in the DOM, the ordering of an anchor and selection point depend on whether the range is backwards or forwards.
Here's how Mozilla Developer Network explains it:
A user may make a selection from left to right (in document order) or right to left (reverse of document order). The anchor is where the user began the selection and the focus is where the user ends the selection. If you make a selection with a desktop mouse, the anchor is placed where you pressed the mouse button and the focus is placed where you released the mouse button. Anchor and focus should not be confused with the start and end positions of a selection, since anchor can be placed before the focus or vice versa, depending on the direction you made your selection. β
Selection, MDN
One important distinction is that the anchor and focus points of ranges always reference the leaf-level text nodes in a document and never reference elements. This behavior is different than the DOM, but it simplifies working with ranges as there are fewer edge cases for you to handle.
π€ For more info, check out the Range API reference.
Selection
Ranges are used in many places in Slate's API when you need to refer to a span of content between two points. One of the most common, though, is the user's current "selection".
The selection is a special range that is a property of the top-level Editor. For example, say someone has the whole sentence currently selected:
const editor = {
selection: {
anchor: { path: [0, 0], offset: 0 },
focus: { path: [0, 0], offset: 15 },
},
children: [
{
type: 'paragraph',
children: [
{
text: 'A line of text!',
},
],
},
],
}π€ The selection concept is also borrowed from the DOM, see
Selection, MDN, although in a greatly-simplified form because Slate doesn't allow for multiple ranges inside a single selection, which makes things a lot easier to work with.
There isn't a special Selection interface. It's just an object that happens to respect the more general-purpose Range interface instead.
For example, to find the lowest block that contains all of the current selection:
function getCommonBlock(editor) {
const range = Editor.unhangRange(editor, editor.selection, { voids: true })
let [common, path] = SlateNode.common(
editor,
range.anchor.path,
range.focus.path
)
if (Editor.isBlock(editor, common) || Editor.isEditor(common)) {
return [common, path]
} else {
return Editor.above(editor, {
at: path,
match: n => Editor.isBlock(editor, n) || Editor.isEditor(n),
})
}
}File: docs/concepts/04-transforms.md
Transforms
Slate's data structure is immutable, so you can't modify or delete nodes directly. Instead, Slate comes with a collection of "transform" functions that let you change your editor's value.
Slate's transform functions are designed to be very flexible, to make it possible to represent all kinds of changes you might need to make to your editor. However, that flexibility can be hard to understand at first.
Typically, you'll apply a single operation to zero or more Nodes. For example, here's how you flatten the syntax tree,
by applying unwrapNodes to every parent of block Elements:
Transforms.unwrapNodes(editor, {
at: [], // Path of Editor
match: node =>
!Editor.isEditor(node) &&
node.children?.every(child => Editor.isBlock(editor, child)),
mode: 'all', // also the Editor's children
})Non-standard operations (or debugging/tracing which Nodes will be affected by a set of NodeOptions) may require usingEditor.nodes to create a JavaScript Iterator of NodeEntries and a for..of loop to act.
For example, to replace all image elements with their alt text:
const imageElements = Editor.nodes(editor, {
at: [], // Path of Editor
match: (node, path) => 'image' === node.type,
// mode defaults to "all", so this also searches the Editor's children
})
for (const nodeEntry of imageElements) {
const altText =
nodeEntry[0].alt ||
nodeEntry[0].title ||
/\/([^/]+)$/.exec(nodeEntry[0].url)?.[1] ||
'βΉοΈ'
Transforms.select(editor, nodeEntry[1])
Editor.insertFragment(editor, [{ text: altText }])
}π€ Check out the Transforms reference for a full list of Slate's transforms.
Selection Transforms
Selection-related transforms are some of the simpler ones. For example, here's how you set the selection to a new range:
Transforms.select(editor, {
anchor: { path: [0, 0], offset: 0 },
focus: { path: [1, 0], offset: 2 },
})But they can be more complex too.
For example, it's common to need to move a cursor forwards or backwards by varying distancesβby character, by word, by line. Here's how you'd move the cursor backwards by three words:
Transforms.move(editor, {
distance: 3,
unit: 'word',
reverse: true,
})π€ For more info, check out the Selection Transforms API Reference
Text Transforms
Text transforms act on the text content of the editor. For example, here's how you'd insert a string of text as a specific point:
Transforms.insertText(editor, 'some words', {
at: { path: [0, 0], offset: 3 },
})Or you could delete all of the content in an entire range from the editor:
Transforms.delete(editor, {
at: {
anchor: { path: [0, 0], offset: 0 },
focus: { path: [1, 0], offset: 2 },
},
})π€ For more info, check out the Text Transforms API Reference
Node Transforms
Node transforms act on the individual element and text nodes that make up the editor's value. For example you could insert a new text node at a specific path:
Transforms.insertNodes(
editor,
{
text: 'A new string of text.',
},
{
at: [0, 1],
}
)Or you could move nodes from one path to another:
Transforms.moveNodes(editor, {
at: [0, 0],
to: [0, 1],
})π€ For more info, check out the Node Transforms API Reference
The `at` Option
Many transforms act on a specific location in the document. By default, they will use the user's current selection. But this can be overridden with the at option.
For example when inserting text, this would insert the string at the user's current cursor:
Transforms.insertText(editor, 'some words')Whereas this would insert it at a specific point:
Transforms.insertText(editor, 'some words', {
at: { path: [0, 0], offset: 3 },
})The at option is very versatile, and can be used to implement more complex transforms very easily. Since it is a Location it can always be either a Path, Point, or Range. And each of those types of locations will result in slightly different transformations.
For example, in the case of inserting text, if you specify a Range location, the range will first be deleted, collapsing to a single point where your text is then inserted.
So to replace a range of text with a new string you can do:
Transforms.insertText(editor, 'some words', {
at: {
anchor: { path: [0, 0], offset: 0 },
focus: { path: [0, 0], offset: 3 },
},
})Or, if you specify a Path location, it will expand to a range that covers the entire node at that path. Then, using the range-based behavior it will delete all of the content of the node, and replace it with your text.
So to replace the text of an entire node with a new string you can do:
Transforms.insertText(editor, 'some words', {
at: [0, 0],
})These location-based behaviors work for all the transforms that take an at option. It can be hard to wrap your head around at first, but it makes the API very powerful and capable of expressing many subtly different transforms.
The `match` Option
Many of the node-based transforms take a match function option, which restricts the transform to only apply to nodes for which the function returns true. When combined with at, match can also be very powerful.
For example, consider a basic transform that moves a node from one path to another:
Transforms.moveNodes(editor, {
at: [2],
to: [5],
})Although it looks like it simply takes a path and moves it to another place. Under the hood two things are happeningβ¦
First, the at option is expanded to be a range representing all of the content inside the node at [2]. Which might look something like:
at: {
anchor: { path: [2, 0], offset: 0 },
focus: { path: [2, 2], offset: 19 }
}Second, the match option is defaulted to a function that only matches the specific path, in this case [2]:
match: (node, path) => Path.equals(path, [2])Then Slate iterates over the range and moves any nodes that pass the matcher function to the new location. In this case, since match is defaulted to only match the exact [2] path, that node is moved.
But what if you wanted to move the children of the node at [2] instead?
You might consider looping over the node's children and moving them one at a time, but this gets very complex to manage because as you move the nodes the paths you're referring to become outdated.
Instead, you can take advantage of the at and match options to match all of the children:
Transforms.moveNodes(editor, {
// This will again be expanded to a range of the entire node at `[2]`.
at: [2],
// Matches nodes with a longer path, which are the children.
match: (node, path) => path.length === 2,
to: [5],
})Here we're using the same at path (which is expanded to a range), but instead of letting it match just that path by default, we're supplying our own match function which happens to match only the children of the node.
Using match can make representing complex logic a lot simpler.
For example, consider wanting to add a bold mark to any text nodes that aren't already italic:
Transforms.setNodes(
editor,
{ bold: true },
{
// This path references the editor, and is expanded to a range that
// will encompass all the content of the editor.
at: [],
// This only matches text nodes that are not already italic.
match: (node, path) => Text.isText(node) && node.italic !== true,
}
)When performing transforms, if you're ever looping over nodes and transforming them one at a time, consider seeing if match can solve your use case, and offload the complexity of managing loops to Slate instead.
The match function can examine the children of a node, in node.children, or use Node.parent to examine its parent.
Transforms and Normalization
Sequences of Transforms may need to be wrapped in Editor.withoutNormalizing if the node tree should not be normalized between Transforms.
See Normalization - Implications for Other Code;
File: docs/concepts/05-operations.md
Operations
Operations are the granular, low-level actions that occur while invoking transforms. A single transform could result in many low-level operations being applied to the editor.
Slate's core defines all of the possible operations that can occur on a richtext document. For example:
editor.apply({
type: 'insert_text',
path: [0, 0],
offset: 15,
text: 'A new string of text to be inserted.',
})
editor.apply({
type: 'remove_node',
path: [0, 0],
node: {
text: 'A line of text!',
},
})
editor.apply({
type: 'set_selection',
properties: {
anchor: { path: [0, 0], offset: 0 },
},
newProperties: {
anchor: { path: [0, 0], offset: 15 },
},
})Under the covers Slate converts complex transforms into the low-level operations and applies them to the editor automatically. So you rarely have to think about operations unless you're implementing collaborative editing.
π€ Slate's editing behaviors being defined as operations is what makes things like collaborative editing possible, because each change is easily define-able, apply-able, compose-able and even undo-able!
File: docs/concepts/06-commands.md
Commands
While editing richtext content, your users will be doing things like inserting text, deleting text, splitting paragraphs, adding formatting, etc. Under the cover these edits are expressed using transforms and operations. But at a high level we talk about them as "commands".
Commands are the high-level actions that represent a specific intent of the user. They are represented as helper functions on the Editor interface. A handful of helpers are included in core for common richtext behaviors, but you are encouraged to write your own that model your specific domain.
For example, here are some of the built-in commands:
Editor.insertText(editor, 'A new string of text to be inserted.')
Editor.deleteBackward(editor, { unit: 'word' })
Editor.insertBreak(editor)But you can (and will!) also define your own custom commands that model your domain. For example, you might want to define a formatQuote command, or an insertImage command, or a toggleBold command depending on what types of content you allow.
Commands always describe an action to be taken as if the user was performing the action. For that reason, they never need to define a location to perform the command, because they always act on the user's current selection.
π€ The concept of commands is loosely based on the DOM's built-in
execCommandAPIs. However Slate defines its own simpler (and extendable!) version of the API, because the DOM's version is too opinionated and inconsistent.
Under the covers, Slate takes care of converting each command into a set of low-level "operations" that are applied to produce a new value. This is what makes collaborative editing implementations possible. But you don't have to worry about that, because it happens automatically.
Custom Commands
When defining custom commands, you can create your own namespace:
const MyEditor = {
...Editor,
insertParagraph(editor) {
// ...
},
}When writing your own commands, you'll often make use of the Transforms helpers that ship with Slate.
Transforms
Transforms are a specific set of helpers that allow you to perform a wide variety of specific changes to the document, for example:
// Set a "bold" format on all of the text nodes in a range.
// Normally you would apply a style like bold using the Editor.addMark() command.
// The addMark() command performs a similar setNodes transform, but it uses a more
// complicated match function in order to apply marks within markableVoid elements.
Transforms.setNodes(
editor,
{ bold: true },
{
at: range,
match: node => Text.isText(node),
split: true,
}
)
// Wrap the lowest block at a point in the document in a quote block.
Transforms.wrapNodes(
editor,
{ type: 'quote', children: [] },
{
at: point,
match: node => Editor.isBlock(editor, node),
mode: 'lowest',
}
)
// Insert new text to replace the text in a node at a specific path.
Transforms.insertText(editor, 'A new string of text.', { at: path })
// ...there are many more transforms!The transform helpers are designed to be composed together. So you might use a handful of them for each command.