@wordpress/editor 14.41.0 → 14.41.1-next.v.202603102151.0

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.
Files changed (207) hide show
  1. package/build/components/collab-sidebar/index.cjs +7 -4
  2. package/build/components/collab-sidebar/index.cjs.map +2 -2
  3. package/build/components/collab-sidebar/utils.cjs +13 -15
  4. package/build/components/collab-sidebar/utils.cjs.map +2 -2
  5. package/build/components/collaborators-overlay/avatar-iframe-styles.cjs +133 -0
  6. package/build/components/collaborators-overlay/avatar-iframe-styles.cjs.map +7 -0
  7. package/build/components/collaborators-overlay/collaborator-styles.cjs +38 -2
  8. package/build/components/collaborators-overlay/collaborator-styles.cjs.map +2 -2
  9. package/build/components/collaborators-overlay/overlay-iframe-styles.cjs +142 -0
  10. package/build/components/collaborators-overlay/overlay-iframe-styles.cjs.map +7 -0
  11. package/build/components/collaborators-overlay/overlay.cjs +59 -201
  12. package/build/components/collaborators-overlay/overlay.cjs.map +3 -3
  13. package/build/components/collaborators-overlay/use-block-highlighting.cjs +91 -42
  14. package/build/components/collaborators-overlay/use-block-highlighting.cjs.map +2 -2
  15. package/build/components/collaborators-overlay/use-debounced-recompute.cjs +49 -0
  16. package/build/components/collaborators-overlay/use-debounced-recompute.cjs.map +7 -0
  17. package/build/components/collaborators-overlay/use-render-cursors.cjs +49 -50
  18. package/build/components/collaborators-overlay/use-render-cursors.cjs.map +2 -2
  19. package/build/components/collaborators-presence/avatar/component.cjs +121 -0
  20. package/build/components/collaborators-presence/avatar/component.cjs.map +7 -0
  21. package/build/components/collaborators-presence/avatar/index.cjs +37 -0
  22. package/build/components/collaborators-presence/avatar/index.cjs.map +7 -0
  23. package/build/components/collaborators-presence/avatar/types.cjs +19 -0
  24. package/build/components/collaborators-presence/avatar/types.cjs.map +7 -0
  25. package/build/components/collaborators-presence/avatar/use-image-loading-status.cjs +44 -0
  26. package/build/components/collaborators-presence/avatar/use-image-loading-status.cjs.map +7 -0
  27. package/build/components/collaborators-presence/avatar-group/component.cjs +78 -0
  28. package/build/components/collaborators-presence/avatar-group/component.cjs.map +7 -0
  29. package/build/components/collaborators-presence/avatar-group/index.cjs +37 -0
  30. package/build/components/collaborators-presence/avatar-group/index.cjs.map +7 -0
  31. package/build/components/collaborators-presence/avatar-group/types.cjs +19 -0
  32. package/build/components/collaborators-presence/avatar-group/types.cjs.map +7 -0
  33. package/build/components/collaborators-presence/index.cjs +17 -6
  34. package/build/components/collaborators-presence/index.cjs.map +3 -3
  35. package/build/components/collaborators-presence/list.cjs +20 -17
  36. package/build/components/collaborators-presence/list.cjs.map +3 -3
  37. package/build/components/entities-saved-states/hooks/use-is-dirty.cjs +14 -5
  38. package/build/components/entities-saved-states/hooks/use-is-dirty.cjs.map +2 -2
  39. package/build/components/global-styles/index.cjs +15 -24
  40. package/build/components/global-styles/index.cjs.map +3 -3
  41. package/build/components/global-styles-sidebar/index.cjs +6 -3
  42. package/build/components/global-styles-sidebar/index.cjs.map +2 -2
  43. package/build/components/page-attributes/parent.cjs +1 -0
  44. package/build/components/page-attributes/parent.cjs.map +2 -2
  45. package/build/components/post-revisions-preview/revisions-canvas.cjs +17 -4
  46. package/build/components/post-revisions-preview/revisions-canvas.cjs.map +2 -2
  47. package/build/components/post-url/panel.cjs +1 -0
  48. package/build/components/post-url/panel.cjs.map +2 -2
  49. package/build/components/provider/use-block-editor-settings.cjs +4 -1
  50. package/build/components/provider/use-block-editor-settings.cjs.map +3 -3
  51. package/build/components/sidebar/dataform-post-summary.cjs +167 -0
  52. package/build/components/sidebar/dataform-post-summary.cjs.map +7 -0
  53. package/build/components/sidebar/post-summary.cjs +11 -0
  54. package/build/components/sidebar/post-summary.cjs.map +3 -3
  55. package/build/components/visual-editor/index.cjs +1 -1
  56. package/build/components/visual-editor/index.cjs.map +2 -2
  57. package/build/dataviews/store/private-actions.cjs +4 -0
  58. package/build/dataviews/store/private-actions.cjs.map +2 -2
  59. package/build/utils/media-upload/on-success.cjs +46 -0
  60. package/build/utils/media-upload/on-success.cjs.map +7 -0
  61. package/build-module/components/collab-sidebar/index.mjs +7 -4
  62. package/build-module/components/collab-sidebar/index.mjs.map +2 -2
  63. package/build-module/components/collab-sidebar/utils.mjs +13 -15
  64. package/build-module/components/collab-sidebar/utils.mjs.map +2 -2
  65. package/build-module/components/collaborators-overlay/avatar-iframe-styles.mjs +120 -0
  66. package/build-module/components/collaborators-overlay/avatar-iframe-styles.mjs.map +7 -0
  67. package/build-module/components/collaborators-overlay/collaborator-styles.mjs +25 -1
  68. package/build-module/components/collaborators-overlay/collaborator-styles.mjs.map +2 -2
  69. package/build-module/components/collaborators-overlay/overlay-iframe-styles.mjs +124 -0
  70. package/build-module/components/collaborators-overlay/overlay-iframe-styles.mjs.map +7 -0
  71. package/build-module/components/collaborators-overlay/overlay.mjs +49 -201
  72. package/build-module/components/collaborators-overlay/overlay.mjs.map +2 -2
  73. package/build-module/components/collaborators-overlay/use-block-highlighting.mjs +92 -43
  74. package/build-module/components/collaborators-overlay/use-block-highlighting.mjs.map +2 -2
  75. package/build-module/components/collaborators-overlay/use-debounced-recompute.mjs +24 -0
  76. package/build-module/components/collaborators-overlay/use-debounced-recompute.mjs.map +7 -0
  77. package/build-module/components/collaborators-overlay/use-render-cursors.mjs +50 -51
  78. package/build-module/components/collaborators-overlay/use-render-cursors.mjs.map +2 -2
  79. package/build-module/components/collaborators-presence/avatar/component.mjs +90 -0
  80. package/build-module/components/collaborators-presence/avatar/component.mjs.map +7 -0
  81. package/build-module/components/collaborators-presence/avatar/index.mjs +6 -0
  82. package/build-module/components/collaborators-presence/avatar/index.mjs.map +7 -0
  83. package/build-module/components/collaborators-presence/avatar/types.mjs +1 -0
  84. package/build-module/components/collaborators-presence/avatar/types.mjs.map +7 -0
  85. package/build-module/components/collaborators-presence/avatar/use-image-loading-status.mjs +19 -0
  86. package/build-module/components/collaborators-presence/avatar/use-image-loading-status.mjs.map +7 -0
  87. package/build-module/components/collaborators-presence/avatar-group/component.mjs +47 -0
  88. package/build-module/components/collaborators-presence/avatar-group/component.mjs.map +7 -0
  89. package/build-module/components/collaborators-presence/avatar-group/index.mjs +6 -0
  90. package/build-module/components/collaborators-presence/avatar-group/index.mjs.map +7 -0
  91. package/build-module/components/collaborators-presence/avatar-group/types.mjs +1 -0
  92. package/build-module/components/collaborators-presence/avatar-group/types.mjs.map +7 -0
  93. package/build-module/components/collaborators-presence/index.mjs +7 -9
  94. package/build-module/components/collaborators-presence/index.mjs.map +2 -2
  95. package/build-module/components/collaborators-presence/list.mjs +11 -22
  96. package/build-module/components/collaborators-presence/list.mjs.map +2 -2
  97. package/build-module/components/entities-saved-states/hooks/use-is-dirty.mjs +14 -5
  98. package/build-module/components/entities-saved-states/hooks/use-is-dirty.mjs.map +2 -2
  99. package/build-module/components/global-styles/index.mjs +15 -24
  100. package/build-module/components/global-styles/index.mjs.map +2 -2
  101. package/build-module/components/global-styles-sidebar/index.mjs +6 -3
  102. package/build-module/components/global-styles-sidebar/index.mjs.map +2 -2
  103. package/build-module/components/page-attributes/parent.mjs +1 -0
  104. package/build-module/components/page-attributes/parent.mjs.map +2 -2
  105. package/build-module/components/post-revisions-preview/revisions-canvas.mjs +17 -4
  106. package/build-module/components/post-revisions-preview/revisions-canvas.mjs.map +2 -2
  107. package/build-module/components/post-url/panel.mjs +1 -0
  108. package/build-module/components/post-url/panel.mjs.map +2 -2
  109. package/build-module/components/provider/use-block-editor-settings.mjs +4 -1
  110. package/build-module/components/provider/use-block-editor-settings.mjs.map +2 -2
  111. package/build-module/components/sidebar/dataform-post-summary.mjs +136 -0
  112. package/build-module/components/sidebar/dataform-post-summary.mjs.map +7 -0
  113. package/build-module/components/sidebar/post-summary.mjs +11 -0
  114. package/build-module/components/sidebar/post-summary.mjs.map +2 -2
  115. package/build-module/components/visual-editor/index.mjs +1 -1
  116. package/build-module/components/visual-editor/index.mjs.map +2 -2
  117. package/build-module/dataviews/store/private-actions.mjs +8 -1
  118. package/build-module/dataviews/store/private-actions.mjs.map +2 -2
  119. package/build-module/utils/media-upload/on-success.mjs +25 -0
  120. package/build-module/utils/media-upload/on-success.mjs.map +7 -0
  121. package/build-style/style-rtl.css +876 -137
  122. package/build-style/style.css +876 -137
  123. package/build-types/components/collab-sidebar/index.d.ts.map +1 -1
  124. package/build-types/components/collab-sidebar/utils.d.ts.map +1 -1
  125. package/build-types/components/collaborators-overlay/avatar-iframe-styles.d.ts +11 -0
  126. package/build-types/components/collaborators-overlay/avatar-iframe-styles.d.ts.map +1 -0
  127. package/build-types/components/collaborators-overlay/collaborator-styles.d.ts +17 -2
  128. package/build-types/components/collaborators-overlay/collaborator-styles.d.ts.map +1 -1
  129. package/build-types/components/collaborators-overlay/overlay-iframe-styles.d.ts +6 -0
  130. package/build-types/components/collaborators-overlay/overlay-iframe-styles.d.ts.map +1 -0
  131. package/build-types/components/collaborators-overlay/overlay.d.ts.map +1 -1
  132. package/build-types/components/collaborators-overlay/use-block-highlighting.d.ts +21 -5
  133. package/build-types/components/collaborators-overlay/use-block-highlighting.d.ts.map +1 -1
  134. package/build-types/components/collaborators-overlay/use-debounced-recompute.d.ts +10 -0
  135. package/build-types/components/collaborators-overlay/use-debounced-recompute.d.ts.map +1 -0
  136. package/build-types/components/collaborators-overlay/use-render-cursors.d.ts +2 -1
  137. package/build-types/components/collaborators-overlay/use-render-cursors.d.ts.map +1 -1
  138. package/build-types/components/collaborators-presence/avatar/component.d.ts +7 -0
  139. package/build-types/components/collaborators-presence/avatar/component.d.ts.map +1 -0
  140. package/build-types/components/collaborators-presence/avatar/index.d.ts +3 -0
  141. package/build-types/components/collaborators-presence/avatar/index.d.ts.map +1 -0
  142. package/build-types/components/collaborators-presence/avatar/types.d.ts +66 -0
  143. package/build-types/components/collaborators-presence/avatar/types.d.ts.map +1 -0
  144. package/build-types/components/collaborators-presence/avatar/use-image-loading-status.d.ts +17 -0
  145. package/build-types/components/collaborators-presence/avatar/use-image-loading-status.d.ts.map +1 -0
  146. package/build-types/components/collaborators-presence/avatar-group/component.d.ts +7 -0
  147. package/build-types/components/collaborators-presence/avatar-group/component.d.ts.map +1 -0
  148. package/build-types/components/collaborators-presence/avatar-group/index.d.ts +3 -0
  149. package/build-types/components/collaborators-presence/avatar-group/index.d.ts.map +1 -0
  150. package/build-types/components/collaborators-presence/avatar-group/types.d.ts +14 -0
  151. package/build-types/components/collaborators-presence/avatar-group/types.d.ts.map +1 -0
  152. package/build-types/components/collaborators-presence/index.d.ts.map +1 -1
  153. package/build-types/components/collaborators-presence/list.d.ts.map +1 -1
  154. package/build-types/components/entities-saved-states/hooks/use-is-dirty.d.ts.map +1 -1
  155. package/build-types/components/global-styles/index.d.ts +2 -1
  156. package/build-types/components/global-styles/index.d.ts.map +1 -1
  157. package/build-types/components/global-styles-sidebar/index.d.ts.map +1 -1
  158. package/build-types/components/page-attributes/parent.d.ts.map +1 -1
  159. package/build-types/components/post-author/hook.d.ts +1 -1
  160. package/build-types/components/post-revisions-preview/revisions-canvas.d.ts.map +1 -1
  161. package/build-types/components/provider/use-block-editor-settings.d.ts.map +1 -1
  162. package/build-types/components/sidebar/dataform-post-summary.d.ts +4 -0
  163. package/build-types/components/sidebar/dataform-post-summary.d.ts.map +1 -0
  164. package/build-types/components/sidebar/post-summary.d.ts.map +1 -1
  165. package/build-types/dataviews/store/private-actions.d.ts.map +1 -1
  166. package/build-types/utils/media-upload/on-success.d.ts +9 -0
  167. package/build-types/utils/media-upload/on-success.d.ts.map +1 -0
  168. package/package.json +45 -44
  169. package/src/components/collab-sidebar/index.js +7 -4
  170. package/src/components/collab-sidebar/utils.js +9 -10
  171. package/src/components/collaborators-overlay/avatar-iframe-styles.ts +126 -0
  172. package/src/components/collaborators-overlay/collaborator-styles.ts +43 -2
  173. package/src/components/collaborators-overlay/overlay-iframe-styles.ts +125 -0
  174. package/src/components/collaborators-overlay/overlay.tsx +54 -207
  175. package/src/components/collaborators-overlay/use-block-highlighting.ts +147 -64
  176. package/src/components/collaborators-overlay/use-debounced-recompute.ts +32 -0
  177. package/src/components/collaborators-overlay/use-render-cursors.ts +72 -66
  178. package/src/components/collaborators-presence/avatar/component.tsx +123 -0
  179. package/src/components/collaborators-presence/avatar/index.ts +2 -0
  180. package/src/components/collaborators-presence/avatar/styles.scss +168 -0
  181. package/src/components/collaborators-presence/avatar/test/index.tsx +389 -0
  182. package/src/components/collaborators-presence/avatar/types.ts +66 -0
  183. package/src/components/collaborators-presence/avatar/use-image-loading-status.ts +36 -0
  184. package/src/components/collaborators-presence/avatar-group/component.tsx +55 -0
  185. package/src/components/collaborators-presence/avatar-group/index.ts +2 -0
  186. package/src/components/collaborators-presence/avatar-group/styles.scss +33 -0
  187. package/src/components/collaborators-presence/avatar-group/test/index.tsx +139 -0
  188. package/src/components/collaborators-presence/avatar-group/types.ts +13 -0
  189. package/src/components/collaborators-presence/index.tsx +4 -6
  190. package/src/components/collaborators-presence/list.tsx +7 -17
  191. package/src/components/collaborators-presence/styles/collaborators-list.scss +26 -19
  192. package/src/components/collaborators-presence/styles/collaborators-presence.scss +6 -2
  193. package/src/components/entities-saved-states/hooks/use-is-dirty.js +14 -5
  194. package/src/components/global-styles/index.js +20 -27
  195. package/src/components/global-styles-sidebar/index.js +3 -0
  196. package/src/components/page-attributes/parent.js +1 -0
  197. package/src/components/post-publish-panel/test/__snapshots__/index.js.snap +2 -2
  198. package/src/components/post-revisions-preview/revisions-canvas.js +15 -6
  199. package/src/components/post-url/panel.js +1 -0
  200. package/src/components/post-url/style.scss +5 -0
  201. package/src/components/provider/use-block-editor-settings.js +5 -0
  202. package/src/components/sidebar/dataform-post-summary.js +149 -0
  203. package/src/components/sidebar/post-summary.js +15 -0
  204. package/src/components/visual-editor/index.js +1 -1
  205. package/src/dataviews/store/private-actions.ts +14 -0
  206. package/src/style.scss +3 -0
  207. package/src/utils/media-upload/on-success.js +34 -0
