@portabletext/editor 1.4.1 → 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
@@ -2,21 +2,22 @@ import {Patch} from '@portabletext/patches'
2
2
  import type {
3
3
  ArrayDefinition,
4
4
  ArraySchemaType,
5
- BlockDecoratorDefinition,
6
5
  BlockListDefinition,
7
- BlockSchemaType,
8
6
  BlockStyleDefinition,
9
7
  KeyedSegment,
10
8
  ObjectSchemaType,
11
9
  Path,
12
- PortableTextBlock,
13
- PortableTextChild,
14
10
  PortableTextListBlock,
15
11
  PortableTextObject,
16
- SpanSchemaType,
17
12
  TypedObject,
18
13
  } from '@sanity/types'
19
- import {PortableTextSpan, PortableTextTextBlock} from '@sanity/types'
14
+ import {
15
+ BlockDecoratorDefinition,
16
+ PortableTextBlock,
17
+ PortableTextChild,
18
+ PortableTextSpan,
19
+ PortableTextTextBlock,
20
+ } from '@sanity/types'
20
21
  import type {
21
22
  BaseSyntheticEvent,
22
23
  ClipboardEvent as ClipboardEvent_2,
@@ -64,6 +65,15 @@ import {
64
65
  Values,
65
66
  } from 'xstate'
66
67
 
68
+ /**
69
+ * @alpha
70
+ */
71
+ export declare type BaseDefinition = {
72
+ name: string
73
+ title?: string
74
+ icon?: BlockDecoratorDefinition['icon']
75
+ }
76
+
67
77
  /**
68
78
  * @alpha
69
79
  */
@@ -303,10 +313,10 @@ export declare type createEditorOptions = {
303
313
  export declare function createMarkdownBehaviors(
304
314
  config: MarkdownBehaviorsConfig,
305
315
  ): Behavior<
306
- | 'insert break'
307
- | 'insert soft break'
308
316
  | 'delete backward'
309
317
  | 'delete forward'
318
+ | 'insert soft break'
319
+ | 'insert break'
310
320
  | 'insert text',
311
321
  true
312
322
  >[]
@@ -319,12 +329,23 @@ export declare function defineBehavior<
319
329
  TGuardResponse = true,
320
330
  >(behavior: Behavior<TBehaviorEventType, TGuardResponse>): Behavior
321
331
 
332
+ /**
333
+ * @alpha
334
+ */
335
+ export declare function defineSchema<
336
+ const TSchemaDefinition extends SchemaDefinition,
337
+ >(definition: TSchemaDefinition): TSchemaDefinition
338
+
322
339
  /** @beta */
323
340
  export declare interface EditableAPI {
324
341
  activeAnnotations: () => PortableTextObject[]
325
342
  isAnnotationActive: (annotationType: PortableTextObject['_type']) => boolean
326
- addAnnotation: (
327
- type: ObjectSchemaType,
343
+ addAnnotation: <
344
+ TSchemaType extends {
345
+ name: string
346
+ },
347
+ >(
348
+ type: TSchemaType,
328
349
  value?: {
329
350
  [prop: string]: unknown
330
351
  },
@@ -354,14 +375,22 @@ export declare interface EditableAPI {
354
375
  getValue: () => PortableTextBlock[] | undefined
355
376
  hasBlockStyle: (style: string) => boolean
356
377
  hasListStyle: (listStyle: string) => boolean
357
- insertBlock: (
358
- type: BlockSchemaType | ObjectSchemaType,
378
+ insertBlock: <
379
+ TSchemaType extends {
380
+ name: string
381
+ },
382
+ >(
383
+ type: TSchemaType,
359
384
  value?: {
360
385
  [prop: string]: unknown
361
386
  },
362
387
  ) => Path
363
- insertChild: (
364
- type: SpanSchemaType | ObjectSchemaType,
388
+ insertChild: <
389
+ TSchemaType extends {
390
+ name: string
391
+ },
392
+ >(
393
+ type: TSchemaType,
365
394
  value?: {
366
395
  [prop: string]: unknown
367
396
  },
@@ -377,7 +406,13 @@ export declare interface EditableAPI {
377
406
  isVoid: (element: PortableTextBlock | PortableTextChild) => boolean
378
407
  marks: () => string[]
379
408
  redo: () => void
380
- removeAnnotation: (type: ObjectSchemaType) => void
409
+ removeAnnotation: <
410
+ TSchemaType extends {
411
+ name: string
412
+ },
413
+ >(
414
+ type: TSchemaType,
415
+ ) => void
381
416
  select: (selection: EditorSelection) => void
382
417
  toggleBlockStyle: (blockStyle: string) => void
383
418
  toggleList: (listStyle: string) => void
@@ -430,8 +465,16 @@ export declare type EditorChanges = Subject<EditorChange>
430
465
  export declare type EditorConfig = {
431
466
  behaviors?: Array<Behavior>
432
467
  keyGenerator?: () => string
433
- schema: ArraySchemaType<PortableTextBlock> | ArrayDefinition
434
- }
468
+ } & (
469
+ | {
470
+ schemaDefinition: SchemaDefinition
471
+ schema?: undefined
472
+ }
473
+ | {
474
+ schemaDefinition?: undefined
475
+ schema: ArraySchemaType<PortableTextBlock> | ArrayDefinition
476
+ }
477
+ )
435
478
 
436
479
  /**
437
480
  * @internal
@@ -867,10 +910,10 @@ export declare const editorMachine: StateMachine<
867
910
  >
868
911
  }) => {
869
912
  behaviors: Behavior<
870
- | 'insert break'
871
- | 'insert soft break'
872
913
  | 'delete backward'
873
914
  | 'delete forward'
915
+ | 'insert soft break'
916
+ | 'insert break'
874
917
  | 'insert text',
875
918
  true
876
919
  >[]
@@ -3092,6 +3135,10 @@ export declare type PickFromUnion<
3092
3135
  TPickedTags extends TUnion[TTagKey],
3093
3136
  > = TUnion extends Record<TTagKey, TPickedTags> ? TUnion : never
3094
3137
 
3138
+ export {PortableTextBlock}
3139
+
3140
+ export {PortableTextChild}
3141
+
3095
3142
  /**
3096
3143
  * @public
3097
3144
  */
@@ -3156,9 +3203,13 @@ export declare class PortableTextEditor extends Component<
3156
3203
  editor: PortableTextEditor,
3157
3204
  annotationType: PortableTextObject['_type'],
3158
3205
  ) => boolean
3159
- static addAnnotation: (
3206
+ static addAnnotation: <
3207
+ TSchemaType extends {
3208
+ name: string
3209
+ },
3210
+ >(
3160
3211
  editor: PortableTextEditor,
3161
- type: ObjectSchemaType,
3212
+ type: TSchemaType,
3162
3213
  value?: {
3163
3214
  [prop: string]: unknown
3164
3215
  },
@@ -3231,16 +3282,24 @@ export declare class PortableTextEditor extends Component<
3231
3282
  editor: PortableTextEditor,
3232
3283
  mark: string,
3233
3284
  ) => boolean | undefined
3234
- static insertChild: (
3285
+ static insertChild: <
3286
+ TSchemaType extends {
3287
+ name: string
3288
+ },
3289
+ >(
3235
3290
  editor: PortableTextEditor,
3236
- type: SpanSchemaType | ObjectSchemaType,
3291
+ type: TSchemaType,
3237
3292
  value?: {
3238
3293
  [prop: string]: unknown
3239
3294
  },
3240
3295
  ) => Path | undefined
3241
- static insertBlock: (
3296
+ static insertBlock: <
3297
+ TSchemaType extends {
3298
+ name: string
3299
+ },
3300
+ >(
3242
3301
  editor: PortableTextEditor,
3243
- type: BlockSchemaType | ObjectSchemaType,
3302
+ type: TSchemaType,
3244
3303
  value?: {
3245
3304
  [prop: string]: unknown
3246
3305
  },
@@ -3256,9 +3315,13 @@ export declare class PortableTextEditor extends Component<
3256
3315
  editor: PortableTextEditor,
3257
3316
  selection: EditorSelection | null,
3258
3317
  ) => void
3259
- static removeAnnotation: (
3318
+ static removeAnnotation: <
3319
+ TSchemaType extends {
3320
+ name: string
3321
+ },
3322
+ >(
3260
3323
  editor: PortableTextEditor,
3261
- type: ObjectSchemaType,
3324
+ type: TSchemaType,
3262
3325
  ) => void | undefined
3263
3326
  static toggleBlockStyle: (
3264
3327
  editor: PortableTextEditor,
@@ -3551,6 +3614,20 @@ export declare type RenderStyleFunction = (
3551
3614
  props: BlockStyleRenderProps,
3552
3615
  ) => JSX.Element
3553
3616
 
3617
+ /**
3618
+ * @alpha
3619
+ */
3620
+ export declare type SchemaDefinition<
3621
+ TBaseDefinition extends BaseDefinition = BaseDefinition,
3622
+ > = {
3623
+ decorators?: ReadonlyArray<TBaseDefinition>
3624
+ blockObjects?: ReadonlyArray<TBaseDefinition>
3625
+ inlineObjects?: ReadonlyArray<TBaseDefinition>
3626
+ annotations?: ReadonlyArray<TBaseDefinition>
3627
+ lists?: ReadonlyArray<TBaseDefinition>
3628
+ styles?: ReadonlyArray<TBaseDefinition>
3629
+ }
3630
+
3554
3631
  /** @beta */
3555
3632
  export declare type ScrollSelectionIntoViewFunction = (
3556
3633
  editor: PortableTextEditor,
@@ -4015,10 +4092,10 @@ export declare function useEditor(config: EditorConfig): Actor<
4015
4092
  >
4016
4093
  }) => {
4017
4094
  behaviors: Behavior<
4018
- | 'insert break'
4019
- | 'insert soft break'
4020
4095
  | 'delete backward'
4021
4096
  | 'delete forward'
4097
+ | 'insert soft break'
4098
+ | 'insert break'
4022
4099
  | 'insert text',
4023
4100
  true
4024
4101
  >[]