@wordpress/core-data 7.37.1-next.v.0 → 7.38.1-next.v.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 (92) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/actions.cjs +14 -2
  3. package/build/actions.cjs.map +2 -2
  4. package/build/awareness/config.cjs +34 -0
  5. package/build/awareness/config.cjs.map +7 -0
  6. package/build/awareness/post-editor-awareness.cjs +144 -0
  7. package/build/awareness/post-editor-awareness.cjs.map +7 -0
  8. package/build/awareness/types.cjs +19 -0
  9. package/build/awareness/types.cjs.map +7 -0
  10. package/build/awareness/utils.cjs +116 -0
  11. package/build/awareness/utils.cjs.map +7 -0
  12. package/build/entities.cjs +48 -45
  13. package/build/entities.cjs.map +2 -2
  14. package/build/private-selectors.cjs +2 -4
  15. package/build/private-selectors.cjs.map +2 -2
  16. package/build/resolvers.cjs +14 -4
  17. package/build/resolvers.cjs.map +2 -2
  18. package/build/types.cjs.map +1 -1
  19. package/build/utils/crdt-blocks.cjs +63 -41
  20. package/build/utils/crdt-blocks.cjs.map +2 -2
  21. package/build/utils/crdt-user-selections.cjs +171 -0
  22. package/build/utils/crdt-user-selections.cjs.map +7 -0
  23. package/build/utils/crdt-utils.cjs +44 -0
  24. package/build/utils/crdt-utils.cjs.map +7 -0
  25. package/build/utils/crdt.cjs +33 -37
  26. package/build/utils/crdt.cjs.map +2 -2
  27. package/build-module/actions.mjs +14 -2
  28. package/build-module/actions.mjs.map +2 -2
  29. package/build-module/awareness/config.mjs +8 -0
  30. package/build-module/awareness/config.mjs.map +7 -0
  31. package/build-module/awareness/post-editor-awareness.mjs +125 -0
  32. package/build-module/awareness/post-editor-awareness.mjs.map +7 -0
  33. package/build-module/awareness/types.mjs +1 -0
  34. package/build-module/awareness/types.mjs.map +7 -0
  35. package/build-module/awareness/utils.mjs +90 -0
  36. package/build-module/awareness/utils.mjs.map +7 -0
  37. package/build-module/entities.mjs +48 -47
  38. package/build-module/entities.mjs.map +2 -2
  39. package/build-module/private-selectors.mjs +2 -4
  40. package/build-module/private-selectors.mjs.map +2 -2
  41. package/build-module/resolvers.mjs +14 -4
  42. package/build-module/resolvers.mjs.map +2 -2
  43. package/build-module/utils/crdt-blocks.mjs +64 -42
  44. package/build-module/utils/crdt-blocks.mjs.map +2 -2
  45. package/build-module/utils/crdt-user-selections.mjs +144 -0
  46. package/build-module/utils/crdt-user-selections.mjs.map +7 -0
  47. package/build-module/utils/crdt-utils.mjs +17 -0
  48. package/build-module/utils/crdt-utils.mjs.map +7 -0
  49. package/build-module/utils/crdt.mjs +37 -37
  50. package/build-module/utils/crdt.mjs.map +2 -2
  51. package/build-types/actions.d.ts.map +1 -1
  52. package/build-types/awareness/config.d.ts +9 -0
  53. package/build-types/awareness/config.d.ts.map +1 -0
  54. package/build-types/awareness/post-editor-awareness.d.ts +39 -0
  55. package/build-types/awareness/post-editor-awareness.d.ts.map +1 -0
  56. package/build-types/awareness/types.d.ts +32 -0
  57. package/build-types/awareness/types.d.ts.map +1 -0
  58. package/build-types/awareness/utils.d.ts +22 -0
  59. package/build-types/awareness/utils.d.ts.map +1 -0
  60. package/build-types/entities.d.ts.map +1 -1
  61. package/build-types/index.d.ts.map +1 -1
  62. package/build-types/private-selectors.d.ts.map +1 -1
  63. package/build-types/resolvers.d.ts.map +1 -1
  64. package/build-types/types.d.ts +0 -5
  65. package/build-types/types.d.ts.map +1 -1
  66. package/build-types/utils/crdt-blocks.d.ts +14 -5
  67. package/build-types/utils/crdt-blocks.d.ts.map +1 -1
  68. package/build-types/utils/crdt-user-selections.d.ts +66 -0
  69. package/build-types/utils/crdt-user-selections.d.ts.map +1 -0
  70. package/build-types/utils/crdt-utils.d.ts +53 -0
  71. package/build-types/utils/crdt-utils.d.ts.map +1 -0
  72. package/build-types/utils/crdt.d.ts +19 -1
  73. package/build-types/utils/crdt.d.ts.map +1 -1
  74. package/package.json +19 -18
  75. package/src/actions.js +13 -2
  76. package/src/awareness/config.ts +9 -0
  77. package/src/awareness/post-editor-awareness.ts +187 -0
  78. package/src/awareness/types.ts +38 -0
  79. package/src/awareness/utils.ts +159 -0
  80. package/src/entities.js +52 -54
  81. package/src/private-selectors.ts +3 -4
  82. package/src/resolvers.js +17 -8
  83. package/src/test/entities.js +11 -9
  84. package/src/test/entity-provider.js +2 -0
  85. package/src/test/resolvers.js +3 -45
  86. package/src/types.ts +0 -6
  87. package/src/utils/crdt-blocks.ts +101 -99
  88. package/src/utils/crdt-user-selections.ts +332 -0
  89. package/src/utils/crdt-utils.ts +77 -0
  90. package/src/utils/crdt.ts +76 -57
  91. package/src/utils/test/crdt-blocks.ts +11 -4
  92. package/src/utils/test/crdt.ts +28 -16
