@wordpress/core-data 7.46.0 → 7.48.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 (148) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +0 -27
  3. package/build/actions.cjs +0 -19
  4. package/build/actions.cjs.map +2 -2
  5. package/build/awareness/block-lookup.cjs +13 -0
  6. package/build/awareness/block-lookup.cjs.map +2 -2
  7. package/build/awareness/post-editor-awareness.cjs +21 -9
  8. package/build/awareness/post-editor-awareness.cjs.map +2 -2
  9. package/build/hooks/use-entity-block-editor.cjs +4 -4
  10. package/build/hooks/use-entity-block-editor.cjs.map +2 -2
  11. package/build/hooks/use-post-editor-awareness-state.cjs +2 -1
  12. package/build/hooks/use-post-editor-awareness-state.cjs.map +2 -2
  13. package/build/hooks/use-resource-permissions.cjs +3 -5
  14. package/build/hooks/use-resource-permissions.cjs.map +2 -2
  15. package/build/index.cjs +0 -6
  16. package/build/index.cjs.map +2 -2
  17. package/build/parsed-blocks-cache.cjs +36 -0
  18. package/build/parsed-blocks-cache.cjs.map +7 -0
  19. package/build/private-actions.cjs +25 -2
  20. package/build/private-actions.cjs.map +2 -2
  21. package/build/private-apis.cjs +9 -5
  22. package/build/private-apis.cjs.map +3 -3
  23. package/build/private-selectors.cjs +15 -0
  24. package/build/private-selectors.cjs.map +2 -2
  25. package/build/resolvers.cjs +12 -2
  26. package/build/resolvers.cjs.map +2 -2
  27. package/build/selectors.cjs +0 -15
  28. package/build/selectors.cjs.map +2 -2
  29. package/build/sync.cjs +5 -0
  30. package/build/sync.cjs.map +2 -2
  31. package/build/types.cjs +0 -16
  32. package/build/types.cjs.map +3 -3
  33. package/build/utils/block-selection-history.cjs +5 -4
  34. package/build/utils/block-selection-history.cjs.map +2 -2
  35. package/build/utils/crdt-blocks.cjs +3 -0
  36. package/build/utils/crdt-blocks.cjs.map +2 -2
  37. package/build/utils/crdt-user-selections.cjs +10 -2
  38. package/build/utils/crdt-user-selections.cjs.map +3 -3
  39. package/build/utils/crdt-utils.cjs +23 -0
  40. package/build/utils/crdt-utils.cjs.map +2 -2
  41. package/build/utils/crdt.cjs +28 -4
  42. package/build/utils/crdt.cjs.map +2 -2
  43. package/build-module/actions.mjs +0 -18
  44. package/build-module/actions.mjs.map +2 -2
  45. package/build-module/awareness/block-lookup.mjs +12 -0
  46. package/build-module/awareness/block-lookup.mjs.map +2 -2
  47. package/build-module/awareness/post-editor-awareness.mjs +26 -9
  48. package/build-module/awareness/post-editor-awareness.mjs.map +2 -2
  49. package/build-module/hooks/use-entity-block-editor.mjs +2 -2
  50. package/build-module/hooks/use-entity-block-editor.mjs.map +2 -2
  51. package/build-module/hooks/use-post-editor-awareness-state.mjs +2 -1
  52. package/build-module/hooks/use-post-editor-awareness-state.mjs.map +2 -2
  53. package/build-module/hooks/use-resource-permissions.mjs +3 -5
  54. package/build-module/hooks/use-resource-permissions.mjs.map +2 -2
  55. package/build-module/index.mjs +0 -4
  56. package/build-module/index.mjs.map +2 -2
  57. package/build-module/parsed-blocks-cache.mjs +10 -0
  58. package/build-module/parsed-blocks-cache.mjs.map +7 -0
  59. package/build-module/private-actions.mjs +23 -1
  60. package/build-module/private-actions.mjs.map +2 -2
  61. package/build-module/private-apis.mjs +12 -5
  62. package/build-module/private-apis.mjs.map +3 -3
  63. package/build-module/private-selectors.mjs +14 -0
  64. package/build-module/private-selectors.mjs.map +2 -2
  65. package/build-module/resolvers.mjs +12 -2
  66. package/build-module/resolvers.mjs.map +2 -2
  67. package/build-module/selectors.mjs +0 -14
  68. package/build-module/selectors.mjs.map +2 -2
  69. package/build-module/sync.mjs +4 -0
  70. package/build-module/sync.mjs.map +2 -2
  71. package/build-module/types.mjs +0 -9
  72. package/build-module/types.mjs.map +4 -4
  73. package/build-module/utils/block-selection-history.mjs +6 -4
  74. package/build-module/utils/block-selection-history.mjs.map +2 -2
  75. package/build-module/utils/crdt-blocks.mjs +3 -0
  76. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  77. package/build-module/utils/crdt-user-selections.mjs +10 -2
  78. package/build-module/utils/crdt-user-selections.mjs.map +3 -3
  79. package/build-module/utils/crdt-utils.mjs +22 -0
  80. package/build-module/utils/crdt-utils.mjs.map +2 -2
  81. package/build-module/utils/crdt.mjs +32 -5
  82. package/build-module/utils/crdt.mjs.map +2 -2
  83. package/build-types/actions.d.ts +0 -11
  84. package/build-types/actions.d.ts.map +1 -1
  85. package/build-types/awareness/block-lookup.d.ts +12 -0
  86. package/build-types/awareness/block-lookup.d.ts.map +1 -1
  87. package/build-types/awareness/post-editor-awareness.d.ts +2 -5
  88. package/build-types/awareness/post-editor-awareness.d.ts.map +1 -1
  89. package/build-types/hooks/use-post-editor-awareness-state.d.ts.map +1 -1
  90. package/build-types/hooks/use-resource-permissions.d.ts.map +1 -1
  91. package/build-types/index.d.ts +0 -8
  92. package/build-types/index.d.ts.map +1 -1
  93. package/build-types/parsed-blocks-cache.d.ts +10 -0
  94. package/build-types/parsed-blocks-cache.d.ts.map +1 -0
  95. package/build-types/private-actions.d.ts +12 -0
  96. package/build-types/private-actions.d.ts.map +1 -1
  97. package/build-types/private-apis.d.ts +20 -0
  98. package/build-types/private-apis.d.ts.map +1 -1
  99. package/build-types/private-selectors.d.ts +10 -0
  100. package/build-types/private-selectors.d.ts.map +1 -1
  101. package/build-types/queried-data/selectors.d.ts +1 -1
  102. package/build-types/queried-data/selectors.d.ts.map +1 -1
  103. package/build-types/resolvers.d.ts.map +1 -1
  104. package/build-types/selectors.d.ts +0 -9
  105. package/build-types/selectors.d.ts.map +1 -1
  106. package/build-types/sync.d.ts +6 -0
  107. package/build-types/sync.d.ts.map +1 -1
  108. package/build-types/types.d.ts +3 -10
  109. package/build-types/types.d.ts.map +1 -1
  110. package/build-types/utils/block-selection-history.d.ts.map +1 -1
  111. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  112. package/build-types/utils/crdt-user-selections.d.ts +10 -1
  113. package/build-types/utils/crdt-user-selections.d.ts.map +1 -1
  114. package/build-types/utils/crdt-utils.d.ts +11 -0
  115. package/build-types/utils/crdt-utils.d.ts.map +1 -1
  116. package/build-types/utils/crdt.d.ts +5 -1
  117. package/build-types/utils/crdt.d.ts.map +1 -1
  118. package/package.json +18 -18
  119. package/src/actions.js +0 -29
  120. package/src/awareness/block-lookup.ts +34 -0
  121. package/src/awareness/post-editor-awareness.ts +32 -14
  122. package/src/awareness/test/block-lookup.ts +70 -0
  123. package/src/awareness/test/post-editor-awareness.ts +243 -0
  124. package/src/hooks/test/use-post-editor-awareness-state.ts +3 -0
  125. package/src/hooks/test/use-resource-permissions.js +57 -0
  126. package/src/hooks/use-entity-block-editor.js +2 -2
  127. package/src/hooks/use-post-editor-awareness-state.ts +1 -0
  128. package/src/hooks/use-resource-permissions.ts +5 -7
  129. package/src/index.js +0 -7
  130. package/src/parsed-blocks-cache.js +12 -0
  131. package/src/private-actions.js +34 -0
  132. package/src/{private-apis.js → private-apis.ts} +13 -3
  133. package/src/private-selectors.ts +33 -0
  134. package/src/resolvers.js +27 -5
  135. package/src/selectors.ts +0 -32
  136. package/src/sync.ts +9 -0
  137. package/src/test/resolvers.js +13 -7
  138. package/src/types.ts +16 -11
  139. package/src/utils/block-selection-history.ts +10 -7
  140. package/src/utils/crdt-blocks.ts +24 -0
  141. package/src/utils/crdt-user-selections.ts +15 -2
  142. package/src/utils/crdt-utils.ts +41 -0
  143. package/src/utils/crdt.ts +83 -10
  144. package/src/utils/test/block-selection-history.test.ts +42 -0
  145. package/src/utils/test/crdt-blocks.ts +37 -0
  146. package/src/utils/test/crdt-user-selections.ts +39 -0
  147. package/src/utils/test/crdt-utils.ts +52 -0
  148. package/src/utils/test/crdt.ts +208 -2
