Skip to content

Commit

Permalink
refactor(form/inputs): Custom rendering support via schema for PT-input
Browse files Browse the repository at this point in the history
This will also update the types used by the input.
  • Loading branch information
skogsmaskin committed Dec 20, 2022
1 parent 27d5133 commit f1152f7
Show file tree
Hide file tree
Showing 30 changed files with 520 additions and 341 deletions.
26 changes: 3 additions & 23 deletions packages/sanity/src/core/form/inputs/PortableText/BlockActions.tsx
@@ -1,10 +1,7 @@
import {
PortableTextEditor,
PortableTextBlock,
usePortableTextEditor,
} from '@sanity/portable-text-editor'
import {PortableTextEditor, usePortableTextEditor} from '@sanity/portable-text-editor'
import React, {useCallback, useMemo} from 'react'
import styled from 'styled-components'
import {PortableTextBlock} from '@sanity/types'
import {PatchArg} from '../../patch'
import {RenderBlockActionsCallback, RenderBlockActionsProps} from './types'
import {createInsertCallback, createSetCallback, createUnsetCallback} from './callbacks'
Expand All @@ -20,21 +17,10 @@ const Root = styled.div`
pointer-events: 'all';
`

// function isClassComponent(component: React.ComponentType) {
// return typeof component === 'function' && !!component.prototype?.isReactComponent
// }

// function isFunctionComponent(component: React.ComponentType) {
// return typeof component === 'function' && String(component).includes('return React.createElement')
// }

