Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
39 changes: 39 additions & 0 deletions packages/block-editor/src/components/block-canvas/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { useMergeRefs, useViewportMatch } from '@wordpress/compose';
import { useRef } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { createSlotFill } from '@wordpress/components';

/**
* Internal dependencies
Expand All @@ -19,6 +20,26 @@ import { useBlockCommands } from '../use-block-commands';
import { store as blockEditorStore } from '../../store';
import { unlock } from '../../lock-unlock';

export const BlockCanvasCover = createSlotFill( Symbol( 'BlockCanvasCover' ) );

function BlockCanvasCoverWrapper( { children } ) {
return (
<div
className="block-canvas-cover"
style={ {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
pointerEvents: 'none',
} }
>
{ children }
</div>
);
}

// EditorStyles is a memoized component, so avoid passing a new
// object reference on each render.
const EDITOR_STYLE_TRANSFORM_OPTIONS = {
Expand Down Expand Up @@ -75,6 +96,15 @@ export function ExperimentalBlockCanvas( {
>
{ children }
</WritingFlow>
<BlockCanvasCover.Slot fillProps={ { containerRef: localRef } }>
{ ( covers ) =>
covers.map( ( cover, index ) => (
<BlockCanvasCoverWrapper key={ index }>
{ cover }
</BlockCanvasCoverWrapper>
) )
}
</BlockCanvasCover.Slot>
</BlockTools>
);
}
Expand All @@ -96,6 +126,15 @@ export function ExperimentalBlockCanvas( {
>
<EditorStyles styles={ styles } />
{ children }
<BlockCanvasCover.Slot fillProps={ { containerRef: localRef } }>
{ ( covers ) =>
covers.map( ( cover, index ) => (
<BlockCanvasCoverWrapper key={ index }>
{ cover }
</BlockCanvasCoverWrapper>
) )
}
</BlockCanvasCover.Slot>
</Iframe>
</BlockTools>
);
Expand Down
6 changes: 5 additions & 1 deletion packages/block-editor/src/private-apis.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ import DimensionsTool from './components/dimensions-tool';
import ResolutionTool from './components/resolution-tool';
import TextAlignmentControl from './components/text-alignment-control';
import { usesContextKey } from './components/rich-text/format-edit';
import { ExperimentalBlockCanvas } from './components/block-canvas';
import {
ExperimentalBlockCanvas,
BlockCanvasCover,
} from './components/block-canvas';
import { getDuotoneFilter } from './components/duotone/utils';
import { useFlashEditableBlocks } from './components/use-flash-editable-blocks';
import {
Expand Down Expand Up @@ -77,6 +80,7 @@ export const privateApis = {};
lock( privateApis, {
...globalStyles,
ExperimentalBlockCanvas,
BlockCanvasCover,
ExperimentalBlockEditorProvider,
getDuotoneFilter,
getRichTextValues,
Expand Down
7 changes: 7 additions & 0 deletions packages/core-data/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,17 @@ unlock( store ).registerPrivateSelectors( privateSelectors );
unlock( store ).registerPrivateActions( privateActions );
register( store ); // Register store after unlocking private selectors to allow resolvers to use them.

/**
* Enums cannot be exported private without losing the ability to narrow types
* based on their values (they blur to string type).
*/
export { SelectionType } from './utils/crdt-user-selections';

export { default as EntityProvider } from './entity-provider';
export * from './entity-provider';
export * from './entity-types';
export * from './awareness/types';
export * from './fetch';
export * from './hooks';
export * from './private-apis';
export * from './types';
6 changes: 5 additions & 1 deletion packages/core-data/src/private-apis.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
*/
import { useEntityRecordsWithPermissions } from './hooks/use-entity-records';
import { RECEIVE_INTERMEDIATE_RESULTS } from './utils';
import { useActiveCollaborators } from './hooks/use-post-editor-awareness-state';
import {
useActiveCollaborators,
useGetAbsolutePositionIndex,
} from './hooks/use-post-editor-awareness-state';
import { lock } from './lock-unlock';

export const privateApis = {};
lock( privateApis, {
useEntityRecordsWithPermissions,
RECEIVE_INTERMEDIATE_RESULTS,
useActiveCollaborators,
useGetAbsolutePositionIndex,
} );
43 changes: 43 additions & 0 deletions packages/editor/src/components/collaborators-overlay/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* WordPress dependencies
*/
// @ts-expect-error No exported types
import { privateApis } from '@wordpress/block-editor';

