@portabletext/editor 1.5.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,8 +10,348 @@
10
10
  > The official editor for editing [Portable Text](https://github.com/portabletext/portabletext) – the JSON based rich text specification for modern content editing platforms.
11
11
 
12
12
  > [!NOTE]
13
- > We are currently working hard on the general release of this component. Docs and refined APIs are coming.
13
+ > We are currently working hard on the general release of this component. Better docs and refined APIs are coming.
14
14
 
15
15
  ## End-User Experience
16
16
 
17
17
  In order to provide a robust and consistent end-user experience, the editor is backed by an elaborate E2E test suite generated from a [human-readable Gherkin spec](/packages/editor/gherkin-spec/).
18
+
19
+ ## Build Your Own Portable Text Editor
20
+
21
+ Check [/examples/basic/src/App.tsx](/examples/basic/src/App.tsx) for a basic example of how to set up the edior. Most of the source code from this example app can also be found in the instructions below.
22
+
23
+ ### Define the Schema
24
+
25
+ The first thing to do is to define the editor schema definition. The schema definition is later passed into the editor where it's compiled and used in various callbacks and render functions.
26
+
27
+ ```ts
28
+ // All options are optional
29
+ // Only the `name` property is required, but you can define a `title` and an `icon` as well
30
+ // You can use this schema definition later to build your toolbar
31
+ const schemaDefinition = defineSchema({
32
+ // Decorators are simple marks that don't hold any data
33
+ decorators: [{name: 'strong'}, {name: 'em'}, {name: 'underline'}],
34
+ // Annotations are more complex marks that can hold data
35
+ annotations: [{name: 'link'}],
36
+ // Styles apply to entire text blocks
37
+ // There's always a 'normal' style that can be considered the paragraph style
38
+ styles: [
39
+ {name: 'normal'},
40
+ {name: 'h1'},
41
+ {name: 'h2'},
42
+ {name: 'h3'},
43
+ {name: 'blockqoute'},
44
+ ],
45
+ // Lists apply to entire text blocks as well
46
+ lists: [{name: 'bullet'}, {name: 'number'}],
47
+ // Inline objects hold arbitrary data that can be inserted into the text
48
+ inlineObjects: [{name: 'stock-ticker'}],
49
+ // Block objects hold arbitrary data that live side-by-side with text blocks
50
+ blockObjects: [{name: 'image'}],
51
+ })
52
+ ```
53
+
54
+ ### Render the Editor Component
55
+
56
+ Use `useEditor` to create an `editor` and pass that into `PortableTextEditor`. Use the `editor.on` method to listen for `mutation` changes inside the editor so you can use and store the value produced.
57
+
58
+ ```tsx
59
+ function App() {
60
+ // Creata an editor
61
+ const editor = useEditor({
62
+ schemaDefinition,
63
+ })
64
+
65
+ const [value, setValue] = useState<Array<PortableTextBlock> | undefined>(
66
+ undefined,
67
+ )
68
+
69
+ // Subscribe to editor changes
70
+ useEffect(() => {
71
+ const subscription = editor.on('mutation', (mutation) => {
72
+ setValue(mutation.snapshot)
73
+ })
74
+
75
+ return () => {
76
+ subscription.unsubscribe()
77
+ }
78
+ }, [editor])
79
+
80
+ return (
81
+ <>
82
+ <PortableTextEditor
83
+ // Pass in the `editor` you created earlier
84
+ editor={editor}
85
+ // And an optional value
86
+ value={value}
87
+ >
88
+ {/* Toolbar needs to be rendered inside the `PortableTextEditor` component */}
89
+ <Toolbar />
90
+ {/* Component that controls the actual rendering of the editor */}
91
+ <PortableTextEditable
92
+ style={{border: '1px solid black', padding: '0.5em'}}
93
+ // Control how decorators are rendered
94
+ renderDecorator={renderDecorator}
95
+ // Control how annotations are rendered
96
+ renderAnnotation={renderAnnotation}
97
+ // Required to render block objects but also to make `renderStyle` take effect
98
+ renderBlock={renderBlock}
99
+ // Control how styles are rendered
100
+ renderStyle={renderStyle}
101
+ // Control how inline objects are rendered
102
+ renderChild={renderChild}
103
+ // Rendering lists is harder and most likely requires a fair amount of CSS
104
+ // However, we still need to return and render the list item's children to ensure proper rendering
105
+ renderListItem={(props) => <>{props.children}</>}
106
+ />
107
+ </PortableTextEditor>
108
+ <pre style={{border: '1px dashed black', padding: '0.5em'}}>
109
+ {JSON.stringify(value, null, 2)}
110
+ </pre>
111
+ </>
112
+ )
113
+ }
114
+ ```
115
+
116
+ ### Render Marks, Blocks and Objects
117
+
118
+ All the different render functions passed to `PortableTextEditable` can be defined as stand-alone React components. Most of these are fairly straightforward to render because everything you need is provided via `props`. However, lists are a little special. Since Portable Text has no concept of block nesting, the easiest way get something looking like lists is with pure CSS. Head over to [/examples/basic/src/editor.css](/examples/basic/src/editor.css) for a full example.
119
+
120
+ ```tsx
121
+ const renderDecorator: RenderDecoratorFunction = (props) => {
122
+ if (props.value === 'strong') {
123
+ return <strong>{props.children}</strong>
124
+ }
125
+ if (props.value === 'em') {
126
+ return <em>{props.children}</em>
127
+ }
128
+ if (props.value === 'underline') {
129
+ return <u>{props.children}</u>
130
+ }
131
+ return <>{props.children}</>
132
+ }
133
+
134
+ const renderAnnotation: RenderAnnotationFunction = (props) => {
135
+ if (props.schemaType.name === 'link') {
136
+ return <span style={{textDecoration: 'underline'}}>{props.children}</span>
137
+ }
138
+
139
+ return <>{props.children}</>
140
+ }
141
+
142
+ const renderBlock: RenderBlockFunction = (props) => {
143
+ if (props.schemaType.name === 'image' && isImage(props.value)) {
144
+ return (
145
+ <div
146
+ style={{
147
+ border: '1px dotted grey',
148
+ padding: '0.25em',
149
+ marginBlockEnd: '0.25em',
150
+ }}
151
+ >
152
+ IMG: {props.value.src}
153
+ </div>
154
+ )
155
+ }
156
+
157
+ return <div style={{marginBlockEnd: '0.25em'}}>{props.children}</div>
158
+ }
159
+
160
+ function isImage(
161
+ props: PortableTextBlock,
162
+ ): props is PortableTextBlock & {src: string} {
163
+ return 'src' in props
164
+ }
165
+
166
+ const renderStyle: RenderStyleFunction = (props) => {
167
+ if (props.schemaType.value === 'h1') {
168
+ return <h1>{props.children}</h1>
169
+ }
170
+ if (props.schemaType.value === 'h2') {
171
+ return <h2>{props.children}</h2>
172
+ }
173
+ if (props.schemaType.value === 'h3') {
174
+ return <h3>{props.children}</h3>
175
+ }
176
+ if (props.schemaType.value === 'blockquote') {
177
+ return <blockquote>{props.children}</blockquote>
178
+ }
179
+ return <>{props.children}</>
180
+ }
181
+
182
+ const renderChild: RenderChildFunction = (props) => {
183
+ if (props.schemaType.name === 'stock-ticker' && isStockTicker(props.value)) {
184
+ return (
185
+ <span
186
+ style={{
187
+ border: '1px dotted grey',
188
+ padding: '0.15em',
189
+ }}
190
+ >
191
+ {props.value.symbol}
192
+ </span>
193
+ )
194
+ }
195
+
196
+ return <>{props.children}</>
197
+ }
198
+
199
+ function isStockTicker(
200
+ props: PortableTextChild,
201
+ ): props is PortableTextChild & {symbol: string} {
202
+ return 'symbol' in props
203
+ }
204
+ ```
205
+
206
+ ### Render the Toolbar
207
+
208
+ Your toolbar needs to be rendered within `PortableTextEditor` because it requires a reference to the `editorInstance` that it produces. To toggle marks and styles and to insert objects, you'll have to use this `editorInstance` together with static methods on the `PortableTextEditor` class.
209
+
210
+ ```tsx
211
+ function Toolbar() {
212
+ // Obtain the editor instance provided from the `PortableTextEditor` component
213
+ const editorInstance = usePortableTextEditor()
214
+ // Rerender the toolbar whenever the selection changes
215
+ usePortableTextEditorSelection()
216
+
217
+ const decoratorButtons = schemaDefinition.decorators.map((decorator) => {
218
+ return (
219
+ <button
220
+ key={decorator.name}
221
+ style={{
222
+ textDecoration: PortableTextEditor.isMarkActive(
223
+ editorInstance,
224
+ decorator.name,
225
+ )
226
+ ? 'underline'
227
+ : 'unset',
228
+ }}
229
+ onClick={() => {
230
+ // Toggle the decorator by name
231
+ PortableTextEditor.toggleMark(editorInstance, decorator.name)
232
+ // Pressing this button steals focus so let's focus the editor again
233
+ PortableTextEditor.focus(editorInstance)
234
+ }}
235
+ >
236
+ {decorator.name}
237
+ </button>
238
+ )
239
+ })
240
+
241
+ const linkButton = (
242
+ <button
243
+ style={{
244
+ textDecoration: PortableTextEditor.isAnnotationActive(
245
+ editorInstance,
246
+ schemaDefinition.annotations[0].name,
247
+ )
248
+ ? 'underline'
249
+ : 'unset',
250
+ }}
251
+ onClick={() => {
252
+ if (
253
+ PortableTextEditor.isAnnotationActive(
254
+ editorInstance,
255
+ schemaDefinition.annotations[0].name,
256
+ )
257
+ ) {
258
+ PortableTextEditor.removeAnnotation(
259
+ editorInstance,
260
+ schemaDefinition.annotations[0],
261
+ )
262
+ } else {
263
+ PortableTextEditor.addAnnotation(
264
+ editorInstance,
265
+ schemaDefinition.annotations[0],
266
+ {href: 'https://example.com'},
267
+ )
268
+ }
269
+ PortableTextEditor.focus(editorInstance)
270
+ }}
271
+ >
272
+ link
273
+ </button>
274
+ )
275
+
276
+ const styleButtons = schemaDefinition.styles.map((style) => (
277
+ <button
278
+ key={style.name}
279
+ style={{
280
+ textDecoration: PortableTextEditor.hasBlockStyle(
281
+ editorInstance,
282
+ style.name,
283
+ )
284
+ ? 'underline'
285
+ : 'unset',
286
+ }}
287
+ onClick={() => {
288
+ PortableTextEditor.toggleBlockStyle(editorInstance, style.name)
289
+ PortableTextEditor.focus(editorInstance)
290
+ }}
291
+ >
292
+ {style.name}
293
+ </button>
294
+ ))
295
+
296
+ const listButtons = schemaDefinition.lists.map((list) => (
297
+ <button
298
+ key={list.name}
299
+ style={{
300
+ textDecoration: PortableTextEditor.hasListStyle(
301
+ editorInstance,
302
+ list.name,
303
+ )
304
+ ? 'underline'
305
+ : 'unset',
306
+ }}
307
+ onClick={() => {
308
+ PortableTextEditor.toggleList(editorInstance, list.name)
309
+ PortableTextEditor.focus(editorInstance)
310
+ }}
311
+ >
312
+ {list.name}
313
+ </button>
314
+ ))
315
+
316
+ const imageButton = (
317
+ <button
318
+ onClick={() => {
319
+ PortableTextEditor.insertBlock(
320
+ editorInstance,
321
+ schemaDefinition.blockObjects[0],
322
+ {src: 'https://example.com/image.jpg'},
323
+ )
324
+ PortableTextEditor.focus(editorInstance)
325
+ }}
326
+ >
327
+ {schemaDefinition.blockObjects[0].name}
328
+ </button>
329
+ )
330
+
331
+ const stockTickerButton = (
332
+ <button
333
+ onClick={() => {
334
+ PortableTextEditor.insertChild(
335
+ editorInstance,
336
+ schemaDefinition.inlineObjects[0],
337
+ {symbol: 'AAPL'},
338
+ )
339
+ PortableTextEditor.focus(editorInstance)
340
+ }}
341
+ >
342
+ {schemaDefinition.inlineObjects[0].name}
343
+ </button>
344
+ )
345
+
346
+ return (
347
+ <>
348
+ <div>{decoratorButtons}</div>
349
+ <div>{linkButton}</div>
350
+ <div>{styleButtons}</div>
351
+ <div>{listButtons}</div>
352
+ <div>{imageButton}</div>
353
+ <div>{stockTickerButton}</div>
354
+ </>
355
+ )
356
+ }
357
+ ```
package/lib/index.d.mts CHANGED
@@ -7,7 +7,6 @@ import type {
7
7
  KeyedSegment,
8
8
  ObjectSchemaType,
9
9
  Path,
10
- PortableTextChild,
11
10
  PortableTextListBlock,
12
11
  PortableTextObject,
13
12
  TypedObject,
@@ -15,6 +14,7 @@ import type {
15
14
  import {
16
15
  BlockDecoratorDefinition,
17
16
  PortableTextBlock,
17
+ PortableTextChild,
18
18
  PortableTextSpan,
19
19
  PortableTextTextBlock,
20
20
  } from '@sanity/types'
@@ -3137,6 +3137,8 @@ export declare type PickFromUnion<
3137
3137
 
3138
3138
  export {PortableTextBlock}
3139
3139
 
3140
+ export {PortableTextChild}
3141
+
3140
3142
  /**
3141
3143
  * @public
3142
3144
  */
package/lib/index.d.ts CHANGED
@@ -7,7 +7,6 @@ import type {
7
7
  KeyedSegment,
8
8
  ObjectSchemaType,
9
9
  Path,
10
- PortableTextChild,
11
10
  PortableTextListBlock,
12
11
  PortableTextObject,
13
12
  TypedObject,
@@ -15,6 +14,7 @@ import type {
15
14
  import {
16
15
  BlockDecoratorDefinition,
17
16
  PortableTextBlock,
17
+ PortableTextChild,
18
18
  PortableTextSpan,
19
19
  PortableTextTextBlock,
20
20
  } from '@sanity/types'
@@ -3137,6 +3137,8 @@ export declare type PickFromUnion<
3137
3137
 
3138
3138
  export {PortableTextBlock}
3139
3139
 
3140
+ export {PortableTextChild}
3141
+
3140
3142
  /**
3141
3143
  * @public
3142
3144
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/editor",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export type {Patch} from '@portabletext/patches'
2
- export type {PortableTextBlock} from '@sanity/types'
2
+ export type {PortableTextBlock, PortableTextChild} from '@sanity/types'
3
3
  export {
4
4
  createMarkdownBehaviors,
5
5
  type MarkdownBehaviorsConfig,