@kerebron/extension-yjs 0.0.1
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/LICENSE +23 -0
- package/README.md +34 -0
- package/esm/ExtensionYjs.d.ts +12 -0
- package/esm/ExtensionYjs.d.ts.map +1 -0
- package/esm/ExtensionYjs.js +44 -0
- package/esm/SyncPlugin.d.ts +83 -0
- package/esm/SyncPlugin.d.ts.map +1 -0
- package/esm/SyncPlugin.js +923 -0
- package/esm/package.json +3 -0
- package/esm/yCursorPlugin.d.ts +42 -0
- package/esm/yCursorPlugin.d.ts.map +1 -0
- package/esm/yCursorPlugin.js +169 -0
- package/package.json +15 -0
package/esm/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as awarenessProtocol from 'y-protocols/awareness';
|
|
2
|
+
import { DecorationAttrs } from 'prosemirror-view';
|
|
3
|
+
/**
|
|
4
|
+
* Default awareness state filter
|
|
5
|
+
*
|
|
6
|
+
* @param {number} currentClientId current client id
|
|
7
|
+
* @param {number} userClientId user client id
|
|
8
|
+
* @param {any} _user user data
|
|
9
|
+
* @return {boolean}
|
|
10
|
+
*/
|
|
11
|
+
export declare const defaultAwarenessStateFilter: (currentClientId: number, userClientId: number, _user: any) => boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Default generator for a cursor element
|
|
14
|
+
*
|
|
15
|
+
* @param {any} user user data
|
|
16
|
+
* @return {HTMLElement}
|
|
17
|
+
*/
|
|
18
|
+
export declare const defaultCursorBuilder: (user: any) => any;
|
|
19
|
+
/**
|
|
20
|
+
* Default generator for the selection attributes
|
|
21
|
+
*
|
|
22
|
+
* @param {any} user user data
|
|
23
|
+
* @return {import('prosemirror-view').DecorationAttrs}
|
|
24
|
+
*/
|
|
25
|
+
export declare const defaultSelectionBuilder: (user: any) => {
|
|
26
|
+
style: string;
|
|
27
|
+
class: string;
|
|
28
|
+
};
|
|
29
|
+
export declare const createDecorations: (state: any, awareness: awarenessProtocol.Awareness, awarenessFilter: (arg0: number, arg1: number, arg2: any) => boolean, createCursor: (user: {
|
|
30
|
+
name: string;
|
|
31
|
+
color: string;
|
|
32
|
+
}, clientId: number) => Element, createSelection: (user: {
|
|
33
|
+
name: string;
|
|
34
|
+
color: string;
|
|
35
|
+
}, clientId: number) => DecorationAttrs) => any;
|
|
36
|
+
export declare const yCursorPlugin: (awareness: awarenessProtocol.Awareness, { awarenessStateFilter, cursorBuilder, selectionBuilder, getSelection, }?: {
|
|
37
|
+
awarenessStateFilter?: (arg0: any, arg1: any, arg2: any) => boolean;
|
|
38
|
+
cursorBuilder?: (user: any, clientId: number) => HTMLElement;
|
|
39
|
+
selectionBuilder?: (user: any, clientId: number) => DecorationAttrs;
|
|
40
|
+
getSelection?: (arg0: any) => any;
|
|
41
|
+
}, cursorStateField?: string) => any;
|
|
42
|
+
//# sourceMappingURL=yCursorPlugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"yCursorPlugin.d.ts","sourceRoot":"","sources":["../src/yCursorPlugin.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,iBAAiB,MAAM,uBAAuB,CAAC;AAI3D,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAYnD;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,oBACrB,MAAM,gBACT,MAAM,SACb,GAAG,KACT,OAA2C,CAAC;AAE/C;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,oBAahC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB;;;CAKnC,CAAC;AAIF,eAAO,MAAM,iBAAiB,UACrB,GAAG,aACC,iBAAiB,CAAC,SAAS,mBACrB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,OAAO,gBACrD,CACZ,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EACrC,QAAQ,EAAE,MAAM,KACb,OAAO,mBACK,CACf,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EACrC,QAAQ,EAAE,MAAM,KACb,eAAe,KACnB,GA6DF,CAAC;AAEF,eAAO,MAAM,aAAa,cACb,iBAAiB,CAAC,SAAS,6EAMnC;IACD,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC;IACpE,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,KAAK,WAAW,CAAC;IAC7D,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,KAAK,eAAe,CAAC;IACpE,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,GAAG,CAAC;CACnC,qBACiB,MAAM,QAiHzB,CAAC"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import * as Y from 'yjs';
|
|
2
|
+
import { Decoration, DecorationSet } from 'prosemirror-view'; // eslint-disable-line
|
|
3
|
+
import { Plugin } from 'prosemirror-state'; // eslint-disable-line
|
|
4
|
+
import { absolutePositionToRelativePosition, relativePositionToAbsolutePosition, setMeta, } from 'y-prosemirror';
|
|
5
|
+
import { yCursorPluginKey, ySyncPluginKey } from 'y-prosemirror';
|
|
6
|
+
import * as math from 'lib0/math';
|
|
7
|
+
/**
|
|
8
|
+
* Default awareness state filter
|
|
9
|
+
*
|
|
10
|
+
* @param {number} currentClientId current client id
|
|
11
|
+
* @param {number} userClientId user client id
|
|
12
|
+
* @param {any} _user user data
|
|
13
|
+
* @return {boolean}
|
|
14
|
+
*/
|
|
15
|
+
export const defaultAwarenessStateFilter = (currentClientId, userClientId, _user) => currentClientId !== userClientId;
|
|
16
|
+
/**
|
|
17
|
+
* Default generator for a cursor element
|
|
18
|
+
*
|
|
19
|
+
* @param {any} user user data
|
|
20
|
+
* @return {HTMLElement}
|
|
21
|
+
*/
|
|
22
|
+
export const defaultCursorBuilder = (user) => {
|
|
23
|
+
const cursor = document.createElement('span');
|
|
24
|
+
cursor.classList.add('ProseMirror-yjs-cursor');
|
|
25
|
+
cursor.setAttribute('style', `border-color: ${user.color}`);
|
|
26
|
+
const userDiv = document.createElement('div');
|
|
27
|
+
userDiv.setAttribute('style', `background-color: ${user.color}`);
|
|
28
|
+
userDiv.insertBefore(document.createTextNode(user.name), null);
|
|
29
|
+
const nonbreakingSpace1 = document.createTextNode('\u2060');
|
|
30
|
+
const nonbreakingSpace2 = document.createTextNode('\u2060');
|
|
31
|
+
cursor.insertBefore(nonbreakingSpace1, null);
|
|
32
|
+
cursor.insertBefore(userDiv, null);
|
|
33
|
+
cursor.insertBefore(nonbreakingSpace2, null);
|
|
34
|
+
return cursor;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Default generator for the selection attributes
|
|
38
|
+
*
|
|
39
|
+
* @param {any} user user data
|
|
40
|
+
* @return {import('prosemirror-view').DecorationAttrs}
|
|
41
|
+
*/
|
|
42
|
+
export const defaultSelectionBuilder = (user) => {
|
|
43
|
+
return {
|
|
44
|
+
style: `background-color: ${user.color}70`,
|
|
45
|
+
class: 'ProseMirror-yjs-selection',
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
const rxValidColor = /^#[0-9a-fA-F]{6}$/;
|
|
49
|
+
export const createDecorations = (state, awareness, awarenessFilter, createCursor, createSelection) => {
|
|
50
|
+
const ystate = ySyncPluginKey.getState(state);
|
|
51
|
+
const y = ystate.doc;
|
|
52
|
+
const decorations = [];
|
|
53
|
+
if (ystate.snapshot != null || ystate.prevSnapshot != null ||
|
|
54
|
+
ystate.binding.mapping.size === 0) {
|
|
55
|
+
// do not render cursors while snapshot is active
|
|
56
|
+
return DecorationSet.create(state.doc, []);
|
|
57
|
+
}
|
|
58
|
+
awareness.getStates().forEach((aw, clientId) => {
|
|
59
|
+
if (!awarenessFilter(y.clientID, clientId, aw)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (aw.cursor != null) {
|
|
63
|
+
const user = aw.user || {};
|
|
64
|
+
if (user.color == null) {
|
|
65
|
+
user.color = '#ffa500';
|
|
66
|
+
}
|
|
67
|
+
else if (!rxValidColor.test(user.color)) {
|
|
68
|
+
// We only support 6-digit RGB colors in y-prosemirror
|
|
69
|
+
console.warn('A user uses an unsupported color format', user);
|
|
70
|
+
}
|
|
71
|
+
if (user.name == null) {
|
|
72
|
+
user.name = `User: ${clientId}`;
|
|
73
|
+
}
|
|
74
|
+
let anchor = relativePositionToAbsolutePosition(y, ystate.type, Y.createRelativePositionFromJSON(aw.cursor.anchor), ystate.binding.mapping);
|
|
75
|
+
let head = relativePositionToAbsolutePosition(y, ystate.type, Y.createRelativePositionFromJSON(aw.cursor.head), ystate.binding.mapping);
|
|
76
|
+
if (anchor !== null && head !== null) {
|
|
77
|
+
const maxsize = math.max(state.doc.content.size - 1, 0);
|
|
78
|
+
anchor = math.min(anchor, maxsize);
|
|
79
|
+
head = math.min(head, maxsize);
|
|
80
|
+
decorations.push(Decoration.widget(head, () => createCursor(user, clientId), {
|
|
81
|
+
key: clientId + '',
|
|
82
|
+
side: 10,
|
|
83
|
+
}));
|
|
84
|
+
const from = math.min(anchor, head);
|
|
85
|
+
const to = math.max(anchor, head);
|
|
86
|
+
decorations.push(Decoration.inline(from, to, createSelection(user, clientId), {
|
|
87
|
+
inclusiveEnd: true,
|
|
88
|
+
inclusiveStart: false,
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return DecorationSet.create(state.doc, decorations);
|
|
94
|
+
};
|
|
95
|
+
export const yCursorPlugin = (awareness, { awarenessStateFilter = defaultAwarenessStateFilter, cursorBuilder = defaultCursorBuilder, selectionBuilder = defaultSelectionBuilder, getSelection = (state) => state.selection, } = {}, cursorStateField = 'cursor') => {
|
|
96
|
+
return new Plugin({
|
|
97
|
+
key: yCursorPluginKey,
|
|
98
|
+
state: {
|
|
99
|
+
init(_, state) {
|
|
100
|
+
return createDecorations(state, awareness, awarenessStateFilter, cursorBuilder, selectionBuilder);
|
|
101
|
+
},
|
|
102
|
+
apply(tr, prevState, _oldState, newState) {
|
|
103
|
+
const ystate = ySyncPluginKey.getState(newState);
|
|
104
|
+
const yCursorState = tr.getMeta(yCursorPluginKey);
|
|
105
|
+
if ((ystate && ystate.isChangeOrigin) ||
|
|
106
|
+
(yCursorState && yCursorState.awarenessUpdated)) {
|
|
107
|
+
return createDecorations(newState, awareness, awarenessStateFilter, cursorBuilder, selectionBuilder);
|
|
108
|
+
}
|
|
109
|
+
return prevState.map(tr.mapping, tr.doc);
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
props: {
|
|
113
|
+
decorations: (state) => {
|
|
114
|
+
return yCursorPluginKey.getState(state);
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
view: (view) => {
|
|
118
|
+
const awarenessListener = () => {
|
|
119
|
+
// @ts-ignore
|
|
120
|
+
if (view.docView) {
|
|
121
|
+
setMeta(view, yCursorPluginKey, { awarenessUpdated: true });
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const updateCursorInfo = () => {
|
|
125
|
+
const ystate = ySyncPluginKey.getState(view.state);
|
|
126
|
+
// @note We make implicit checks when checking for the cursor property
|
|
127
|
+
const current = awareness.getLocalState() || {};
|
|
128
|
+
const selection = getSelection(view.state);
|
|
129
|
+
if (view.hasFocus()) {
|
|
130
|
+
const selection = getSelection(view.state);
|
|
131
|
+
/**
|
|
132
|
+
* @type {Y.RelativePosition}
|
|
133
|
+
*/
|
|
134
|
+
const anchor = absolutePositionToRelativePosition(selection.anchor, ystate.type, ystate.binding.mapping);
|
|
135
|
+
/**
|
|
136
|
+
* @type {Y.RelativePosition}
|
|
137
|
+
*/
|
|
138
|
+
const head = absolutePositionToRelativePosition(selection.head, ystate.type, ystate.binding.mapping);
|
|
139
|
+
if (current.cursor == null ||
|
|
140
|
+
!Y.compareRelativePositions(Y.createRelativePositionFromJSON(current.cursor.anchor), anchor) ||
|
|
141
|
+
!Y.compareRelativePositions(Y.createRelativePositionFromJSON(current.cursor.head), head)) {
|
|
142
|
+
awareness.setLocalStateField(cursorStateField, {
|
|
143
|
+
anchor,
|
|
144
|
+
head,
|
|
145
|
+
});
|
|
146
|
+
awareness.setLocalStateField('cm-cursor', null);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (current.cursor != null &&
|
|
150
|
+
relativePositionToAbsolutePosition(ystate.doc, ystate.type, Y.createRelativePositionFromJSON(current.cursor.anchor), ystate.binding.mapping) !== null) {
|
|
151
|
+
// delete cursor information if current cursor information is owned by this editor binding
|
|
152
|
+
awareness.setLocalStateField(cursorStateField, null);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
awareness.on('change', awarenessListener);
|
|
156
|
+
view.dom.addEventListener('focusin', updateCursorInfo);
|
|
157
|
+
view.dom.addEventListener('focusout', updateCursorInfo);
|
|
158
|
+
return {
|
|
159
|
+
update: updateCursorInfo,
|
|
160
|
+
destroy: () => {
|
|
161
|
+
view.dom.removeEventListener('focusin', updateCursorInfo);
|
|
162
|
+
view.dom.removeEventListener('focusout', updateCursorInfo);
|
|
163
|
+
awareness.off('change', awarenessListener);
|
|
164
|
+
awareness.setLocalStateField(cursorStateField, null);
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kerebron/extension-yjs",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"module": "./esm/ExtensionYjs.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./esm/ExtensionYjs.js"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/node": "^20.9.0"
|
|
13
|
+
},
|
|
14
|
+
"_generatedBy": "dnt@dev"
|
|
15
|
+
}
|