@wordpress/editor 14.49.1 → 14.51.1-next.v.202607070741.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 (262) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/build/components/collab-sidebar/format.cjs +83 -0
  3. package/build/components/collab-sidebar/format.cjs.map +7 -0
  4. package/build/components/collab-sidebar/hooks.cjs +117 -16
  5. package/build/components/collab-sidebar/hooks.cjs.map +2 -2
  6. package/build/components/collab-sidebar/index.cjs +18 -2
  7. package/build/components/collab-sidebar/index.cjs.map +2 -2
  8. package/build/components/collab-sidebar/note-highlight-styles.cjs +67 -0
  9. package/build/components/collab-sidebar/note-highlight-styles.cjs.map +7 -0
  10. package/build/components/collab-sidebar/utils.cjs +139 -0
  11. package/build/components/collab-sidebar/utils.cjs.map +2 -2
  12. package/build/components/collaborators-presence/use-collaborator-notifications.cjs +55 -30
  13. package/build/components/collaborators-presence/use-collaborator-notifications.cjs.map +2 -2
  14. package/build/components/commands/index.cjs +99 -3
  15. package/build/components/commands/index.cjs.map +2 -2
  16. package/build/components/document-bar/index.cjs +28 -34
  17. package/build/components/document-bar/index.cjs.map +2 -2
  18. package/build/components/entities-saved-states/index.cjs +1 -4
  19. package/build/components/entities-saved-states/index.cjs.map +2 -2
  20. package/build/components/header/index.cjs +1 -8
  21. package/build/components/header/index.cjs.map +2 -2
  22. package/build/components/media-categories/index.cjs +92 -6
  23. package/build/components/media-categories/index.cjs.map +2 -2
  24. package/build/components/page-attributes/parent.cjs +0 -1
  25. package/build/components/page-attributes/parent.cjs.map +2 -2
  26. package/build/components/post-author/combobox.cjs +0 -1
  27. package/build/components/post-author/combobox.cjs.map +2 -2
  28. package/build/components/post-author/select.cjs +0 -1
  29. package/build/components/post-author/select.cjs.map +2 -2
  30. package/build/components/post-preview-button/index.cjs +29 -2
  31. package/build/components/post-preview-button/index.cjs.map +2 -2
  32. package/build/components/post-publish-panel/maybe-upload-media.cjs +15 -26
  33. package/build/components/post-publish-panel/maybe-upload-media.cjs.map +3 -3
  34. package/build/components/post-revisions-preview/revisions-slider.cjs +3 -1
  35. package/build/components/post-revisions-preview/revisions-slider.cjs.map +2 -2
  36. package/build/components/post-taxonomies/flat-term-selector.cjs +0 -1
  37. package/build/components/post-taxonomies/flat-term-selector.cjs.map +2 -2
  38. package/build/components/post-taxonomies/hierarchical-term-selector.cjs +0 -2
  39. package/build/components/post-taxonomies/hierarchical-term-selector.cjs.map +2 -2
  40. package/build/components/post-template/classic-theme.cjs +0 -1
  41. package/build/components/post-template/classic-theme.cjs.map +2 -2
  42. package/build/components/preferences-modal/index.cjs +54 -28
  43. package/build/components/preferences-modal/index.cjs.map +2 -2
  44. package/build/components/preview-dropdown/index.cjs +38 -9
  45. package/build/components/preview-dropdown/index.cjs.map +3 -3
  46. package/build/components/provider/use-block-editor-settings.cjs +23 -9
  47. package/build/components/provider/use-block-editor-settings.cjs.map +3 -3
  48. package/build/components/resizable-editor/index.cjs +54 -9
  49. package/build/components/resizable-editor/index.cjs.map +3 -3
  50. package/build/components/sidebar/dataform-post-summary.cjs +5 -1
  51. package/build/components/sidebar/dataform-post-summary.cjs.map +3 -3
  52. package/build/components/sidebar/index.cjs +29 -38
  53. package/build/components/sidebar/index.cjs.map +3 -3
  54. package/build/components/sidebar/post-summary.cjs +0 -13
  55. package/build/components/sidebar/post-summary.cjs.map +3 -3
  56. package/build/components/visual-editor/index.cjs +15 -11
  57. package/build/components/visual-editor/index.cjs.map +3 -3
  58. package/build/dataviews/store/private-actions.cjs +24 -19
  59. package/build/dataviews/store/private-actions.cjs.map +2 -2
  60. package/build/store/actions.cjs +5 -3
  61. package/build/store/actions.cjs.map +2 -2
  62. package/build/store/constants.cjs +26 -0
  63. package/build/store/constants.cjs.map +2 -2
  64. package/build/store/private-actions.cjs +25 -18
  65. package/build/store/private-actions.cjs.map +3 -3
  66. package/build/store/private-selectors.cjs +5 -0
  67. package/build/store/private-selectors.cjs.map +2 -2
  68. package/build/store/reducer.cjs +6 -6
  69. package/build/store/reducer.cjs.map +2 -2
  70. package/build/store/selectors.cjs +3 -1
  71. package/build/store/selectors.cjs.map +2 -2
  72. package/build/utils/device-type.cjs +52 -0
  73. package/build/utils/device-type.cjs.map +7 -0
  74. package/build/utils/media-sideload-from-url/index.cjs +63 -0
  75. package/build/utils/media-sideload-from-url/index.cjs.map +7 -0
  76. package/build-module/components/collab-sidebar/format.mjs +57 -0
  77. package/build-module/components/collab-sidebar/format.mjs.map +7 -0
  78. package/build-module/components/collab-sidebar/hooks.mjs +121 -16
  79. package/build-module/components/collab-sidebar/hooks.mjs.map +2 -2
  80. package/build-module/components/collab-sidebar/index.mjs +21 -5
  81. package/build-module/components/collab-sidebar/index.mjs.map +2 -2
  82. package/build-module/components/collab-sidebar/note-highlight-styles.mjs +41 -0
  83. package/build-module/components/collab-sidebar/note-highlight-styles.mjs.map +7 -0
  84. package/build-module/components/collab-sidebar/utils.mjs +133 -0
  85. package/build-module/components/collab-sidebar/utils.mjs.map +2 -2
  86. package/build-module/components/collaborators-presence/use-collaborator-notifications.mjs +55 -30
  87. package/build-module/components/collaborators-presence/use-collaborator-notifications.mjs.map +2 -2
  88. package/build-module/components/commands/index.mjs +99 -3
  89. package/build-module/components/commands/index.mjs.map +2 -2
  90. package/build-module/components/document-bar/index.mjs +28 -34
  91. package/build-module/components/document-bar/index.mjs.map +2 -2
  92. package/build-module/components/entities-saved-states/index.mjs +1 -4
  93. package/build-module/components/entities-saved-states/index.mjs.map +2 -2
  94. package/build-module/components/header/index.mjs +1 -13
  95. package/build-module/components/header/index.mjs.map +2 -2
  96. package/build-module/components/media-categories/index.mjs +93 -7
  97. package/build-module/components/media-categories/index.mjs.map +2 -2
  98. package/build-module/components/page-attributes/parent.mjs +0 -1
  99. package/build-module/components/page-attributes/parent.mjs.map +2 -2
  100. package/build-module/components/post-author/combobox.mjs +0 -1
  101. package/build-module/components/post-author/combobox.mjs.map +2 -2
  102. package/build-module/components/post-author/select.mjs +0 -1
  103. package/build-module/components/post-author/select.mjs.map +2 -2
  104. package/build-module/components/post-preview-button/index.mjs +29 -2
  105. package/build-module/components/post-preview-button/index.mjs.map +2 -2
  106. package/build-module/components/post-publish-panel/maybe-upload-media.mjs +19 -27
  107. package/build-module/components/post-publish-panel/maybe-upload-media.mjs.map +2 -2
  108. package/build-module/components/post-revisions-preview/revisions-slider.mjs +3 -1
  109. package/build-module/components/post-revisions-preview/revisions-slider.mjs.map +2 -2
  110. package/build-module/components/post-taxonomies/flat-term-selector.mjs +0 -1
  111. package/build-module/components/post-taxonomies/flat-term-selector.mjs.map +2 -2
  112. package/build-module/components/post-taxonomies/hierarchical-term-selector.mjs +0 -2
  113. package/build-module/components/post-taxonomies/hierarchical-term-selector.mjs.map +2 -2
  114. package/build-module/components/post-template/classic-theme.mjs +0 -1
  115. package/build-module/components/post-template/classic-theme.mjs.map +2 -2
  116. package/build-module/components/preferences-modal/index.mjs +54 -28
  117. package/build-module/components/preferences-modal/index.mjs.map +2 -2
  118. package/build-module/components/preview-dropdown/index.mjs +38 -9
  119. package/build-module/components/preview-dropdown/index.mjs.map +2 -2
  120. package/build-module/components/provider/use-block-editor-settings.mjs +23 -9
  121. package/build-module/components/provider/use-block-editor-settings.mjs.map +2 -2
  122. package/build-module/components/resizable-editor/index.mjs +55 -10
  123. package/build-module/components/resizable-editor/index.mjs.map +2 -2
  124. package/build-module/components/sidebar/dataform-post-summary.mjs +6 -2
  125. package/build-module/components/sidebar/dataform-post-summary.mjs.map +2 -2
  126. package/build-module/components/sidebar/index.mjs +29 -42
  127. package/build-module/components/sidebar/index.mjs.map +2 -2
  128. package/build-module/components/sidebar/post-summary.mjs +0 -13
  129. package/build-module/components/sidebar/post-summary.mjs.map +2 -2
  130. package/build-module/components/visual-editor/index.mjs +16 -13
  131. package/build-module/components/visual-editor/index.mjs.map +2 -2
  132. package/build-module/dataviews/store/private-actions.mjs +26 -19
  133. package/build-module/dataviews/store/private-actions.mjs.map +2 -2
  134. package/build-module/store/actions.mjs +5 -3
  135. package/build-module/store/actions.mjs.map +2 -2
  136. package/build-module/store/constants.mjs +25 -0
  137. package/build-module/store/constants.mjs.map +2 -2
  138. package/build-module/store/private-actions.mjs +27 -17
  139. package/build-module/store/private-actions.mjs.map +2 -2
  140. package/build-module/store/private-selectors.mjs +4 -0
  141. package/build-module/store/private-selectors.mjs.map +2 -2
  142. package/build-module/store/reducer.mjs +5 -5
  143. package/build-module/store/reducer.mjs.map +2 -2
  144. package/build-module/store/selectors.mjs +3 -1
  145. package/build-module/store/selectors.mjs.map +2 -2
  146. package/build-module/utils/device-type.mjs +25 -0
  147. package/build-module/utils/device-type.mjs.map +7 -0
  148. package/build-module/utils/media-sideload-from-url/index.mjs +32 -0
  149. package/build-module/utils/media-sideload-from-url/index.mjs.map +7 -0
  150. package/build-style/style-rtl.css +84 -39
  151. package/build-style/style.css +84 -39
  152. package/build-types/components/collab-sidebar/format.d.ts +17 -0
  153. package/build-types/components/collab-sidebar/format.d.ts.map +1 -0
  154. package/build-types/components/collab-sidebar/hooks.d.ts.map +1 -1
  155. package/build-types/components/collab-sidebar/index.d.ts.map +1 -1
  156. package/build-types/components/collab-sidebar/note-highlight-styles.d.ts +31 -0
  157. package/build-types/components/collab-sidebar/note-highlight-styles.d.ts.map +1 -0
  158. package/build-types/components/collab-sidebar/utils.d.ts +82 -2
  159. package/build-types/components/collab-sidebar/utils.d.ts.map +1 -1
  160. package/build-types/components/collaborators-presence/use-collaborator-notifications.d.ts.map +1 -1
  161. package/build-types/components/commands/index.d.ts.map +1 -1
  162. package/build-types/components/document-bar/index.d.ts.map +1 -1
  163. package/build-types/components/entities-saved-states/index.d.ts.map +1 -1
  164. package/build-types/components/header/index.d.ts.map +1 -1
  165. package/build-types/components/media-categories/index.d.ts +12 -3
  166. package/build-types/components/media-categories/index.d.ts.map +1 -1
  167. package/build-types/components/page-attributes/parent.d.ts.map +1 -1
  168. package/build-types/components/post-author/combobox.d.ts.map +1 -1
  169. package/build-types/components/post-author/select.d.ts.map +1 -1
  170. package/build-types/components/post-preview-button/index.d.ts.map +1 -1
  171. package/build-types/components/post-publish-panel/maybe-upload-media.d.ts.map +1 -1
  172. package/build-types/components/post-revisions-preview/revisions-slider.d.ts.map +1 -1
  173. package/build-types/components/post-taxonomies/flat-term-selector.d.ts.map +1 -1
  174. package/build-types/components/post-taxonomies/hierarchical-term-selector.d.ts.map +1 -1
  175. package/build-types/components/post-template/classic-theme.d.ts.map +1 -1
  176. package/build-types/components/preview-dropdown/index.d.ts.map +1 -1
  177. package/build-types/components/provider/use-block-editor-settings.d.ts.map +1 -1
  178. package/build-types/components/resizable-editor/index.d.ts.map +1 -1
  179. package/build-types/components/sidebar/dataform-post-summary.d.ts.map +1 -1
  180. package/build-types/components/sidebar/index.d.ts.map +1 -1
  181. package/build-types/components/sidebar/post-summary.d.ts.map +1 -1
  182. package/build-types/components/visual-editor/index.d.ts.map +1 -1
  183. package/build-types/dataviews/store/private-actions.d.ts.map +1 -1
  184. package/build-types/store/actions.d.ts.map +1 -1
  185. package/build-types/store/constants.d.ts +6 -0
  186. package/build-types/store/constants.d.ts.map +1 -1
  187. package/build-types/store/index.d.ts +2 -2
  188. package/build-types/store/private-actions.d.ts +9 -15
  189. package/build-types/store/private-actions.d.ts.map +1 -1
  190. package/build-types/store/private-selectors.d.ts +7 -0
  191. package/build-types/store/private-selectors.d.ts.map +1 -1
  192. package/build-types/store/reducer.d.ts +6 -7
  193. package/build-types/store/reducer.d.ts.map +1 -1
  194. package/build-types/store/selectors.d.ts +1 -1
  195. package/build-types/store/selectors.d.ts.map +1 -1
  196. package/build-types/utils/device-type.d.ts +23 -0
  197. package/build-types/utils/device-type.d.ts.map +1 -0
  198. package/build-types/utils/get-template-part-icon.d.ts.map +1 -1
  199. package/build-types/utils/media-sideload-from-url/index.d.ts +18 -0
  200. package/build-types/utils/media-sideload-from-url/index.d.ts.map +1 -0
  201. package/package.json +48 -46
  202. package/src/components/collab-sidebar/format.js +72 -0
  203. package/src/components/collab-sidebar/hooks.js +203 -22
  204. package/src/components/collab-sidebar/index.js +19 -5
  205. package/src/components/collab-sidebar/note-highlight-styles.js +83 -0
  206. package/src/components/collab-sidebar/style.scss +9 -0
  207. package/src/components/collab-sidebar/test/note-highlight-styles.js +106 -0
  208. package/src/components/collab-sidebar/test/utils.js +471 -0
  209. package/src/components/collab-sidebar/utils.js +225 -2
  210. package/src/components/collaborators-presence/test/use-collaborator-notifications.ts +265 -262
  211. package/src/components/collaborators-presence/use-collaborator-notifications.ts +70 -34
  212. package/src/components/commands/index.js +142 -0
  213. package/src/components/document-bar/index.js +0 -9
  214. package/src/components/document-bar/style.scss +0 -17
  215. package/src/components/entities-saved-states/index.js +1 -5
  216. package/src/components/header/index.js +1 -15
  217. package/src/components/header/style.scss +7 -3
  218. package/src/components/media-categories/index.js +160 -6
  219. package/src/components/media-categories/test/index.js +202 -0
  220. package/src/components/page-attributes/parent.js +0 -1
  221. package/src/components/post-author/combobox.js +0 -1
  222. package/src/components/post-author/select.js +0 -1
  223. package/src/components/post-preview-button/index.js +68 -2
  224. package/src/components/post-publish-panel/maybe-upload-media.js +25 -30
  225. package/src/components/post-publish-panel/style.scss +1 -1
  226. package/src/components/post-publish-panel/test/__snapshots__/index.js.snap +11 -127
  227. package/src/components/post-revisions-preview/revisions-slider.js +4 -1
  228. package/src/components/post-revisions-preview/test/block-diff.js +2 -2
  229. package/src/components/post-taxonomies/flat-term-selector.js +0 -1
  230. package/src/components/post-taxonomies/hierarchical-term-selector.js +0 -2
  231. package/src/components/post-template/classic-theme.js +0 -1
  232. package/src/components/preferences-modal/index.js +40 -22
  233. package/src/components/preferences-modal/test/index.js +83 -0
  234. package/src/components/preview-dropdown/index.js +42 -3
  235. package/src/components/provider/use-block-editor-settings.js +42 -7
  236. package/src/components/resizable-editor/index.js +71 -9
  237. package/src/components/resizable-editor/style.scss +7 -0
  238. package/src/components/sidebar/dataform-post-summary.js +13 -1
  239. package/src/components/sidebar/index.js +32 -47
  240. package/src/components/sidebar/post-summary.js +0 -17
  241. package/src/components/styles-canvas/style.scss +1 -2
  242. package/src/components/visual-editor/index.js +19 -15
  243. package/src/components/visual-editor/style.scss +11 -4
  244. package/src/dataviews/store/private-actions.ts +30 -26
  245. package/src/store/actions.js +6 -3
  246. package/src/store/constants.ts +45 -0
  247. package/src/store/private-actions.js +38 -28
  248. package/src/store/private-selectors.js +10 -0
  249. package/src/store/reducer.js +7 -9
  250. package/src/store/selectors.js +3 -1
  251. package/src/store/test/actions.js +31 -22
  252. package/src/utils/device-type.js +46 -0
  253. package/src/utils/media-sideload-from-url/index.js +54 -0
  254. package/src/utils/media-sideload-from-url/test/index.js +162 -0
  255. package/build/components/post-publish-panel/media-util.cjs +0 -72
  256. package/build/components/post-publish-panel/media-util.cjs.map +0 -7
  257. package/build-module/components/post-publish-panel/media-util.mjs +0 -46
  258. package/build-module/components/post-publish-panel/media-util.mjs.map +0 -7
  259. package/build-types/components/post-publish-panel/media-util.d.ts +0 -20
  260. package/build-types/components/post-publish-panel/media-util.d.ts.map +0 -1
  261. package/src/components/post-publish-panel/media-util.js +0 -87
  262. package/src/components/post-publish-panel/test/media-util.js +0 -118
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import { buildHighlightCss } from '../note-highlight-styles';
5
+ import { getAvatarBorderColor } from '../utils';
6
+
7
+ describe( 'buildHighlightCss', () => {
8
+ it( 'always emits the mark reset so the browser default yellow does not bleed through', () => {
9
+ expect( buildHighlightCss( [] ) ).toContain(
10
+ 'mark.wp-note{background-color:transparent;color:inherit;}'
11
+ );
12
+ } );
13
+
14
+ it( 'tints each thread with its author color at the rest alpha (0x40)', () => {
15
+ const css = buildHighlightCss( [
16
+ { id: 7, author: 1 },
17
+ { id: 12, author: 3 },
18
+ ] );
19
+ expect( css ).toContain(
20
+ `mark.wp-note[data-id="7"]{background-color:${ getAvatarBorderColor(
21
+ 1
22
+ ) }40;}`
23
+ );
24
+ expect( css ).toContain(
25
+ `mark.wp-note[data-id="12"]{background-color:${ getAvatarBorderColor(
26
+ 3
27
+ ) }40;}`
28
+ );
29
+ } );
30
+
31
+ it( 'emits a higher-alpha (0x80) rule on hover and focus-within for each thread', () => {
32
+ const css = buildHighlightCss( [ { id: 7, author: 1 } ] );
33
+ const color = getAvatarBorderColor( 1 );
34
+ expect( css ).toContain(
35
+ `mark.wp-note[data-id="7"]:hover,mark.wp-note[data-id="7"]:focus-within{background-color:${ color }80;}`
36
+ );
37
+ } );
38
+
39
+ it( 'boosts opacity for the selected thread by appending a second rule', () => {
40
+ const css = buildHighlightCss(
41
+ [ { id: 7, author: 1 } ],
42
+ '7' // selected
43
+ );
44
+ const color = getAvatarBorderColor( 1 );
45
+ // Rest rule still present.
46
+ expect( css ).toContain(
47
+ `mark.wp-note[data-id="7"]{background-color:${ color }40;}`
48
+ );
49
+ // Active rule appended later, so the cascade picks it.
50
+ const restIndex = css.indexOf(
51
+ `mark.wp-note[data-id="7"]{background-color:${ color }40;}`
52
+ );
53
+ const activeIndex = css.lastIndexOf(
54
+ `mark.wp-note[data-id="7"]{background-color:${ color }80;}`
55
+ );
56
+ expect( activeIndex ).toBeGreaterThan( restIndex );
57
+ } );
58
+
59
+ it( 'matches numeric and string selectedId variants', () => {
60
+ const cssNum = buildHighlightCss( [ { id: 7, author: 1 } ], 7 );
61
+ const cssStr = buildHighlightCss( [ { id: 7, author: 1 } ], '7' );
62
+ expect( cssNum ).toEqual( cssStr );
63
+ } );
64
+
65
+ it( 'skips threads without an id', () => {
66
+ const css = buildHighlightCss( [
67
+ { id: null, author: 1 },
68
+ { author: 1 },
69
+ ] );
70
+ expect( css ).not.toMatch( /data-id="(null|undefined)"/ );
71
+ } );
72
+
73
+ it( 'cycles through AVATAR_BORDER_COLORS by author id modulo length', () => {
74
+ // Authors 1 and 8 collide (1 % 7 === 8 % 7), so both threads should
75
+ // share the same color — guards the modulo behavior in
76
+ // getAvatarBorderColor.
77
+ const css = buildHighlightCss( [
78
+ { id: 'a', author: 1 },
79
+ { id: 'b', author: 8 },
80
+ ] );
81
+ const color = getAvatarBorderColor( 1 );
82
+ expect( css ).toContain(
83
+ `mark.wp-note[data-id="a"]{background-color:${ color }40;}`
84
+ );
85
+ expect( css ).toContain(
86
+ `mark.wp-note[data-id="b"]{background-color:${ color }40;}`
87
+ );
88
+ } );
89
+
90
+ it( 'falls back to author 0 when the field is missing', () => {
91
+ const css = buildHighlightCss( [ { id: 'x' } ] );
92
+ const color = getAvatarBorderColor( 0 );
93
+ expect( css ).toContain(
94
+ `mark.wp-note[data-id="x"]{background-color:${ color }40;}`
95
+ );
96
+ } );
97
+
98
+ it( 'returns just the reset when no threads are provided', () => {
99
+ expect( buildHighlightCss() ).toBe(
100
+ 'mark.wp-note{background-color:transparent;color:inherit;}'
101
+ );
102
+ expect( buildHighlightCss( null ) ).toBe(
103
+ 'mark.wp-note{background-color:transparent;color:inherit;}'
104
+ );
105
+ } );
106
+ } );
@@ -1,12 +1,31 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import {
5
+ RichTextData,
6
+ create,
7
+ applyFormat,
8
+ registerFormatType,
9
+ unregisterFormatType,
10
+ store as richTextStore,
11
+ } from '@wordpress/rich-text';
12
+ import { select } from '@wordpress/data';
13
+
1
14
  /**
2
15
  * Internal dependencies
3
16
  */
