Skip to content

Commit

Permalink
React package rewrite
Browse files Browse the repository at this point in the history
Rewrite react package to make it compatible with async react, add utilities for custom cursor rendering on specific nodes, and add cursor decorations
  • Loading branch information
BitPhinix committed Oct 1, 2022
1 parent 95d5203 commit 3710c48
Show file tree
Hide file tree
Showing 38 changed files with 986 additions and 307 deletions.
5 changes: 5 additions & 0 deletions .changeset/old-beds-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@slate-yjs/core': patch
---

Loosen `CursorEditor.isCursorEditor` to not check awareness instance.
14 changes: 14 additions & 0 deletions .changeset/rotten-beans-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@slate-yjs/react': minor
---

Changed:

- Rewrite of `useRemoteCursorOverlayPositions` to provide stricter typings, make it react 18 safe and add new `shouldGenerateOverlay` option.

Added:

- Remote cursor decorations using the new `useDecorateRemoteCursors` hook
- Remote cursor data hooks `useRemoteCursorStatesSelector` and `useRemoteCursorStates`
- Utility hooks to un-send the current cursor position on window/editor blur: `useUnsetCursorPositionOnBlur`
- `getCursorRange` helper
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ I'm currently looking for sponsors to found further development of slate-yjs. Th
<br/>
<br/>

Hosting provided by:

<p>
<a href="https://www.digitalocean.com/?utm_medium=opensource&utm_source=slate-yjs">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px">
</a>
</p>

<br/>
<br/>

## Why Yjs?

