@portabletext/editor 1.16.3 → 1.16.4
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 +133 -117
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +2 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/editor/create-editor.ts +2 -0
package/README.md
CHANGED
|
@@ -9,20 +9,8 @@
|
|
|
9
9
|
|
|
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
|
-
> [!NOTE]
|
|
13
|
-
> We are currently working hard on the general release of this component. Better docs and refined APIs are coming.
|
|
14
|
-
|
|
15
|
-
## End-User Experience
|
|
16
|
-
|
|
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
12
|
## Build Your Own Portable Text Editor
|
|
20
13
|
|
|
21
|
-
> [!WARNING]
|
|
22
|
-
> The `@portabletext/editor` is currently on the path to deprecate legacy APIs and introduce new ones. The end goals are to make the editor easier to use outside of `Sanity` (and without `@sanity/*` libraries) as well as providing a brand new API to configure the behavior of the editor.
|
|
23
|
-
>
|
|
24
|
-
> This means that the `defineSchema` and `EditorProvider` APIs showcased here are still experimental APIs tagged with `@alpha` and cannot be considered stable yet. At the same time, the examples below showcase usages of legacy static methods on the `PortableTextEditor` (for example, `PortableTextEditor.isMarkActive(...)` and `PortableTextEditor.toggleMark(...)`) that will soon be discouraged and deprecrated.
|
|
25
|
-
|
|
26
14
|
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.
|
|
27
15
|
|
|
28
16
|
### Define the Schema
|
|
@@ -219,153 +207,177 @@ function isStockTicker(
|
|
|
219
207
|
|
|
220
208
|
### Render the Toolbar
|
|
221
209
|
|
|
222
|
-
Your toolbar needs to be rendered within `EditorProvider` because it requires a reference to the `
|
|
210
|
+
Your toolbar needs to be rendered within `EditorProvider` because it requires a reference to the `editor` that it produces. To toggle marks and styles and to insert objects, you'll have to use the `.send` method on this `editor` instance.
|
|
223
211
|
|
|
224
212
|
```tsx
|
|
225
213
|
function Toolbar() {
|
|
226
214
|
// Obtain the editor instance
|
|
227
|
-
const
|
|
228
|
-
// Rerender the toolbar whenever the selection changes
|
|
229
|
-
usePortableTextEditorSelection()
|
|
215
|
+
const editor = useEditor()
|
|
230
216
|
|
|
231
|
-
const decoratorButtons = schemaDefinition.decorators.map((decorator) =>
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}}
|
|
243
|
-
onClick={() => {
|
|
244
|
-
// Toggle the decorator by name
|
|
245
|
-
PortableTextEditor.toggleMark(editorInstance, decorator.name)
|
|
246
|
-
// Pressing this button steals focus so let's focus the editor again
|
|
247
|
-
PortableTextEditor.focus(editorInstance)
|
|
248
|
-
}}
|
|
249
|
-
>
|
|
250
|
-
{decorator.name}
|
|
251
|
-
</button>
|
|
252
|
-
)
|
|
253
|
-
})
|
|
217
|
+
const decoratorButtons = schemaDefinition.decorators.map((decorator) => (
|
|
218
|
+
<DecoratorButton key={decorator.name} decorator={decorator.name} />
|
|
219
|
+
))
|
|
220
|
+
|
|
221
|
+
const annotationButtons = schemaDefinition.annotations.map((annotation) => (
|
|
222
|
+
<AnnotationButton key={annotation.name} annotation={annotation} />
|
|
223
|
+
))
|
|
224
|
+
|
|
225
|
+
const styleButtons = schemaDefinition.styles.map((style) => (
|
|
226
|
+
<StyleButton key={style.name} style={style.name} />
|
|
227
|
+
))
|
|
254
228
|
|
|
255
|
-
const
|
|
229
|
+
const listButtons = schemaDefinition.lists.map((list) => (
|
|
230
|
+
<ListButton key={list.name} list={list.name} />
|
|
231
|
+
))
|
|
232
|
+
|
|
233
|
+
const imageButton = (
|
|
256
234
|
<button
|
|
257
|
-
style={{
|
|
258
|
-
textDecoration: PortableTextEditor.isAnnotationActive(
|
|
259
|
-
editorInstance,
|
|
260
|
-
schemaDefinition.annotations[0].name,
|
|
261
|
-
)
|
|
262
|
-
? 'underline'
|
|
263
|
-
: 'unset',
|
|
264
|
-
}}
|
|
265
235
|
onClick={() => {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
)
|
|
276
|
-
} else {
|
|
277
|
-
PortableTextEditor.addAnnotation(
|
|
278
|
-
editorInstance,
|
|
279
|
-
schemaDefinition.annotations[0],
|
|
280
|
-
{href: 'https://example.com'},
|
|
281
|
-
)
|
|
282
|
-
}
|
|
283
|
-
PortableTextEditor.focus(editorInstance)
|
|
236
|
+
editor.send({
|
|
237
|
+
type: 'insert.block object',
|
|
238
|
+
blockObject: {
|
|
239
|
+
name: 'image',
|
|
240
|
+
value: {src: 'https://example.com/image.jpg'},
|
|
241
|
+
},
|
|
242
|
+
placement: 'auto',
|
|
243
|
+
})
|
|
244
|
+
editor.send({type: 'focus'})
|
|
284
245
|
}}
|
|
285
246
|
>
|
|
286
|
-
|
|
247
|
+
{schemaDefinition.blockObjects[0].name}
|
|
287
248
|
</button>
|
|
288
249
|
)
|
|
289
250
|
|
|
290
|
-
const
|
|
251
|
+
const stockTickerButton = (
|
|
291
252
|
<button
|
|
292
|
-
key={style.name}
|
|
293
|
-
style={{
|
|
294
|
-
textDecoration: PortableTextEditor.hasBlockStyle(
|
|
295
|
-
editorInstance,
|
|
296
|
-
style.name,
|
|
297
|
-
)
|
|
298
|
-
? 'underline'
|
|
299
|
-
: 'unset',
|
|
300
|
-
}}
|
|
301
253
|
onClick={() => {
|
|
302
|
-
|
|
303
|
-
|
|
254
|
+
editor.send({
|
|
255
|
+
type: 'insert.inline object',
|
|
256
|
+
inlineObject: {
|
|
257
|
+
name: 'stock-ticker',
|
|
258
|
+
value: {symbol: 'AAPL'},
|
|
259
|
+
},
|
|
260
|
+
})
|
|
261
|
+
editor.send({type: 'focus'})
|
|
304
262
|
}}
|
|
305
263
|
>
|
|
306
|
-
{
|
|
264
|
+
{schemaDefinition.inlineObjects[0].name}
|
|
307
265
|
</button>
|
|
308
|
-
)
|
|
266
|
+
)
|
|
309
267
|
|
|
310
|
-
|
|
268
|
+
return (
|
|
269
|
+
<>
|
|
270
|
+
<div>{decoratorButtons}</div>
|
|
271
|
+
<div>{annotationButtons}</div>
|
|
272
|
+
<div>{styleButtons}</div>
|
|
273
|
+
<div>{listButtons}</div>
|
|
274
|
+
<div>{imageButton}</div>
|
|
275
|
+
<div>{stockTickerButton}</div>
|
|
276
|
+
</>
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function DecoratorButton(props: {decorator: string}) {
|
|
281
|
+
// Obtain the editor instance
|
|
282
|
+
const editor = useEditor()
|
|
283
|
+
// Check if the decorator is active using a selector
|
|
284
|
+
const active = useEditorSelector(
|
|
285
|
+
editor,
|
|
286
|
+
selectors.isActiveDecorator(props.decorator),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
return (
|
|
311
290
|
<button
|
|
312
|
-
key={list.name}
|
|
313
291
|
style={{
|
|
314
|
-
textDecoration:
|
|
315
|
-
editorInstance,
|
|
316
|
-
list.name,
|
|
317
|
-
)
|
|
318
|
-
? 'underline'
|
|
319
|
-
: 'unset',
|
|
292
|
+
textDecoration: active ? 'underline' : 'unset',
|
|
320
293
|
}}
|
|
321
294
|
onClick={() => {
|
|
322
|
-
|
|
323
|
-
|
|
295
|
+
// Toggle the decorator
|
|
296
|
+
editor.send({
|
|
297
|
+
type: 'decorator.toggle',
|
|
298
|
+
decorator: props.decorator,
|
|
299
|
+
})
|
|
300
|
+
// Pressing this button steals focus so let's focus the editor again
|
|
301
|
+
editor.send({type: 'focus'})
|
|
324
302
|
}}
|
|
325
303
|
>
|
|
326
|
-
{
|
|
304
|
+
{props.decorator}
|
|
327
305
|
</button>
|
|
328
|
-
)
|
|
306
|
+
)
|
|
307
|
+
}
|
|
329
308
|
|
|
330
|
-
|
|
309
|
+
function AnnotationButton(props: {annotation: {name: string}}) {
|
|
310
|
+
const editor = useEditor()
|
|
311
|
+
const active = useEditorSelector(
|
|
312
|
+
editor,
|
|
313
|
+
selectors.isActiveAnnotation(props.annotation.name),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
return (
|
|
331
317
|
<button
|
|
318
|
+
style={{
|
|
319
|
+
textDecoration: active ? 'underline' : 'unset',
|
|
320
|
+
}}
|
|
332
321
|
onClick={() => {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
322
|
+
editor.send({
|
|
323
|
+
type: 'annotation.toggle',
|
|
324
|
+
annotation: {
|
|
325
|
+
name: props.annotation.name,
|
|
326
|
+
value:
|
|
327
|
+
props.annotation.name === 'link'
|
|
328
|
+
? {href: 'https://example.com'}
|
|
329
|
+
: {},
|
|
330
|
+
},
|
|
331
|
+
})
|
|
332
|
+
editor.send({type: 'focus'})
|
|
339
333
|
}}
|
|
340
334
|
>
|
|
341
|
-
{
|
|
335
|
+
{props.annotation.name}
|
|
342
336
|
</button>
|
|
343
337
|
)
|
|
338
|
+
}
|
|
344
339
|
|
|
345
|
-
|
|
340
|
+
function StyleButton(props: {style: string}) {
|
|
341
|
+
const editor = useEditor()
|
|
342
|
+
const active = useEditorSelector(editor, selectors.isActiveStyle(props.style))
|
|
343
|
+
|
|
344
|
+
return (
|
|
346
345
|
<button
|
|
346
|
+
style={{
|
|
347
|
+
textDecoration: active ? 'underline' : 'unset',
|
|
348
|
+
}}
|
|
347
349
|
onClick={() => {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
schemaDefinition.inlineObjects[0],
|
|
351
|
-
{symbol: 'AAPL'},
|
|
352
|
-
)
|
|
353
|
-
PortableTextEditor.focus(editorInstance)
|
|
350
|
+
editor.send({type: 'style.toggle', style: props.style})
|
|
351
|
+
editor.send({type: 'focus'})
|
|
354
352
|
}}
|
|
355
353
|
>
|
|
356
|
-
{
|
|
354
|
+
{props.style}
|
|
357
355
|
</button>
|
|
358
356
|
)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function ListButton(props: {list: string}) {
|
|
360
|
+
const editor = useEditor()
|
|
361
|
+
const active = useEditorSelector(
|
|
362
|
+
editor,
|
|
363
|
+
selectors.isActiveListItem(props.list),
|
|
364
|
+
)
|
|
359
365
|
|
|
360
366
|
return (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
367
|
+
<button
|
|
368
|
+
style={{
|
|
369
|
+
textDecoration: active ? 'underline' : 'unset',
|
|
370
|
+
}}
|
|
371
|
+
onClick={() => {
|
|
372
|
+
editor.send({
|
|
373
|
+
type: 'list item.toggle',
|
|
374
|
+
listItem: props.list,
|
|
375
|
+
})
|
|
376
|
+
editor.send({type: 'focus'})
|
|
377
|
+
}}
|
|
378
|
+
>
|
|
379
|
+
{props.list}
|
|
380
|
+
</button>
|
|
369
381
|
)
|
|
370
382
|
}
|
|
371
383
|
```
|
|
@@ -379,6 +391,10 @@ The Behavior API is a new way of interfacing with the Portable Text Editor. It a
|
|
|
379
391
|
3. Deriving editor **state** using **pure functions**.
|
|
380
392
|
4. Subscribe to **emitted** editor **events** using `editor.on(…)`.
|
|
381
393
|
|
|
394
|
+
## End-User Experience
|
|
395
|
+
|
|
396
|
+
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/).
|
|
397
|
+
|
|
382
398
|
## Development
|
|
383
399
|
|
|
384
400
|
### Develop Together with Sanity Studio
|