@@ -86,6 +86,17 @@ export declare function asRichTextOffset(offset: number): RichTextOffset;
86
86
  * @return The branded HTML string index.
87
87
  */
88
88
  export declare function asHtmlStringIndex(index: number): HtmlStringIndex;
89
+ /**
90
+ * Resolve a selection attribute key to a Y.Text value.
91
+ *
92
+ * RichText identifiers are normally top-level block attribute keys, but nested
93
+ * rich-text fields can provide a dot path such as `body.0.cells.0.content`.
94
+ *
95
+ * @param attributes The block attributes map.
96
+ * @param attributeKey The top-level attribute key or nested attribute path.
97
+ * @return The matching Y.Text, or null if the path is not a rich-text field.
98
+ */
99
+ export declare function getYTextByAttributeKey(attributes: Y.Map<unknown>, attributeKey: string): Y.Text | null;
89
100
  /**
90
101
  * Given a block ID and a Y.Doc, find the block in the document.
91
102
  *
@@ -1 +1 @@
1
- {"version":3,"file":"crdt-utils.d.ts","sourceRoot":"","sources":["../../src/utils/crdt-utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,iBAAiB,CAAC;AAGpC;;GAEG;AACH,OAAO,KAAK,EAAE,MAAM,EAAW,MAAM,eAAe,CAAC;AAIrD;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,CAAE,MAAM,EAAE,OAAO,CAAE,CAAC;AAEnD;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,QAAQ,CAAE,CAAC,SAAS,UAAU,CAAG,SAAQ,CAAC,CAAC,YAAY,CAAE,CAAC,CAAE;IAC5E,MAAM,EAAE,CAAE,CAAC,SAAS,MAAM,CAAC,EAAI,GAAG,EAAE,CAAC,KAAM,IAAI,CAAC;IAChD,OAAO,EAAE,CACR,QAAQ,EAAE,CACT,KAAK,EAAE,CAAC,CAAE,MAAM,CAAC,CAAE,EACnB,GAAG,EAAE,MAAM,CAAC,EACZ,GAAG,EAAE,QAAQ,CAAE,CAAC,CAAE,KACd,IAAI,KACL,IAAI,CAAC;IACV,GAAG,EAAE,CAAE,CAAC,SAAS,MAAM,CAAC,EAAI,GAAG,EAAE,CAAC,KAAM,OAAO,CAAC;IAChD,GAAG,EAAE,CAAE,CAAC,SAAS,MAAM,CAAC,EAAI,GAAG,EAAE,CAAC,KAAM,CAAC,CAAE,CAAC,CAAE,GAAG,SAAS,CAAC;IAC3D,GAAG,EAAE,CAAE,CAAC,SAAS,MAAM,CAAC,EAAI,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAE,CAAC,CAAE,KAAM,IAAI,CAAC;IAC5D,MAAM,EAAE,MAAM,CAAC,CAAC;CAEhB;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAE,CAAC,SAAS,UAAU,EAC/C,GAAG,EAAE,CAAC,CAAC,GAAG,EACV,GAAG,EAAE,MAAM,GACT,QAAQ,CAAE,CAAC,CAAE,CAEf;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAE,CAAC,SAAS,UAAU,EAC/C,OAAO,GAAE,OAAO,CAAE,CAAC,CAAO,GACxB,QAAQ,CAAE,CAAC,CAAE,CAEf;AAED;;;;GAIG;AACH,wBAAgB,MAAM,CAAE,CAAC,SAAS,UAAU,EAC3C,KAAK,EAAE,QAAQ,CAAE,CAAC,CAAE,GAAG,SAAS,GAC9B,KAAK,IAAI,QAAQ,CAAE,CAAC,CAAE,CAExB;AAED,OAAO,CAAC,MAAM,mBAAmB,EAAE,OAAO,MAAM,CAAC;AACjD,OAAO,CAAC,MAAM,oBAAoB,EAAE,OAAO,MAAM,CAAC;AAElD;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG;IACrC,QAAQ,CAAC,CAAE,mBAAmB,CAAE,EAAE,gBAAgB,CAAC;CACnD,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG;IACtC,QAAQ,CAAC,CAAE,oBAAoB,CAAE,EAAE,iBAAiB,CAAC;CACrD,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAE,MAAM,EAAE,MAAM,GAAI,cAAc,CAEjE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAE,KAAK,EAAE,MAAM,GAAI,eAAe,CAElE;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACvC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,CAAC,CAAC,GAAG,GACT,MAAM,GAAG,IAAI,CASf;AA2BD;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACxC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,eAAe,GACxB,cAAc,CAiBhB;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACxC,IAAI,EAAE,MAAM,EACZ,cAAc,EAAE,cAAc,GAC5B,eAAe,CA8BjB"}
1
+ {"version":3,"file":"crdt-utils.d.ts","sourceRoot":"","sources":["../../src/utils/crdt-utils.ts"],"names":[],"mappings":"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"}
@@ -3,9 +3,12 @@ import { type Block, type YBlocks } from './crdt-blocks';
3
3
  import { type Post } from '../entity-types/post';
4
4
  import type { WPSelection } from '../types';
5
5
  import { type YMapRecord, type YMapWrap } from './crdt-utils';
6
+ type ContentFromBlocksFn = (args?: {
7
+ blocks: Block[];
8
+ }) => string;
6
9
  export type PostChanges = Partial<Post> & {
7
10
  blocks?: Block[];
8
- content?: Post['content'] | string;
11
+ content?: Post['content'] | string | ContentFromBlocksFn;
9
12
  excerpt?: Post['excerpt'] | string;
10
13
  selection?: WPSelection;
11
14
  title?: Post['title'] | string;
@@ -63,4 +66,5 @@ export declare const defaultSyncConfig: SyncConfig;
63
66
  * when changes are detected.
64
67
  */
