Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): implement presence cursors #6081

Merged
merged 36 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4056387
feat(portable-text-editor): improve range decorations perf by compari…
skogsmaskin Apr 22, 2024
7911308
test(portable-text-editor): add tests for range decorations rendering
skogsmaskin Apr 22, 2024
fe4a016
fixup! feat(portable-text-editor): improve range decorations perf by …
skogsmaskin Apr 22, 2024
7babe0f
test(portable-text-editor): improve some minor test things
skogsmaskin Apr 22, 2024
11d9fde
fix(core): update z-index on PTE activate overlay
hermanwikner Apr 11, 2024
91f5ff2
feat(core): extend presence data model with `selection`
hermanwikner Apr 11, 2024
175fc86
feat(structure): include `selection` in presence data
hermanwikner Apr 11, 2024
df92de1
feat(core): implement presence cursors in PTE
hermanwikner Apr 11, 2024
295455c
test(core): add presence cursor workshop story
hermanwikner Apr 11, 2024
25a5e24
refactor(core): use `getTheme_v2` to access theme values
hermanwikner Apr 16, 2024
0df4e98
fix(core): introduce `OnPathFocusPayload` to improve typing
hermanwikner Apr 16, 2024
c82d9fd
fix(form/inputs): perf optimization for PT-input decorators
skogsmaskin Apr 17, 2024
def0d46
refactor(form/inputs): also display user presence when user is select…
skogsmaskin Apr 17, 2024
6256217
fix(core): render presence cursors inline instead of in a portal
hermanwikner Apr 18, 2024
b8ec79c
fix(core): broken workshop story
hermanwikner Apr 18, 2024
8e72405
feat(core): reset presence selection on blur in PTE
hermanwikner Apr 18, 2024
8f1fe3b
fix(structure): add accidentially removed setFocusPath (DocumentPaneP…
skogsmaskin Apr 18, 2024
a8e21ca
fix(core): use `useFormFieldPresence` in `usePresenceCursorDecorations`
hermanwikner Apr 18, 2024
6d87a9e
fix(core): set `focusPath` when receiving a mutation event if there a…
hermanwikner Apr 18, 2024
1c48355
fix(form/inputs): break don't return
skogsmaskin Apr 18, 2024
7f4e25f
fix(form/inputs): remove ref that should not be set here
skogsmaskin Apr 18, 2024
ce64287
fix(form/inputs): sort hook deps list
skogsmaskin Apr 18, 2024
8d913b4
fix(form/inputs): reconcile presence decorations for PT-Input
skogsmaskin Apr 19, 2024
4852256
fix(form/inputs): return early if focusPath is already selected in PT…
skogsmaskin Apr 19, 2024
1416e9e
refactor(form/inputs): throttle reporting of focusPath and presence u…
skogsmaskin Apr 19, 2024
4edd935
fix(core): import of `FormNodePresence`
hermanwikner Apr 19, 2024
5aa6583
fix(form/inputs): remove lastActive as dep for PT-presence range deco…
skogsmaskin Apr 19, 2024
a30362c
test(core): add presence cursor workshop story
hermanwikner Apr 19, 2024
134d33a
fix(core): remove unnecessary z-index in PTE activate overlay
hermanwikner Apr 19, 2024
b91ccf2
fix(core): prevent presence cursor user name from being selected
hermanwikner Apr 22, 2024
04838dc
fix(form/inputs): fix issue where perf opt on focusPath tracking brok…
skogsmaskin Apr 23, 2024
90634e7
test(core): update snapshots in `Studio.test`
hermanwikner Apr 25, 2024
865fd72
refactor(core): move presence decorations according to user edits
skogsmaskin Apr 25, 2024
4cd93f6
refactor(core/form): remove complexity from PortableTextInput
skogsmaskin Apr 26, 2024
1f8fc66
refactor(structure): announce presence throttled
skogsmaskin Apr 26, 2024
6c8a26c
refactor(core): clean up presence cursors code + add comments
hermanwikner Apr 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ export function Compositor(props: Omit<InputProps, 'schemaType' | 'arrayFunction
onPathFocus,
onToggleFullscreen,
path,
readOnly,
rangeDecorations,
readOnly,
renderAnnotation,
renderBlock,
renderBlockActions,
Expand Down Expand Up @@ -414,21 +414,21 @@ export function Compositor(props: Omit<InputProps, 'schemaType' | 'arrayFunction
// Keep only stable ones here!
[
ariaDescribedBy,
editorHotkeys,
editorRenderAnnotation,
editorRenderBlock,
editorRenderChild,
elementRef,
handleToggleFullscreen,
initialSelection,
editorHotkeys,
isActive,
isFullscreen,
onItemOpen,
onCopy,
onItemOpen,
onPaste,
handleToggleFullscreen,
path,
rangeDecorations,
readOnly,
editorRenderAnnotation,
editorRenderBlock,
editorRenderChild,
scrollElement,
],
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {
type EditorChange,
type EditorSelection,
type InvalidValue,
type Patch as EditorPatch,
type Patch,
PortableTextEditor,
type RangeDecoration,
} from '@sanity/portable-text-editor'
import {useTelemetry} from '@sanity/telemetry/react'
import {isKeySegment, type Path, type PortableTextBlock} from '@sanity/types'
import {isKeySegment, type PortableTextBlock} from '@sanity/types'
import {Box, useToast} from '@sanity/ui'
import {
type MutableRefObject,
Expand All @@ -27,12 +29,17 @@ import {
} from '../../__telemetry__/form.telemetry'
import {SANITY_PATCH_TYPE} from '../../patch'
import {type ArrayOfObjectsItemMember, type ObjectFormNode} from '../../store'
import {immutableReconcile} from '../../store/utils/immutableReconcile'
import {type PortableTextInputProps} from '../../types'
import {Compositor, type PortableTextEditorElement} from './Compositor'
import {PortableTextMarkersProvider} from './contexts/PortableTextMarkers'
import {PortableTextMemberItemsProvider} from './contexts/PortableTextMembers'
import {usePortableTextMemberItemsFromProps} from './hooks/usePortableTextMembers'
import {InvalidValue as RespondToInvalidContent} from './InvalidValue'
import {
type PresenceCursorDecorationsHookProps,
usePresenceCursorDecorations,
} from './presence-cursors'
import {usePatches} from './usePatches'

/** @internal */
Expand Down Expand Up @@ -73,17 +80,26 @@ export function PortableTextInput(props: PortableTextInputProps): ReactNode {
onPathFocus,
path,
readOnly,
rangeDecorations,
rangeDecorations: rangeDecorationsProp,
renderBlockActions,
renderCustomMarkers,
schemaType,
value,
} = props

const {onBlur, onFocus, ref: elementRef} = elementProps
const {onBlur, ref: elementRef} = elementProps
const defaultEditorRef = useRef<PortableTextEditor | null>(null)
const editorRef = editorRefProp || defaultEditorRef

const presenceCursorDecorations = usePresenceCursorDecorations(
useMemo(
(): PresenceCursorDecorationsHookProps => ({
path: props.path,
}),
[props.path],
),
)

const {subscribe} = usePatches({path})
const [ignoreValidationError, setIgnoreValidationError] = useState(false)
const [invalidValue, setInvalidValue] = useState<InvalidValue | null>(null)
Expand Down Expand Up @@ -139,31 +155,41 @@ export function PortableTextInput(props: PortableTextInputProps): ReactNode {
}
}, [hasFocusWithin])

// Report focus on spans with `.text` appended to the reported focusPath.
// This is done to support the Presentation tool which uses this kind of paths to refer to texts.
// The PT-input already supports these paths the other way around.
// It's a bit ugly right here, but it's a rather simple way to support the Presentation tool without
// having to change the PTE's internals.
const setFocusPathFromEditorSelection = useCallback(
(focusPath: Path) => {
// Test if the focusPath is pointing directly to a span
const isSpanPath =
focusPath.length === 3 && // A span path is always 3 segments long
focusPath[1] === 'children' && // Is a child of a block
isKeySegment(focusPath[2]) && // Contains the key of the child
!portableTextMemberItems.some(
(item) => isKeySegment(focusPath[2]) && item.member.key === focusPath[2]._key,
) // Not an inline object (it would be a member in this list, where spans are not). By doing this check we avoid depending on the value.
if (isSpanPath) {
// Append `.text` to the focusPath
onPathFocus(focusPath.concat('text'))
} else {
// Call normally
onPathFocus(focusPath)
}
},
[onPathFocus, portableTextMemberItems],
)
const setFocusPathFromEditorSelection = useCallback(() => {
const selection = nextSelectionRef.current
const focusPath = selection?.focus.path
if (!focusPath) return

// Report focus on spans with `.text` appended to the reported focusPath.
// This is done to support the Presentation tool which uses this kind of paths to refer to texts.
// The PT-input already supports these paths the other way around.
// It's a bit ugly right here, but it's a rather simple way to support the Presentation tool without
// having to change the PTE's internals.
const isSpanPath =
focusPath.length === 3 && // A span path is always 3 segments long
focusPath[1] === 'children' && // Is a child of a block
isKeySegment(focusPath[2]) && // Contains the key of the child
!portableTextMemberItems.some(
(item) => isKeySegment(focusPath[2]) && item.member.key === focusPath[2]._key,
)
const nextFocusPath = isSpanPath ? focusPath.concat(['text']) : focusPath

// Must called in a transition useTrackFocusPath hook
// will try to effectuate a focusPath that is different from what currently is the editor focusPath
startTransition(() => {
onPathFocus(nextFocusPath, {
selection,
})
})
}, [onPathFocus, portableTextMemberItems])

const resetSelectionPresence = useCallback(() => {
onPathFocus(props.path, {
selection: null,
})
}, [onPathFocus, props.path])

const nextSelectionRef = useRef<EditorSelection | null>(null)

// Handle editor changes
const handleEditorChange = useCallback(
Expand All @@ -180,13 +206,8 @@ export function PortableTextInput(props: PortableTextInputProps): ReactNode {
}
break
case 'selection':
// This doesn't need to be immediate,
// call through startTransition
startTransition(() => {
if (change.selection) {
setFocusPathFromEditorSelection(change.selection.focus.path)
}
})
nextSelectionRef.current = change.selection
setFocusPathFromEditorSelection()
break
case 'focus':
setIsActive(true)
Expand All @@ -195,6 +216,11 @@ export function PortableTextInput(props: PortableTextInputProps): ReactNode {
case 'blur':
onBlur(change.event)
setHasFocusWithin(false)

// When the editor blurs, we reset the presence selection
// in order to remove the presence cursor for the current user
// since they no longer have an active selection in the editor.
resetSelectionPresence()
break
case 'undo':
case 'redo':
Expand All @@ -215,7 +241,15 @@ export function PortableTextInput(props: PortableTextInputProps): ReactNode {
onEditorChange(change, editorRef.current)
}
},
[editorRef, onBlur, onChange, onEditorChange, setFocusPathFromEditorSelection, toast],
[
editorRef,
onEditorChange,
onChange,
setFocusPathFromEditorSelection,
onBlur,
resetSelectionPresence,
toast,
],
)

useEffect(() => {
Expand Down Expand Up @@ -251,8 +285,17 @@ export function PortableTextInput(props: PortableTextInputProps): ReactNode {
}
}, [editorRef, isActive])

const previousRangeDecorations = useRef<RangeDecoration[]>([])

const rangeDecorations = useMemo((): RangeDecoration[] => {
const result = [...(rangeDecorationsProp || []), ...presenceCursorDecorations]
const reconciled = immutableReconcile(previousRangeDecorations.current, result)
previousRangeDecorations.current = reconciled
return reconciled
}, [presenceCursorDecorations, rangeDecorationsProp])

return (
<Box ref={elementProps.ref}>
hermanwikner marked this conversation as resolved.
Show resolved Hide resolved
<Box>
{!ignoreValidationError && respondToInvalidContent}
{(!invalidValue || ignoreValidationError) && (
<PortableTextMarkersProvider markers={markers}>
Expand All @@ -274,9 +317,9 @@ export function PortableTextInput(props: PortableTextInputProps): ReactNode {
isActive={isActive}
isFullscreen={isFullscreen}
onActivate={handleActivate}
onItemRemove={onItemRemove}
onCopy={onCopy}
onInsert={onInsert}
onItemRemove={onItemRemove}
onPaste={onPaste}
onToggleFullscreen={handleToggleFullscreen}
rangeDecorations={rangeDecorations}
Expand Down
Loading
Loading