/**
* Internal dependencies
*/
import { unlock } from '../../lock-unlock';
import { Overlay } from './overlay';

const { BlockCanvasCover } = unlock( privateApis );

interface Props {
postId: number | null;
postType: string | null;
}

/**
* Collaborators Overlay component
* @param props - The props for the CollaboratorsOverlay component
* @param props.postId - The ID of the post
* @param props.postType - The type of the post
* @return The CollaboratorsOverlay component
*/
export function CollaboratorsOverlay( { postId, postType }: Props ) {
return (
<BlockCanvasCover.Fill>
{ ( {
containerRef,
}: {
containerRef: React.MutableRefObject< HTMLElement | null >;
} ) => (
<Overlay
blockEditorDocument={ containerRef.current?.ownerDocument }
postId={ postId }
postType={ postType }
/>
) }
</BlockCanvasCover.Fill>
);
}
138 changes: 138 additions & 0 deletions packages/editor/src/components/collaborators-overlay/overlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// @ts-expect-error No exported types
import { useStyleOverride } from '@wordpress/block-editor';
import { useResizeObserver, useMergeRefs } from '@wordpress/compose';
import { useEffect, useState } from '@wordpress/element';

import { useBlockHighlighting } from './use-block-highlighting';
import { useRenderCursors } from './use-render-cursors';

const COLLABORATORS_OVERLAY_STYLES = `
.block-canvas-cover {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 20000;
}
.block-canvas-cover .collaborators-overlay-full {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.block-canvas-cover .collaborators-overlay-fixed {
position: fixed;
width: 100%;
height: 100%;
}
.collaborators-overlay-user {
position: absolute;
}
.collaborators-overlay-user-cursor {
position: absolute;
width: 2px;
animation: collaborators-overlay-cursor-blink 1s infinite;
}
.collaborators-overlay-user-label {
position: absolute;
font-size: 11px;
padding: 2px 6px;
border-radius: 4px;
transform: translateY(-100%);
margin-top: -2px;
background-color: var(--wp--preset--color--black);
color: var(--wp--preset--color--white);
white-space: nowrap;
}
@keyframes collaborators-overlay-cursor-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
`;

interface OverlayProps {
blockEditorDocument?: Document;
postId: number | null;
postType: string | null;
}

/**
* This component is responsible for rendering the overlay components within the editor iframe.
*
* @param props - The overlay props.
* @param props.blockEditorDocument - The block editor document.
* @param props.postId - The ID of the post.
* @param props.postType - The type of the post.
* @return The Overlay component.
*/
export function Overlay( {
blockEditorDocument,
postId,
postType,
}: OverlayProps ) {
useStyleOverride( {
id: 'collaborators-overlay',
css: COLLABORATORS_OVERLAY_STYLES,
} );

// Use state for the overlay element so that the hook re-runs once the ref is attached.
const [ overlayElement, setOverlayElement ] =
useState< HTMLDivElement | null >( null );

const { cursors, rerenderCursorsAfterDelay } = useRenderCursors(
overlayElement,
blockEditorDocument ?? null,
postId ?? null,
postType ?? null
);

// Detect layout changes on overlay (e.g. turning on "Show Template") and window
// resizes, and re-render the cursors.
const resizeObserverRef = useResizeObserver( rerenderCursorsAfterDelay );
useEffect( rerenderCursorsAfterDelay, [ rerenderCursorsAfterDelay ] );

// Merge the refs to use the same element for both overlay and resize observation
const mergedRef = useMergeRefs( [ setOverlayElement, resizeObserverRef ] );

useBlockHighlighting(
blockEditorDocument ?? null,
postId ?? null,
postType ?? null
);

// This is a full overlay that covers the entire iframe document. Good for
// scrollable elements like cursor indicators.
return (
<div className="collaborators-overlay-full" ref={ mergedRef }>
{ cursors.map( ( cursor ) => (
<div
key={ cursor.clientId }
className="collaborators-overlay-user"
style={ {
left: `${ cursor.x }px`,
top: `${ cursor.y }px`,
} }
>
<div
className="collaborators-overlay-user-cursor"
style={ {
backgroundColor: cursor.color,
height: `${ cursor.height }px`,
} }
/>
<div
className="collaborators-overlay-user-label"
style={ {
backgroundColor: cursor.color,
} }
>
{ cursor.userName }
</div>
</div>
) ) }
</div>
);
}
Loading
Loading