@@ -4,31 +4,53 @@
4
4
  import {
5
5
  privateApis as coreDataPrivateApis,
6
6
  SelectionType,
7
+ type PostEditorAwarenessState,
7
8
  } from '@wordpress/core-data';
8
- import { useEffect, useRef } from '@wordpress/element';
9
+ import { useEffect, useRef, useState } from '@wordpress/element';
9
10
 
10
11
  /**
11
12
  * Internal dependencies
12
13
  */
13
14
  import { unlock } from '../../lock-unlock';
14
15
  import { getAvatarBorderColor } from '../collab-sidebar/utils';
16
+ import { getAvatarUrl } from './get-avatar-url';
17
+ import { useDebouncedRecompute } from './use-debounced-recompute';
15
18
 
16
19
  const { useActiveCollaborators, useResolvedSelection } =
17
20
  unlock( coreDataPrivateApis );
18
21
 
22
+ export interface BlockHighlightData {
23
+ blockId: string;
24
+ userName: string;
25
+ avatarUrl?: string;
26
+ color: string;
27
+ x: number;
28
+ y: number;
29
+ }
30
+
19
31
  /**
20
- * Custom hook for highlighting selected blocks in the editor
21
- * @param blockEditorDocument - Ref to the block editor document, used to directly style block elements.
22
- * @param postId - The ID of the post
23
- * @param postType - The type of the post
32
+ * Custom hook for highlighting selected blocks in the editor and computing
33
+ * their positions for rendering avatar labels in the overlay.
34
+ *
35
+ * @param overlayElement - The overlay element used as position reference.
36
+ * @param blockEditorDocument - Ref to the block editor document.
37
+ * @param postId - The ID of the post.
38
+ * @param postType - The type of the post.
39
+ * @param delayMs - Milliseconds to wait before recomputing highlight positions.
40
+ * @return Highlight data for rendering and a delayed recompute function.
24
41
  */