Yjs offers a feature-rich rich text CRDT with best-in-class performance. It's used in production by multiple fortune 500 companies and is the core of many collaborative editing applications. Moreover, it offers a very mature ecosystem with server-side solutions like [hocuspocus](https://www.hocuspocus.dev/), enabling you to build robust and highly scalable collaborative/offline-first applications.
Expand Down Expand Up @@ -114,6 +125,7 @@ These products use slate-yjs, and can give you an idea of what's possible:
- [Dropdeck](https://dropdeck.com/)
- [Hugo](https://hugo.team/)
- [Living Spec](https://www.livingspec.com/)
- [Saga](https://saga.so/)
- [Sana](https://www.sanalabs.com/)

<br/>
Expand All @@ -130,6 +142,6 @@ Any questions about slate-yjs? Thead over to the #slate-yjs channel inside the [

## Contributing!

All contributions are super welcome! Check out the Contributing instructions for more info!
All contributions are super welcome! Check out the [contributing instructions](https://docs.slate-yjs.dev/contributing/contributing) for more info!

Slate-yjs is [MIT-licensed](https://github.com/Bitphinix/slate-yjs/blob/main/LICENSE.md).
15 changes: 14 additions & 1 deletion docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@

- [Installation](walkthroughs/installation.md)
- [Collaboration (Hocuspocus)](walkthroughs/collaboration-hocuspocus.md)
- [Cursors (Overlay)](walkthroughs/cursors-overlay.md)

## 💻 Examples

- [Example Backend](https://github.com/BitPhinix/slate-yjs/blob/main/examples/backend/src)
- [Simple Client](https://github.com/BitPhinix/slate-yjs/blob/main/examples/frontend/src/pages/Simple.tsx)
- [Cursor Overlay](https://github.com/BitPhinix/slate-yjs/tree/main/examples/frontend/src/pages/RemoteCursorOverlay)
- [Cursor Decorations](https://github.com/BitPhinix/slate-yjs/tree/main/examples/frontend/src/pages/RemoteCursorDecorations.tsx)

## 💡 Concepts

Expand All @@ -22,8 +28,15 @@
- [Changelog](https://github.com/BitPhinix/slate-yjs/blob/main/packages/core/CHANGELOG.md)
- [@slate-yjs/react](api/slate-yjs-react/README.md)
- [Cursor Overlay](api/slate-yjs-react/cursor-overlay.md)
- [Cursor Decorations](api/slate-yjs-react/cursor-decorations.md)
- [Cursor Selectors](api/slate-yjs-react/cursor-selectors.md)
- [Utilities](api/slate-yjs-react/utilities.md)
- [Changelog](https://github.com/BitPhinix/slate-yjs/blob/main/packages/react/CHANGELOG.md)

## 🧑‍🚒 Contributing

- [Contributing](contributing/contributing.md)

## 🌎 External resources

- [Yjs Docs](https://docs.yjs.dev)
Expand Down
4 changes: 2 additions & 2 deletions docs/api/slate-yjs-core/cursor-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

## withCursors

`withCursors` facilitates the base layer used by the withCursorDecorations (coming soon) and frontend specific implementations/utils like `@slate-yjs/react'`s `useRemoteCursorOverlayPositions`. It contains a common way to send and subscribe to cursor data backed by Yjs's [awareness](https://docs.yjs.dev/getting-started/adding-awareness) feature.
`withCursors` facilitates the base layer used by frontend specific implementations/utils like `@slate-yjs/react'`s `useRemoteCursorOverlayPositions`. It contains a common way to send and subscribe to cursor data backed by Yjs's [awareness](https://docs.yjs.dev/getting-started/adding-awareness) feature.

> Not all yjs transports support awareness yet. Depending on which transport you use, this feature might not be available to you.
> Not all yjs transports support awareness yet. Depending on which transport you use, this feature might not be available.
<br/>

Expand Down
43 changes: 43 additions & 0 deletions docs/api/slate-yjs-react/cursor-decorations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Cursor Decorations

## useDecorateRemoteCursors

`useDecorateRemoteCursors` provides a simple way to implement an editor overlay displaying remote cursors using [slate decorations](https://docs.slatejs.org/concepts/09-rendering#decorations). Displaying remote cursors using decorations has a few tradeoffs to keep in mind:

Pros:

- They are part of the actual editor content which makes them easier to keep in sync leading to them never visually lagging behind.
- It's easier to customize node rendering/behavior based on remote selection since changes of them cause node re-renders.

Cons:

- Since cursors overlays are part of the by slate rendered content, they change the underlying dom structure causing e.g. different line breaks when a remote user changes his selection.
- They potentially mess with autocorrect
- Animating them is harder
- They potentially provide worse performance since they require re-rendering of parts of the editor content on remote cursor change

<br/>

**`useDecorateRemoteCursors`** takes an optional options parameter with the following options:

**`carets`**

If set to true, useDecorateRemoteCursors will provide explicit decorations for remote carets. Defaults to true. Carets will be decorated
as `` { [`remote-cursor-${id}`]: CursorState<TCursorData> } ``

<br/>

and returns a decorate function meant to be passed to `Editable.decorate`. Cursor ranges will be decorated as `` { [`remote-cursor-${id}`]: CursorState<TCursorData> } `` and carets (if not disabled) as `` { [`remote-caret-${id}`]: CursorState<TCursorData> & { isBackward: boolean } } ``.
Both decoration types can be received on a leaf-level using the `getRemoteCursorsOnLeaf` and `getRemoteCaretsOnLeaf` helpers.

<br/>

`useDecorateRemoteCursors` should be used inside the context of a [CursorEditor](../slate-yjs-core/cursor-plugin.md):

## getRemoteCursorsOnLeaf

Get remote cursor decorations on the given `Leaf`.

## getRemoteCaretsOnLeaf

Get remote caret decorations on the given `Leaf`.
23 changes: 15 additions & 8 deletions docs/api/slate-yjs-react/cursor-overlay.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# useRemoteCursorOverlayPositions
# Cursor Overlay

## useRemoteCursorOverlayPositions

`useRemoteCursorOverlayPositions` provides a simple way to implement an editor overlay displaying remote cursors. Displaying remote cursors using overlays has a few tradeoffs to keep in mind:

Expand All @@ -7,7 +9,7 @@ Pros:
- Since cursors overlays aren't part of the by slate rendered content, they don't change the underlying dom structure causing e.g. different line breaks when a remote user changes his selection.
- They don't mess with autocorrect
- Animating them is easier
- They potentially provide better performance since they don't requite re-rendering of parts of the editor content on remote cursor change
- They potentially provide better performance since they don't require re-rendering of parts of the editor content on remote cursor change

Cons:

Expand All @@ -24,20 +26,25 @@ If set, all returned positions will be relative to the containers position.

**`refreshOnResize`**

Set whether the cursors overlay positions should be automatically refreshed on container (provided via containerRef) resize. Defaults to `true`.
Set whether the cursors overlay positions should be automatically refreshed on container (provided via containerRef) resize. Defaults to `true`. If set to `'debounced'` the overlay positions will only be updated each animationFrame.

<br/>
**`shouldGenerateOverlay`**

and returns and object containing:
`NodeMatch` to filter out text nodes that should not be included when retuning overlay positions. Useful for handing the rendering of remote
selections inside certain blocks yourself. If not set no nodes will be ignored.

**`refresh(sync?: boolean): void`**
<br/>

Used to refresh the cursor overlay positions. If sync != true, calls to refresh will be batched into one until the next animation frame.
and returns and array containing:

**`cursors: CursorOverlayState<TCursorData>[]`**
**`overlays: CursorOverlayState<TCursorData>[]`**

The cursor overlay states that need to be painted.

**`refresh(sync?: boolean): void`**

Used to refresh the cursor overlay positions. If sync != true, calls to refresh will be batched into one until the next animation frame.

<br/>

`useRemoteCursorOverlayPositions` should be used inside the context of a [CursorEditor](../slate-yjs-core/cursor-plugin.md):
13 changes: 13 additions & 0 deletions docs/api/slate-yjs-react/cursor-selectors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Cursor selectors

`@slate-yjs/react` provides various helper hooks to allow for custom rendering of remote selections on/inside editor elements. All these
helper hooks are based upon a per-editor unique store to allow for good performance even when rendering 100s of elements reacting to
remote changes.

## useRemoteCursorStatesSelector

Hook to react to all cursor changes inside a component using a selector.

## useRemoteCursorStates

Hook to react to all cursor changes inside a component. Returns a `Record<string, CursorState<TCursorData>>` containing all remote cursor states.
5 changes: 5 additions & 0 deletions docs/api/slate-yjs-react/utilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Utilities

## useUnsetCursorPositionOnBlur

Unset the `selectionStateField` while the editor/window is blurred.
75 changes: 75 additions & 0 deletions docs/contributing/contributing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Contributing

Want to contribute to slate-yjs? That would be awesome!

- [Reporting Bugs](contributing.md#reporting-bugs)
- [Asking Questions](contributing.md#asking-questions)
- [Submitting Pull Requests](contributing.md#submitting-pull-requests)
- [Repository Setup](contributing.md#repository-setup)
- [Running Examples](contributing.md#running-examples)
- [Running Tests](contributing.md#running-tests)

## Reporting Bugs

If you run into any weird behavior while using slate-yjs, 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](https://jsfiddle.net/) (or similar) that reproduces the bug with a minimal setup.
- A GIF showing the issue in action. \(Using something like [Loom](https://loom.com).\)
- A clear explanation of what the issue is.

## Asking Questions

For questions around yjs, head over to the [Yjs Community](https://discuss.yjs.dev/). Trying to build a backend with [hocuspocus](https://www.hocuspocus.dev/) and have questions? Take a look at the #hocuspocus channel in the [TipTap Discord](https://discord.com/invite/WtJ49jGshW). Having issues with slate? There's a there's a [Slack](https://slate-slack.herokuapp.com/) for that as well.

Any questions about slate-yjs itself? Thead over to the #slate-yjs channel inside the [Slate Slack](https://slate-slack.herokuapp.com/) or post something in the [Discussions](https://github.com/BitPhinix/slate-yjs/discussions)

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 wanted`](https://github.com/BitPhinix/slate-yjs/labels/help%20wanted) label if you're looking for somewhere to start.

Please include tests and docs with every pull request!

## Repository Setup

The slate-yjs repository is a monorepo that is managed with [yarn workspaces](https://yarnpkg.com/features/workspaces). Unlike more traditional repositories, this means that the repository must be built in order for common development activities to function as expected.

To run the build, you need to have the slate-yjs 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:

```text
yarn install
yarn build
```

## Running Examples

To run the examples, start by building the monorepo as described in the [Repository Setup](contributing.md#repository-setup) section.

Then you can start the examples server with:

```text
yarn dev
```

## Running Tests

To run the tests, start by building the monorepo as described in the [Repository Setup](contributing.md#repository-setup) section.

Then you can rerun the tests with:

```text
yarn test
```

For instructions on how to debug something, head over to the [vitest docs](https://vitest.dev/guide/debugging.html).

In addition to tests you should also run the linter:

```text
yarn lint
```

This will catch TypeScript, Prettier, and Eslint errors.
3 changes: 0 additions & 3 deletions docs/walkthroughs/cursors-overlay.md

This file was deleted.

3 changes: 1 addition & 2 deletions examples/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@
"dev": "yarn build --watch --onSuccess \"yarn run start\""
},
"devDependencies": {
"slate": "~0.75.0",
"tsup": "^5.12.1"
},
"dependencies": {
"@hocuspocus/extension-logger": "^1.0.0-alpha.76",
"@hocuspocus/extension-sqlite": "^1.0.0-alpha.16",
"@hocuspocus/server": "^1.0.0-alpha.102",
"@slate-yjs/core": "workspace:^",
"slate": "0.75.0",
"slate": "^0.82.1",
"yjs": "^13.5.29"
}
}
4 changes: 2 additions & 2 deletions examples/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^6.0.1",
"slate": "^0.75.0",
"slate-react": "^0.75.0",
"slate": "^0.82.1",
"slate-react": "^0.83.0",
"yjs": "^13.5.29"
},
"devDependencies": {
Expand Down
15 changes: 11 additions & 4 deletions examples/frontend/src/components/CustomEditable/CustomEditable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ import { Spinner } from '../Spinner/Spinner';
type CustomEditableProps = Omit<
ComponentProps<typeof Editable>,
'renderElement' | 'renderLeaf'
>;
> &
Partial<
Pick<ComponentProps<typeof Editable>, 'renderElement' | 'renderLeaf'>
>;

export function CustomEditable(props: CustomEditableProps) {
export function CustomEditable({
renderElement = Element,
renderLeaf = Leaf,
...props
}: CustomEditableProps) {
const editor = useSlate();

if (editor.sharedRoot.length === 0) {
Expand All @@ -20,8 +27,8 @@ export function CustomEditable(props: CustomEditableProps) {
<Editable
placeholder="Write something ..."
{...props}
renderElement={Element}
renderLeaf={Leaf}
renderElement={renderElement}
renderLeaf={renderLeaf}
/>
);
}
5 changes: 4 additions & 1 deletion examples/frontend/src/components/Navigator/Navigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,17 @@ export function Navigator() {
<nav
className={clsx(
'navigation-dropdown-content border-2 bg-white invisible border-gray-800 rounded',
'w-60 absolute left-0 top-full transition-all opacity-0 mt-2 z-10'
'w-70 absolute left-0 top-full transition-all opacity-0 mt-2 z-10'
)}
>
<ul className="py-1">
<DropdownElement to="/simple">Simple</DropdownElement>
<DropdownElement to="/remote-cursors-overlay">
Remote cursors (overlay)
</DropdownElement>
<DropdownElement to="/remote-cursors-decoration">
Remote cursors (decorations)
</DropdownElement>
</ul>
</nav>
</div>
Expand Down
5 changes: 5 additions & 0 deletions examples/frontend/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Navigator } from './components/Navigator/Navigator';
import { NotFound } from './pages/NotFound';
import { RemoteCursorsOverlayPage } from './pages/RemoteCursorOverlay';
import { SimplePage } from './pages/Simple';
import { RemoteCursorDecorations } from './pages/RemoteCursorDecorations';

ReactDOM.render(
<StrictMode>
Expand All @@ -16,6 +17,10 @@ ReactDOM.render(
path="/remote-cursors-overlay"
element={<RemoteCursorsOverlayPage />}
/>
<Route
path="/remote-cursors-decoration"
element={<RemoteCursorDecorations />}
/>
<Route path="/simple" element={<SimplePage />} />
<Route path="/" element={<Navigate to="/remote-cursors-overlay" />} />
<Route path="*" element={<NotFound />} />
Expand Down

0 comments on commit 3710c48

Please sign in to comment.