Skip to content

Commit 56f53d9

Browse files
committed
added react-prosemirror example
1 parent 957ab75 commit 56f53d9

File tree

7 files changed

+1880
-0
lines changed

7 files changed

+1880
-0
lines changed

react-prosemirror/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# react-prosemirror
2+
> Short example on how to set up `@nytimes/prosemirror` with y-prosemirror

react-prosemirror/index.html

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>React-ProseMirror Demo</title>
7+
<link rel=stylesheet href="./prosemirror.css" async defer>
8+
<style>
9+
.ProseMirror {
10+
border: thin solid #ccc;
11+
border-radius: 0.25rem;
12+
padding: 1rem;
13+
outline: none;
14+
min-height: 700px;
15+
}
16+
17+
main {
18+
margin: auto;
19+
width: 80%;
20+
max-width: 700px;
21+
}
22+
23+
.menu {
24+
display: flex;
25+
margin-bottom: 5px;
26+
}
27+
28+
.button {
29+
cursor: pointer;
30+
width: 36px;
31+
height: 36px;
32+
margin-right: 5px;
33+
display: flex;
34+
align-items: center;
35+
justify-content: center;
36+
box-shadow: none;
37+
border: thin solid #ccc;
38+
border-radius: 0.25rem;
39+
color: black;
40+
background-color: white;
41+
42+
&[aria-pressed="true"] {
43+
background-color: #ddd;
44+
color: blue;
45+
}
46+
}
47+
48+
.button.bold {
49+
font-weight: 700;
50+
}
51+
52+
.button.italic {
53+
font-style: italic;
54+
}
55+
56+
.visually-hidden {
57+
clip: rect(0 0 0 0);
58+
clip-path: inset(50%);
59+
height: 1px;
60+
overflow: hidden;
61+
position: absolute;
62+
white-space: nowrap;
63+
width: 1px;
64+
}
65+
</style>
66+
<!-- y-prosemirror specific styles -->
67+
<style>
68+
placeholder {
69+
display: inline;
70+
border: 1px solid #ccc;
71+
color: #ccc;
72+
}
73+
placeholder:after {
74+
content: "☁";
75+
font-size: 200%;
76+
line-height: 0.1;
77+
font-weight: bold;
78+
}
79+
.ProseMirror img { max-width: 100px }
80+
81+
/* this is a rough fix for the first cursor position when the first paragraph is empty */
82+
.ProseMirror > .ProseMirror-yjs-cursor:first-child {
83+
margin-top: 16px;
84+
}
85+
.ProseMirror p:first-child, .ProseMirror h1:first-child, .ProseMirror h2:first-child, .ProseMirror h3:first-child, .ProseMirror h4:first-child, .ProseMirror h5:first-child, .ProseMirror h6:first-child {
86+
margin-top: 16px
87+
}
88+
/* This gives the remote user caret. The colors are automatically overwritten*/
89+
.ProseMirror-yjs-cursor {
90+
position: relative;
91+
margin-left: -1px;
92+
margin-right: -1px;
93+
border-left: 1px solid black;
94+
border-right: 1px solid black;
95+
border-color: orange;
96+
word-break: normal;
97+
pointer-events: none;
98+
}
99+
/* This renders the username above the caret */
100+
.ProseMirror-yjs-cursor > div {
101+
position: absolute;
102+
top: -1.05em;
103+
left: -1px;
104+
font-size: 13px;
105+
background-color: rgb(250, 129, 0);
106+
font-family: serif;
107+
font-style: normal;
108+
font-weight: normal;
109+
line-height: normal;
110+
user-select: none;
111+
color: white;
112+
padding-left: 2px;
113+
padding-right: 2px;
114+
white-space: nowrap;
115+
}
116+
#y-functions {
117+
position: absolute;
118+
top: 20px;
119+
right: 20px;
120+
}
121+
#y-functions > * {
122+
display: inline-block;
123+
}
124+
</style>
125+
<style>
126+
.ProseMirror {
127+
position: relative;
128+
}
129+
130+
.ProseMirror {
131+
word-wrap: break-word;
132+
white-space: pre-wrap;
133+
white-space: break-spaces;
134+
-webkit-font-variant-ligatures: none;
135+
font-variant-ligatures: none;
136+
font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */
137+
}
138+
139+
.ProseMirror pre {
140+
white-space: pre-wrap;
141+
}
142+
143+
.ProseMirror li {
144+
position: relative;
145+
}
146+
147+
.ProseMirror-hideselection *::selection { background: transparent; }
148+
.ProseMirror-hideselection *::-moz-selection { background: transparent; }
149+
.ProseMirror-hideselection { caret-color: transparent; }
150+
151+
/* See https://github.com/ProseMirror/prosemirror/issues/1421#issuecomment-1759320191 */
152+
.ProseMirror [draggable][contenteditable=false] { user-select: text }
153+
154+
.ProseMirror-selectednode {
155+
outline: 2px solid #8cf;
156+
}
157+
158+
/* Make sure li selections wrap around markers */
159+
160+
li.ProseMirror-selectednode {
161+
outline: none;
162+
}
163+
164+
li.ProseMirror-selectednode:after {
165+
content: "";
166+
position: absolute;
167+
left: -32px;
168+
right: -2px; top: -2px; bottom: -2px;
169+
border: 2px solid #8cf;
170+
pointer-events: none;
171+
}
172+
173+
/* Protect against generic img rules */
174+
175+
img.ProseMirror-separator {
176+
display: inline !important;
177+
border: none !important;
178+
margin: 0 !important;
179+
}
180+
</style>
181+
</head>
182+
<body>
183+
<div id="root"></div>
184+
<script type="module" src="dist/index.js"></script>
185+
</body>
186+
</html>

