-
Notifications
You must be signed in to change notification settings - Fork 3
/
App.tsx
343 lines (321 loc) · 8.97 KB
/
App.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
import {
defineSchema,
EditorEventListener,
EditorProvider,
keyGenerator,
PortableTextBlock,
PortableTextChild,
PortableTextEditable,
PortableTextEditor,
RenderAnnotationFunction,
RenderBlockFunction,
RenderChildFunction,
RenderDecoratorFunction,
RenderStyleFunction,
usePortableTextEditor,
usePortableTextEditorSelection,
} from '@portabletext/editor'
import {useState} from 'react'
import './editor.css'
// Define the schema for the editor
// All options are optional
// Only the `name` property is required, but you can define a `title` and an `icon` as well
// You can use this schema definition later to build your toolbar
const schemaDefinition = defineSchema({
// Decorators are simple marks that don't hold any data
decorators: [{name: 'strong'}, {name: 'em'}, {name: 'underline'}],
// Annotations are more complex marks that can hold data
annotations: [{name: 'link'}],
// Styles apply to entire text blocks
// There's always a 'normal' style that can be considered the paragraph style
styles: [
{name: 'normal'},
{name: 'h1'},
{name: 'h2'},
{name: 'h3'},
{name: 'blockqoute'},
],
// Lists apply to entire text blocks as well
lists: [{name: 'bullet'}, {name: 'number'}],
// Inline objects hold arbitrary data that can be inserted into the text
inlineObjects: [{name: 'stock-ticker'}],
// Block objects hold arbitrary data that live side-by-side with text blocks
blockObjects: [{name: 'image'}],
})
function App() {
const [value, setValue] = useState<Array<PortableTextBlock> | undefined>(
// Initial value
() => [
{
_type: 'block',
_key: keyGenerator(),
children: [
{_type: 'span', _key: keyGenerator(), text: 'Hello, '},
{
_type: 'span',
_key: keyGenerator(),
text: 'world!',
marks: ['strong'],
},
],
},
],
)
return (
<>
{/* Create an editor */}
<EditorProvider
config={{
schemaDefinition,
initialValue: value,
}}
>
{/* Subscribe to editor changes */}
<EditorEventListener
on={(event) => {
if (event.type === 'mutation') {
setValue(event.snapshot)
}
}}
/>
{/* Toolbar needs to be rendered inside the `EditorProvider` component */}
<Toolbar />
{/* Component that controls the actual rendering of the editor */}
<PortableTextEditable
style={{border: '1px solid black', padding: '0.5em'}}
// Control how decorators are rendered
renderDecorator={renderDecorator}
// Control how annotations are rendered
renderAnnotation={renderAnnotation}
// Required to render block objects but also to make `renderStyle` take effect
renderBlock={renderBlock}
// Control how styles are rendered
renderStyle={renderStyle}
// Control how inline objects are rendered
renderChild={renderChild}
// Rendering lists is harder and most likely requires a fair amount of CSS
// First, return the children like here
// Next, look in the imported `editor.css` file to see how list styles are implemented
renderListItem={(props) => <>{props.children}</>}
/>
</EditorProvider>
<pre style={{border: '1px dashed black', padding: '0.5em'}}>
{JSON.stringify(value, null, 2)}
</pre>
</>
)
}
const renderDecorator: RenderDecoratorFunction = (props) => {
if (props.value === 'strong') {
return <strong>{props.children}</strong>
}
if (props.value === 'em') {
return <em>{props.children}</em>
}
if (props.value === 'underline') {
return <u>{props.children}</u>
}
return <>{props.children}</>
}
const renderAnnotation: RenderAnnotationFunction = (props) => {
if (props.schemaType.name === 'link') {
return <span style={{textDecoration: 'underline'}}>{props.children}</span>
}
return <>{props.children}</>
}
const renderBlock: RenderBlockFunction = (props) => {
if (props.schemaType.name === 'image' && isImage(props.value)) {
return (
<div
style={{
border: '1px dotted grey',
padding: '0.25em',
marginBlockEnd: '0.25em',
}}
>
IMG: {props.value.src}
</div>
)
}
return <div style={{marginBlockEnd: '0.25em'}}>{props.children}</div>
}
function isImage(
props: PortableTextBlock,
): props is PortableTextBlock & {src: string} {
return 'src' in props
}
const renderStyle: RenderStyleFunction = (props) => {
if (props.schemaType.value === 'h1') {
return <h1>{props.children}</h1>
}
if (props.schemaType.value === 'h2') {
return <h2>{props.children}</h2>
}
if (props.schemaType.value === 'h3') {
return <h3>{props.children}</h3>
}
if (props.schemaType.value === 'blockquote') {
return <blockquote>{props.children}</blockquote>
}
return <>{props.children}</>
}
const renderChild: RenderChildFunction = (props) => {
if (props.schemaType.name === 'stock-ticker' && isStockTicker(props.value)) {
return (
<span
style={{
border: '1px dotted grey',
padding: '0.15em',
}}
>
{props.value.symbol}
</span>
)
}
return <>{props.children}</>
}
function isStockTicker(
props: PortableTextChild,
): props is PortableTextChild & {symbol: string} {
return 'symbol' in props
}
function Toolbar() {
// Obtain the editor instance
const editorInstance = usePortableTextEditor()
// Rerender the toolbar whenever the selection changes
usePortableTextEditorSelection()
const decoratorButtons = schemaDefinition.decorators.map((decorator) => {
return (
<button
key={decorator.name}
style={{
textDecoration: PortableTextEditor.isMarkActive(
editorInstance,
decorator.name,
)
? 'underline'
: 'unset',
}}
onClick={() => {
// Toggle the decorator by name
PortableTextEditor.toggleMark(editorInstance, decorator.name)
// Pressing this button steals focus so let's focus the editor again
PortableTextEditor.focus(editorInstance)
}}
>
{decorator.name}
</button>
)
})
const linkButton = (
<button
style={{
textDecoration: PortableTextEditor.isAnnotationActive(
editorInstance,
schemaDefinition.annotations[0].name,
)
? 'underline'
: 'unset',
}}
onClick={() => {
if (
PortableTextEditor.isAnnotationActive(
editorInstance,
schemaDefinition.annotations[0].name,
)
) {
PortableTextEditor.removeAnnotation(
editorInstance,
schemaDefinition.annotations[0],
)
} else {
PortableTextEditor.addAnnotation(
editorInstance,
schemaDefinition.annotations[0],
{href: 'https://example.com'},
)
}
PortableTextEditor.focus(editorInstance)
}}
>
link
</button>
)
const styleButtons = schemaDefinition.styles.map((style) => (
<button
key={style.name}
style={{
textDecoration: PortableTextEditor.hasBlockStyle(
editorInstance,
style.name,
)
? 'underline'
: 'unset',
}}
onClick={() => {
PortableTextEditor.toggleBlockStyle(editorInstance, style.name)
PortableTextEditor.focus(editorInstance)
}}
>
{style.name}
</button>
))
const listButtons = schemaDefinition.lists.map((list) => (
<button
key={list.name}
style={{
textDecoration: PortableTextEditor.hasListStyle(
editorInstance,
list.name,
)
? 'underline'
: 'unset',
}}
onClick={() => {
PortableTextEditor.toggleList(editorInstance, list.name)
PortableTextEditor.focus(editorInstance)
}}
>
{list.name}
</button>
))
const imageButton = (
<button
onClick={() => {
PortableTextEditor.insertBlock(
editorInstance,
schemaDefinition.blockObjects[0],
{src: 'https://example.com/image.jpg'},
)
PortableTextEditor.focus(editorInstance)
}}
>
{schemaDefinition.blockObjects[0].name}
</button>
)
const stockTickerButton = (
<button
onClick={() => {
PortableTextEditor.insertChild(
editorInstance,
schemaDefinition.inlineObjects[0],
{symbol: 'AAPL'},
)
PortableTextEditor.focus(editorInstance)
}}
>
{schemaDefinition.inlineObjects[0].name}
</button>
)
return (
<>
<div>{decoratorButtons}</div>
<div>{linkButton}</div>
<div>{styleButtons}</div>
<div>{listButtons}</div>
<div>{imageButton}</div>
<div>{stockTickerButton}</div>
</>
)
}
export default App