@wordpress/editor 14.41.1-next.v.202603102151.0 → 14.42.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 (301) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/build/components/collaborators-overlay/avatar-iframe-styles.cjs +12 -4
  3. package/build/components/collaborators-overlay/avatar-iframe-styles.cjs.map +2 -2
  4. package/build/components/collaborators-overlay/compute-selection.cjs +181 -0
  5. package/build/components/collaborators-overlay/compute-selection.cjs.map +7 -0
  6. package/build/components/collaborators-overlay/cursor-dom-utils.cjs +243 -0
  7. package/build/components/collaborators-overlay/cursor-dom-utils.cjs.map +7 -0
  8. package/build/components/collaborators-overlay/overlay-iframe-styles.cjs +6 -0
  9. package/build/components/collaborators-overlay/overlay-iframe-styles.cjs.map +2 -2
  10. package/build/components/collaborators-overlay/overlay.cjs +61 -37
  11. package/build/components/collaborators-overlay/overlay.cjs.map +2 -2
  12. package/build/components/collaborators-overlay/timing-utils.cjs +46 -0
  13. package/build/components/collaborators-overlay/timing-utils.cjs.map +7 -0
  14. package/build/components/collaborators-overlay/use-block-highlighting.cjs +5 -6
  15. package/build/components/collaborators-overlay/use-block-highlighting.cjs.map +2 -2
  16. package/build/components/collaborators-overlay/use-render-cursors.cjs +50 -140
  17. package/build/components/collaborators-overlay/use-render-cursors.cjs.map +3 -3
  18. package/build/components/collaborators-presence/index.cjs +38 -12
  19. package/build/components/collaborators-presence/index.cjs.map +2 -2
  20. package/build/components/collaborators-presence/list.cjs +27 -24
  21. package/build/components/collaborators-presence/list.cjs.map +2 -2
  22. package/build/components/collaborators-presence/use-collaborator-notifications.cjs +79 -107
  23. package/build/components/collaborators-presence/use-collaborator-notifications.cjs.map +3 -3
  24. package/build/components/editor-interface/index.cjs +9 -6
  25. package/build/components/editor-interface/index.cjs.map +2 -2
  26. package/build/components/inserter-sidebar/index.cjs +2 -1
  27. package/build/components/inserter-sidebar/index.cjs.map +2 -2
  28. package/build/components/page-attributes/parent.cjs +1 -1
  29. package/build/components/page-attributes/parent.cjs.map +2 -2
  30. package/build/components/post-locked-modal/index.cjs +16 -3
  31. package/build/components/post-locked-modal/index.cjs.map +2 -2
  32. package/build/components/post-revisions-preview/block-diff.cjs +39 -11
  33. package/build/components/post-revisions-preview/block-diff.cjs.map +2 -2
  34. package/build/components/post-revisions-preview/diff-markers.cjs +2 -2
  35. package/build/components/post-revisions-preview/diff-markers.cjs.map +2 -2
  36. package/build/components/post-revisions-preview/revisions-canvas.cjs +12 -75
  37. package/build/components/post-revisions-preview/revisions-canvas.cjs.map +3 -3
  38. package/build/components/post-revisions-preview/revisions-slider.cjs +5 -1
  39. package/build/components/post-revisions-preview/revisions-slider.cjs.map +2 -2
  40. package/build/components/post-template/block-theme.cjs +7 -4
  41. package/build/components/post-template/block-theme.cjs.map +2 -2
  42. package/build/components/post-template/hooks.cjs +39 -2
  43. package/build/components/post-template/hooks.cjs.map +2 -2
  44. package/build/components/post-template/panel.cjs +5 -42
  45. package/build/components/post-template/panel.cjs.map +3 -3
  46. package/build/components/preferences-modal/index.cjs +24 -0
  47. package/build/components/preferences-modal/index.cjs.map +2 -2
  48. package/build/components/provider/disable-non-page-content-blocks.cjs +31 -28
  49. package/build/components/provider/disable-non-page-content-blocks.cjs.map +3 -3
  50. package/build/components/provider/index.cjs +17 -5
  51. package/build/components/provider/index.cjs.map +2 -2
  52. package/build/components/provider/use-block-editor-settings.cjs +19 -5
  53. package/build/components/provider/use-block-editor-settings.cjs.map +3 -3
  54. package/build/components/provider/{use-post-content-blocks.cjs → use-post-content-block-types.cjs} +8 -19
  55. package/build/components/provider/use-post-content-block-types.cjs.map +7 -0
  56. package/build/components/provider/use-revision-blocks.cjs +106 -0
  57. package/build/components/provider/use-revision-blocks.cjs.map +7 -0
  58. package/build/components/revision-block-diff/index.cjs +84 -0
  59. package/build/components/revision-block-diff/index.cjs.map +7 -0
  60. package/build/components/sidebar/dataform-post-summary.cjs +36 -6
  61. package/build/components/sidebar/dataform-post-summary.cjs.map +2 -2
  62. package/build/components/sidebar/header.cjs +1 -1
  63. package/build/components/sidebar/header.cjs.map +2 -2
  64. package/build/components/sidebar/index.cjs +5 -1
  65. package/build/components/sidebar/index.cjs.map +3 -3
  66. package/build/components/{sync-connection-modal → sync-connection-error-modal}/index.cjs +90 -78
  67. package/build/components/sync-connection-error-modal/index.cjs.map +7 -0
  68. package/build/components/{sync-connection-modal → sync-connection-error-modal}/use-retry-countdown.cjs +14 -27
  69. package/build/components/sync-connection-error-modal/use-retry-countdown.cjs.map +7 -0
  70. package/build/components/template-content-panel/index.cjs +35 -31
  71. package/build/components/template-content-panel/index.cjs.map +3 -3
  72. package/build/components/visual-editor/index.cjs +2 -2
  73. package/build/components/visual-editor/index.cjs.map +2 -2
  74. package/build/store/actions.cjs +1 -3
  75. package/build/store/actions.cjs.map +2 -2
  76. package/build/store/private-actions.cjs +11 -2
  77. package/build/store/private-actions.cjs.map +2 -2
  78. package/build/store/private-selectors.cjs +52 -13
  79. package/build/store/private-selectors.cjs.map +2 -2
  80. package/build/store/reducer.cjs +12 -0
  81. package/build/store/reducer.cjs.map +2 -2
  82. package/build/utils/media-finalize/index.cjs +43 -0
  83. package/build/utils/media-finalize/index.cjs.map +7 -0
  84. package/build/utils/sync-error-messages.cjs +29 -16
  85. package/build/utils/sync-error-messages.cjs.map +3 -3
  86. package/build-module/components/collaborators-overlay/avatar-iframe-styles.mjs +12 -4
  87. package/build-module/components/collaborators-overlay/avatar-iframe-styles.mjs.map +2 -2
  88. package/build-module/components/collaborators-overlay/compute-selection.mjs +162 -0
  89. package/build-module/components/collaborators-overlay/compute-selection.mjs.map +7 -0
  90. package/build-module/components/collaborators-overlay/cursor-dom-utils.mjs +213 -0
  91. package/build-module/components/collaborators-overlay/cursor-dom-utils.mjs.map +7 -0
  92. package/build-module/components/collaborators-overlay/overlay-iframe-styles.mjs +6 -0
  93. package/build-module/components/collaborators-overlay/overlay-iframe-styles.mjs.map +2 -2
  94. package/build-module/components/collaborators-overlay/overlay.mjs +61 -37
  95. package/build-module/components/collaborators-overlay/overlay.mjs.map +2 -2
  96. package/build-module/components/collaborators-overlay/timing-utils.mjs +21 -0
  97. package/build-module/components/collaborators-overlay/timing-utils.mjs.map +7 -0
  98. package/build-module/components/collaborators-overlay/use-block-highlighting.mjs +5 -6
  99. package/build-module/components/collaborators-overlay/use-block-highlighting.mjs.map +2 -2
  100. package/build-module/components/collaborators-overlay/use-render-cursors.mjs +50 -140
  101. package/build-module/components/collaborators-overlay/use-render-cursors.mjs.map +2 -2
  102. package/build-module/components/collaborators-presence/index.mjs +39 -13
  103. package/build-module/components/collaborators-presence/index.mjs.map +2 -2
  104. package/build-module/components/collaborators-presence/list.mjs +27 -24
  105. package/build-module/components/collaborators-presence/list.mjs.map +2 -2
  106. package/build-module/components/collaborators-presence/use-collaborator-notifications.mjs +80 -108
  107. package/build-module/components/collaborators-presence/use-collaborator-notifications.mjs.map +2 -2
  108. package/build-module/components/editor-interface/index.mjs +10 -7
  109. package/build-module/components/editor-interface/index.mjs.map +2 -2
  110. package/build-module/components/inserter-sidebar/index.mjs +2 -1
  111. package/build-module/components/inserter-sidebar/index.mjs.map +2 -2
  112. package/build-module/components/page-attributes/parent.mjs +1 -1
  113. package/build-module/components/page-attributes/parent.mjs.map +2 -2
  114. package/build-module/components/post-locked-modal/index.mjs +16 -3
  115. package/build-module/components/post-locked-modal/index.mjs.map +2 -2
  116. package/build-module/components/post-revisions-preview/block-diff.mjs +39 -11
  117. package/build-module/components/post-revisions-preview/block-diff.mjs.map +2 -2
  118. package/build-module/components/post-revisions-preview/diff-markers.mjs +2 -2
  119. package/build-module/components/post-revisions-preview/diff-markers.mjs.map +2 -2
  120. package/build-module/components/post-revisions-preview/revisions-canvas.mjs +14 -80
  121. package/build-module/components/post-revisions-preview/revisions-canvas.mjs.map +2 -2
  122. package/build-module/components/post-revisions-preview/revisions-slider.mjs +5 -1
  123. package/build-module/components/post-revisions-preview/revisions-slider.mjs.map +2 -2
  124. package/build-module/components/post-template/block-theme.mjs +7 -4
  125. package/build-module/components/post-template/block-theme.mjs.map +2 -2
  126. package/build-module/components/post-template/hooks.mjs +37 -1
  127. package/build-module/components/post-template/hooks.mjs.map +2 -2
  128. package/build-module/components/post-template/panel.mjs +5 -42
  129. package/build-module/components/post-template/panel.mjs.map +2 -2
  130. package/build-module/components/preferences-modal/index.mjs +24 -0
  131. package/build-module/components/preferences-modal/index.mjs.map +2 -2
  132. package/build-module/components/provider/disable-non-page-content-blocks.mjs +31 -28
  133. package/build-module/components/provider/disable-non-page-content-blocks.mjs.map +2 -2
  134. package/build-module/components/provider/index.mjs +17 -5
  135. package/build-module/components/provider/index.mjs.map +2 -2
  136. package/build-module/components/provider/use-block-editor-settings.mjs +19 -5
  137. package/build-module/components/provider/use-block-editor-settings.mjs.map +2 -2
  138. package/build-module/components/provider/use-post-content-block-types.mjs +23 -0
  139. package/build-module/components/provider/use-post-content-block-types.mjs.map +7 -0
  140. package/build-module/components/provider/use-revision-blocks.mjs +81 -0
  141. package/build-module/components/provider/use-revision-blocks.mjs.map +7 -0
  142. package/build-module/components/revision-block-diff/index.mjs +53 -0
  143. package/build-module/components/revision-block-diff/index.mjs.map +7 -0
  144. package/build-module/components/sidebar/dataform-post-summary.mjs +36 -6
  145. package/build-module/components/sidebar/dataform-post-summary.mjs.map +2 -2
  146. package/build-module/components/sidebar/header.mjs +1 -1
  147. package/build-module/components/sidebar/header.mjs.map +2 -2
  148. package/build-module/components/sidebar/index.mjs +5 -1
  149. package/build-module/components/sidebar/index.mjs.map +2 -2
  150. package/build-module/components/sync-connection-error-modal/index.mjs +177 -0
  151. package/build-module/components/sync-connection-error-modal/index.mjs.map +7 -0
  152. package/build-module/components/sync-connection-error-modal/use-retry-countdown.mjs +36 -0
  153. package/build-module/components/sync-connection-error-modal/use-retry-countdown.mjs.map +7 -0
  154. package/build-module/components/template-content-panel/index.mjs +25 -31
  155. package/build-module/components/template-content-panel/index.mjs.map +2 -2
  156. package/build-module/components/visual-editor/index.mjs +2 -2
  157. package/build-module/components/visual-editor/index.mjs.map +2 -2
  158. package/build-module/store/actions.mjs +1 -3
  159. package/build-module/store/actions.mjs.map +2 -2
  160. package/build-module/store/private-actions.mjs +10 -2
  161. package/build-module/store/private-actions.mjs.map +2 -2
  162. package/build-module/store/private-selectors.mjs +50 -12
  163. package/build-module/store/private-selectors.mjs.map +2 -2
  164. package/build-module/store/reducer.mjs +11 -0
  165. package/build-module/store/reducer.mjs.map +2 -2
  166. package/build-module/utils/media-finalize/index.mjs +12 -0
  167. package/build-module/utils/media-finalize/index.mjs.map +7 -0
  168. package/build-module/utils/sync-error-messages.mjs +24 -16
  169. package/build-module/utils/sync-error-messages.mjs.map +3 -3
  170. package/build-style/style-rtl.css +95 -16
  171. package/build-style/style.css +95 -16
  172. package/build-types/components/collaborators-overlay/avatar-iframe-styles.d.ts +1 -1
  173. package/build-types/components/collaborators-overlay/avatar-iframe-styles.d.ts.map +1 -1
  174. package/build-types/components/collaborators-overlay/compute-selection.d.ts +24 -0
  175. package/build-types/components/collaborators-overlay/compute-selection.d.ts.map +1 -0
  176. package/build-types/components/collaborators-overlay/cursor-dom-utils.d.ts +72 -0
  177. package/build-types/components/collaborators-overlay/cursor-dom-utils.d.ts.map +1 -0
  178. package/build-types/components/collaborators-overlay/overlay-iframe-styles.d.ts +1 -1
  179. package/build-types/components/collaborators-overlay/overlay-iframe-styles.d.ts.map +1 -1
  180. package/build-types/components/collaborators-overlay/overlay.d.ts.map +1 -1
  181. package/build-types/components/collaborators-overlay/timing-utils.d.ts +11 -0
  182. package/build-types/components/collaborators-overlay/timing-utils.d.ts.map +1 -0
  183. package/build-types/components/collaborators-overlay/use-block-highlighting.d.ts.map +1 -1
  184. package/build-types/components/collaborators-overlay/use-render-cursors.d.ts +4 -0
  185. package/build-types/components/collaborators-overlay/use-render-cursors.d.ts.map +1 -1
  186. package/build-types/components/collaborators-presence/index.d.ts.map +1 -1
  187. package/build-types/components/collaborators-presence/list.d.ts +2 -1
  188. package/build-types/components/collaborators-presence/list.d.ts.map +1 -1
  189. package/build-types/components/collaborators-presence/use-collaborator-notifications.d.ts.map +1 -1
  190. package/build-types/components/editor-interface/index.d.ts.map +1 -1
  191. package/build-types/components/inserter-sidebar/index.d.ts.map +1 -1
  192. package/build-types/components/post-locked-modal/index.d.ts +2 -2
  193. package/build-types/components/post-locked-modal/index.d.ts.map +1 -1
  194. package/build-types/components/post-revisions-preview/block-diff.d.ts.map +1 -1
  195. package/build-types/components/post-revisions-preview/revisions-canvas.d.ts +2 -5
  196. package/build-types/components/post-revisions-preview/revisions-canvas.d.ts.map +1 -1
  197. package/build-types/components/post-revisions-preview/revisions-slider.d.ts.map +1 -1
  198. package/build-types/components/post-template/block-theme.d.ts +1 -3
  199. package/build-types/components/post-template/block-theme.d.ts.map +1 -1
  200. package/build-types/components/post-template/hooks.d.ts +1 -0
  201. package/build-types/components/post-template/hooks.d.ts.map +1 -1
  202. package/build-types/components/post-template/panel.d.ts.map +1 -1
  203. package/build-types/components/provider/disable-non-page-content-blocks.d.ts.map +1 -1
  204. package/build-types/components/provider/index.d.ts.map +1 -1
  205. package/build-types/components/provider/use-block-editor-settings.d.ts.map +1 -1
  206. package/build-types/components/provider/use-post-content-block-types.d.ts +9 -0
  207. package/build-types/components/provider/use-post-content-block-types.d.ts.map +1 -0
  208. package/build-types/components/provider/use-revision-blocks.d.ts +10 -0
  209. package/build-types/components/provider/use-revision-blocks.d.ts.map +1 -0
  210. package/build-types/components/revision-block-diff/index.d.ts +6 -0
  211. package/build-types/components/revision-block-diff/index.d.ts.map +1 -0
  212. package/build-types/components/sidebar/dataform-post-summary.d.ts.map +1 -1
  213. package/build-types/components/sidebar/index.d.ts.map +1 -1
  214. package/build-types/components/sync-connection-error-modal/index.d.ts +22 -0
  215. package/build-types/components/sync-connection-error-modal/index.d.ts.map +1 -0
  216. package/build-types/components/sync-connection-error-modal/use-retry-countdown.d.ts +11 -0
  217. package/build-types/components/sync-connection-error-modal/use-retry-countdown.d.ts.map +1 -0
  218. package/build-types/components/template-content-panel/index.d.ts.map +1 -1
  219. package/build-types/store/actions.d.ts.map +1 -1
  220. package/build-types/store/private-actions.d.ts +7 -0
  221. package/build-types/store/private-actions.d.ts.map +1 -1
  222. package/build-types/store/private-selectors.d.ts +7 -0
  223. package/build-types/store/private-selectors.d.ts.map +1 -1
  224. package/build-types/store/reducer.d.ts +14 -3
  225. package/build-types/store/reducer.d.ts.map +1 -1
  226. package/build-types/utils/media-finalize/index.d.ts +2 -0
  227. package/build-types/utils/media-finalize/index.d.ts.map +1 -0
  228. package/build-types/utils/sync-error-messages.d.ts +17 -3
  229. package/build-types/utils/sync-error-messages.d.ts.map +1 -1
  230. package/package.json +44 -44
  231. package/src/components/collaborators-overlay/avatar-iframe-styles.ts +12 -4
  232. package/src/components/collaborators-overlay/compute-selection.ts +307 -0
  233. package/src/components/collaborators-overlay/cursor-dom-utils.ts +382 -0
  234. package/src/components/collaborators-overlay/overlay-iframe-styles.ts +6 -0
  235. package/src/components/collaborators-overlay/overlay.tsx +59 -27
  236. package/src/components/collaborators-overlay/timing-utils.ts +30 -0
  237. package/src/components/collaborators-overlay/use-block-highlighting.ts +11 -10
  238. package/src/components/collaborators-overlay/use-render-cursors.ts +70 -242
  239. package/src/components/collaborators-presence/avatar/styles.scss +20 -4
  240. package/src/components/collaborators-presence/index.tsx +30 -5
  241. package/src/components/collaborators-presence/list.tsx +38 -24
  242. package/src/components/collaborators-presence/test/use-collaborator-notifications.ts +188 -246
  243. package/src/components/collaborators-presence/use-collaborator-notifications.ts +109 -166
  244. package/src/components/document-bar/style.scss +1 -1
  245. package/src/components/editor-interface/index.js +8 -6
  246. package/src/components/inserter-sidebar/index.js +4 -1
  247. package/src/components/page-attributes/parent.js +1 -1
  248. package/src/components/post-locked-modal/index.js +21 -3
  249. package/src/components/post-revisions-preview/block-diff.js +59 -20
  250. package/src/components/post-revisions-preview/diff-markers.js +2 -2
  251. package/src/components/post-revisions-preview/revisions-canvas.js +20 -98
  252. package/src/components/post-revisions-preview/revisions-slider.js +6 -1
  253. package/src/components/post-revisions-preview/test/block-diff.js +69 -31
  254. package/src/components/post-template/block-theme.js +4 -1
  255. package/src/components/post-template/hooks.js +42 -0
  256. package/src/components/post-template/panel.js +5 -59
  257. package/src/components/preferences-modal/index.js +18 -0
  258. package/src/components/provider/disable-non-page-content-blocks.js +42 -40
  259. package/src/components/provider/index.js +20 -2
  260. package/src/components/provider/use-block-editor-settings.js +21 -8
  261. package/src/components/provider/use-post-content-block-types.js +30 -0
  262. package/src/components/provider/use-revision-blocks.js +105 -0
  263. package/src/components/revision-block-diff/index.js +74 -0
  264. package/src/components/revision-block-diff/style.scss +13 -0
  265. package/src/components/sidebar/dataform-post-summary.js +61 -16
  266. package/src/components/sidebar/header.js +1 -1
  267. package/src/components/sidebar/index.js +2 -0
  268. package/src/components/sync-connection-error-modal/index.tsx +265 -0
  269. package/src/components/sync-connection-error-modal/style.scss +14 -0
  270. package/src/components/sync-connection-error-modal/use-retry-countdown.ts +57 -0
  271. package/src/components/template-content-panel/index.js +30 -38
  272. package/src/components/visual-editor/index.js +2 -2
  273. package/src/store/actions.js +1 -4
  274. package/src/store/private-actions.js +21 -2
  275. package/src/store/private-selectors.js +75 -10
  276. package/src/store/reducer.js +19 -0
  277. package/src/style.scss +2 -1
  278. package/src/utils/media-finalize/index.js +11 -0
  279. package/src/utils/media-finalize/test/index.js +34 -0
  280. package/src/utils/sync-error-messages.ts +72 -0
  281. package/src/utils/test/sync-error-messages.js +9 -32
  282. package/build/components/provider/use-post-content-blocks.cjs.map +0 -7
  283. package/build/components/sync-connection-modal/index.cjs.map +0 -7
  284. package/build/components/sync-connection-modal/use-retry-countdown.cjs.map +0 -7
  285. package/build-module/components/provider/use-post-content-blocks.mjs +0 -34
  286. package/build-module/components/provider/use-post-content-blocks.mjs.map +0 -7
  287. package/build-module/components/sync-connection-modal/index.mjs +0 -167
  288. package/build-module/components/sync-connection-modal/index.mjs.map +0 -7
  289. package/build-module/components/sync-connection-modal/use-retry-countdown.mjs +0 -49
  290. package/build-module/components/sync-connection-modal/use-retry-countdown.mjs.map +0 -7
  291. package/build-types/components/provider/use-post-content-blocks.d.ts +0 -2
  292. package/build-types/components/provider/use-post-content-blocks.d.ts.map +0 -1
  293. package/build-types/components/sync-connection-modal/index.d.ts +0 -8
  294. package/build-types/components/sync-connection-modal/index.d.ts.map +0 -1
  295. package/build-types/components/sync-connection-modal/use-retry-countdown.d.ts +0 -9
  296. package/build-types/components/sync-connection-modal/use-retry-countdown.d.ts.map +0 -1
  297. package/src/components/provider/use-post-content-blocks.js +0 -42
  298. package/src/components/sync-connection-modal/index.js +0 -200
  299. package/src/components/sync-connection-modal/style.scss +0 -9
  300. package/src/components/sync-connection-modal/use-retry-countdown.js +0 -70
  301. package/src/utils/sync-error-messages.js +0 -58