65
68
  export declare const defaultCollectionSyncConfig: SyncConfig;
69
+ export {};
66
70
  //# sourceMappingURL=crdt.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"crdt.d.ts","sourceRoot":"","sources":["../../src/utils/crdt.ts"],"names":[],"mappings":"AASA,OAAO,EACN,KAAK,OAAO,EAIZ,KAAK,UAAU,EACf,CAAC,EACD,MAAM,iBAAiB,CAAC;AAMzB,OAAO,EACN,KAAK,KAAK,EAMV,KAAK,OAAO,EACZ,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAEjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAM5C,OAAO,EAKN,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,MAAM,cAAc,CAAC;AAGtB,MAAM,MAAM,WAAW,GAAG,OAAO,CAAE,IAAI,CAAE,GAAG;IAC3C,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;IACjB,OAAO,CAAC,EAAE,IAAI,CAAE,SAAS,CAAE,GAAG,MAAM,CAAC;IACrC,OAAO,CAAC,EAAE,IAAI,CAAE,SAAS,CAAE,GAAG,MAAM,CAAC;IACrC,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,KAAK,CAAC,EAAE,IAAI,CAAE,OAAO,CAAE,GAAG,MAAM,CAAC;CACjC,CAAC;AAGF,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC9C,MAAM,EAAE,MAAM,CAAC;IAEf,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,QAAQ,CAAE,UAAU,CAAE,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC;CACd;AAED,eAAO,MAAM,sCAAsC,mBAAmB,CAAC;AAsCvE;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACxC,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,WAAW,EACpB,gBAAgB,EAAE,GAAG,CAAE,MAAM,CAAE,GAC7B,IAAI,CAqIN;AA6BD;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACxC,IAAI,EAAE,OAAO,EACb,YAAY,EAAE,IAAI,EAClB,gBAAgB,EAAE,GAAG,CAAE,MAAM,CAAE,GAC7B,WAAW,CAuJb;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,UAI/B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,EAAE,UAKzC,CAAC"}