export function BlockActions(props: BlockActionsProps) {
const editor = usePortableTextEditor()
const {block, onChange, renderBlockActions} = props
const decoratorValues = useMemo(
() => PortableTextEditor.getPortableTextFeatures(editor).decorators.map((d) => d.value),
[editor]
)
const decoratorValues = useMemo(() => editor.types.decorators.map((d) => d.value), [editor])

const blockActions = useMemo(() => {
if (renderBlockActions) {
Expand All @@ -45,12 +31,6 @@ export function BlockActions(props: BlockActionsProps) {
unset: createUnsetCallback({block, onChange}),
insert: createInsertCallback({allowedDecorators: decoratorValues, block, onChange}),
}

// // Support returning a class component for renderBlockActions (to keep backward compatability as it was possible before)
// if (isClassComponent(renderBlockActions) || isFunctionComponent(renderBlockActions)) {
// const RenderComponent = renderBlockActions
// return <RenderComponent {...blockActionProps} />
// }
return renderBlockActions(blockActionProps)
}
return undefined
Expand Down
110 changes: 60 additions & 50 deletions packages/sanity/src/core/form/inputs/PortableText/Compositor.tsx
Expand Up @@ -3,13 +3,13 @@ import {
EditorSelection,
OnCopyFn,
OnPasteFn,
PortableTextBlock,
PortableTextEditor,
usePortableTextEditor,
HotkeyOptions,
RenderAttributes,
BlockRenderProps,
BlockChildRenderProps,
BlockAnnotationRenderProps,
} from '@sanity/portable-text-editor'
import {ObjectSchemaType, Path} from '@sanity/types'
import {BlockSchemaType, Path, PortableTextBlock, PortableTextTextBlock} from '@sanity/types'
import {
BoundaryElementProvider,
Portal,
Expand Down Expand Up @@ -53,7 +53,7 @@ interface InputProps extends ArrayOfObjectsInputProps<PortableTextBlock> {
export type PortableTextEditorElement = HTMLDivElement | HTMLSpanElement | null

/** @internal */
export function Compositor(props: InputProps) {
export function Compositor(props: Omit<InputProps, 'schemaType' | 'arrayFunctions'>) {
const {
changed,
focusPath = EMPTY_ARRAY,
Expand All @@ -65,8 +65,9 @@ export function Compositor(props: InputProps) {
onChange,
onCopy,
onActivate,
onItemOpen,
onItemClose,
onItemOpen,
onItemRemove,
onPaste,
onToggleFullscreen,
path,
Expand Down Expand Up @@ -111,8 +112,8 @@ export function Compositor(props: InputProps) {

const editorHotkeys = useHotkeys(hotkeysWithFullscreenToggle)

const ptFeatures = useMemo(() => PortableTextEditor.getPortableTextFeatures(editor), [editor])
const hasContent = !!value
const _renderBlockActions = !!value && renderBlockActions ? renderBlockActions : undefined
const _renderCustomMarkers = !!value && renderCustomMarkers ? renderCustomMarkers : undefined

const initialSelection = useMemo(
(): EditorSelection =>
Expand All @@ -127,104 +128,113 @@ export function Compositor(props: InputProps) {
)

const renderBlock = useCallback(
(
block: PortableTextBlock,
blockType: ObjectSchemaType,
attributes: RenderAttributes,
defaultRender: (b: PortableTextBlock) => JSX.Element
) => {
const isTextBlock = block._type === ptFeatures.types.block.name
(blockProps: BlockRenderProps) => {
const {
value: block,
type: blockType,
renderDefault,
focused: blockFocused,
selected,
path: blockPath,
} = blockProps
const isTextBlock = block._type === editor.types.block.name
if (isTextBlock) {
return (
<TextBlock
attributes={attributes}
block={block}
block={block as PortableTextTextBlock}
focused={blockFocused}
isFullscreen={isFullscreen}
onChange={onChange}
path={blockPath}
readOnly={readOnly}
renderBlockActions={hasContent ? renderBlockActions : undefined}
renderCustomMarkers={hasContent ? renderCustomMarkers : undefined}
renderBlockActions={_renderBlockActions}
renderCustomMarkers={_renderCustomMarkers}
selected={selected}
type={blockType as BlockSchemaType}
>
{defaultRender(block)}
{renderDefault(blockProps)}
</TextBlock>
)
}

return (
<BlockObject
attributes={attributes}
block={block}
focused={blockFocused}
isFullscreen={isFullscreen}
onChange={onChange}
onItemOpen={onItemOpen}
onItemClose={onItemClose}
onItemRemove={onItemRemove}
path={blockPath}
readOnly={readOnly}
renderBlockActions={hasContent ? renderBlockActions : undefined}
renderCustomMarkers={hasContent ? renderCustomMarkers : undefined}
renderBlockActions={_renderBlockActions}
renderCustomMarkers={_renderCustomMarkers}
renderPreview={renderPreview}
selected={selected}
type={blockType}
/>
)
},
[
hasContent,
_renderBlockActions,
_renderCustomMarkers,
editor.types.block.name,
isFullscreen,
onChange,
onItemOpen,
ptFeatures.types.block.name,
onItemClose,
onItemRemove,
readOnly,
renderBlockActions,
renderCustomMarkers,
renderPreview,
]
)

const renderChild = useCallback(
(child: any, childType: any, attributes: any, defaultRender: any) => {
const isSpan = child._type === ptFeatures.types.span.name
(childProps: BlockChildRenderProps) => {
const {
focused: childFocused,
path: childPath,
renderDefault,
selected,
type: childType,
value: child,
} = childProps
const isSpan = child._type === editor.types.span.name
if (isSpan) {
return defaultRender(child)
return renderDefault(childProps)
}

return (
<InlineObject
attributes={attributes}
focused={childFocused}
onItemOpen={onItemOpen}
path={childPath}
readOnly={readOnly}
renderCustomMarkers={renderCustomMarkers}
renderPreview={renderPreview}
scrollElement={scrollElement}
selected={selected}
type={childType}
value={child}
renderPreview={renderPreview}
/>
)
},
[
onItemOpen,
ptFeatures.types.span.name,
readOnly,
renderCustomMarkers,
renderPreview,
scrollElement,
]
[editor, onItemOpen, readOnly, renderCustomMarkers, renderPreview, scrollElement]
)

const renderAnnotation = useCallback(
(annotation: any, annotationType: any, attributes: any, defaultRender: any) => {
(annotationProps: BlockAnnotationRenderProps) => {
return (
<Annotation
attributes={attributes}
renderProps={annotationProps}
onItemOpen={onItemOpen}
onItemClose={onItemClose}
readOnly={readOnly}
renderCustomMarkers={renderCustomMarkers}
scrollElement={scrollElement}
value={annotation}
type={annotationType}
>
{defaultRender()}
</Annotation>
/>
)
},
[onItemOpen, readOnly, renderCustomMarkers, scrollElement]
[onItemOpen, onItemClose, readOnly, renderCustomMarkers, scrollElement]
)

const [portalElement, setPortalElement] = useState<HTMLDivElement | null>(null)
Expand Down
36 changes: 34 additions & 2 deletions packages/sanity/src/core/form/inputs/PortableText/Editor.tsx
Expand Up @@ -10,6 +10,8 @@ import {
EditorSelection,
PortableTextEditor,
usePortableTextEditor,
RenderStyleFunction,
RenderListItemFunction,
} from '@sanity/portable-text-editor'
import {Path} from '@sanity/types'
import {BoundaryElementProvider, useBoundaryElement, useGlobalKeyDown, useLayer} from '@sanity/ui'
Expand All @@ -26,6 +28,8 @@ import {
} from './Editor.styles'
import {useSpellcheck} from './hooks/useSpellCheck'
import {useScrollSelectionIntoView} from './hooks/useScrollSelectionIntoView'
import {Style} from './text/Style'
import {ListItem} from './text/ListItem'

interface EditorProps {
hotkeys: HotkeyOptions
Expand All @@ -46,8 +50,34 @@ interface EditorProps {
setScrollElement: (scrollElement: HTMLElement | null) => void
}

const renderDecorator: RenderDecoratorFunction = (mark, mType, attributes, defaultRender) => {
return <Decorator mark={mark}>{defaultRender()}</Decorator>
const renderDecorator: RenderDecoratorFunction = (props) => {
const {value, renderDefault, type, focused, selected} = props
const CustomComponent = type.components?.item
const rendered = renderDefault(props)
if (CustomComponent) {
// eslint-disable-next-line react/jsx-no-bind
return (
<CustomComponent
focused={focused}
selected={selected}
title={type.title}
value={value}
// eslint-disable-next-line react/jsx-no-bind
renderDefault={() => <Decorator mark={value}>{rendered}</Decorator>}
>
{rendered}
</CustomComponent>
)
}
return <Decorator mark={value}>{rendered}</Decorator>
}

const renderStyle: RenderStyleFunction = (props) => {
return <Style {...props} />
}

const renderListItem: RenderListItemFunction = (props) => {
return <ListItem {...props} />
}

export function Editor(props: EditorProps) {
Expand Down Expand Up @@ -114,7 +144,9 @@ export function Editor(props: EditorProps) {
renderBlock={renderBlock}
renderChild={renderChild}
renderDecorator={renderDecorator}
renderListItem={renderListItem}
renderPlaceholder={renderPlaceholder}
renderStyle={renderStyle}
scrollSelectionIntoView={scrollSelectionIntoView}
selection={initialSelection}
spellCheck={spellcheck}
Expand Down
@@ -1,10 +1,9 @@
import {ArraySchemaType} from '@sanity/types'
import {ArraySchemaType, PortableTextBlock} from '@sanity/types'
import {
EditorChange,
OnCopyFn,
OnPasteFn,
Patch as EditorPatch,
PortableTextBlock,
PortableTextEditor,
HotkeyOptions,
InvalidValue,
Expand All @@ -29,7 +28,6 @@ import {ArrayOfObjectsItemMember, ObjectFormNode} from '../../store'
import type {ArrayOfObjectsInputProps, PortableTextMarker, RenderCustomMarkers} from '../../types'
import {EMPTY_ARRAY} from '../../../util'
import {pathToString} from '../../../field'
import {FIXME} from '../../../FIXME'
import {isMemberArrayOfObjects} from '../../members/object/fields/asserters'
import {Compositor, PortableTextEditorElement} from './Compositor'
import {InvalidValue as RespondToInvalidContent} from './InvalidValue'
Expand All @@ -39,23 +37,20 @@ import {PortableTextMarkersProvider} from './contexts/PortableTextMarkers'
import {PortableTextMemberItemsProvider} from './contexts/PortableTextMembers'
import {_isArrayOfObjectsFieldMember, _isBlockType} from './_helpers'

/** @internal */
export type PTObjectMember = ArrayOfObjectsItemMember

/** @internal */
export interface PortableTextMemberItem {
kind: 'annotation' | 'textBlock' | 'objectBlock' | 'inlineObject'
key: string
member: PTObjectMember
member: ArrayOfObjectsItemMember
node: ObjectFormNode
elementRef?: React.MutableRefObject<PortableTextEditorElement> | undefined
}

/**
* @alpha
* @beta
*/
export interface PortableTextInputProps
extends ArrayOfObjectsInputProps<PortableTextBlock, ArraySchemaType> {
extends ArrayOfObjectsInputProps<PortableTextBlock, ArraySchemaType<PortableTextBlock>> {
hotkeys?: HotkeyOptions
markers?: PortableTextMarker[]
onCopy?: OnCopyFn
Expand All @@ -67,7 +62,7 @@ export interface PortableTextInputProps
/**
* The root Portable Text Input component
*
* @alpha
* @beta
*/
export function PortableTextInput(props: PortableTextInputProps) {
const {
Expand Down Expand Up @@ -159,7 +154,7 @@ export function PortableTextInput(props: PortableTextInputProps) {
const portableTextMemberItems: PortableTextMemberItem[] = useMemo(() => {
const result: {
kind: PortableTextMemberItem['kind']
member: PTObjectMember
member: ArrayOfObjectsItemMember
node: ObjectFormNode
}[] = []

Expand Down Expand Up @@ -343,7 +338,7 @@ export function PortableTextInput(props: PortableTextInputProps) {
onChange={handleEditorChange}
maxBlocks={undefined} // TODO: from schema?
readOnly={readOnly}
type={type as FIXME}
type={type}
value={value}
>
<Compositor
Expand Down

0 comments on commit f1152f7

Please sign in to comment.