@@ -2,16 +2,34 @@ import { type CRDTDoc, type ObjectData } from '@wordpress/sync';
2
2
  /**
3
3
  * Internal dependencies
4
4
  */
5
- import { type Block } from './crdt-blocks';
5
+ import { type Block, type YBlocks } from './crdt-blocks';
6
6
  import { type Post } from '../entity-types/post';
7
7
  import { type Type } from '../entity-types';
8
8
  import type { WPSelection } from '../types';
9
+ import { type YMapRecord, type YMapWrap } from './crdt-utils';
9
10
  export type PostChanges = Partial<Post> & {
10
11
  blocks?: Block[];
11
12
  excerpt?: Post['excerpt'] | string;
12
13
  selection?: WPSelection;
13
14
  title?: Post['title'] | string;
14
15
  };
16
+ export interface YPostRecord extends YMapRecord {
17
+ author: number;
18
+ blocks: YBlocks;
19
+ comment_status: string;
20
+ date: string | null;
21
+ excerpt: string;
22
+ featured_media: number;
23
+ format: string;
24
+ meta: YMapWrap<YMapRecord>;
25
+ ping_status: string;
26
+ slug: string;
27
+ status: string;
28
+ sticky: boolean;
29
+ tags: number[];
30
+ template: string;
31
+ title: string;
32
+ }
15
33
  /**
16
34
  * Given a set of local changes to a generic entity record, apply those changes
17
35
  * to the local Y.Doc.
@@ -1 +1 @@
1
- {"version":3,"file":"crdt.d.ts","sourceRoot":"","sources":["../../src/utils/crdt.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,UAAU,EAAK,MAAM,iBAAiB,CAAC;AAEnE;;GAEG;AACH,OAAO,EAEN,KAAK,KAAK,EAGV,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAM5C,OAAO,KAAK,EAAoB,WAAW,EAAE,MAAM,UAAU,CAAC;AAE9D,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,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,KAAK,CAAC,EAAE,IAAI,CAAE,OAAO,CAAE,GAAG,MAAM,CAAC;CACjC,CAAC;AA6BF;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAC3C,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,UAAU,GACjB,IAAI,CAuBN;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACxC,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,IAAI,GACb,IAAI,CAoHN;AAED,wBAAgB,4BAA4B,CAAE,OAAO,EAAE,OAAO,GAAI,UAAU,CAE3E;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACxC,IAAI,EAAE,OAAO,EACb,YAAY,EAAE,IAAI,EAClB,SAAS,EAAE,IAAI,GACb,WAAW,CAwHb"}
1
+ {"version":3,"file":"crdt.d.ts","sourceRoot":"","sources":["../../src/utils/crdt.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,UAAU,EAAK,MAAM,iBAAiB,CAAC;AAEnE;;GAEG;AACH,OAAO,EAEN,KAAK,KAAK,EAEV,KAAK,OAAO,EACZ,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAM5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAIN,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,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;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,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,MAAM,CAAC;CACd;AA0BD;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAC3C,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,UAAU,GACjB,IAAI,CAkBN;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACxC,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,IAAI,GACb,IAAI,CA8GN;AAED,wBAAgB,4BAA4B,CAAE,OAAO,EAAE,OAAO,GAAI,UAAU,CAE3E;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACxC,IAAI,EAAE,OAAO,EACb,YAAY,EAAE,IAAI,EAClB,SAAS,EAAE,IAAI,GACb,WAAW,CAwHb"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/core-data",
3
- "version": "7.37.1-next.v.0+500f87dd8",
3
+ "version": "7.38.1-next.v.0+b8934fcf9",
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.37.1-next.v.0+500f87dd8",
53
- "@wordpress/block-editor": "^15.10.1-next.v.0+500f87dd8",
54
- "@wordpress/blocks": "^15.10.1-next.v.0+500f87dd8",
55
- "@wordpress/compose": "^7.37.1-next.v.0+500f87dd8",
56
- "@wordpress/data": "^10.37.1-next.v.0+500f87dd8",
57
- "@wordpress/deprecated": "^4.37.1-next.v.0+500f87dd8",
58
- "@wordpress/element": "^6.37.1-next.v.0+500f87dd8",
59
- "@wordpress/html-entities": "^4.37.1-next.v.0+500f87dd8",
60
- "@wordpress/i18n": "^6.10.1-next.v.0+500f87dd8",
61
- "@wordpress/is-shallow-equal": "^5.37.1-next.v.0+500f87dd8",
62
- "@wordpress/private-apis": "^1.37.1-next.v.0+500f87dd8",
63
- "@wordpress/rich-text": "^7.37.1-next.v.0+500f87dd8",
64
- "@wordpress/sync": "^1.37.1-next.v.0+500f87dd8",
65
- "@wordpress/undo-manager": "^1.37.1-next.v.0+500f87dd8",
66
- "@wordpress/url": "^4.37.1-next.v.0+500f87dd8",
67
- "@wordpress/warning": "^3.37.1-next.v.0+500f87dd8",
52
+ "@wordpress/api-fetch": "^7.38.1-next.v.0+b8934fcf9",
53
+ "@wordpress/block-editor": "^15.11.1-next.v.0+b8934fcf9",
54
+ "@wordpress/blocks": "^15.11.1-next.v.0+b8934fcf9",
55
+ "@wordpress/compose": "^7.38.1-next.v.0+b8934fcf9",
56
+ "@wordpress/data": "^10.38.1-next.v.0+b8934fcf9",
57
+ "@wordpress/deprecated": "^4.38.1-next.v.0+b8934fcf9",
58
+ "@wordpress/element": "^6.38.1-next.v.0+b8934fcf9",
59
+ "@wordpress/html-entities": "^4.38.1-next.v.0+b8934fcf9",
60
+ "@wordpress/i18n": "^6.11.1-next.v.0+b8934fcf9",
61
+ "@wordpress/is-shallow-equal": "^5.38.1-next.v.0+b8934fcf9",
62
+ "@wordpress/private-apis": "^1.38.1-next.v.0+b8934fcf9",
63
+ "@wordpress/rich-text": "^7.38.1-next.v.0+b8934fcf9",
64
+ "@wordpress/sync": "^1.38.1-next.v.0+b8934fcf9",
65
+ "@wordpress/undo-manager": "^1.38.1-next.v.0+b8934fcf9",
66
+ "@wordpress/url": "^4.38.1-next.v.0+b8934fcf9",
67
+ "@wordpress/warning": "^3.38.1-next.v.0+b8934fcf9",
68
68
  "change-case": "^4.1.2",
69
69
  "equivalent-key-map": "^0.2.2",
70
70
  "fast-deep-equal": "^3.1.3",
@@ -72,6 +72,7 @@
72
72
  "uuid": "^9.0.1"
73
73
  },
74
74
  "devDependencies": {
75
+ "@types/node": "^20.17.10",
75
76
  "deep-freeze": "0.0.1"
76
77
  },
77
78
  "peerDependencies": {
@@ -81,5 +82,5 @@
81
82
  "publishConfig": {
82
83
  "access": "public"
83
84
  },
84
- "gitHead": "ca0db0ee8ac2116cd307650136027d26d0cdd9bd"
85
+ "gitHead": "17529010285784fd2e735348a28391c79de87c88"
85
86
  }
package/src/actions.js CHANGED
@@ -411,8 +411,8 @@ export const editEntityRecord =
411
411
  return acc;
412
412
  }, {} ),
413
413
  };
414
- if ( window.__experimentalEnableSync && entityConfig.syncConfig ) {
415
- if ( globalThis.IS_GUTENBERG_PLUGIN ) {
414
+ if ( globalThis.IS_GUTENBERG_PLUGIN ) {
415
+ if ( entityConfig.syncConfig ) {
416
416
  const objectType = `${ kind }/${ name }`;
417
417
  const objectId = recordId;
418
418
 
@@ -714,6 +714,17 @@ export const saveEntityRecord =
714
714
  true,
715
715
  edits
716
716
  );
717
+ if ( globalThis.IS_GUTENBERG_PLUGIN ) {
718
+ if ( entityConfig.syncConfig ) {
719
+ getSyncManager()?.update(
720
+ `${ kind }/${ name }`,
721
+ recordId,
722
+ updatedRecord,
723
+ LOCAL_EDITOR_ORIGIN,
724
+ true // isSave
725
+ );
726
+ }
727
+ }
717
728
  }
718
729
  } catch ( _error ) {
719
730
  hasError = true;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Delay in milliseconds before throttling the cursor position updates.
3
+ */
4
+ export const AWARENESS_CURSOR_UPDATE_THROTTLE_IN_MS = 100;
5
+
6
+ /**
7
+ * Delay in milliseconds before updating the cursor position.
8
+ */
9
+ export const LOCAL_CURSOR_UPDATE_DEBOUNCE_IN_MS = 5;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { dispatch, select, subscribe } from '@wordpress/data';
5
+ import { AwarenessState, type Y } from '@wordpress/sync';
6
+ // @ts-ignore No exported types for block editor store selectors.
7
+ import { store as blockEditorStore } from '@wordpress/block-editor';
8
+
9
+ /**
10
+ * Internal dependencies
11
+ */
12
+ import {
13
+ AWARENESS_CURSOR_UPDATE_THROTTLE_IN_MS,
14
+ LOCAL_CURSOR_UPDATE_DEBOUNCE_IN_MS,
15
+ } from './config';
16
+ import { STORE_NAME as coreStore } from '../name';
17
+ import { generateUserInfo, areUserInfosEqual } from './utils';
18
+ import {
19
+ areSelectionsStatesEqual,
20
+ getSelectionState,
21
+ } from '../utils/crdt-user-selections';
22
+
23
+ import type { WPBlockSelection } from '../types';
24
+ import type { EditorState, PostEditorState } from './types';
25
+
26
+ export class PostEditorAwareness extends AwarenessState< PostEditorState > {
27
+ protected equalityFieldChecks = {
28
+ editorState: this.areEditorStatesEqual,
29
+ userInfo: areUserInfosEqual,
30
+ };
31
+
32
+ public constructor(
33
+ doc: Y.Doc,
34
+ private kind: string,
35
+ private name: string,
36
+ private postId: number
37
+ ) {
38
+ super( doc );
39
+ }
40
+
41
+ public setUp(): void {
42
+ super.setUp();
43
+
44
+ this.setCurrentUserInfo();
45
+ this.subscribeToUserSelectionChanges();
46
+ }
47
+
48
+ /**
49
+ * Set the current user info in the local state.
50
+ */
51
+ private setCurrentUserInfo(): void {
52
+ const states = this.getStates();
53
+ const otherUserColors = Array.from( states.entries() )
54
+ .filter(
55
+ ( [ clientId, state ] ) =>
56
+ state.userInfo && clientId !== this.clientID
57
+ )
58
+ .map( ( [ , state ] ) => state.userInfo.color )
59
+ .filter( Boolean );
60
+
61
+ // Get current user info and set it in local state.
62
+ const currentUser = select( coreStore ).getCurrentUser();
63
+ const userInfo = generateUserInfo( currentUser, otherUserColors );
64
+ this.setLocalStateField( 'userInfo', userInfo );
65
+ }
66
+
67
+ /**
68
+ * Subscribe to user selection changes and update the selection state.
69
+ */
70
+ private subscribeToUserSelectionChanges(): void {
71
+ const {
72
+ getSelectionStart,
73
+ getSelectionEnd,
74
+ getSelectedBlocksInitialCaretPosition,
75
+ } = select( blockEditorStore );
76
+
77
+ // Keep track of the current selection in the outer scope so we can compare
78
+ // in the subscription.
79
+ let selectionStart = getSelectionStart();
80
+ let selectionEnd = getSelectionEnd();
81
+ let localCursorTimeout: NodeJS.Timeout | null = null;
82
+
83
+ subscribe( () => {
84
+ const newSelectionStart = getSelectionStart();
85
+ const newSelectionEnd = getSelectionEnd();
86
+
87
+ if (
88
+ newSelectionStart === selectionStart &&
89
+ newSelectionEnd === selectionEnd
90
+ ) {
91
+ return;
92
+ }
93
+
94
+ selectionStart = newSelectionStart;
95
+ selectionEnd = newSelectionEnd;
96
+
97
+ // Typically selection position is only persisted after typing in a block, which
98
+ // can cause selection position to be reset by other users making block updates.
99
+ // Ensure we update the controlled selection right away, persisting our cursor position locally.
100
+ const initialPosition = getSelectedBlocksInitialCaretPosition();
101
+ void this.updateSelectionInEntityRecord(
102
+ selectionStart,
103
+ selectionEnd,
104
+ initialPosition
105
+ );
106
+
107
+ // We receive two selection changes in quick succession
108
+ // from local selection events:
109
+ // { clientId: "123...", attributeKey: "content", offset: undefined }
110
+ // { clientId: "123...", attributeKey: "content", offset: 554 }
111
+ // Add a short debounce to avoid sending the first selection change.
112
+ if ( localCursorTimeout ) {
113
+ clearTimeout( localCursorTimeout );
114
+ }
115
+
116
+ localCursorTimeout = setTimeout( () => {
117
+ const selectionState = getSelectionState(
118
+ selectionStart,
119
+ selectionEnd,
120
+ this.doc
121
+ );
122
+
123
+ this.setThrottledLocalStateField(
124
+ 'editorState',
125
+ { selection: selectionState },
126
+ AWARENESS_CURSOR_UPDATE_THROTTLE_IN_MS
127
+ );
128
+ }, LOCAL_CURSOR_UPDATE_DEBOUNCE_IN_MS );
129
+ } );
130
+ }
131
+
132
+ /**
133
+ * Update the entity record with the current user's selection.
134
+ *
135
+ * @param selectionStart - The start position of the selection.
136
+ * @param selectionEnd - The end position of the selection.
137
+ * @param initialPosition - The initial position of the selection.
138
+ */
139
+ private async updateSelectionInEntityRecord(
140
+ selectionStart: WPBlockSelection,
141
+ selectionEnd: WPBlockSelection,
142
+ initialPosition: number | null
143
+ ): Promise< void > {
144
+ // Send an entityRecord `selection` update if we have a selection.
145
+ //
146
+ // Normally WordPress updates the `selection` property of the post when changes are made to blocks.
147
+ // In a multi-user setup, block changes can occur from other users. When an entity is updated from another
148
+ // user's changes, useBlockSync() in Gutenberg will reset the user's selection to the last saved selection.
149
+ //
150
+ // Manually adding an edit for each movement ensures that other user's changes to the document will
151
+ // not cause the local user's selection to reset to the last local change location.
152
+ const edits = {
153
+ selection: { selectionStart, selectionEnd, initialPosition },
154
+ };
155
+
156
+ const options = {
157
+ undoIgnore: true,
158
+ };
159
+
160
+ // @ts-ignore Types are not provided when using store name instead of store instance.
161
+ dispatch( coreStore ).editEntityRecord(
162
+ this.kind,
163
+ this.name,
164
+ this.postId,
165
+ edits,
166
+ options
167
+ );
168
+ }
169
+
170
+ /**
171
+ * Check if two editor states are equal.
172
+ *
173
+ * @param state1 - The first editor state.
174
+ * @param state2 - The second editor state.
175
+ * @return True if the editor states are equal, false otherwise.
176
+ */
177
+ private areEditorStatesEqual(
178
+ state1?: EditorState,
179
+ state2?: EditorState
180
+ ): boolean {
181
+ if ( ! state1 || ! state2 ) {
182
+ return state1 === state2;
183
+ }
184
+
185
+ return areSelectionsStatesEqual( state1.selection, state2.selection );
186
+ }
187
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import type { SelectionState } from '../utils/crdt-user-selections';
5
+ import type { User } from '../entity-types';
6
+
7
+ export type UserInfo = Pick<
8
+ User< 'view' >,
9
+ 'id' | 'name' | 'slug' | 'avatar_urls'
10
+ > & {
11
+ browserType: string;
12
+ color: string;
13
+ enteredAt: number;
14
+ };
15
+
16
+ /**
17
+ * This base state represents the presence of the user. We expect it to be
18
+ * extended to include additional state describing the user's current activity.
19
+ * This state must be serializable and compact.
20
+ */
21
+ export interface BaseState {
22
+ userInfo: UserInfo;
23
+ }
24
+
25
+ /**
26
+ * The editor state includes information about the user's current selection.
27
+ */
28
+ export interface EditorState {
29
+ selection: SelectionState;
30
+ }
31
+
32
+ /**
33
+ * The post editor state extends the base state with information used to render
34
+ * presence indicators in the post editor.
35
+ */
36
+ export interface PostEditorState extends BaseState {
37
+ editorState?: EditorState;
38
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import type { User } from '../entity-types';
5
+ import type { UserInfo } from './types';
6
+
7
+ /**
8
+ * The color palette for the user highlight.
9
+ */
10
+ const COLOR_PALETTE = [
11
+ '#3858E9', // blueberry
12
+ '#B42AED', // purple
13
+ '#E33184', // pink
14
+ '#F3661D', // orange
15
+ '#ECBD3A', // yellow
16
+ '#97FE17', // green
17
+ '#00FDD9', // teal
18
+ '#37C5F0', // cyan
19
+ ];
20
+
21
+ /**
22
+ * Generate a random integer between min and max, inclusive.
23
+ *
24
+ * @param min - The minimum value.
25
+ * @param max - The maximum value.
26
+ * @return A random integer between min and max.
27
+ */
28
+ function generateRandomInt( min: number, max: number ): number {
29
+ return Math.floor( Math.random() * ( max - min + 1 ) ) + min;
30
+ }
31
+
32
+ /**
33
+ * Get a unique user color from the palette, or generate a variation if none are available.
34
+ * If the previously used color is available from localStorage, use it.
35
+ *
36
+ * @param existingColors - Colors that are already in use.
37
+ * @return The new user color, in hex format.
38
+ */
39
+ function getNewUserColor( existingColors: string[] ): string {
40
+ const availableColors = COLOR_PALETTE.filter(
41
+ ( color ) => ! existingColors.includes( color )
42
+ );
43
+
44
+ let hexColor: string;
45
+
46
+ if ( availableColors.length > 0 ) {
47
+ const randomIndex = generateRandomInt( 0, availableColors.length - 1 );
48
+ hexColor = availableColors[ randomIndex ];
49
+ } else {
50
+ // All colors are used, generate a variation of a random palette color
51
+ const randomIndex = generateRandomInt( 0, COLOR_PALETTE.length - 1 );
52
+ const baseColor = COLOR_PALETTE[ randomIndex ];
53
+ hexColor = generateColorVariation( baseColor );
54
+ }
55
+
56
+ return hexColor;
57
+ }
58
+
59
+ /**
60
+ * Generate a variation of a hex color by adjusting its lightness.
61
+ *
62
+ * @param hexColor - The base hex color (e.g., '#3858E9').
63
+ * @return A varied hex color.
64
+ */
65
+ function generateColorVariation( hexColor: string ): string {
66
+ // Parse hex to RGB
67
+ const r = parseInt( hexColor.slice( 1, 3 ), 16 );
68
+ const g = parseInt( hexColor.slice( 3, 5 ), 16 );
69
+ const b = parseInt( hexColor.slice( 5, 7 ), 16 );
70
+
71
+ // Apply a random lightness shift (-30 to +30)
72
+ const shift = generateRandomInt( -30, 30 );
73
+ const newR = Math.min( 255, Math.max( 0, r + shift ) );
74
+ const newG = Math.min( 255, Math.max( 0, g + shift ) );
75
+ const newB = Math.min( 255, Math.max( 0, b + shift ) );
76
+
77
+ // Convert back to hex
78
+ const toHex = ( n: number ) =>
79
+ n.toString( 16 ).padStart( 2, '0' ).toUpperCase();
80
+ return `#${ toHex( newR ) }${ toHex( newG ) }${ toHex( newB ) }`;
81
+ }
82
+
83
+ /**
84
+ * Get the browser name from the user agent.
85
+ * @return The browser name.
86
+ */
87
+ function getBrowserName(): string {
88
+ const userAgent = window.navigator.userAgent;
89
+ let browserName = 'Unknown';
90
+
91
+ if ( userAgent.includes( 'Firefox' ) ) {
92
+ browserName = 'Firefox';
93
+ } else if ( userAgent.includes( 'Edg' ) ) {
94
+ browserName = 'Microsoft Edge';
95
+ } else if (
96
+ userAgent.includes( 'Chrome' ) &&
97
+ ! userAgent.includes( 'Edg' )
98
+ ) {
99
+ browserName = 'Chrome';
100
+ } else if (
101
+ userAgent.includes( 'Safari' ) &&
102
+ ! userAgent.includes( 'Chrome' )
103
+ ) {
104
+ browserName = 'Safari';
105
+ } else if (
106
+ userAgent.includes( 'MSIE' ) ||
107
+ userAgent.includes( 'Trident' )
108
+ ) {
109
+ browserName = 'Internet Explorer';
110
+ } else if ( userAgent.includes( 'Opera' ) || userAgent.includes( 'OPR' ) ) {
111
+ browserName = 'Opera';
112
+ }
113
+
114
+ return browserName;
115
+ }
116
+
117
+ /**
118
+ * Check if two user infos are equal.
119
+ *
120
+ * @param userInfo1 - The first user info.
121
+ * @param userInfo2 - The second user info.
122
+ * @return True if the user infos are equal, false otherwise.
123
+ */
124
+ export function areUserInfosEqual(
125
+ userInfo1?: UserInfo,
126
+ userInfo2?: UserInfo
127
+ ): boolean {
128
+ if ( ! userInfo1 || ! userInfo2 ) {
129
+ return userInfo1 === userInfo2;
130
+ }
131
+
132
+ if ( Object.keys( userInfo1 ).length !== Object.keys( userInfo2 ).length ) {
133
+ return false;
134
+ }
135
+
136
+ return Object.entries( userInfo1 ).every( ( [ key, value ] ) => {
137
+ // Update this function with any non-primitive fields added to UserInfo.
138
+ return value === userInfo2[ key as keyof UserInfo ];
139
+ } );
140
+ }
141
+
142
+ /**
143
+ * Generate a user info object from a current user and a list of existing colors.
144
+ *
145
+ * @param currentUser - The current user.
146
+ * @param existingColors - The existing colors.
147
+ * @return The user info object.
148
+ */
149
+ export function generateUserInfo(
150
+ currentUser: User< 'view' >,
151
+ existingColors: string[]
152
+ ): UserInfo {
153
+ return {
154
+ ...currentUser,
155
+ browserType: getBrowserName(),
156
+ color: getNewUserColor( existingColors ),
157
+ enteredAt: Date.now(),
158
+ };
159
+ }
package/src/entities.js CHANGED
@@ -13,11 +13,10 @@ import { __ } from '@wordpress/i18n';
13
13
  /**
14
14
  * Internal dependencies
15
15
  */
16
+ import { PostEditorAwareness } from './awareness/post-editor-awareness';
16
17
  import { getSyncManager } from './sync';
17
18
  import {
18
19
  applyPostChangesToCRDTDoc,
19
- defaultApplyChangesToCRDTDoc,
20
- defaultGetChangesFromCRDTDoc,
21
20
  getPostChangesFromCRDTDoc,
22
21
  } from './utils/crdt';
23
22
 
@@ -280,8 +279,8 @@ export const prePersistPostType = (
280
279
  }
281
280
 
282
281
  // Add meta for persisted CRDT document.
283
- if ( persistedRecord && window.__experimentalEnableSync ) {
284
- if ( globalThis.IS_GUTENBERG_PLUGIN ) {
282
+ if ( globalThis.IS_GUTENBERG_PLUGIN ) {
283
+ if ( persistedRecord ) {
285
284
  const objectType = `postType/${ name }`;
286
285
  const objectId = persistedRecord.id;
287
286
  const meta = getSyncManager()?.createMeta( objectType, objectId );
@@ -344,48 +343,59 @@ async function loadPostTypeEntities() {
344
343
  : DEFAULT_ENTITY_KEY,
345
344
  };
346
345
 
347
- if ( window.__experimentalEnableSync ) {
348
- if ( globalThis.IS_GUTENBERG_PLUGIN ) {
346
+ if ( globalThis.IS_GUTENBERG_PLUGIN ) {
347
+ /**
348
+ * @type {import('@wordpress/sync').SyncConfig}
349
+ */
350
+ entity.syncConfig = {
351
+ /**
352
+ * Apply changes from the local editor to the local CRDT document so
353
+ * that those changes can be synced to other peers (via the provider).
354
+ *
355
+ * @param {import('@wordpress/sync').CRDTDoc} crdtDoc
356
+ * @param {Partial< import('@wordpress/sync').ObjectData >} changes
357
+ * @return {void}
358
+ */
359
+ applyChangesToCRDTDoc: ( crdtDoc, changes ) =>
360
+ applyPostChangesToCRDTDoc( crdtDoc, changes, postType ),
361
+
349
362
  /**
350
- * @type {import('@wordpress/sync').SyncConfig}
363
+ * Create the awareness instance for the entity's CRDT document.
364
+ *
365
+ * @param {import('@wordpress/sync').CRDTDoc} ydoc
366
+ * @param {import('@wordpress/sync').ObjectID} objectId
367
+ * @return {import('@wordpress/sync').AwarenessState} AwarenessState instance
351
368
  */
352
- entity.syncConfig = {
353
- /**
354
- * Apply changes from the local editor to the local CRDT document so
355
- * that those changes can be synced to other peers (via the provider).
356
- *
357
- * @param {import('@wordpress/sync').CRDTDoc} crdtDoc
358
- * @param {Partial< import('@wordpress/sync').ObjectData >} changes
359
- * @return {void}
360
- */
361
- applyChangesToCRDTDoc: ( crdtDoc, changes ) =>
362
- applyPostChangesToCRDTDoc( crdtDoc, changes, postType ),
369
+ createAwareness: ( ydoc, objectId ) => {
370
+ const kind = 'postType';
371
+ const id = parseInt( objectId, 10 );
372
+ return new PostEditorAwareness( ydoc, kind, name, id );
373
+ },
363
374
 
364
- /**
365
- * Extract changes from a CRDT document that can be used to update the
366
- * local editor state.
367
- *
368
- * @param {import('@wordpress/sync').CRDTDoc} crdtDoc
369
- * @param {import('@wordpress/sync').ObjectData} editedRecord
370
- * @return {Partial< import('@wordpress/sync').ObjectData >} Changes to record
371
- */
372
- getChangesFromCRDTDoc: ( crdtDoc, editedRecord ) =>
373
- getPostChangesFromCRDTDoc(
374
- crdtDoc,
375
- editedRecord,
376
- postType
377
- ),
375
+ /**
376
+ * Extract changes from a CRDT document that can be used to update the
377
+ * local editor state.
378
+ *
379
+ * @param {import('@wordpress/sync').CRDTDoc} crdtDoc
380
+ * @param {import('@wordpress/sync').ObjectData} editedRecord
381
+ * @return {Partial< import('@wordpress/sync').ObjectData >} Changes to record
382
+ */
383
+ getChangesFromCRDTDoc: ( crdtDoc, editedRecord ) =>
384
+ getPostChangesFromCRDTDoc(
385
+ crdtDoc,
386
+ editedRecord,
387
+ postType
388
+ ),
378
389
 
379
- /**
380
- * Sync features supported by the entity.
381
- *
382
- * @type {Record< string, boolean >}
383
- */
384
- supports: {
385
- crdtPersistence: true,
386
- },
387
- };
388
- }
390
+ /**
391
+ * Sync features supported by the entity.
392
+ *
393
+ * @type {Record< string, boolean >}
394
+ */
395
+ supports: {
396
+ crdtPersistence: true,
397
+ },
398
+ };
389
399
  }
390
400
 
391
401
  return entity;
@@ -430,18 +440,6 @@ async function loadSiteEntity() {
430
440
  meta: {},
431
441
  };
432
442
 
433
- if ( window.__experimentalEnableSync ) {
434
- if ( globalThis.IS_GUTENBERG_PLUGIN ) {
435
- /**
436
- * @type {import('@wordpress/sync').SyncConfig}
437
- */
438
- entity.syncConfig = {
439
- applyChangesToCRDTDoc: defaultApplyChangesToCRDTDoc,
440
- getChangesFromCRDTDoc: defaultGetChangesFromCRDTDoc,
441
- };
442
- }
443
- }
444
-
445
443
  const site = await apiFetch( {
446
444
  path: entity.baseURL,
447
445
  method: 'OPTIONS',
@@ -35,10 +35,9 @@ type EntityRecordKey = string | number;
35
35
  * @return The undo manager.
36
36
  */
37
37
  export function getUndoManager( state: State ) {
38
- if ( window.__experimentalEnableSync ) {
39
- if ( globalThis.IS_GUTENBERG_PLUGIN ) {
40
- return getSyncManager()?.undoManager ?? state.undoManager;
41
- }
38
+ if ( globalThis.IS_GUTENBERG_PLUGIN ) {
39
+ // undoManager is undefined until the first sync-enabled entity is loaded.
40
+ return getSyncManager()?.undoManager ?? state.undoManager;
42
41
  }
43
42
 
44
43
  return state.undoManager;