1
+ {"version":3,"file":"crdt.d.ts","sourceRoot":"","sources":["../../src/utils/crdt.ts"],"names":[],"mappings":"AAaA,OAAO,EACN,KAAK,OAAO,EAIZ,KAAK,UAAU,EACf,CAAC,EACD,MAAM,iBAAiB,CAAC;AAMzB,OAAO,EACN,KAAK,KAAK,EAMV,KAAK,OAAO,EACZ,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAEjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAM5C,OAAO,EAKN,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,MAAM,cAAc,CAAC;AAMtB,KAAK,mBAAmB,GAAG,CAAE,IAAI,CAAC,EAAE;IAAE,MAAM,EAAE,KAAK,EAAE,CAAA;CAAE,KAAM,MAAM,CAAC;AAGpE,MAAM,MAAM,WAAW,GAAG,OAAO,CAAE,IAAI,CAAE,GAAG;IAC3C,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;IACjB,OAAO,CAAC,EAAE,IAAI,CAAE,SAAS,CAAE,GAAG,MAAM,GAAG,mBAAmB,CAAC;IAC3D,OAAO,CAAC,EAAE,IAAI,CAAE,SAAS,CAAE,GAAG,MAAM,CAAC;IACrC,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,KAAK,CAAC,EAAE,IAAI,CAAE,OAAO,CAAE,GAAG,MAAM,CAAC;CACjC,CAAC;AAGF,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC9C,MAAM,EAAE,MAAM,CAAC;IAEf,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC;IAChB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,QAAQ,CAAE,UAAU,CAAE,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC;CACd;AAED,eAAO,MAAM,sCAAsC,mBAAmB,CAAC;AAsCvE;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACxC,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,WAAW,EACpB,gBAAgB,EAAE,GAAG,CAAE,MAAM,CAAE,GAC7B,IAAI,CAuJN;AA2DD;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACxC,IAAI,EAAE,OAAO,EACb,YAAY,EAAE,IAAI,EAClB,gBAAgB,EAAE,GAAG,CAAE,MAAM,CAAE,GAC7B,WAAW,CAsKb;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,UAI/B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,EAAE,UAKzC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/core-data",
3
- "version": "7.46.0",
3
+ "version": "7.48.0",
4
4
  "description": "Access to and manipulation of core WordPress entities.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -49,22 +49,22 @@
49
49
  "build-module/index.mjs"
50
50
  ],
51
51
  "dependencies": {
52
- "@wordpress/api-fetch": "^7.46.0",
53
- "@wordpress/block-editor": "^15.19.0",
54
- "@wordpress/blocks": "^15.19.0",
55
- "@wordpress/compose": "^7.46.0",
56
- "@wordpress/data": "^10.46.0",
57
- "@wordpress/deprecated": "^4.46.0",
58
- "@wordpress/element": "^6.46.0",
59
- "@wordpress/html-entities": "^4.46.0",
60
- "@wordpress/i18n": "^6.19.0",
61
- "@wordpress/is-shallow-equal": "^5.46.0",
62
- "@wordpress/private-apis": "^1.46.0",
63
- "@wordpress/rich-text": "^7.46.0",
64
- "@wordpress/sync": "^1.46.0",
65
- "@wordpress/undo-manager": "^1.46.0",
66
- "@wordpress/url": "^4.46.0",
67
- "@wordpress/warning": "^3.46.0",
52
+ "@wordpress/api-fetch": "^7.48.0",
53
+ "@wordpress/block-editor": "^15.21.0",
54
+ "@wordpress/blocks": "^15.21.0",
55
+ "@wordpress/compose": "^8.1.0",
56
+ "@wordpress/data": "^10.48.0",
57
+ "@wordpress/deprecated": "^4.48.0",
58
+ "@wordpress/element": "^8.0.0",
59
+ "@wordpress/html-entities": "^4.48.0",
60
+ "@wordpress/i18n": "^6.21.0",
61
+ "@wordpress/is-shallow-equal": "^5.48.0",
62
+ "@wordpress/private-apis": "^1.48.0",
63
+ "@wordpress/rich-text": "^7.48.0",
64
+ "@wordpress/sync": "^1.48.0",
65
+ "@wordpress/undo-manager": "^1.48.0",
66
+ "@wordpress/url": "^4.48.0",
67
+ "@wordpress/warning": "^3.48.0",
68
68
  "change-case": "^4.1.2",
69
69
  "equivalent-key-map": "^0.2.2",
70
70
  "fast-deep-equal": "^3.1.3",
@@ -84,5 +84,5 @@
84
84
  "publishConfig": {
85
85
  "access": "public"
86
86
  },
87
- "gitHead": "51264e33b95fadff9a06b68141e674ce9cde9675"
87
+ "gitHead": "e7856693aeb4e2522d13608cd32c994e4a97cb9c"
88
88
  }
package/src/actions.js CHANGED
@@ -1114,32 +1114,3 @@ export const receiveRevisions =
1114
1114
  invalidateCache,
1115
1115
  } );