4
17
  import {
18
+ findNoteRange,
19
+ findNoteInBlock,
20
+ applyNoteFormat,
21
+ removeNoteFormat,
5
22
  getNoteIdsFromMetadata,
6
23
  addNoteIdToMetadata,
7
24
  removeNoteIdFromMetadata,
8
25
  calculateNotePositions,
9
26
  pickPrimaryNote,
27
+ BLOCK_LEVEL_NOTE_START,
28
+ getInlineMarkerStart,
10
29
  } from '../utils';
11
30
 
12
31
  function makeRect( top ) {
@@ -527,3 +546,455 @@ describe( 'calculateNotePositions', () => {
527
546
  expect( positions ).toEqual( { 1: 114, 2: 184, 3: 254 } );
528
547
  } );
529
548
  } );
549
+
550
+ describe( 'findNoteRange', () => {
551
+ const FORMAT_NAME = 'core/note';
552
+
553
+ const isRegistered = () =>
554
+ !! select( richTextStore ).getFormatType( FORMAT_NAME );
555
+
556
+ beforeAll( () => {
557
+ if ( ! isRegistered() ) {
558
+ registerFormatType( FORMAT_NAME, {
559
+ title: 'Note',
560
+ tagName: 'span',
561
+ className: 'wp-note',
562
+ attributes: { 'data-id': 'data-id' },
563
+ edit: () => null,
564
+ } );
565
+ }
566
+ } );
567
+
568
+ afterAll( () => {
569
+ if ( isRegistered() ) {
570
+ unregisterFormatType( FORMAT_NAME );
571
+ }
572
+ } );
573
+
574
+ it( 'returns null for null/undefined input', () => {
575
+ expect( findNoteRange( null, 7 ) ).toBeNull();
576
+ expect( findNoteRange( undefined, 7 ) ).toBeNull();
577
+ } );
578
+
579
+ it( 'returns null when no marker is present', () => {
580
+ const value = RichTextData.fromHTMLString( 'hello world' );
581
+ expect( findNoteRange( value, 7 ) ).toBeNull();
582
+ } );
583
+
584
+ it( 'returns range for a marker matching the note id (RichTextData)', () => {
585
+ const value = RichTextData.fromHTMLString(
586
+ 'hello <span class="wp-note" data-id="7">marked</span> world'
587
+ );
588
+ expect( findNoteRange( value, 7 ) ).toEqual( {
589
+ start: 6,
590
+ end: 12,
591
+ } );
592
+ } );
593
+
594
+ it( 'returns range for a marker matching the note id (string)', () => {
595
+ const html =
596
+ 'hello <span class="wp-note" data-id="7">marked</span> world';
597
+ expect( findNoteRange( html, 7 ) ).toEqual( { start: 6, end: 12 } );
598
+ } );
599
+
600
+ it( 'returns null when the marker id does not match', () => {
601
+ const value = RichTextData.fromHTMLString(
602
+ '<span class="wp-note" data-id="3">x</span>'
603
+ );
604
+ expect( findNoteRange( value, 7 ) ).toBeNull();
605
+ } );
606
+
607
+ it( 'coerces ids to strings so numeric vs string ids match', () => {
608
+ const value = RichTextData.fromHTMLString(
609
+ '<span class="wp-note" data-id="7">x</span>'
610
+ );
611
+ expect( findNoteRange( value, '7' ) ).toEqual( { start: 0, end: 1 } );
612
+ } );
613
+
614
+ it( 'returns null when noteId itself is null/undefined', () => {
615
+ const value = RichTextData.fromHTMLString(
616
+ '<span class="wp-note" data-id="7">x</span>'
617
+ );
618
+ expect( findNoteRange( value, null ) ).toBeNull();
619
+ expect( findNoteRange( value, undefined ) ).toBeNull();
620
+ } );
621
+ } );
622
+
623
+ describe( 'findNoteInBlock', () => {
624
+ const FORMAT_NAME = 'core/note';
625
+
626
+ const isRegistered = () =>
627
+ !! select( richTextStore ).getFormatType( FORMAT_NAME );
628
+
629
+ beforeAll( () => {
630
+ if ( ! isRegistered() ) {
631
+ registerFormatType( FORMAT_NAME, {
632
+ title: 'Note',
633
+ tagName: 'span',
634
+ className: 'wp-note',
635
+ attributes: { 'data-id': 'data-id' },
636
+ edit: () => null,
637
+ } );
638
+ }
639
+ } );
640
+
641
+ afterAll( () => {
642
+ if ( isRegistered() ) {
643
+ unregisterFormatType( FORMAT_NAME );
644
+ }
645
+ } );
646
+
647
+ it( 'returns null when attributes are null/undefined', () => {
648
+ expect( findNoteInBlock( null, 7 ) ).toBeNull();
649
+ expect( findNoteInBlock( undefined, 7 ) ).toBeNull();
650
+ } );
651
+
652
+ it( 'returns null when no attribute carries a matching marker', () => {
653
+ const attributes = {
654
+ content: RichTextData.fromHTMLString( 'hello world' ),
655
+ align: 'left',
656
+ };
657
+ expect( findNoteInBlock( attributes, 7 ) ).toBeNull();
658
+ } );
659
+
660
+ it( 'returns the attribute key and range of the matching marker', () => {
661
+ const attributes = {
662
+ content: RichTextData.fromHTMLString(
663
+ 'hello <span class="wp-note" data-id="7">marked</span> world'
664
+ ),
665
+ };
666
+ expect( findNoteInBlock( attributes, 7 ) ).toEqual( {
667
+ attributeKey: 'content',
668
+ start: 6,
669
+ end: 12,
670
+ } );
671
+ } );
672
+
673
+ it( 'discovers the marker in a non-primary rich-text attribute', () => {
674
+ const attributes = {
675
+ content: RichTextData.fromHTMLString( 'no marker here' ),
676
+ caption: RichTextData.fromHTMLString(
677
+ '<span class="wp-note" data-id="9">cap</span>'
678
+ ),
679
+ };
680
+ expect( findNoteInBlock( attributes, 9 ) ).toEqual( {
681
+ attributeKey: 'caption',
682
+ start: 0,
683
+ end: 3,
684
+ } );
685
+ } );
686
+
687
+ it( 'ignores non-rich-text attribute values without throwing', () => {
688
+ const attributes = {
689
+ level: 2,
690
+ anchor: '',
691
+ content: RichTextData.fromHTMLString(
692
+ '<span class="wp-note" data-id="3">x</span>'
693
+ ),
694
+ };
695
+ expect( findNoteInBlock( attributes, 3 ) ).toEqual( {
696
+ attributeKey: 'content',
697
+ start: 0,
698
+ end: 1,
699
+ } );
700
+ } );
701
+
702
+ it( 'coerces ids to strings so numeric vs string ids match', () => {
703
+ const attributes = {
704
+ content: RichTextData.fromHTMLString(
705
+ '<span class="wp-note" data-id="7">x</span>'
706
+ ),
707
+ };
708
+ expect( findNoteInBlock( attributes, '7' ) ).toEqual( {
709
+ attributeKey: 'content',
710
+ start: 0,
711
+ end: 1,
712
+ } );
713
+ } );
714
+ } );
715
+
716
+ describe( 'applyNoteFormat', () => {
717
+ const FORMAT_NAME = 'core/note';
718
+
719
+ const isRegistered = () =>
720
+ !! select( richTextStore ).getFormatType( FORMAT_NAME );
721
+
722
+ beforeAll( () => {
723
+ if ( ! isRegistered() ) {
724
+ registerFormatType( FORMAT_NAME, {
725
+ title: 'Note',
726
+ tagName: 'mark',
727
+ className: 'wp-note',
728
+ attributes: { 'data-id': 'data-id' },
729
+ edit: () => null,
730
+ } );
731
+ }
732
+ if ( ! select( richTextStore ).getFormatType( 'core/bold' ) ) {
733
+ registerFormatType( 'core/bold', {
734
+ title: 'Bold',
735
+ tagName: 'strong',
736
+ className: null,
737
+ edit: () => null,
738
+ } );
739
+ }
740
+ } );
741
+
742
+ afterAll( () => {
743
+ if ( isRegistered() ) {
744
+ unregisterFormatType( FORMAT_NAME );
745
+ }
746
+ if ( select( richTextStore ).getFormatType( 'core/bold' ) ) {
747
+ unregisterFormatType( 'core/bold' );
748
+ }
749
+ } );
750
+
751
+ const note = ( id ) => ( {
752
+ type: FORMAT_NAME,
753
+ attributes: { 'data-id': String( id ) },
754
+ } );
755
+
756
+ // Apply a sequence of [ id, start, end ] notes, then round-trip through HTML
757
+ // to a normalised value (matching how wrapInlineNote stores the result).
758
+ const applyAll = ( html, ops ) => {
759
+ let record = create( { html } );
760
+ for ( const [ id, start, end ] of ops ) {
761
+ record = applyNoteFormat( record, note( id ), start, end );
762
+ }
763
+ return RichTextData.fromHTMLString(
764
+ new RichTextData( record ).toHTMLString()
765
+ );
766
+ };
767
+
768
+ it( 'adds a single marker over plain text', () => {
769
+ const value = applyAll( 'the quick brown fox', [ [ 7, 4, 9 ] ] );
770
+ expect( value.toHTMLString() ).toBe(
771
+ 'the <mark data-id="7" class="wp-note">quick</mark> brown fox'
772
+ );
773
+ } );
774
+
775
+ it( 'nests a contained note inside its parent (outer applied first)', () => {
776
+ const value = applyAll( 'the quick brown fox jumps over', [
777
+ [ 2, 0, 29 ],
778
+ [ 1, 16, 22 ],
779
+ ] );
780
+ expect( value.toHTMLString() ).toBe(
781
+ '<mark data-id="2" class="wp-note">the quick brown <mark data-id="1" class="wp-note">fox ju</mark>mps ove</mark>r'
782
+ );
783
+ } );
784
+
785
+ it( 'nests the same regardless of application order (inner applied first)', () => {
786
+ const value = applyAll( 'the quick brown fox jumps over', [
787
+ [ 1, 16, 22 ],
788
+ [ 2, 0, 29 ],
789
+ ] );
790
+ expect( value.toHTMLString() ).toBe(
791
+ '<mark data-id="2" class="wp-note">the quick brown <mark data-id="1" class="wp-note">fox ju</mark>mps ove</mark>r'
792
+ );
793
+ } );
794
+
795
+ it( 'keeps both notes when a larger note covers an existing one (no clobber)', () => {
796
+ const value = applyAll( 'the quick brown fox jumps over', [
797
+ [ 1, 16, 22 ],
798
+ [ 2, 0, 29 ],
799
+ ] );
800
+ expect( findNoteRange( value, 1 ) ).toEqual( { start: 16, end: 22 } );
801
+ expect( findNoteRange( value, 2 ) ).toEqual( { start: 0, end: 29 } );
802
+ } );
803
+
804
+ it( 'preserves both full ranges across a partial (crossing) overlap', () => {
805
+ const value = applyAll( 'the quick brown fox jumps over', [
806
+ [ 1, 0, 18 ],
807
+ [ 2, 10, 30 ],
808
+ ] );
809
+ expect( findNoteRange( value, 1 ) ).toEqual( { start: 0, end: 18 } );
810
+ expect( findNoteRange( value, 2 ) ).toEqual( { start: 10, end: 30 } );
811
+ } );
812
+
813
+ it( 'keeps disjoint notes separate', () => {
814
+ const value = applyAll( 'the quick brown fox jumps over', [
815
+ [ 1, 0, 9 ],
816
+ [ 2, 16, 25 ],
817
+ ] );
818
+ expect( value.toHTMLString() ).toBe(
819
+ '<mark data-id="1" class="wp-note">the quick</mark> brown <mark data-id="2" class="wp-note">fox jumps</mark> over'
820
+ );
821
+ } );
822
+
823
+ it( 'preserves the note range when wrapping already-formatted text', () => {
824
+ let record = create( { html: 'the quick brown fox jumps over' } );
825
+ record = applyFormat( record, { type: 'core/bold' }, 4, 9 );
826
+ record = applyNoteFormat( record, note( 1 ), 0, 18 );
827
+ const value = RichTextData.fromHTMLString(
828
+ new RichTextData( record ).toHTMLString()
829
+ );
830
+ expect( findNoteRange( value, 1 ) ).toEqual( { start: 0, end: 18 } );
831
+ } );
832
+ } );
833
+
834
+ describe( 'getInlineMarkerStart', () => {
835
+ // `findNoteInBlock` (which getInlineMarkerStart delegates to) walks rich-text
836
+ // values and needs the `core/note` format registered to recognize the marker
837
+ // elements the tests construct below.
838
+ const FORMAT_NAME = 'core/note';
839
+
840
+ const isRegistered = () =>
841
+ !! select( richTextStore ).getFormatType( FORMAT_NAME );
842
+
843
+ beforeAll( () => {
844
+ if ( ! isRegistered() ) {
845
+ registerFormatType( FORMAT_NAME, {
846
+ title: 'Note',
847
+ tagName: 'mark',
848
+ className: 'wp-note',
849
+ attributes: { 'data-id': 'data-id' },
850
+ edit: () => null,
851
+ } );
852
+ }
853
+ } );
854
+
855
+ afterAll( () => {
856
+ if ( isRegistered() ) {
857
+ unregisterFormatType( FORMAT_NAME );
858
+ }
859
+ } );
860
+
861
+ it( 'returns the block-level sentinel when the block carries no marker for the note', () => {
862
+ const attributes = {
863
+ content: RichTextData.fromHTMLString( 'hello world' ),
864
+ };
865
+ const thread = { id: 1 };
866
+ expect( getInlineMarkerStart( thread, attributes ) ).toBe(
867
+ BLOCK_LEVEL_NOTE_START
868
+ );
869
+ } );
870
+
871
+ it( 'returns the marker start offset when the block carries a matching marker', () => {
872
+ const attributes = {
873
+ content: RichTextData.fromHTMLString(
874
+ 'hello <mark class="wp-note" data-id="7">marked</mark> world'
875
+ ),
876
+ };
877
+ expect( getInlineMarkerStart( { id: 7 }, attributes ) ).toBe( 6 );
878
+ } );
879
+
880
+ it( 'discovers the marker in a non-primary rich-text attribute', () => {
881
+ const attributes = {
882
+ content: RichTextData.fromHTMLString( 'no marker here' ),
883
+ caption: RichTextData.fromHTMLString(
884
+ 'xx <mark class="wp-note" data-id="7">y</mark>'
885
+ ),
886
+ };
887
+ expect( getInlineMarkerStart( { id: 7 }, attributes ) ).toBe( 3 );
888
+ } );
889
+
890
+ it( 'returns the block-level sentinel when block attributes are empty', () => {
891
+ expect( getInlineMarkerStart( { id: 7 }, {} ) ).toBe(
892
+ BLOCK_LEVEL_NOTE_START
893
+ );
894
+ } );
895
+
896
+ it( 'returns the block-level sentinel when block attributes are null', () => {
897
+ expect( getInlineMarkerStart( { id: 7 }, null ) ).toBe(
898
+ BLOCK_LEVEL_NOTE_START
899
+ );
900
+ } );
901
+
902
+ it( 'is order-stable when used as a sort key: block-level first, then by start offset, then by id', () => {
903
+ const attributes = {
904
+ content: RichTextData.fromHTMLString(
905
+ 'a <mark class="wp-note" data-id="2">x</mark> b <mark class="wp-note" data-id="3">y</mark> c <mark class="wp-note" data-id="1">z</mark>'
906
+ ),
907
+ };
908
+ const threads = [
909
+ // Block-level note (no marker) — should sort first.
910
+ { id: 99 },
911
+ // Inline notes — should sort by marker offset, then id.
912
+ { id: 1 },
913
+ { id: 2 },
914
+ { id: 3 },
915
+ ];
916
+ const sorted = [ ...threads ].sort( ( a, b ) => {
917
+ const aStart = getInlineMarkerStart( a, attributes );
918
+ const bStart = getInlineMarkerStart( b, attributes );
919
+ if ( aStart !== bStart ) {
920
+ return aStart - bStart;
921
+ }
922
+ return a.id - b.id;
923
+ } );
924
+ expect( sorted.map( ( t ) => t.id ) ).toEqual( [ 99, 2, 3, 1 ] );
925
+ } );
926
+ } );
927
+
928
+ describe( 'removeNoteFormat', () => {
929
+ // `create`/`findNoteRange` parse marker elements through the registered
930
+ // `core/note` format, so register it for the fixtures below.
931
+ const FORMAT_NAME = 'core/note';
932
+
933
+ const isRegistered = () =>
934
+ !! select( richTextStore ).getFormatType( FORMAT_NAME );
935
+
936
+ beforeAll( () => {
937
+ if ( ! isRegistered() ) {
938
+ registerFormatType( FORMAT_NAME, {
939
+ title: 'Note',
940
+ tagName: 'mark',
941
+ className: 'wp-note',
942
+ attributes: { 'data-id': 'data-id' },
943
+ edit: () => null,
944
+ } );
945
+ }
946
+ } );
947
+
948
+ afterAll( () => {
949
+ if ( isRegistered() ) {
950
+ unregisterFormatType( FORMAT_NAME );
951
+ }
952
+ } );
953
+
954
+ it( 'returns null when the value is not rich text', () => {
955
+ expect( removeNoteFormat( 'plain string', 1 ) ).toBeNull();
956
+ expect( removeNoteFormat( undefined, 1 ) ).toBeNull();
957
+ } );
958
+
959
+ it( 'returns null when no marker matches the note id', () => {
960
+ const value = RichTextData.fromHTMLString(
961
+ 'the <mark class="wp-note" data-id="2">quick</mark> brown fox'
962
+ );
963
+ expect( removeNoteFormat( value, 1 ) ).toBeNull();
964
+ } );
965
+
966
+ it( 'removes the marker while leaving the text intact', () => {
967
+ const value = RichTextData.fromHTMLString(
968
+ 'the <mark class="wp-note" data-id="7">quick</mark> brown fox'
969
+ );
970
+ expect( removeNoteFormat( value, 7 ).toHTMLString() ).toBe(
971
+ 'the quick brown fox'
972
+ );
973
+ } );
974
+
975
+ it( 'removes only the targeted note, leaving a co-located note intact', () => {
976
+ // Note 2 wraps the whole string; note 1 nests inside it.
977
+ const value = RichTextData.fromHTMLString(
978
+ '<mark data-id="2" class="wp-note">the quick brown <mark data-id="1" class="wp-note">fox ju</mark>mps ove</mark>r'
979
+ );
980
+ const result = removeNoteFormat( value, 1 );
981
+ expect( findNoteRange( result, 1 ) ).toBeNull();
982
+ expect( findNoteRange( result, 2 ) ).toEqual( { start: 0, end: 29 } );
983
+ } );
984
+
985
+ it( 'matches numeric and string note ids', () => {
986
+ const html = 'a <mark class="wp-note" data-id="5">b</mark> c';
987
+ expect(
988
+ removeNoteFormat(
989
+ RichTextData.fromHTMLString( html ),
990
+ 5
991
+ ).toHTMLString()
992
+ ).toBe( 'a b c' );
993
+ expect(
994
+ removeNoteFormat(
995
+ RichTextData.fromHTMLString( html ),
996
+ '5'
997
+ ).toHTMLString()
998
+ ).toBe( 'a b c' );
999
+ } );
1000
+ } );