@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.
- package/CHANGELOG.md +2 -0
- package/build/actions.cjs +14 -2
- package/build/actions.cjs.map +2 -2
- package/build/awareness/config.cjs +34 -0
- package/build/awareness/config.cjs.map +7 -0
- package/build/awareness/post-editor-awareness.cjs +144 -0
- package/build/awareness/post-editor-awareness.cjs.map +7 -0
- package/build/awareness/types.cjs +19 -0
- package/build/awareness/types.cjs.map +7 -0
- package/build/awareness/utils.cjs +116 -0
- package/build/awareness/utils.cjs.map +7 -0
- package/build/entities.cjs +48 -45
- package/build/entities.cjs.map +2 -2
- package/build/private-selectors.cjs +2 -4
- package/build/private-selectors.cjs.map +2 -2
- package/build/resolvers.cjs +14 -4
- package/build/resolvers.cjs.map +2 -2
- package/build/types.cjs.map +1 -1
- package/build/utils/crdt-blocks.cjs +63 -41
- package/build/utils/crdt-blocks.cjs.map +2 -2
- package/build/utils/crdt-user-selections.cjs +171 -0
- package/build/utils/crdt-user-selections.cjs.map +7 -0
- package/build/utils/crdt-utils.cjs +44 -0
- package/build/utils/crdt-utils.cjs.map +7 -0
- package/build/utils/crdt.cjs +33 -37
- package/build/utils/crdt.cjs.map +2 -2
- package/build-module/actions.mjs +14 -2
- package/build-module/actions.mjs.map +2 -2
- package/build-module/awareness/config.mjs +8 -0
- package/build-module/awareness/config.mjs.map +7 -0
- package/build-module/awareness/post-editor-awareness.mjs +125 -0
- package/build-module/awareness/post-editor-awareness.mjs.map +7 -0
- package/build-module/awareness/types.mjs +1 -0
- package/build-module/awareness/types.mjs.map +7 -0
- package/build-module/awareness/utils.mjs +90 -0
- package/build-module/awareness/utils.mjs.map +7 -0
- package/build-module/entities.mjs +48 -47
- package/build-module/entities.mjs.map +2 -2
- package/build-module/private-selectors.mjs +2 -4
- package/build-module/private-selectors.mjs.map +2 -2
- package/build-module/resolvers.mjs +14 -4
- package/build-module/resolvers.mjs.map +2 -2
- package/build-module/utils/crdt-blocks.mjs +64 -42
- package/build-module/utils/crdt-blocks.mjs.map +2 -2
- package/build-module/utils/crdt-user-selections.mjs +144 -0
- package/build-module/utils/crdt-user-selections.mjs.map +7 -0
- package/build-module/utils/crdt-utils.mjs +17 -0
- package/build-module/utils/crdt-utils.mjs.map +7 -0
- package/build-module/utils/crdt.mjs +37 -37
- package/build-module/utils/crdt.mjs.map +2 -2
- package/build-types/actions.d.ts.map +1 -1
- package/build-types/awareness/config.d.ts +9 -0
- package/build-types/awareness/config.d.ts.map +1 -0
- package/build-types/awareness/post-editor-awareness.d.ts +39 -0
- package/build-types/awareness/post-editor-awareness.d.ts.map +1 -0
- package/build-types/awareness/types.d.ts +32 -0
- package/build-types/awareness/types.d.ts.map +1 -0
- package/build-types/awareness/utils.d.ts +22 -0
- package/build-types/awareness/utils.d.ts.map +1 -0
- package/build-types/entities.d.ts.map +1 -1
- package/build-types/index.d.ts.map +1 -1
- package/build-types/private-selectors.d.ts.map +1 -1
- package/build-types/resolvers.d.ts.map +1 -1
- package/build-types/types.d.ts +0 -5
- package/build-types/types.d.ts.map +1 -1
- package/build-types/utils/crdt-blocks.d.ts +14 -5
- package/build-types/utils/crdt-blocks.d.ts.map +1 -1
- package/build-types/utils/crdt-user-selections.d.ts +66 -0
- package/build-types/utils/crdt-user-selections.d.ts.map +1 -0
- package/build-types/utils/crdt-utils.d.ts +53 -0
- package/build-types/utils/crdt-utils.d.ts.map +1 -0
- package/build-types/utils/crdt.d.ts +19 -1
- package/build-types/utils/crdt.d.ts.map +1 -1
- package/package.json +19 -18
- package/src/actions.js +13 -2
- package/src/awareness/config.ts +9 -0
- package/src/awareness/post-editor-awareness.ts +187 -0
- package/src/awareness/types.ts +38 -0
- package/src/awareness/utils.ts +159 -0
- package/src/entities.js +52 -54
- package/src/private-selectors.ts +3 -4
- package/src/resolvers.js +17 -8
- package/src/test/entities.js +11 -9
- package/src/test/entity-provider.js +2 -0
- package/src/test/resolvers.js +3 -45
- package/src/types.ts +0 -6
- package/src/utils/crdt-blocks.ts +101 -99
- package/src/utils/crdt-user-selections.ts +332 -0
- package/src/utils/crdt-utils.ts +77 -0
- package/src/utils/crdt.ts +76 -57
- package/src/utils/test/crdt-blocks.ts +11 -4
- 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,
|
|
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.
|
|
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.
|
|
53
|
-
"@wordpress/block-editor": "^15.
|
|
54
|
-
"@wordpress/blocks": "^15.
|
|
55
|
-
"@wordpress/compose": "^7.
|
|
56
|
-
"@wordpress/data": "^10.
|
|
57
|
-
"@wordpress/deprecated": "^4.
|
|
58
|
-
"@wordpress/element": "^6.
|
|
59
|
-
"@wordpress/html-entities": "^4.
|
|
60
|
-
"@wordpress/i18n": "^6.
|
|
61
|
-
"@wordpress/is-shallow-equal": "^5.
|
|
62
|
-
"@wordpress/private-apis": "^1.
|
|
63
|
-
"@wordpress/rich-text": "^7.
|
|
64
|
-
"@wordpress/sync": "^1.
|
|
65
|
-
"@wordpress/undo-manager": "^1.
|
|
66
|
-
"@wordpress/url": "^4.
|
|
67
|
-
"@wordpress/warning": "^3.
|
|
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": "
|
|
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 (
|
|
415
|
-
if (
|
|
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 (
|
|
284
|
-
if (
|
|
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 (
|
|
348
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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',
|
package/src/private-selectors.ts
CHANGED
|
@@ -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 (
|
|
39
|
-
|
|
40
|
-
|
|
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;
|