1116
1116
  };
1117
-
1118
- /**
1119
- * Returns an action object used to set the sync connection status for an entity or collection.
1120
- *
1121
- * @param {string} kind Kind of the entity.
1122
- * @param {string} name Name of the entity.
1123
- * @param {number|string|null} key The entity key, or null for collections.
1124
- * @param {Object|null} status The connection state object or null on unload.
1125
- *
1126
- * @return {Object} Action object.
1127
- */
1128
- export function setSyncConnectionStatus( kind, name, key, status ) {
1129
- if ( ! status ) {
1130
- return {
1131
- type: 'CLEAR_SYNC_CONNECTION_STATUS',
1132
- kind,
1133
- name,
1134
- key,
1135
- };
1136
- }
1137
-
1138
- return {
1139
- type: 'SET_SYNC_CONNECTION_STATUS',
1140
- kind,
1141
- name,
1142
- key,
1143
- status,
1144
- };
1145
- }
@@ -22,6 +22,40 @@ interface EditorStoreBlock {
22
22
  innerBlocks: EditorStoreBlock[];
23
23
  }
24
24
 
25
+ /**
26
+ * Find the block Y.Map that contains a nested Yjs type.
27
+ *
28
+ * Rich-text attributes are often stored directly at attributes.content, but
29
+ * blocks can also store rich text deeper inside object or array attributes.
30
+ * Walk upward until we find the block map instead of assuming a fixed parent
31
+ * depth.
32
+ *
33
+ * @param yType - The nested Yjs type to start from.
34
+ * @return The containing block Y.Map, or null if no block ancestor exists.
35
+ */
36
+ export function getContainingBlockYMap(
37
+ yType: Y.AbstractType< any >
38
+ ): Y.Map< unknown > | null {
39
+ let current: Y.AbstractType< any > | null = yType;
40
+
41
+ while ( current ) {
42
+ const parent = current.parent;
43
+
44
+ if (
45
+ parent instanceof Y.Map &&
46
+ parent.parent instanceof Y.Array &&
47
+ parent.get( 'clientId' ) !== undefined &&
48
+ parent.get( 'innerBlocks' ) instanceof Y.Array
49
+ ) {
50
+ return parent;
51
+ }
52
+
53
+ current = parent instanceof Y.AbstractType ? parent : null;
54
+ }
55
+
56
+ return null;
57
+ }
58
+
25
59
  /**
26
60
  * Given a Y.Map within a Ydoc, traverse up the Yjs block tree to compute the
27
61
  * index path from the root.
@@ -10,7 +10,11 @@ import { store as blockEditorStore } from '@wordpress/block-editor';
10
10
  * Internal dependencies
11
11
  */
12
12
  import { BaseAwarenessState, baseEqualityFieldChecks } from './base-awareness';
13
- import { getBlockPathInYdoc, resolveBlockClientIdByPath } from './block-lookup';
13
+ import {
14
+ getBlockPathInYdoc,
15
+ getContainingBlockYMap,
16
+ resolveBlockClientIdByPath,
17
+ } from './block-lookup';
14
18
  import {
15
19
  AWARENESS_CURSOR_UPDATE_THROTTLE_IN_MS,
16
20
  LOCAL_CURSOR_UPDATE_DEBOUNCE_IN_MS,
@@ -24,10 +28,14 @@ import {
24
28
  areSelectionsStatesEqual,
25
29
  getSelectionState,
26
30
  SelectionType,
31
+ SelectionDirection,
27
32
  } from '../utils/crdt-user-selections';
28
33
 
29
- import { SelectionDirection } from '../types';
30
- import type { SelectionState, WPBlockSelection } from '../types';
34
+ import type {
35
+ ResolvedSelection,
36
+ SelectionState,
37
+ WPBlockSelection,
38
+ } from '../types';
31
39
  import type { YBlocks } from '../utils/crdt-blocks';
32
40
  import type {
33
41
  DebugCollaboratorData,
@@ -239,12 +247,15 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
239
247
  * @param selection - The selection state.
240
248
  * @return The rich-text offset and block client ID, or nulls if not resolvable.
241
249
  */
242
- public convertSelectionStateToAbsolute( selection: SelectionState ): {
243
- richTextOffset: number | null;
244
- localClientId: string | null;
245
- } {
250
+ public convertSelectionStateToAbsolute(
251
+ selection: SelectionState
252
+ ): ResolvedSelection {
246
253
  if ( selection.type === SelectionType.None ) {
247
- return { richTextOffset: null, localClientId: null };
254
+ return {
255
+ richTextOffset: null,
256
+ localClientId: null,
257
+ attributeKey: null,
258
+ };
248
259
  }
249
260
 
250
261
  if ( selection.type === SelectionType.WholeBlock ) {
@@ -267,7 +278,11 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
267
278
  }
268
279
  }
269
280
 
270
- return { richTextOffset: null, localClientId };
281
+ return {
282
+ richTextOffset: null,
283
+ localClientId,
284
+ attributeKey: null,
285
+ };
271
286
  }
272
287
 
273
288
  // Text-based selections: resolve cursor position and navigate up.
@@ -282,13 +297,15 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
282
297
  );
283
298
 
284
299
  if ( ! absolutePosition ) {
285
- return { richTextOffset: null, localClientId: null };
300
+ return {
301
+ richTextOffset: null,
302
+ localClientId: null,
303
+ attributeKey: null,
304
+ };
286
305
  }
287
306
 
288
- // Navigate up: Y.Text -> attributes Y.Map -> block Y.Map
289
- const yType = absolutePosition.type.parent?.parent;
290
- const path =
291
- yType instanceof Y.Map ? getBlockPathInYdoc( yType ) : null;
307
+ const yType = getContainingBlockYMap( absolutePosition.type );
308
+ const path = yType ? getBlockPathInYdoc( yType ) : null;
292
309
  const localClientId = path ? resolveBlockClientIdByPath( path ) : null;
293
310
 
294
311
  return {
@@ -297,6 +314,7 @@ export class PostEditorAwareness extends BaseAwarenessState< PostEditorState > {
297
314
  asHtmlStringIndex( absolutePosition.index )
298
315
  ),
299
316
  localClientId,
317
+ attributeKey: cursorPos.attributeKey ?? null,
300
318
  };
301
319
  }