react-prosemirror/index.jsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React, { useState } from "react"
2+
import { createRoot } from "react-dom/client"
3+
import { EditorState } from "prosemirror-state"
4+
import { ProseMirror } from "@nytimes/react-prosemirror"
5+
6+
import { exampleSetup } from 'prosemirror-example-setup'
7+
import { schema } from "prosemirror-schema-basic"
8+
import {keymap} from "prosemirror-keymap"
9+
import {baseKeymap} from "prosemirror-commands"
10+
11+
import * as Y from 'yjs'
12+
import { WebsocketProvider } from 'y-websocket'
13+
import { ySyncPlugin, yCursorPlugin, yUndoPlugin, undo, redo, initProseMirrorDoc } from 'y-prosemirror'
14+
15+
16+
const ydoc = new Y.Doc()
17+
const provider = new WebsocketProvider(
18+
'wss://demos.yjs.dev/ws', // use the public ws server
19+
// `ws${location.protocol.slice(4)}//${location.host}/ws`, // alternatively: use the local ws server (run `npm start` in root directory)
20+
'prosemirror-demo-2024/06',
21+
ydoc
22+
)
23+
24+
function App () {
25+
const yXmlFragment = ydoc.getXmlFragment('prosemirror')
26+
const { doc, mapping } = initProseMirrorDoc(yXmlFragment, schema)
27+
const defaultState = EditorState.create({
28+
doc,
29+
schema,
30+
plugins: [
31+
ySyncPlugin(yXmlFragment, { mapping }),
32+
yCursorPlugin(provider.awareness),
33+
yUndoPlugin(),
34+
keymap({
35+
'Mod-z': undo,
36+
'Mod-y': redo,
37+
'Mod-Shift-z': redo
38+
}),
39+
keymap(baseKeymap)
40+
].concat(exampleSetup({ schema }))
41+
});
42+
43+
44+
// It's important that mount is stored as state,
45+
// rather than a ref, so that the ProseMirror component
46+
// is re-rendered when it's set
47+
const [mount, setMount] = useState(null);
48+
49+
return (
50+
<ProseMirror mount={mount} defaultState={defaultState}>
51+
<div ref={setMount} />
52+
</ProseMirror>
53+
);
54+
}
55+
56+
setTimeout(() => {
57+
const root = createRoot(document.getElementById('root'))
58+
root.render(<App />)
59+
}, 1000)

react-prosemirror/menu.jsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { toggleMark } from "prosemirror-commands";
2+
import React from "react";
3+
4+
import { useEditorEventCallback, useEditorState } from "@nytimes/react-prosemirror";
5+
6+
// lifted from:
7+
// https://github.com/ProseMirror/prosemirror-example-setup/blob/master/src/menu.ts#L58
8+
function isMarkActive(mark, state) {
9+
const { from, $from, to, empty } = state.selection;
10+
return empty
11+
? !!mark.isInSet(state.storedMarks || $from.marks())
12+
: state.doc.rangeHasMark(from, to, mark);
13+
}
14+
15+
export function Button(props) {
16+
return (
17+
<button
18+
type="button"
19+
title={props.title}
20+
aria-pressed={props.isActive}
21+
className={`button ${props.className}`}
22+
onClick={props.onClick}
23+
>
24+
<span className="visually-hidden">{props.title}</span>
25+
<span aria-hidden>{props.children}</span>
26+
</button>
27+
);
28+
}
29+
30+
export default function Menu() {
31+
const state = useEditorState();
32+
33+
const toggleBold = useEditorEventCallback((view) => {
34+
const toggleBoldMark = toggleMark(view.state.schema.marks["strong"]);
35+
toggleBoldMark(view.state, view.dispatch, view);
36+
});
37+
38+
const toggleItalic = useEditorEventCallback((view) => {
39+
const toggleItalicMark = toggleMark(view.state.schema.marks["em"]);
40+
toggleItalicMark(view.state, view.dispatch, view);
41+
});
42+
43+
return (
44+
<div className="menu">
45+
<Button
46+
className="bold"
47+
title="Bold (⌘b)"
48+
isActive={isMarkActive(state.schema.marks["strong"], state)}
49+
onClick={toggleBold}
50+
>
51+
B
52+
</Button>
53+
<Button
54+
className="italic"
55+
title="Italic (⌘i)"
56+
isActive={isMarkActive(state.schema.marks["em"], state)}
57+
onClick={toggleItalic}
58+
>
59+
I
60+
</Button>
61+
</div>
62+
);
63+
}

0 commit comments

Comments
 (0)