diff --git a/docs/Summary.md b/docs/Summary.md
index 3fcfc24ce..2b8b2faa1 100644
--- a/docs/Summary.md
+++ b/docs/Summary.md
@@ -10,6 +10,7 @@
- [Applying Custom Formatting](walkthroughs/04-applying-custom-formatting.md)
- [Executing Commands](walkthroughs/05-executing-commands.md)
- [Saving to a Database](walkthroughs/06-saving-to-a-database.md)
+- [Enabling Collaborative Editing](walkthroughs/07-enabling-collaborative-editing.md)
- [Using the Bundled Source](walkthroughs/xx-using-the-bundled-source.md)
## Concepts
diff --git a/docs/general/resources.md b/docs/general/resources.md
index 89f2f2071..00165b75b 100644
--- a/docs/general/resources.md
+++ b/docs/general/resources.md
@@ -6,16 +6,21 @@ A few resources that are helpful for building with Slate.
These libraries are helpful when developing with Slate:
-- [`is-hotkey`](https://github.com/ianstormtaylor/is-hotkey) is a simple way to check whether an `onKeyDown` handler should fire for a given hotkey, handling cross-platform concerns like cmd vs. ctrl keys for you automatically.
+- [`is-hotkey`](https://github.com/ianstormtaylor/is-hotkey) is a simple way to check whether an `onKeyDown` handler
+ 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](https://liveblocks.io/docs/api-reference/liveblocks-yjs) A fully-hosted WebSocket infrastructure and
+ persisted data store for Yjs documents
- [Plate](https://github.com/udecode/plate) Rich text editor plugin system for Slate & React
-- [`slate-angular`](https://github.com/worktile/slate-angular) Angular-based view layer, which is a useful supplement to Slate for building a rich text editor using Angular.
+- [`slate-angular`](https://github.com/worktile/slate-angular) Angular-based view layer, which is a useful supplement to
+ Slate for building a rich text editor using Angular.
- [`slate-yjs`](https://github.com/BitPhinix/slate-yjs/) Collaborative editing utilities for Slate leveraging Yjs
-- [`slate-collaborative`](https://github.com/cudr/slate-collaborative) Collaborative editing utilities for Slate leveraging Automerge
+- [`slate-collaborative`](https://github.com/cudr/slate-collaborative) Collaborative editing utilities for Slate
+ leveraging Automerge
## Products
@@ -53,15 +58,22 @@ These products use Slate, and can give you an idea of what's possible:
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](https://github.com/accordproject/web-components) is a WYSIWYG editor for [CommonMark](https://commonmark.org/).
+- [Accord Project Markdown Editor](https://github.com/accordproject/web-components) is a WYSIWYG editor
+ for [CommonMark](https://commonmark.org/).
- [Canner Editor](https://github.com/Canner/canner-slate-editor) is a rich text editor.
-- [Chatterslate](https://github.com/chatterbugapp/chatterslate) helps teach language grammar and more at [Chatterbug](https://chatterbug.com).
+- [Chatterslate](https://github.com/chatterbugapp/chatterslate) helps teach language grammar and more
+ at [Chatterbug](https://chatterbug.com).
- [CoCalc](https://github.com/sagemathinc/cocalc/) Collaborative Calculation editor in the Cloud
-- [French Press Editor](https://github.com/roast-cms/french-press-editor) is a customizeable editor with offline support.
+- [French Press Editor](https://github.com/roast-cms/french-press-editor) is a customizeable editor with offline
+ support.
- [Nossas Editor](http://slate-editor.bonde.org/) is a drop-in WYSIWYG editor.
-- [React Force Slate Editor](https://github.com/nareshbhatia/react-force/tree/master/packages/slate-editor) is a light-weight medium-style editor with no editor chrome.
-- [React Page](https://github.com/react-page/react-page) is a self-contained, customizable inline WYSIWYG editor library.
-- [Slate Plugins Next](https://github.com/zbeyens/slate-plugins-next) provides an editor with configurable and extendable plugins.
+- [React Force Slate Editor](https://github.com/nareshbhatia/react-force/tree/master/packages/slate-editor) is a
+ light-weight medium-style editor with no editor chrome.
+- [React Page](https://github.com/react-page/react-page) is a self-contained, customizable inline WYSIWYG editor
+ library.
+- [Plate (Plugins for Slate)](https://github.com/udecode/plate) provides an editor with configurable and
+ extendable plugins.
+-
- [Tripdocs](https://github.com/ctripcorp/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.\)
diff --git a/docs/walkthroughs/07-enabling-collaborative-editing.md b/docs/walkthroughs/07-enabling-collaborative-editing.md
new file mode 100644
index 000000000..8b6b2cf1a
--- /dev/null
+++ b/docs/walkthroughs/07-enabling-collaborative-editing.md
@@ -0,0 +1,401 @@
+# Enabling Collaborative Editing
+
+A common use case for text editors is collaborative editing, and the Slate editor was designed with this in
+mind. You can enable multiplayer editing with [Yjs](https://github.com/yjs/yjs), a network-agnostic CRDT implementation
+that allows you to share data among connected users. Because Yjs is network-agnostic, each project requires
+a [communication provider](https://github.com/yjs/yjs#providers) set up on the back end to link users together.
+
+In this guide, we'll show you how to set up a collaborative Slate editor using a Yjs provider. We'll also be
+adding [slate-yjs](https://github.com/BitPhinix/slate-yjs) which allows you to add multiplayer features to Slate, such
+as live cursors.
+
+Let's start with a basic editor:
+
+```jsx
+import { Slate } from 'slate-react'
+
+const initialValue = {
+ children: [{ text: '' }],
+}
+
+export const CollaborativeEditor = () => {
+ return
+}
+
+const SlateEditor = () => {
+ const [editor] = useState(() => withReact(createEditor()))
+
+ return (
+
+
+
+ )
+}
+```
+
+Yjs is network-agnostic, which means each Yjs provider is set up in a slightly different way. For
+example [@liveblocks/yjs](https://liveblocks.io/docs/api-reference/liveblocks-yjs) is
+fully-hosted, whereas others such as [y-websocket](https://github.com/yjs/y-websocket) require you to host your own
+WebSocket server. Because of this, we'll use code snippets that work for each provider, without going into too much
+detail about setting up the provider itself.
+
+This is how to connect to a collaborative Yjs document, ready to be used in your Slate editor.
+
+```jsx
+import { useEffect, useMemo, useState } from 'react'
+import { createEditor, Editor, Transforms } from 'slate'
+import { Editable, Slate, withReact } from 'slate-react'
+import * as Y from 'yjs'
+
+const initialValue = {
+ children: [{ text: '' }],
+}
+
+export const CollaborativeEditor = () => {
+ const [connected, setConnected] = useState(false)
+ const [sharedType, setSharedType] = useState()
+ const [provider, setProvider] = useState()
+
+ // Set up your Yjs provider and document
+ useEffect(() => {
+ const yDoc = new Y.Doc()
+ const sharedDoc = yDoc.get('slate', Y.XmlText)
+
+ // Set up your Yjs provider. This line of code is different for each provider.
+ const yProvider = new YjsProvider(/* ... */)
+
+ yProvider.on('sync', setConnected)
+ setSharedType(sharedDoc)
+ setProvider(yProvider)
+
+ return () => {
+ yDoc?.destroy()
+ yProvider?.off('sync', setConnected)
+ yProvider?.destroy()
+ }
+ }, [])
+
+ if (!connected || !sharedType || !provider) {
+ return
Loading…
+ }
+
+ return
+}
+
+const SlateEditor = () => {
+ const [editor] = useState(() => withReact(createEditor()))
+
+ return (
+
+
+
+ )
+}
+```
+
+After setting up your Yjs document like this, you can then link it your editor by passing down `sharedType`, which
+contains the multiplayer text, and by using functions from `slate-yjs`. We're also passing down `provider` which will be
+helpful later.
+
+```jsx
+import { useEffect, useMemo, useState } from 'react'
+import { createEditor, Editor, Transforms } from 'slate'
+import { Editable, Slate, withReact } from 'slate-react'
+import { withYjs, YjsEditor } from '@slate-yjs/core'
+import * as Y from 'yjs'
+
+const initialValue = {
+ children: [{ text: '' }],
+}
+
+export const CollaborativeEditor = () => {
+ const [connected, setConnected] = useState(false)
+ const [sharedType, setSharedType] = useState()
+ const [provider, setProvider] = useState()
+
+ // Connect to your Yjs provider and document
+ useEffect(() => {
+ const yDoc = new Y.Doc()
+ const sharedDoc = yDoc.get('slate', Y.XmlText)
+
+ // Set up your Yjs provider. This line of code is different for each provider.
+ const yProvider = new YjsProvider(/* ... */)
+
+ yProvider.on('sync', setConnected)
+ setSharedType(sharedDoc)
+ setProvider(yProvider)
+
+ return () => {
+ yDoc?.destroy()
+ yProvider?.off('sync', setConnected)
+ yProvider?.destroy()
+ }
+ }, [])
+
+ if (!connected || !sharedType || !provider) {
+ return Loading…
+ }
+
+ return
+}
+
+const SlateEditor = ({ sharedType, provider }) => {
+ const editor = useMemo(() => {
+ const e = withReact(withYjs(createEditor(), sharedType))
+
+ // Ensure editor always has at least 1 valid child
+ const { normalizeNode } = e
+ e.normalizeNode = entry => {
+ const [node] = entry
+
+ if (!Editor.isEditor(node) || node.children.length > 0) {
+ return normalizeNode(entry)
+ }
+
+ Transforms.insertNodes(editor, initialValue, { at: [0] })
+ }
+
+ return e
+ }, [])
+
+ useEffect(() => {
+ YjsEditor.connect(editor)
+ return () => YjsEditor.disconnect(editor)
+ }, [editor])
+
+ return (
+
+
+
+ )
+}
+```
+
+That's all you need to attach Yjs to Slate!
+
+Let's look at a real-world example of setting up Yjs—here's a code snippet for setting up
+a [Liveblocks provider](https://liveblocks.io/docs/get-started/yjs-slate-react). Liveblocks uses the concept of rooms,
+spaces where users can
+collaborative. To use a Liveblocks provider, you join a multiplayer room with `RoomProvider`, then pass the room
+to `new LiveblocksProvider`, along with the Yjs document.
+
+```jsx
+import LiveblocksProvider from '@liveblocks/yjs'
+import { RoomProvider, useRoom } from '../liveblocks.config'
+
+// Join a Liveblocks room and show the editor after connecting
+export const App = () => {
+ return (
+
+ Loading…}>
+ {() => }
+
+
+ )
+}
+
+export const CollaborativeEditor = () => {
+ const room = useRoom()
+ const [connected, setConnected] = useState(false)
+ const [sharedType, setSharedType] = useState()
+ const [provider, setProvider] = useState()
+
+ // Connect to your Yjs provider and document
+ useEffect(() => {
+ const yDoc = new Y.Doc()
+ const sharedDoc = yDoc.get('slate', Y.XmlText)
+
+ // Set up your Liveblocks provider with the current room and document
+ const yProvider = new LiveblocksProvider(room, yDoc)
+
+ yProvider.on('sync', setConnected)
+ setSharedType(sharedDoc)
+ setProvider(yProvider)
+
+ return () => {
+ yDoc?.destroy()
+ yProvider?.off('sync', setConnected)
+ yProvider?.destroy()
+ }
+ }, [room])
+
+ if (!connected || !sharedType || !provider) {
+ return Loading…
+ }
+
+ return
+}
+
+const SlateEditor = ({ sharedType, provider }) => {
+ // ...
+}
+```
+
+Unlike other providers, Liveblocks hosts your Yjs back end for you, which means you don't need to run your own server
+to get this working. For more information on setting up Liveblocks providers, make sure to read
+their [Slate getting started](https://liveblocks.io/docs/get-started/yjs-slate-react) guide.
+
+> Note that Liveblocks is independent of the Slate project, and isn't required for collaboration, but it may be
+> convenient depending on your needs. [Other providers](https://github.com/yjs/yjs#providers) are available
+> should you wish to set up and host a Yjs back end yourself.
+
+After setting up Yjs, it's possible to add multiplayer cursors to your app. You can do this with hooks supplied by
+[slate-yjs](), which allow you to find the cursor positions of other users. Here's an example of setting up a cursor
+component.
+
+```jsx
+import {
+ CursorOverlayData,
+ useRemoteCursorOverlayPositions,
+} from '@slate-yjs/react'
+import { useRef } from 'react'
+
+export function Cursors({ children }) {
+ const containerRef = useRef(null)
+ const [cursors] = useRemoteCursorOverlayPositions({ containerRef })
+
+ return (
+
+ {children}
+ {cursors.map(cursor => (
+
+ ))}
+
+ )
+}
+
+function Selection({ data, selectionRects, caretPosition }) {
+ if (!data) {
+ return null
+ }
+
+ const selectionStyle = {
+ backgroundColor: data.color,
+ }
+
+ return (
+ <>
+ {selectionRects.map((position, i) => (
+
+ ))}
+ {caretPosition && }
+ >
+ )
+}
+
+function Caret({ caretPosition, data }) {
+ const caretStyle = {
+ ...caretPosition,
+ background: data?.color,
+ }
+
+ const labelStyle = {
+ transform: 'translateY(-100%)',
+ background: data?.color,
+ }
+
+ return (
+
+ )
+}
+```
+
+With some matching styles to set up the positioning:
+
+```css
+.cursors {
+ position: relative;
+}
+
+.caretMarker {
+ position: absolute;
+ width: 2px;
+}
+
+.caret {
+ position: absolute;
+ font-size: 14px;
+ color: #fff;
+ white-space: nowrap;
+ top: 0;
+ border-radius: 6px;
+ border-bottom-left-radius: 0;
+ padding: 2px 6px;
+ pointer-events: none;
+}
+
+.selection {
+ position: absolute;
+ pointer-events: none;
+ opacity: 0.2;
+}
+```
+
+You can then import this into your `SlateEditor` component. Notice that we're using `withCursors` from `slate-yjs`,
+adding `provider.awareness` and the current user's name to it. We're then wrapping `` in the new ``
+component we've just created.
+
+```jsx
+import { useEffect, useMemo, useState } from 'react'
+import { createEditor, Editor, Transforms } from 'slate'
+import { Editable, Slate, withReact } from 'slate-react'
+import { withCursors, withYjs, YjsEditor } from '@slate-yjs/core'
+import { Cursors } from './Cursors'
+import * as Y from 'yjs'
+
+export const CollaborativeEditor = () => {
+ // ...
+}
+
+const SlateEditor = ({ sharedType, provider }) => {
+ const editor = useMemo(() => {
+ const e = withReact(
+ withCursors(withYjs(createEditor(), sharedType), provider.awareness, {
+ // The current user's name and color
+ data: {
+ name: 'Chris',
+ color: '##00ff00',
+ },
+ })
+ )
+
+ // Ensure editor always has at least 1 valid child
+ const { normalizeNode } = e
+ e.normalizeNode = entry => {
+ const [node] = entry
+
+ if (!Editor.isEditor(node) || node.children.length > 0) {
+ return normalizeNode(entry)
+ }
+
+ Transforms.insertNodes(editor, initialValue, { at: [0] })
+ }
+
+ return e
+ }, [])
+
+ useEffect(() => {
+ YjsEditor.connect(editor)
+ return () => YjsEditor.disconnect(editor)
+ }, [editor])
+
+ return (
+
+
+
+
+
+ )
+}
+```
+
+You should now be seeing multiplayer cursors! To learn more, make sure to read
+the [slate-yjs documentation](https://docs.slate-yjs.dev/).