302
320
 
@@ -9,6 +9,7 @@ import { select } from '@wordpress/data';
9
9
  */
10
10
  import {
11
11
  getBlockPathInYdoc,
12
+ getContainingBlockYMap,
12
13
  resolveBlockClientIdByPath,
13
14
  } from '../block-lookup';
14
15
 
@@ -242,6 +243,75 @@ describe( 'getBlockPathInYdoc', () => {
242
243
  } );
243
244
  } );
244
245
 
246
+ describe( 'getContainingBlockYMap', () => {
247
+ it( 'should find the containing block for direct rich text content', () => {
248
+ const block = createTestYBlock( 'block' );
249
+ const attributes = new Y.Map< any >();
250
+ const text = new Y.Text( 'Direct text' );
251
+ attributes.set( 'content', text );
252
+ block.set( 'attributes', attributes );
253
+
254
+ const ydoc = new Y.Doc();
255
+ const rootMap = ydoc.getMap( 'test' );
256
+ const blocks = new Y.Array< Y.Map< any > >();
257
+ rootMap.set( 'blocks', blocks );
258
+ blocks.push( [ block ] );
259
+
260
+ expect( getContainingBlockYMap( text ) ).toBe( block );
261
+ } );
262
+
263
+ it( 'should find the containing block for deeply nested rich text attributes', () => {
264
+ const block = createTestYBlock( 'block' );
265
+ const attributes = new Y.Map< any >();
266
+ const cards = new Y.Array< Y.Map< any > >();
267
+ const card = new Y.Map< any >();
268
+ const meta = new Y.Map< any >();
269
+ const caption = new Y.Text( 'Nested caption' );
270
+
271
+ meta.set( 'caption', caption );
272
+ card.set( 'meta', meta );
273
+ cards.push( [ card ] );
274
+ attributes.set( 'cards', cards );
275
+ block.set( 'attributes', attributes );
276
+
277
+ const ydoc = new Y.Doc();
278
+ const rootMap = ydoc.getMap( 'test' );
279
+ const blocks = new Y.Array< Y.Map< any > >();
280
+ rootMap.set( 'blocks', blocks );
281
+ blocks.push( [ block ] );
282
+
283
+ expect( getContainingBlockYMap( caption ) ).toBe( block );
284
+ } );
285
+
286
+ it( 'should return null when no block ancestor exists', () => {
287
+ const orphanAttributes = new Y.Map< any >();
288
+ const text = new Y.Text( 'Orphan text' );
289
+ orphanAttributes.set( 'content', text );
290
+
291
+ expect( getContainingBlockYMap( text ) ).toBeNull();
292
+ } );
293
+
294
+ it( 'should skip nested attribute maps that look like blocks', () => {
295
+ const block = createTestYBlock( 'block' );
296
+ const attributes = new Y.Map< any >();
297
+ const blockLikeAttribute = new Y.Map< any >();
298
+ const text = new Y.Text( 'Nested text' );
299
+ blockLikeAttribute.set( 'clientId', 'attribute-client-id' );
300
+ blockLikeAttribute.set( 'innerBlocks', new Y.Array() );
301
+ blockLikeAttribute.set( 'content', text );
302
+ attributes.set( 'nested', blockLikeAttribute );
303
+ block.set( 'attributes', attributes );
304
+
305
+ const ydoc = new Y.Doc();
306
+ const rootMap = ydoc.getMap( 'test' );
307
+ const blocks = new Y.Array< Y.Map< any > >();
308
+ rootMap.set( 'blocks', blocks );
309
+ blocks.push( [ block ] );
310
+
311
+ expect( getContainingBlockYMap( text ) ).toBe( block );
312
+ } );
313
+ } );
314
+
245
315
  describe( 'resolveBlockClientIdByPath', () => {
246
316
  afterEach( () => {
247
317
  jest.restoreAllMocks();
@@ -69,6 +69,65 @@ interface MockBlockEditorOverrides {
69
69
  getSelectionEnd?: jest.Mock;
70
70
  }
71
71
 
72
+ type SeededRandom = {
73
+ bool: ( probability?: number ) => boolean;
74
+ int: ( maxExclusive: number ) => number;
75
+ intBetween: ( minInclusive: number, maxInclusive: number ) => number;
76
+ pick: < T >( values: readonly T[] ) => T;
77
+ };
78
+
79
+ /* eslint-disable no-bitwise */
80
+ function createSeededRandom( seed: number ): SeededRandom {
81
+ let state = seed >>> 0;
82
+
83
+ if ( state === 0 ) {
84
+ state = 0x9e3779b9;
85
+ }
86
+
87
+ function nextUint32(): number {
88
+ state += 0x6d2b79f5;
89
+ let value = state;
90
+ value = Math.imul( value ^ ( value >>> 15 ), value | 1 );
91
+ value ^= value + Math.imul( value ^ ( value >>> 7 ), value | 61 );
92
+ return ( value ^ ( value >>> 14 ) ) >>> 0;
93
+ }
94
+
95
+ function next(): number {
96
+ return nextUint32() / 0x100000000;
97
+ }
98
+
99
+ function int( maxExclusive: number ): number {
100
+ if ( maxExclusive <= 0 ) {
101
+ return 0;
102
+ }
103
+
104
+ return Math.floor( next() * maxExclusive );
105
+ }
106
+
107
+ return {
108
+ bool( probability = 0.5 ) {
109
+ return next() < probability;
110
+ },
111
+ int,
112
+ intBetween( minInclusive, maxInclusive ) {
113
+ return minInclusive + int( maxInclusive - minInclusive + 1 );
114
+ },
115
+ pick< T >( values: readonly T[] ): T {
116
+ if ( values.length === 0 ) {
117
+ throw new Error( 'Cannot pick from an empty array.' );
118
+ }
119
+
120
+ return values[ int( values.length ) ];
121
+ },
122
+ };
123
+ }
124
+ /* eslint-enable no-bitwise */
125
+
126
+ const NESTED_SELECTION_SEEDS = Array.from(
127
+ { length: 8 },
128
+ ( _value, index ) => 1401 + index
129
+ );
130
+
72
131
  /**
73
132
  * Mock the block-editor store selectors returned by `select( blockEditorStore )`.
74
133
  *
@@ -170,6 +229,66 @@ function createTestDocWithBlocks( blocks?: Y.Map< any >[] ) {
170
229
  return ydoc;
171
230
  }
172
231
 
232
+ type NestedTextTarget = {
233
+ label: string;
234
+ text: Y.Text;
235
+ };
236
+
237
+ function createNestedAttributeBlock(
238
+ clientId: string,
239
+ seed: number
240
+ ): {
241
+ block: Y.Map< any >;
242
+ targets: NestedTextTarget[];
243
+ } {
244
+ const block = new Y.Map();
245
+ block.set( 'clientId', clientId );
246
+ block.set( 'name', 'test/nested-rich-text' );
247
+
248
+ const attrs = new Y.Map();
249
+ const hero = new Y.Map();
250
+ const headline = new Y.Text( `Headline ${ seed } alpha beta` );
251
+ const caption = new Y.Text( `Caption ${ seed } gamma delta` );
252
+ hero.set( 'headline', headline );
253
+ hero.set( 'caption', caption );
254
+
255
+ const cards = new Y.Array();
256
+ const card0 = new Y.Map();
257
+ const card0Title = new Y.Text( `Card ${ seed } title one` );
258
+ const card0Body = new Y.Text( `Card ${ seed } body one two` );
259
+ const card0Meta = new Y.Map();
260
+ const card0Caption = new Y.Text( `Meta ${ seed } caption` );
261
+ card0.set( 'title', card0Title );
262
+ card0.set( 'body', card0Body );
263
+ card0Meta.set( 'caption', card0Caption );
264
+ card0.set( 'meta', card0Meta );
265
+
266
+ const card1 = new Y.Map();
267
+ const card1Title = new Y.Text( `Card ${ seed } title two` );
268
+ const card1Body = new Y.Text( `Card ${ seed } body three four` );
269
+ card1.set( 'title', card1Title );
270
+ card1.set( 'body', card1Body );
271
+ cards.push( [ card0, card1 ] );
272
+
273
+ attrs.set( 'hero', hero );
274
+ attrs.set( 'cards', cards );
275
+ block.set( 'attributes', attrs );
276
+ block.set( 'innerBlocks', new Y.Array() );
277
+
278
+ return {
279
+ block,
280
+ targets: [
281
+ { label: 'hero.headline', text: headline },
282
+ { label: 'hero.caption', text: caption },
283
+ { label: 'cards.0.title', text: card0Title },
284
+ { label: 'cards.0.body', text: card0Body },
285
+ { label: 'cards.0.meta.caption', text: card0Caption },
286
+ { label: 'cards.1.title', text: card1Title },
287
+ { label: 'cards.1.body', text: card1Body },
288
+ ],
289
+ };
290
+ }
291
+
173
292
  describe( 'PostEditorAwareness', () => {
174
293
  let doc: Y.Doc;
175
294
  let subscribeCallback: ( () => void ) | null = null;
@@ -528,6 +647,7 @@ describe( 'PostEditorAwareness', () => {
528
647
  cursorPosition: {
529
648
  relativePosition,
530
649
  absoluteOffset: 5,
650
+ attributeKey: 'content',
531
651
  },
532
652
  };
533
653
 
@@ -536,6 +656,7 @@ describe( 'PostEditorAwareness', () => {
536
656
 
537
657
  expect( result.richTextOffset ).toBe( 5 );
538
658
  expect( result.localClientId ).toBe( 'block-1' );
659
+ expect( result.attributeKey ).toBe( 'content' );
539
660
  } );
540
661
 
541
662
  test( 'should resolve WholeBlock selection to block client ID', () => {
@@ -568,6 +689,58 @@ describe( 'PostEditorAwareness', () => {
568
689
 
569
690
  expect( result.richTextOffset ).toBeNull();
570
691
  expect( result.localClientId ).toBe( 'block-1' );
692
+ expect( result.attributeKey ).toBeNull();
693
+ } );
694
+
695
+ test( 'should return null attributeKey for SelectionType.None', () => {
696
+ const awareness = new PostEditorAwareness(
697
+ doc,
698
+ 'postType',
699
+ 'post',
700
+ 123
701
+ );
702
+
703
+ const result = awareness.convertSelectionStateToAbsolute( {
704
+ type: SelectionType.None,
705
+ } );
706
+
707
+ expect( result.attributeKey ).toBeNull();
708
+ } );
709
+
710
+ test( 'should pass through nested attributeKey for a cursor selection', () => {
711
+ const awareness = new PostEditorAwareness(
712
+ doc,
713
+ 'postType',
714
+ 'post',
715
+ 123
716
+ );
717
+
718
+ const documentMap = doc.getMap( CRDT_RECORD_MAP_KEY );
719
+ const blocks = documentMap.get( 'blocks' ) as Y.Array<
720
+ Y.Map< any >
721
+ >;
722
+ const block = blocks.get( 0 );
723
+ const attrs = block.get( 'attributes' ) as Y.Map< Y.Text >;
724
+ const yText = attrs.get( 'content' );
725
+
726
+ const relativePosition = Y.createRelativePositionFromTypeIndex(
727
+ yText as Y.Text,
728
+ 3
729
+ );
730
+
731
+ const selection: SelectionCursor = {
732
+ type: SelectionType.Cursor,
733
+ cursorPosition: {
734
+ relativePosition,
735
+ absoluteOffset: 3,
736
+ attributeKey: 'body.0.cells.0.content',
737
+ },
738
+ };
739
+
740
+ const result =
741
+ awareness.convertSelectionStateToAbsolute( selection );
742
+
743
+ expect( result.attributeKey ).toBe( 'body.0.cells.0.content' );
571
744
  } );
572
745
  } );
573
746
 
@@ -1001,6 +1174,76 @@ describe( 'PostEditorAwareness', () => {
1001
1174
  } );
1002
1175
  } );
1003
1176
 
1177
+ describe( 'convertSelectionStateToAbsolute with nested rich-text attributes', () => {
1178
+ test.each( NESTED_SELECTION_SEEDS )(
1179
+ 'resolves fuzzed nested rich-text cursor (seed %i)',
1180
+ ( seed ) => {
1181
+ const rng = createSeededRandom( seed );
1182
+ const { block, targets } = createNestedAttributeBlock(
1183
+ 'yjs-nested-attrs',
1184
+ seed
1185
+ );
1186
+ const nestedDoc = createTestDocWithBlocks( [ block ] );
1187
+
1188
+ mockBlockEditorStore( {
1189
+ blocks: [
1190
+ {
1191
+ clientId: 'local-nested-attrs',
1192
+ innerBlocks: [],
1193
+ },
1194
+ ],
1195
+ } );
1196
+
1197
+ const target = rng.pick( targets );
1198
+ const initialOffset = rng.intBetween(
1199
+ 1,
1200
+ Math.max( 1, target.text.length - 1 )
1201
+ );
1202
+ const relativePosition = Y.createRelativePositionFromTypeIndex(
1203
+ target.text,
1204
+ initialOffset
1205
+ );
1206
+ let expectedOffset = initialOffset;
1207
+
1208
+ if ( rng.bool() ) {
1209
+ const prefix = `p${ seed % 97 } `;
1210
+ target.text.insert( 0, prefix );
1211
+ expectedOffset += prefix.length;
1212
+ } else {
1213
+ const deleteLength = Math.min(
1214
+ initialOffset,
1215
+ rng.intBetween( 1, 3 )
1216
+ );
1217
+ target.text.delete( 0, deleteLength );
1218
+ expectedOffset -= deleteLength;
1219
+ }
1220
+
1221
+ const awareness = new PostEditorAwareness(
1222
+ nestedDoc,
1223
+ 'postType',
1224
+ 'post',
1225
+ 123
1226
+ );
1227
+
1228
+ const selection: SelectionCursor = {
1229
+ type: SelectionType.Cursor,
1230
+ cursorPosition: {
1231
+ relativePosition,
1232
+ absoluteOffset: initialOffset,
1233
+ },
1234
+ };
1235
+
1236
+ const result =
1237
+ awareness.convertSelectionStateToAbsolute( selection );
1238
+
1239
+ expect( result.richTextOffset ).toBe( expectedOffset );
1240
+ expect( result.localClientId ).toBe( 'local-nested-attrs' );
1241
+
1242
+ nestedDoc.destroy();
1243
+ }
1244
+ );
1245
+ } );
1246
+
1004
1247
  describe( 'template mode (core/post-content handling)', () => {
1005
1248
  test( 'should resolve cursor when getBlocks returns template tree with core/post-content', () => {
1006
1249
  // Yjs doc has only the post content blocks (no template wrapper)
@@ -295,6 +295,7 @@ describe( 'use-post-editor-awareness-state hooks', () => {
295
295
  expect( result.current( mockSelection ) ).toEqual( {
296
296
  richTextOffset: null,
297
297
  localClientId: null,
298
+ attributeKey: null,
298
299
  } );
299
300
  } );
300
301
 
@@ -309,6 +310,7 @@ describe( 'use-post-editor-awareness-state hooks', () => {
309
310
  mockAwareness.convertSelectionStateToAbsolute.mockReturnValue( {
310
311
  richTextOffset: 10,
311
312
  localClientId: 'block-1',
313
+ attributeKey: 'content',
312
314
  } );
313
315
 
314
316
  const { result } = renderHook( () =>
@@ -323,6 +325,7 @@ describe( 'use-post-editor-awareness-state hooks', () => {
323
325
  expect( position ).toEqual( {
324
326
  richTextOffset: 10,
325
327
  localClientId: 'block-1',
328
+ attributeKey: 'content',
326
329
  } );
327
330
  } );
328
331
  } );