@@ -1,16 +1,23 @@
1
- // @ts-expect-error No exported types
2
- import { useStyleOverride } from '@wordpress/block-editor';
3
1
  import { useResizeObserver, useMergeRefs } from '@wordpress/compose';
4
2
  import { useCallback, useEffect, useState } from '@wordpress/element';
3
+ import { __ } from '@wordpress/i18n';
5
4
 
6
5
  import Avatar from '../collaborators-presence/avatar';
7
6
  import { AVATAR_IFRAME_STYLES } from './avatar-iframe-styles';
8
7
  import { OVERLAY_IFRAME_STYLES } from './overlay-iframe-styles';
8
+ import { setDelayedInterval } from './timing-utils';
9
9
  import { useBlockHighlighting } from './use-block-highlighting';
10
10
  import { useRenderCursors } from './use-render-cursors';
11
11
 
12
+ // Milliseconds to wait after a change before recomputing cursor positions.
12
13
  const RERENDER_DELAY_MS = 500;
13
14
 
15
+ // Periodically recompute cursor positions to account for DOM layout
16
+ // changes that don't trigger awareness state updates (e.g. a collaborator
17
+ // applying formatting shifts text but the cursor's logical position is
18
+ // unchanged). Only active when remote cursors are visible.
19
+ const CURSOR_REDRAW_INTERVAL_MS = 10_000;
20
+
14
21
  interface OverlayProps {
15
22
  blockEditorDocument?: Document;
16
23
  postId: number | null;
@@ -31,11 +38,6 @@ export function Overlay( {
31
38
  postId,
32
39
  postType,
33
40
  }: OverlayProps ) {
34
- useStyleOverride( {
35
- id: 'collaborators-overlay',
36
- css: AVATAR_IFRAME_STYLES + OVERLAY_IFRAME_STYLES,
37
- } );
38
-
39
41
  // Use state for the overlay element so that the hook re-runs once the ref is attached.
40
42
  const [ overlayElement, setOverlayElement ] =
41
43
  useState< HTMLDivElement | null >( null );
@@ -74,6 +76,17 @@ export function Overlay( {
74
76
  };
75
77
  }, [ rerenderCursorsAfterDelay, rerenderHighlightsAfterDelay ] );
76
78
 
79
+ useEffect( () => {
80
+ if ( cursors.length === 0 ) {
81
+ return;
82
+ }
83
+
84
+ return setDelayedInterval(
85
+ rerenderCursorsAfterDelay,
86
+ CURSOR_REDRAW_INTERVAL_MS
87
+ );
88
+ }, [ cursors.length, rerenderCursorsAfterDelay ] );
89
+
77
90
  // Merge the refs to use the same element for both overlay and resize observation
78
91
  const mergedRef = useMergeRefs< HTMLDivElement | null >( [
79
92
  setOverlayElement,
@@ -84,30 +97,49 @@ export function Overlay( {
84
97
  // scrollable elements like cursor indicators.
85
98
  return (
86
99
  <div className="collaborators-overlay-full" ref={ mergedRef }>
100
+ <style>{ AVATAR_IFRAME_STYLES + OVERLAY_IFRAME_STYLES }</style>
87
101
  { cursors.map( ( cursor ) => (
88
- <div
89
- key={ cursor.clientId }
90
- className="collaborators-overlay-user"
91
- style={ {
92
- left: `${ cursor.x }px`,
93
- top: `${ cursor.y }px`,
94
- } }
95
- >
102
+ <div key={ cursor.clientId }>
103
+ { ! cursor.isMe &&
104
+ cursor.selectionRects?.map( ( rect, index ) => (
105
+ <div
106
+ key={ `${ cursor.clientId }-sel-${ index }` }
107
+ className="collaborators-overlay-selection-rect"
108
+ style={ {
109
+ left: `${ rect.x }px`,
110
+ top: `${ rect.y }px`,
111
+ width: `${ rect.width }px`,
112
+ height: `${ rect.height }px`,
113
+ backgroundColor: cursor.color,
114
+ } }
115
+ />
116
+ ) ) }
96
117
  <div
97
- className="collaborators-overlay-user-cursor"
118
+ className="collaborators-overlay-user"
98
119
  style={ {
99
- backgroundColor: cursor.color,
100
- height: `${ cursor.height }px`,
120
+ left: `${ cursor.x }px`,
121
+ top: `${ cursor.y }px`,
101
122
  } }
102
- />
103
- <Avatar
104
- className="collaborators-overlay-user-label"
105
- variant="badge"
106
- size="small"
107
- src={ cursor.avatarUrl }
108
- name={ cursor.userName }
109
- borderColor={ cursor.color }
110
- />
123
+ >
124
+ { ! cursor.isMe && (
125
+ <div
126
+ className="collaborators-overlay-user-cursor"
127
+ style={ {
128
+ backgroundColor: cursor.color,
129
+ height: `${ cursor.height }px`,
130
+ } }
131
+ />
132
+ ) }
133
+ <Avatar
134
+ className="collaborators-overlay-user-label"
135
+ variant="badge"
136
+ size="small"
137
+ src={ cursor.avatarUrl }
138
+ name={ cursor.userName }
139
+ label={ cursor.isMe ? __( 'You' ) : undefined }
140
+ borderColor={ cursor.color }
141
+ />
142
+ </div>
111
143
  </div>
112
144
  ) ) }
113
145
  { highlights.map( ( highlight ) => (
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Like setInterval but chains setTimeout calls, so the delay is measured from
3
+ * the end of one run to the start of the next. This prevents callbacks from
4
+ * stacking up when the main thread is busy.
5
+ *
6
+ * @param callback The function to call repeatedly.
7
+ * @param delayMs Milliseconds between runs.
8
+ * @return A cleanup function that stops the timer.
9
+ */
10
+ export function setDelayedInterval( callback: () => void, delayMs: number ) {
11
+ let timerHandle: ReturnType< typeof setTimeout > | null = null;
12
+
13
+ const runner = () => {
14
+ try {
15
+ callback();
16
+ } catch ( error ) {
17
+ // Do nothing
18
+ }
19
+
20
+ timerHandle = setTimeout( runner, delayMs );
21
+ };
22
+
23
+ timerHandle = setTimeout( runner, delayMs );
24
+
25
+ return () => {
26
+ if ( timerHandle ) {
27
+ clearTimeout( timerHandle );
28
+ }
29
+ };
30
+ }
@@ -4,7 +4,7 @@
4
4
  import {
5
5
  privateApis as coreDataPrivateApis,
6
6
  SelectionType,
7
- type PostEditorAwarenessState,
7
+ type PostEditorAwarenessState as ActiveCollaborator,
8
8
  } from '@wordpress/core-data';
9
9
  import { useEffect, useRef, useState } from '@wordpress/element';
10
10
 
@@ -50,7 +50,7 @@ export function useBlockHighlighting(
50
50
  rerenderHighlightsAfterDelay: () => () => void;
51
51
  } {
52
52
  const highlightedBlockIds = useRef< Set< string > >( new Set() );
53
- const userStates: PostEditorAwarenessState[] = useActiveCollaborators(
53
+ const userStates: ActiveCollaborator[] = useActiveCollaborators(
54
54
  postId ?? null,
55
55
  postType ?? null
56
56
  );
@@ -82,12 +82,13 @@ export function useBlockHighlighting(
82
82
  // same block, only the first one gets the highlight and avatar label.
83
83
  const seen = new Set< string >();
84
84
  const blocksToHighlight = userStates
85
- .filter(
86
- ( userState ) =>
87
- ! userState.isMe &&
85
+ .filter( ( userState: ActiveCollaborator ) => {
86
+ const isWholeBlockSelected =
88
87
  userState.editorState?.selection?.type ===
89
- SelectionType.WholeBlock
90
- )
88
+ SelectionType.WholeBlock;
89
+
90
+ return ! userState.isMe && isWholeBlockSelected;
91
+ } )
91
92
  .map( ( userState ) => {
92
93
  let localClientId;
93
94
  try {
@@ -104,9 +105,9 @@ export function useBlockHighlighting(
104
105
 
105
106
  return {
106
107
  blockId: localClientId,
107
- color: getAvatarBorderColor(
108
- userState.collaboratorInfo.id
109
- ),
108
+ color: userState.isMe
109
+ ? 'var(--wp-admin-theme-color)'
110
+ : getAvatarBorderColor( userState.collaboratorInfo.id ),
110
111
  userName: userState.collaboratorInfo.name,
111
112
  avatarUrl: getAvatarUrl(
112
113
  userState.collaboratorInfo.avatar_urls
@@ -1,17 +1,25 @@
1
1
  import {
2
2
  privateApis as coreDataPrivateApis,
3
3
  SelectionType,
4
+ type PostEditorAwarenessState as ActiveCollaborator,
4
5
  } from '@wordpress/core-data';
6
+ import { useSelect } from '@wordpress/data';
5
7
  import { useEffect, useState } from '@wordpress/element';
8
+ import { store as preferencesStore } from '@wordpress/preferences';
9
+ import type { ResolvedSelection } from '@wordpress/core-data';
6
10
 
7
11
  import { unlock } from '../../lock-unlock';
8
12
  import { getAvatarUrl } from './get-avatar-url';
9
13
  import { getAvatarBorderColor } from '../collab-sidebar/utils';
14
+ import { computeSelectionVisual } from './compute-selection';
10
15
  import { useDebouncedRecompute } from './use-debounced-recompute';
16
+ import type { SelectionRect } from './cursor-dom-utils';
11
17
 
12
18
  const { useActiveCollaborators, useResolvedSelection } =
13
19
  unlock( coreDataPrivateApis );
14
20
 
21
+ export type { SelectionRect };
22
+
15
23
  export interface CursorData {
16
24
  userName: string;
17
25
  clientId: number;
@@ -20,6 +28,8 @@ export interface CursorData {
20
28
  x: number;
21
29
  y: number;
22
30
  height: number;
31
+ isMe?: boolean;
32
+ selectionRects?: SelectionRect[];
23
33
  }
24
34
 
25
35
  /**
@@ -48,6 +58,12 @@ export function useRenderCursors(
48
58
  postType ?? null
49
59
  );
50
60
 
61
+ const showOwnCursor = useSelect(
62
+ ( select ) =>
63
+ select( preferencesStore ).get( 'core', 'showCollaborationCursor' ),
64
+ []
65
+ );
66
+
51
67
  const [ cursorPositions, setCursorPositions ] = useState< CursorData[] >(
52
68
  []
53
69
  );
@@ -63,76 +79,90 @@ export function useRenderCursors(
63
79
  return;
64
80
  }
65
81
 
82
+ // Pre-compute the overlay rect once, same for every user.
83
+ const overlayRect = overlayElement.getBoundingClientRect();
84
+ const overlayContext = {
85
+ editorDocument: blockEditorDocument,
86
+ overlayRect,
87
+ };
88
+
66
89
  const results: CursorData[] = [];
67
90
 
68
- sortedUsers.forEach( ( user: any ) => {
69
- if ( user.isMe ) {
91
+ const hasOtherCollaborators = sortedUsers.some(
92
+ ( u: ActiveCollaborator ) => ! u.isMe
93
+ );
94
+
95
+ sortedUsers.forEach( ( user: ActiveCollaborator ) => {
96
+ if ( user.isMe && ( ! showOwnCursor || ! hasOtherCollaborators ) ) {
70
97
  return;
71
98
  }
72
99
 
73
100
  const selection = user.editorState?.selection ?? {
74
101
  type: SelectionType.None,
75
102
  };
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
103
 
81
- let coords: {
82
- x: number;
83
- y: number;
84
- height: number;
85
- } | null = null;
104
+ let start: ResolvedSelection = {
105
+ richTextOffset: null,
106
+ localClientId: null,
107
+ };
108
+ let end: ResolvedSelection | undefined;
86
109
 
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 ) {
110
+ if ( selection.type === SelectionType.Cursor ) {
92
111
  try {
93
- const { textIndex, localClientId } =
94
- resolveSelection( selection );
95
- if ( localClientId ) {
96
- coords = getCursorPosition(
97
- textIndex,
98
- localClientId,
99
- blockEditorDocument,
100
- overlayElement
101
- );
102
- }
112
+ start = resolveSelection( selection );
103
113
  } catch {
104
114
  // Selection may reference a stale Yjs position.
115
+ return;
105
116
  }
106
117
  } else if (
107
118
  selection.type === SelectionType.SelectionInOneBlock ||
108
119
  selection.type === SelectionType.SelectionInMultipleBlocks
109
120
  ) {
110
121
  try {
111
- const { textIndex, localClientId } = resolveSelection( {
122
+ start = resolveSelection( {
112
123
  type: SelectionType.Cursor,
113
124
  cursorPosition: selection.cursorStartPosition,
114
125
  } );
115
- if ( localClientId ) {
116
- coords = getCursorPosition(
117
- textIndex,
118
- localClientId,
119
- blockEditorDocument,
120
- overlayElement
121
- );
122
- }
126
+
127
+ end = resolveSelection( {
128
+ type: SelectionType.Cursor,
129
+ cursorPosition: selection.cursorEndPosition,
130
+ } );
123
131
  } catch {
124
132
  // Selection may reference a stale Yjs position.
133
+ return;
125
134
  }
126
135
  }
127
136
 
128
- if ( coords ) {
129
- results.push( {
137
+ const userName = user.collaboratorInfo.name;
138
+ const clientId = user.clientId;
139
+ const color = user.isMe
140
+ ? 'var(--wp-admin-theme-color)'
141
+ : getAvatarBorderColor( user.collaboratorInfo.id );
142
+ const avatarUrl = getAvatarUrl( user.collaboratorInfo.avatar_urls );
143
+
144
+ const selectionVisual = computeSelectionVisual(
145
+ selection,
146
+ start,
147
+ end,
148
+ overlayContext
149
+ );
150
+
151
+ if ( selectionVisual.coords ) {
152
+ const cursorData: CursorData = {
130
153
  userName,
131
154
  clientId,
132
155
  color,
133
156
  avatarUrl,
134
- ...coords,
135
- } );
157
+ isMe: user.isMe,
158
+ ...selectionVisual.coords,
159
+ };
160
+
161
+ if ( selectionVisual.selectionRects ) {
162
+ cursorData.selectionRects = selectionVisual.selectionRects;
163
+ }
164
+
165
+ results.push( cursorData );
136
166
  }
137
167
  } );
138
168
 
@@ -142,211 +172,9 @@ export function useRenderCursors(
142
172
  resolveSelection,
143
173
  overlayElement,
144
174
  sortedUsers,
175
+ showOwnCursor,
145
176
  recomputeToken,
146
177
  ] );
147
178
 
148
179
  return { cursors: cursorPositions, rerenderCursorsAfterDelay };
149
180
  }
150
-
151
- /**
152
- * Given a selection, returns the coordinates of the cursor in the block.
153
- *
154
- * @param absolutePositionIndex - The absolute position index
155
- * @param blockId - The block ID
156
- * @param editorDocument - The editor document
157
- * @param overlay - The overlay element
158
- * @return The position of the cursor
159
- */
160
- const getCursorPosition = (
161
- absolutePositionIndex: number | null,
162
- blockId: string,
163
- editorDocument: Document,
164
- overlay: HTMLElement
165
- ): { x: number; y: number; height: number } | null => {
166
- if ( absolutePositionIndex === null ) {
167
- // An absolute position index can be null if a cursor was set in a block that
168
- // has since been deleted.
169
- // Return null so we don't try to draw it.
170
- return null;
171
- }
172
-
173
- const blockElement = editorDocument.querySelector(
174
- `[data-block="${ blockId }"]`
175
- ) as HTMLElement;
176
-
177
- if ( ! blockElement ) {
178
- return null;
179
- }
180
-
181
- return (
182
- getOffsetPositionInBlock(
183
- blockElement,
184
- absolutePositionIndex,
185
- editorDocument,
186
- overlay
187
- ) ?? null
188
- );
189
- };
190
-
191
- /**
192
- * Given a block element and a character offset, returns the coordinates for drawing a visual cursor in the block.
193
- *
194
- * @param blockElement - The block element
195
- * @param charOffset - The character offset
196
- * @param editorDocument - The editor document
197
- * @param overlay - The overlay element
198
- * @return The position of the cursor
199
- */
200
- const getOffsetPositionInBlock = (
201
- blockElement: HTMLElement,
202
- charOffset: number,
203
- editorDocument: Document,
204
- overlay: HTMLElement
205
- ) => {
206
- const { node, offset } = findInnerBlockOffset(
207
- blockElement,
208
- charOffset,
209
- editorDocument
210
- );
211
-
212
- const cursorRange = editorDocument.createRange();
213
-
214
- try {
215
- cursorRange.setStart( node, offset );
216
- } catch ( error ) {
217
- return null;
218
- }
219
-
220
- // Ensure the range only represents single point in the DOM.
221
- cursorRange.collapse( true );
222
-
223
- const cursorRect = cursorRange.getBoundingClientRect();
224
- const overlayRect = overlay.getBoundingClientRect();
225
- const blockRect = blockElement.getBoundingClientRect();
226
-
227
- let cursorX = 0;
228
- let cursorY = 0;
229
-
230
- if (
231
- cursorRect.x === 0 &&
232
- cursorRect.y === 0 &&
233
- cursorRect.width === 0 &&
234
- cursorRect.height === 0
235
- ) {
236
- // This can happen for empty blocks.
237
- cursorX = blockRect.left - overlayRect.left;
238
- cursorY = blockRect.top - overlayRect.top;
239
- } else {
240
- cursorX = cursorRect.left - overlayRect.left;
241
- cursorY = cursorRect.top - overlayRect.top;
242
- }
243
-
244
- let cursorHeight = cursorRect.height;
245
- if ( cursorHeight === 0 ) {
246
- const view = editorDocument.defaultView ?? window;
247
- cursorHeight =
248
- parseInt( view.getComputedStyle( blockElement ).lineHeight, 10 ) ||
249
- blockRect.height;
250
- }
251
-
252
- return {
253
- x: cursorX,
254
- y: cursorY,
255
- height: cursorHeight,
256
- };
257
- };
258
-
259
- const MAX_NODE_OFFSET_COUNT = 1000;
260
-
261
- /**
262
- * Given a block element and a character offset, returns an exact inner node and offset for use in a range.
263
- *
264
- * @param blockElement - The block element
265
- * @param offset - The character offset
266
- * @param editorDocument - The editor document
267
- * @return The node and offset of the character at the offset
268
- */
269
- const findInnerBlockOffset = (
270
- blockElement: HTMLElement,
271
- offset: number,
272
- editorDocument: Document
273
- ) => {
274
- const treeWalker = editorDocument.createTreeWalker(
275
- blockElement,
276
- NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT // eslint-disable-line no-bitwise
277
- );
278
-
279
- let currentOffset = 0;
280
- let lastTextNode: Node | null = null;
281
-
282
- let node: Node | null = null;
283
- let nodeCount = 1;
284
-
285
- while ( ( node = treeWalker.nextNode() ) ) {
286
- nodeCount++;
287
-
288
- if ( nodeCount > MAX_NODE_OFFSET_COUNT ) {
289
- // If we've walked too many nodes, return the last text node or the beginning of the block.
290
- if ( lastTextNode ) {
291
- return { node: lastTextNode, offset: 0 };
292
- }
293
- return { node: blockElement, offset: 0 };
294
- }
295
-
296
- const nodeLength = node.nodeValue?.length ?? 0;
297
-
298
- if ( node.nodeType === Node.ELEMENT_NODE ) {
299
- if ( node.nodeName === 'BR' ) {
300
- // Treat <br> as a single "\n" character.
301
-
302
- if ( currentOffset + 1 >= offset ) {
303
- // If the <br> occurs right on the target offset, return the next text node.
304
- const nodeAfterBr = treeWalker.nextNode();
305
-
306
- if ( nodeAfterBr?.nodeType === Node.TEXT_NODE ) {
307
- return { node: nodeAfterBr, offset: 0 };
308
- } else if ( lastTextNode ) {
309
- // If there's no text node after the <br>, return the end offset of the last text node.
310
- return {
311
- node: lastTextNode,
312
- offset: lastTextNode.nodeValue?.length ?? 0,
313
- };
314
- }
315
- // Just in case, if there's no last text node, return the beginning of the block.
316
- return { node: blockElement, offset: 0 };
317
- }
318
-
319
- // The <br> is before the target offset. Count it as a single character.
320
- currentOffset += 1;
321
- continue;
322
- } else {
323
- // Skip other element types.
324
- continue;
325
- }
326
- }
327
-
328
- if ( nodeLength === 0 ) {
329
- // Skip empty nodes.
330
- continue;
331
- }
332
-
333
- if ( currentOffset + nodeLength >= offset ) {
334
- // This node exceeds the target offset. Return the node and the position of the offset within it.
335
- return { node, offset: offset - currentOffset };
336
- }
337
-
338
- currentOffset += nodeLength;
339
-
340
- if ( node.nodeType === Node.TEXT_NODE ) {
341
- lastTextNode = node;
342
- }
343
- }
344
-
345
- if ( lastTextNode && lastTextNode.nodeValue?.length ) {
346
- // We didn't reach the target offset. Return the last text node's last character.
347
- return { node: lastTextNode, offset: lastTextNode.nodeValue.length };
348
- }
349
-
350
- // We didn't find any text nodes. Return the beginning of the block.
351
- return { node: blockElement, offset: 0 };
352
- };
@@ -5,13 +5,14 @@
5
5
  // fallback matching the default WordPress admin blue.
6
6
  $-accent-color: var(--wp-admin-theme-color, #3858e9);
7
7
 
8
+ // overflow: clip is intentionally omitted here — .editor-avatar__image handles
9
+ // its own circular clip. Removing overflow from this container allows the inset
10
+ // box-shadow on __image to render without being clipped by the parent.
8
11
  .editor-avatar {
9
12
  position: relative;
10
13
  display: inline-flex;
11
14
  align-items: center;
12
15
  border-radius: $radius-full;
13
- overflow: hidden;
14
- overflow: clip;
15
16
  flex-shrink: 0;
16
17
  box-shadow: 0 0 0 var(--wp-admin-border-width-focus) $white, $elevation-x-small;
17
18
  }
@@ -37,8 +38,20 @@ $-accent-color: var(--wp-admin-theme-color, #3858e9);
37
38
 
38
39
  .has-avatar-border-color > & {
39
40
  border: var(--wp-admin-border-width-focus) solid var(--editor-avatar-outline-color);
40
- box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) $white;
41
41
  background-clip: padding-box;
42
+
43
+ // Inner contrast ring rendered as a pseudo-element so it paints above
44
+ // the absolutely-positioned <img>. An inset box-shadow would be
45
+ // covered by the image content layer.
46
+ &::after {
47
+ content: "";
48
+ position: absolute;
49
+ inset: 0;
50
+ border-radius: inherit;
51
+ box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus, 2px) $white;
52
+ pointer-events: none;
53
+ z-index: 1;
54
+ }
42
55
  }
43
56
  }
44
57
 
@@ -66,8 +79,11 @@ $-accent-color: var(--wp-admin-theme-color, #3858e9);
66
79
  font-size: $font-size-x-small;
67
80
  font-weight: $font-weight-medium;
68
81
  border: 0;
69
- box-shadow: none;
70
82
  background-clip: border-box;
83
+
84
+ &::after {
85
+ content: none;
86
+ }
71
87
  }
72
88
 
73
89
  .editor-avatar:not(.has-src).has-avatar-border-color > .editor-avatar__image {