25
42
  export function useBlockHighlighting(
43
+ overlayElement: HTMLElement | null,
26
44
  blockEditorDocument: Document | null,
27
45
  postId: number | null,
28
- postType: string | null
29
- ) {
46
+ postType: string | null,
47
+ delayMs: number
48
+ ): {
49
+ highlights: BlockHighlightData[];
50
+ rerenderHighlightsAfterDelay: () => () => void;
51
+ } {
30
52
  const highlightedBlockIds = useRef< Set< string > >( new Set() );
31
- const userStates = useActiveCollaborators(
53
+ const userStates: PostEditorAwarenessState[] = useActiveCollaborators(
32
54
  postId ?? null,
33
55
  postType ?? null
34
56
  );
@@ -37,15 +59,78 @@ export function useBlockHighlighting(
37
59
  postType ?? null
38
60
  );
39
61
 
40
- // Draw block highlights
62
+ const [ highlights, setHighlights ] = useState< BlockHighlightData[] >(
63
+ []
64
+ );
65
+
66
+ // Bump this counter to force the effect to re-run (e.g. after a layout shift).
67
+ const [ recomputeToken, rerenderHighlightsAfterDelay ] =
68
+ useDebouncedRecompute( delayMs );
69
+
70
+ // All DOM mutations and position computations live inside useEffect.
41
71
  useEffect( () => {
42
- // Don't do anything if editor is not rendered yet.
43
- if ( blockEditorDocument === null ) {
72
+ if ( ! blockEditorDocument ) {
73
+ setHighlights( [] );
44
74
  return;
45
75
  }
46
76
 
47
- const unhighlightBlocks = ( blockIds: string[] ) => {
48
- blockIds.forEach( ( blockId ) => {
77
+ // Capture the ref value so the cleanup closure sees the same Set
78
+ // even if a later render replaces it.
79
+ const currentHighlightedIds = highlightedBlockIds.current;
80
+
81
+ // Deduplicate by blockId — when multiple collaborators select the
82
+ // same block, only the first one gets the highlight and avatar label.
83
+ const seen = new Set< string >();
84
+ const blocksToHighlight = userStates
85
+ .filter(
86
+ ( userState ) =>
87
+ ! userState.isMe &&
88
+ userState.editorState?.selection?.type ===
89
+ SelectionType.WholeBlock
90
+ )
91
+ .map( ( userState ) => {
92
+ let localClientId;
93
+ try {
94
+ ( { localClientId } = resolveSelection(
95
+ userState.editorState?.selection
96
+ ) );
97
+ } catch {
98
+ return null;
99
+ }
100
+
101
+ if ( ! localClientId ) {
102
+ return null;
103
+ }
104
+
105
+ return {
106
+ blockId: localClientId,
107
+ color: getAvatarBorderColor(
108
+ userState.collaboratorInfo.id
109
+ ),
110
+ userName: userState.collaboratorInfo.name,
111
+ avatarUrl: getAvatarUrl(
112
+ userState.collaboratorInfo.avatar_urls
113
+ ),
114
+ };
115
+ } )
116
+ .filter( ( block ): block is NonNullable< typeof block > => {
117
+ if ( ! block ) {
118
+ return false;
119
+ }
120
+ if ( seen.has( block.blockId ) ) {
121
+ return false;
122
+ }
123
+ seen.add( block.blockId );
124
+ return true;
125
+ } );
126
+
127
+ // Unhighlight blocks that are no longer selected.
128
+ const selectedBlockIds = new Set(
129
+ blocksToHighlight.map( ( block ) => block.blockId )
130
+ );
131
+
132
+ for ( const blockId of currentHighlightedIds ) {
133
+ if ( ! selectedBlockIds.has( blockId ) ) {
49
134
  const blockElement = getBlockElementById(
50
135
  blockEditorDocument,
51
136
  blockId
@@ -58,51 +143,16 @@ export function useBlockHighlighting(
58
143
  );
59
144
  }
60
145
 
61
- highlightedBlockIds.current.delete( blockId );
62
- } );
63
- };
64
-
65
- const blocksToHighlight = userStates
66
- .map( ( userState: any ) => {
67
- const isWholeBlockSelected =
68
- userState.editorState?.selection?.type ===
69
- SelectionType.WholeBlock;
70
- const shouldDrawUser = ! userState.isMe;
71
-
72
- if ( isWholeBlockSelected && shouldDrawUser ) {
73
- const { localClientId } = resolveSelection(
74
- userState.editorState?.selection
75
- );
76
-
77
- if ( ! localClientId ) {
78
- return null;
79
- }
80
-
81
- return {
82
- blockId: localClientId,
83
- color: getAvatarBorderColor(
84
- userState.collaboratorInfo.id
85
- ),
86
- };
87
- }
88
-
89
- return null;
90
- } )
91
- .filter( ( block: any ) => block !== null );
92
-
93
- // Unhighlight blocks that are no longer highlighted.
94
- const selectedBlockIds = blocksToHighlight.map(
95
- ( block: any ) => block.blockId
96
- );
97
- const blocksIdsToUnhighlight = Array.from(
98
- highlightedBlockIds.current
99
- ).filter( ( blockId ) => ! selectedBlockIds.includes( blockId ) );
146
+ currentHighlightedIds.delete( blockId );
147
+ }
148
+ }
100
149
 
101
- unhighlightBlocks( blocksIdsToUnhighlight );
150
+ // Highlight blocks and compute positions for avatar labels.
151
+ const results: BlockHighlightData[] = [];
152
+ const overlayRect = overlayElement?.getBoundingClientRect() ?? null;
102
153
 
103
- // Highlight blocks that are currently highlighted.
104
- blocksToHighlight.forEach( ( blockColorPair: any ) => {
105
- const { color, blockId } = blockColorPair;
154
+ blocksToHighlight.forEach( ( block ) => {
155
+ const { color, blockId, userName, avatarUrl } = block;
106
156
  const blockElement = getBlockElementById(
107
157
  blockEditorDocument,
108
158
  blockId
@@ -112,16 +162,49 @@ export function useBlockHighlighting(
112
162
  return;
113
163
  }
114
164
 
115
- if ( blockElement ) {
116
- blockElement.classList.add( 'is-collaborator-selected' );
117
- blockElement.style.setProperty(
118
- '--collaborator-outline-color',
119
- color
120
- );
121
- highlightedBlockIds.current.add( blockId );
165
+ blockElement.classList.add( 'is-collaborator-selected' );
166
+ blockElement.style.setProperty(
167
+ '--collaborator-outline-color',
168
+ color
169
+ );
170
+ currentHighlightedIds.add( blockId );
171
+
172
+ if ( overlayRect ) {
173
+ const blockRect = blockElement.getBoundingClientRect();
174
+
175
+ results.push( {
176
+ blockId,
177
+ userName,
178
+ avatarUrl,
179
+ color,
180
+ x: blockRect.left - overlayRect.left,
181
+ y: blockRect.top - overlayRect.top,
182
+ } );
122
183
  }
123
184
  } );
124
- }, [ userStates, blockEditorDocument, resolveSelection ] );
185
+
186
+ setHighlights( results );
187
+
188
+ // Clean up all highlights on unmount.
189
+ return () => {
190
+ for ( const blockId of currentHighlightedIds ) {
191
+ const el = getBlockElementById( blockEditorDocument, blockId );
192
+ if ( el ) {
193
+ el.classList.remove( 'is-collaborator-selected' );
194
+ el.style.removeProperty( '--collaborator-outline-color' );
195
+ }
196
+ }
197
+ currentHighlightedIds.clear();
198
+ };
199
+ }, [
200
+ userStates,
201
+ blockEditorDocument,
202
+ overlayElement,
203
+ recomputeToken,
204
+ resolveSelection,
205
+ ] );
206
+
207
+ return { highlights, rerenderHighlightsAfterDelay };
125
208
  }
126
209
 
127
210
  const getBlockElementById = (
@@ -0,0 +1,32 @@
1
+ import { useCallback, useRef, useState } from '@wordpress/element';
2
+
3
+ /**
4
+ * Returns a recompute token and a debounced callback that bumps it.
5
+ * Rapid successive calls cancel the previous pending timeout so only one
6
+ * recompute fires, `delayMs` after the last call.
7
+ *
8
+ * @param delayMs - Milliseconds to wait before bumping the recompute token.
9
+ * @return A tuple of [recomputeToken, rerenderAfterDelay].
10
+ */
11
+ export function useDebouncedRecompute(
12
+ delayMs: number
13
+ ): [ number, () => () => void ] {
14
+ const [ recomputeToken, setRecomputeToken ] = useState( 0 );
15
+ const timeoutRef = useRef< ReturnType< typeof setTimeout > | null >( null );
16
+
17
+ const rerenderAfterDelay = useCallback( () => {
18
+ if ( timeoutRef.current ) {
19
+ clearTimeout( timeoutRef.current );
20
+ }
21
+ timeoutRef.current = setTimeout( () => {
22
+ setRecomputeToken( ( t ) => t + 1 );
23
+ }, delayMs );
24
+ return () => {
25
+ if ( timeoutRef.current ) {
26
+ clearTimeout( timeoutRef.current );
27
+ }
28
+ };
29
+ }, [ delayMs ] );
30
+
31
+ return [ recomputeToken, rerenderAfterDelay ];
32
+ }
@@ -2,11 +2,12 @@ import {
2
2
  privateApis as coreDataPrivateApis,
3
3
  SelectionType,
4
4
  } from '@wordpress/core-data';
5
- import { useEffect, useMemo, useState } from '@wordpress/element';
5
+ import { useEffect, useState } from '@wordpress/element';
6
6
 
7
7
  import { unlock } from '../../lock-unlock';
8
8
  import { getAvatarUrl } from './get-avatar-url';
9
9
  import { getAvatarBorderColor } from '../collab-sidebar/utils';
10
+ import { useDebouncedRecompute } from './use-debounced-recompute';
10
11
 
11
12
  const { useActiveCollaborators, useResolvedSelection } =
12
13
  unlock( coreDataPrivateApis );
@@ -28,13 +29,15 @@ export interface CursorData {
28
29
  * @param blockEditorDocument - The block editor document
29
30
  * @param postId - The ID of the post
30
31
  * @param postType - The type of the post
32
+ * @param delayMs - Milliseconds to wait before recomputing cursor positions.
31
33
  * @return An array of cursor data for rendering, and a function to trigger a delayed recompute.
32
34
  */
33
35
  export function useRenderCursors(
34
36
  overlayElement: HTMLElement | null,
35
37
  blockEditorDocument: Document | null,
36
38
  postId: number | null,
37
- postType: string | null
39
+ postType: string | null,
40
+ delayMs: number
38
41
  ): { cursors: CursorData[]; rerenderCursorsAfterDelay: () => () => void } {
39
42
  const sortedUsers = useActiveCollaborators(
40
43
  postId ?? null,
@@ -49,41 +52,44 @@ export function useRenderCursors(
49
52
  []
50
53
  );
51
54
 
52
- const computeCursors = useMemo(
53
- () => () => {
54
- if ( ! overlayElement || ! blockEditorDocument ) {
55
- setCursorPositions( [] );
56
- return;
57
- }
55
+ // Bump this counter to force the effect to re-run (e.g. after a layout shift).
56
+ const [ recomputeToken, rerenderCursorsAfterDelay ] =
57
+ useDebouncedRecompute( delayMs );
58
58
 
59
- const results: CursorData[] = [];
59
+ // All DOM position computations live inside useEffect.
60
+ useEffect( () => {
61
+ if ( ! overlayElement || ! blockEditorDocument ) {
62
+ setCursorPositions( [] );
63
+ return;
64
+ }
60
65
 
61
- sortedUsers.forEach( ( user: any ) => {
62
- if ( user.isMe ) {
63
- return;
64
- }
66
+ const results: CursorData[] = [];
65
67
 
66
- const selection = user.editorState?.selection ?? {
67
- type: SelectionType.None,
68
- };
69
- const userName = user.collaboratorInfo.name;
70
- const clientId = user.clientId;
71
- const color = getAvatarBorderColor( user.collaboratorInfo.id );
72
- const avatarUrl = getAvatarUrl(
73
- user.collaboratorInfo.avatar_urls
74
- );
75
-
76
- let coords: {
77
- x: number;
78
- y: number;
79
- height: number;
80
- } | null = null;
81
-
82
- if ( selection.type === SelectionType.None ) {
83
- // Nothing selected.
84
- } else if ( selection.type === SelectionType.WholeBlock ) {
85
- // Don't draw a cursor for a whole block selection.
86
- } else if ( selection.type === SelectionType.Cursor ) {
68
+ sortedUsers.forEach( ( user: any ) => {
69
+ if ( user.isMe ) {
70
+ return;
71
+ }
72
+
73
+ const selection = user.editorState?.selection ?? {
74
+ type: SelectionType.None,
75
+ };
76
+ const userName = user.collaboratorInfo.name;
77
+ const clientId = user.clientId;
78
+ const color = getAvatarBorderColor( user.collaboratorInfo.id );
79
+ const avatarUrl = getAvatarUrl( user.collaboratorInfo.avatar_urls );
80
+
81
+ let coords: {
82
+ x: number;
83
+ y: number;
84
+ height: number;
85
+ } | null = null;
86
+
87
+ if ( selection.type === SelectionType.None ) {
88
+ // Nothing selected.
89
+ } else if ( selection.type === SelectionType.WholeBlock ) {
90
+ // Don't draw a cursor for a whole block selection.
91
+ } else if ( selection.type === SelectionType.Cursor ) {
92
+ try {
87
93
  const { textIndex, localClientId } =
88
94
  resolveSelection( selection );
89
95
  if ( localClientId ) {
@@ -94,10 +100,14 @@ export function useRenderCursors(
94
100
  overlayElement
95
101
  );
96
102
  }
97
- } else if (
98
- selection.type === SelectionType.SelectionInOneBlock ||
99
- selection.type === SelectionType.SelectionInMultipleBlocks
100
- ) {
103
+ } catch {
104
+ // Selection may reference a stale Yjs position.
105
+ }
106
+ } else if (
107
+ selection.type === SelectionType.SelectionInOneBlock ||
108
+ selection.type === SelectionType.SelectionInMultipleBlocks
109
+ ) {
110
+ try {
101
111
  const { textIndex, localClientId } = resolveSelection( {
102
112
  type: SelectionType.Cursor,
103
113
  cursorPosition: selection.cursorStartPosition,
@@ -110,33 +120,30 @@ export function useRenderCursors(
110
120
  overlayElement
111
121
  );
112
122
  }
123
+ } catch {
124
+ // Selection may reference a stale Yjs position.
113
125
  }
126
+ }
114
127
 
115
- if ( coords ) {
116
- results.push( {
117
- userName,
118
- clientId,
119
- color,
120
- avatarUrl,
121
- ...coords,
122
- } );
123
- }
124
- } );
125
-
126
- setCursorPositions( results );
127
- },
128
- [ blockEditorDocument, resolveSelection, overlayElement, sortedUsers ]
129
- );
130
-
131
- useEffect( computeCursors, [ computeCursors ] );
128
+ if ( coords ) {
129
+ results.push( {
130
+ userName,
131
+ clientId,
132
+ color,
133
+ avatarUrl,
134
+ ...coords,
135
+ } );
136
+ }
137
+ } );
132
138
 
133
- const rerenderCursorsAfterDelay = useMemo(
134
- () => () => {
135
- const timeout = setTimeout( computeCursors, 500 );
136
- return () => clearTimeout( timeout );
137
- },
138
- [ computeCursors ]
139
- );
139
+ setCursorPositions( results );
140
+ }, [
141
+ blockEditorDocument,
142
+ resolveSelection,
143
+ overlayElement,
144
+ sortedUsers,
145
+ recomputeToken,
146
+ ] );
140
147
 
141
148
  return { cursors: cursorPositions, rerenderCursorsAfterDelay };
142
149
  }
@@ -236,11 +243,10 @@ const getOffsetPositionInBlock = (
236
243
 
237
244
  let cursorHeight = cursorRect.height;
238
245
  if ( cursorHeight === 0 ) {
246
+ const view = editorDocument.defaultView ?? window;
239
247
  cursorHeight =
240
- parseInt(
241
- window.getComputedStyle( blockElement ).lineHeight,
242
- 10
243
- ) || blockRect.height;
248
+ parseInt( view.getComputedStyle( blockElement ).lineHeight, 10 ) ||
249
+ blockRect.height;
244
250
  }
245
251
 
246
252
  return {
@@ -0,0 +1,123 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import clsx from 'clsx';
5
+ import { colord, extend } from 'colord';
6
+ import a11yPlugin from 'colord/plugins/a11y';
7
+
8
+ extend( [ a11yPlugin ] );
9
+
10
+ /**
11
+ * WordPress dependencies
12
+ */
13
+ import { Icon, Tooltip } from '@wordpress/components';
14
+ import { useMemo } from '@wordpress/element';
15
+
16
+ /**
17
+ * Internal dependencies
18
+ */
19
+ import type { AvatarProps } from './types';
20
+ import { useImageLoadingStatus } from './use-image-loading-status';
21
+
22
+ // Runtime equivalents of @wordpress/base-styles tokens ($gray-900, $white).
23
+ const GRAY_900 = '#1e1e1e';
24
+ const WHITE = '#fff';
25
+
26
+ function Avatar( {
27
+ className,
28
+ src,
29
+ name,
30
+ label,
31
+ variant,
32
+ size = 'default',
33
+ borderColor,
34
+ dimmed = false,
35
+ statusIndicator,
36
+ style,
37
+ ...props
38
+ }: AvatarProps &
39
+ Omit< React.HTMLAttributes< HTMLDivElement >, keyof AvatarProps > ) {
40
+ const {
41
+ status: imageStatus,
42
+ handleLoad,
43
+ handleError,
44
+ } = useImageLoadingStatus( src );
45
+ const imageLoaded = imageStatus === 'loaded';
46
+
47
+ const showBadge = variant === 'badge' && !! name;
48
+ const initials = name
49
+ ? name
50
+ .split( /\s+/ )
51
+ .slice( 0, 2 )
52
+ .map( ( word ) => word[ 0 ] )
53
+ .join( '' )
54
+ .toUpperCase()
55
+ : undefined;
56
+ const nameColor = useMemo(
57
+ () =>
58
+ borderColor &&
59
+ colord( borderColor ).isReadable( GRAY_900, {
60
+ level: 'AA',
61
+ size: 'normal',
62
+ } )
63
+ ? GRAY_900
64
+ : WHITE,
65
+ [ borderColor ]
66
+ );
67
+
68
+ const customProperties = {
69
+ ...style,
70
+ ...( borderColor
71
+ ? {
72
+ '--editor-avatar-outline-color': borderColor,
73
+ '--editor-avatar-name-color': nameColor,
74
+ }
75
+ : {} ),
76
+ } as React.CSSProperties;
77
+
78
+ const avatar = (
79
+ <div
80
+ className={ clsx( 'editor-avatar', className, {
81
+ 'has-avatar-border-color': !! borderColor,
82
+ 'has-src': imageLoaded,
83
+ 'is-badge': showBadge,
84
+ 'is-small': size === 'small',
85
+ 'is-dimmed': dimmed,
86
+ } ) }
87
+ style={ customProperties }
88
+ role={ name ? 'img' : undefined }
89
+ aria-label={ name || undefined }
90
+ { ...props }
91
+ >
92
+ <span className="editor-avatar__image">
93
+ { src && (
94
+ <img
95
+ src={ src }
96
+ alt=""
97
+ crossOrigin="anonymous"
98
+ className="editor-avatar__img"
99
+ onLoad={ handleLoad }
100
+ onError={ handleError }
101
+ />
102
+ ) }
103
+ { ! imageLoaded && initials }
104
+ </span>
105
+ { dimmed && !! statusIndicator && (
106
+ <span className="editor-avatar__status-indicator">
107
+ <Icon icon={ statusIndicator } />
108
+ </span>
109
+ ) }
110
+ { showBadge && (
111
+ <span className="editor-avatar__name">{ label || name }</span>
112
+ ) }
113
+ </div>
114
+ );
115
+
116
+ if ( name && ( ! showBadge || label ) ) {
117
+ return <Tooltip text={ name }>{ avatar }</Tooltip>;
118
+ }
119
+
120
+ return avatar;
121
+ }
122
+
123
+ export default Avatar;
@@ -0,0 +1,2 @@
1
+ export { default } from './component';
2
+ export type { AvatarProps } from './types';