@atlaskit/editor-plugin-collab-edit 0.1.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 +1 -0
- package/LICENSE.md +13 -0
- package/README.md +30 -0
- package/dist/cjs/actions.js +110 -0
- package/dist/cjs/analytics.js +47 -0
- package/dist/cjs/events/handlers.js +88 -0
- package/dist/cjs/events/initialize.js +58 -0
- package/dist/cjs/events/send-transaction.js +48 -0
- package/dist/cjs/index.js +128 -0
- package/dist/cjs/native-collab-provider-plugin.js +37 -0
- package/dist/cjs/participants.js +95 -0
- package/dist/cjs/plugin-key.js +8 -0
- package/dist/cjs/plugin-state.js +241 -0
- package/dist/cjs/plugin.js +102 -0
- package/dist/cjs/types.js +5 -0
- package/dist/cjs/utils.js +150 -0
- package/dist/es2019/actions.js +119 -0
- package/dist/es2019/analytics.js +41 -0
- package/dist/es2019/events/handlers.js +72 -0
- package/dist/es2019/events/initialize.js +44 -0
- package/dist/es2019/events/send-transaction.js +42 -0
- package/dist/es2019/index.js +86 -0
- package/dist/es2019/native-collab-provider-plugin.js +32 -0
- package/dist/es2019/participants.js +57 -0
- package/dist/es2019/plugin-key.js +2 -0
- package/dist/es2019/plugin-state.js +219 -0
- package/dist/es2019/plugin.js +83 -0
- package/dist/es2019/types.js +1 -0
- package/dist/es2019/utils.js +133 -0
- package/dist/esm/actions.js +103 -0
- package/dist/esm/analytics.js +41 -0
- package/dist/esm/events/handlers.js +82 -0
- package/dist/esm/events/initialize.js +51 -0
- package/dist/esm/events/send-transaction.js +42 -0
- package/dist/esm/index.js +116 -0
- package/dist/esm/native-collab-provider-plugin.js +31 -0
- package/dist/esm/participants.js +88 -0
- package/dist/esm/plugin-key.js +2 -0
- package/dist/esm/plugin-state.js +229 -0
- package/dist/esm/plugin.js +85 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/utils.js +136 -0
- package/dist/types/actions.d.ts +11 -0
- package/dist/types/analytics.d.ts +6 -0
- package/dist/types/events/handlers.d.ts +24 -0
- package/dist/types/events/initialize.d.ts +16 -0
- package/dist/types/events/send-transaction.d.ts +11 -0
- package/dist/types/index.d.ts +29 -0
- package/dist/types/native-collab-provider-plugin.d.ts +7 -0
- package/dist/types/participants.d.ts +18 -0
- package/dist/types/plugin-key.d.ts +3 -0
- package/dist/types/plugin-state.d.ts +25 -0
- package/dist/types/plugin.d.ts +11 -0
- package/dist/types/types.d.ts +8 -0
- package/dist/types/utils.d.ts +16 -0
- package/dist/types-ts4.5/actions.d.ts +11 -0
- package/dist/types-ts4.5/analytics.d.ts +6 -0
- package/dist/types-ts4.5/events/handlers.d.ts +24 -0
- package/dist/types-ts4.5/events/initialize.d.ts +16 -0
- package/dist/types-ts4.5/events/send-transaction.d.ts +11 -0
- package/dist/types-ts4.5/index.d.ts +29 -0
- package/dist/types-ts4.5/native-collab-provider-plugin.d.ts +7 -0
- package/dist/types-ts4.5/participants.d.ts +18 -0
- package/dist/types-ts4.5/plugin-key.d.ts +3 -0
- package/dist/types-ts4.5/plugin-state.d.ts +25 -0
- package/dist/types-ts4.5/plugin.d.ts +11 -0
- package/dist/types-ts4.5/types.d.ts +8 -0
- package/dist/types-ts4.5/utils.d.ts +16 -0
- package/package.json +104 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { applyRemoteData, handleConnection, handleInit, handlePresence, handleTelePointer } from '../actions';
|
|
2
|
+
import { addSynchronyEntityAnalytics, addSynchronyErrorAnalytics } from '../analytics';
|
|
3
|
+
const effect = (fn, eq) => {
|
|
4
|
+
let previousDeps;
|
|
5
|
+
let cleanup;
|
|
6
|
+
return (...currentDeps) => {
|
|
7
|
+
if (cleanup && eq(previousDeps, currentDeps)) {
|
|
8
|
+
return cleanup;
|
|
9
|
+
}
|
|
10
|
+
cleanup = fn(...currentDeps);
|
|
11
|
+
previousDeps = currentDeps;
|
|
12
|
+
return cleanup;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export const subscribe = effect((view, provider, options, featureFlags, _providerFactory, editorAnalyticsApi) => {
|
|
16
|
+
let entityRef;
|
|
17
|
+
const entityHandlers = {
|
|
18
|
+
disconnectedHandler: () => {
|
|
19
|
+
addSynchronyEntityAnalytics(view.state, view.state.tr)('disconnected', editorAnalyticsApi);
|
|
20
|
+
},
|
|
21
|
+
errorHandler: () => {
|
|
22
|
+
addSynchronyEntityAnalytics(view.state, view.state.tr)('error', editorAnalyticsApi);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const unsubscribeSynchronyEntity = () => {
|
|
26
|
+
if (entityRef) {
|
|
27
|
+
entityRef.off('disconnected', entityHandlers.disconnectedHandler);
|
|
28
|
+
entityRef.off('error', entityHandlers.errorHandler);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const handlers = {
|
|
32
|
+
initHandler: data => {
|
|
33
|
+
view.dispatch(view.state.tr.setMeta('collabInitialised', true));
|
|
34
|
+
handleInit(data, view, options);
|
|
35
|
+
},
|
|
36
|
+
connectedHandler: data => handleConnection(data, view),
|
|
37
|
+
dataHandler: data => applyRemoteData(data, view, options),
|
|
38
|
+
presenceHandler: data => handlePresence(data, view),
|
|
39
|
+
telepointerHandler: data => handleTelePointer(data, view),
|
|
40
|
+
localStepsHandler: data => {
|
|
41
|
+
const {
|
|
42
|
+
steps
|
|
43
|
+
} = data;
|
|
44
|
+
const {
|
|
45
|
+
state
|
|
46
|
+
} = view;
|
|
47
|
+
const {
|
|
48
|
+
tr
|
|
49
|
+
} = state;
|
|
50
|
+
steps.forEach(step => tr.step(step));
|
|
51
|
+
view.dispatch(tr);
|
|
52
|
+
},
|
|
53
|
+
errorHandler: error => {
|
|
54
|
+
addSynchronyErrorAnalytics(view.state, view.state.tr, featureFlags, editorAnalyticsApi)(error);
|
|
55
|
+
},
|
|
56
|
+
entityHandler: ({
|
|
57
|
+
entity
|
|
58
|
+
}) => {
|
|
59
|
+
unsubscribeSynchronyEntity();
|
|
60
|
+
if (options.EXPERIMENTAL_allowInternalErrorAnalytics) {
|
|
61
|
+
entity.on('disconnected', entityHandlers.disconnectedHandler);
|
|
62
|
+
entity.on('error', entityHandlers.errorHandler);
|
|
63
|
+
entityRef = entity;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
provider.on('init', handlers.initHandler).on('connected', handlers.connectedHandler).on('data', handlers.dataHandler).on('presence', handlers.presenceHandler).on('telepointer', handlers.telepointerHandler).on('local-steps', handlers.localStepsHandler).on('error', handlers.errorHandler).on('entity', handlers.entityHandler);
|
|
68
|
+
return () => {
|
|
69
|
+
unsubscribeSynchronyEntity();
|
|
70
|
+
provider.off('init', handlers.initHandler).off('connected', handlers.connectedHandler).off('data', handlers.dataHandler).off('presence', handlers.presenceHandler).off('telepointer', handlers.telepointerHandler).off('local-steps', handlers.localStepsHandler).off('error', handlers.errorHandler).off('entity', handlers.entityHandler);
|
|
71
|
+
};
|
|
72
|
+
}, (previousDeps, currentDeps) => currentDeps && currentDeps.every((dep, i) => dep === previousDeps[i]));
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import memoizeOne from 'memoize-one';
|
|
2
|
+
import { Step } from '@atlaskit/editor-prosemirror/transform';
|
|
3
|
+
import { pluginKey } from '../plugin-key';
|
|
4
|
+
import { subscribe } from './handlers';
|
|
5
|
+
const initCollab = (collabEditProvider, view) => {
|
|
6
|
+
if (collabEditProvider.initialize) {
|
|
7
|
+
collabEditProvider.initialize(() => view.state, json => Step.fromJSON(view.state.schema, json));
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
const initNewCollab = (collabEditProvider, view, onSyncUpError) => {
|
|
11
|
+
collabEditProvider.setup({
|
|
12
|
+
getState: () => view.state,
|
|
13
|
+
onSyncUpError
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
const initCollabMemo = memoizeOne(initCollab);
|
|
17
|
+
export const initialize = ({
|
|
18
|
+
options,
|
|
19
|
+
providerFactory,
|
|
20
|
+
view,
|
|
21
|
+
featureFlags,
|
|
22
|
+
editorAnalyticsApi
|
|
23
|
+
}) => provider => {
|
|
24
|
+
let cleanup;
|
|
25
|
+
const pluginState = pluginKey.getState(view.state);
|
|
26
|
+
if (pluginState !== null && pluginState !== void 0 && pluginState.isReady && cleanup) {
|
|
27
|
+
cleanup();
|
|
28
|
+
}
|
|
29
|
+
cleanup = subscribe(view, provider, options, featureFlags, providerFactory, editorAnalyticsApi);
|
|
30
|
+
|
|
31
|
+
// Initialize provider
|
|
32
|
+
if (options.useNativePlugin) {
|
|
33
|
+
// ED-13912 For NCS we don't want to use memoizeOne because it causes
|
|
34
|
+
// infinite text while changing page-width
|
|
35
|
+
initNewCollab(provider, view, options.onSyncUpError);
|
|
36
|
+
} else {
|
|
37
|
+
/**
|
|
38
|
+
* We only want to initialise once, if we reload/reconfigure this plugin
|
|
39
|
+
* We dont want to re-init collab, it would break existing sessions
|
|
40
|
+
*/
|
|
41
|
+
initCollabMemo(provider, view);
|
|
42
|
+
}
|
|
43
|
+
return cleanup;
|
|
44
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getSendableSelection } from '../actions';
|
|
2
|
+
import { pluginKey } from '../plugin-key';
|
|
3
|
+
export const sendTransaction = ({
|
|
4
|
+
originalTransaction,
|
|
5
|
+
transactions,
|
|
6
|
+
oldEditorState,
|
|
7
|
+
newEditorState,
|
|
8
|
+
useNativePlugin
|
|
9
|
+
}) => provider => {
|
|
10
|
+
const docChangedTransaction = transactions.find(tr => tr.docChanged);
|
|
11
|
+
const currentPluginState = pluginKey.getState(newEditorState);
|
|
12
|
+
if (!(currentPluginState !== null && currentPluginState !== void 0 && currentPluginState.isReady)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const shouldSendStepForSynchronyCollabProvider = !originalTransaction.getMeta('isRemote') &&
|
|
16
|
+
// TODO: ED-8995
|
|
17
|
+
// We need to do this check to reduce the number of race conditions when working with tables.
|
|
18
|
+
// This metadata is coming from the scaleTable command in table-resizing plugin
|
|
19
|
+
!originalTransaction.getMeta('scaleTable') && docChangedTransaction;
|
|
20
|
+
if (useNativePlugin || shouldSendStepForSynchronyCollabProvider) {
|
|
21
|
+
provider.send(docChangedTransaction, oldEditorState, newEditorState);
|
|
22
|
+
}
|
|
23
|
+
const prevPluginState = pluginKey.getState(oldEditorState);
|
|
24
|
+
const {
|
|
25
|
+
activeParticipants: prevActiveParticipants
|
|
26
|
+
} = prevPluginState || {};
|
|
27
|
+
const {
|
|
28
|
+
activeParticipants,
|
|
29
|
+
sessionId
|
|
30
|
+
} = currentPluginState;
|
|
31
|
+
const selectionChanged = !oldEditorState.selection.eq(newEditorState.selection);
|
|
32
|
+
const participantsChanged = prevActiveParticipants && !prevActiveParticipants.eq(activeParticipants);
|
|
33
|
+
if (sessionId && selectionChanged && !docChangedTransaction || sessionId && participantsChanged) {
|
|
34
|
+
const selection = getSendableSelection(newEditorState.selection);
|
|
35
|
+
const message = {
|
|
36
|
+
type: 'telepointer',
|
|
37
|
+
selection,
|
|
38
|
+
sessionId
|
|
39
|
+
};
|
|
40
|
+
provider.sendMessage(message);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { collab } from 'prosemirror-collab';
|
|
2
|
+
import { addSynchronyErrorAnalytics } from './analytics';
|
|
3
|
+
import { sendTransaction } from './events/send-transaction';
|
|
4
|
+
import { nativeCollabProviderPlugin } from './native-collab-provider-plugin';
|
|
5
|
+
import { createPlugin, pluginKey } from './plugin';
|
|
6
|
+
export { pluginKey };
|
|
7
|
+
import { getAvatarColor } from './utils';
|
|
8
|
+
const providerBuilder = collabEditProviderPromise => async (codeToExecute, onError) => {
|
|
9
|
+
try {
|
|
10
|
+
const provider = await collabEditProviderPromise;
|
|
11
|
+
if (provider) {
|
|
12
|
+
return codeToExecute(provider);
|
|
13
|
+
}
|
|
14
|
+
} catch (err) {
|
|
15
|
+
if (onError) {
|
|
16
|
+
onError(err);
|
|
17
|
+
} else {
|
|
18
|
+
// eslint-disable-next-line no-console
|
|
19
|
+
console.error(err);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
export const collabEditPlugin = ({
|
|
24
|
+
config: options,
|
|
25
|
+
api
|
|
26
|
+
}) => {
|
|
27
|
+
var _api$featureFlags;
|
|
28
|
+
const featureFlags = (api === null || api === void 0 ? void 0 : (_api$featureFlags = api.featureFlags) === null || _api$featureFlags === void 0 ? void 0 : _api$featureFlags.sharedState.currentState()) || {};
|
|
29
|
+
let providerResolver = () => {};
|
|
30
|
+
const collabEditProviderPromise = new Promise(_providerResolver => {
|
|
31
|
+
providerResolver = _providerResolver;
|
|
32
|
+
});
|
|
33
|
+
const executeProviderCode = providerBuilder(collabEditProviderPromise);
|
|
34
|
+
return {
|
|
35
|
+
name: 'collabEdit',
|
|
36
|
+
getSharedState(state) {
|
|
37
|
+
if (!state) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const collabPluginState = pluginKey.getState(state);
|
|
41
|
+
return {
|
|
42
|
+
activeParticipants: collabPluginState === null || collabPluginState === void 0 ? void 0 : collabPluginState.activeParticipants,
|
|
43
|
+
sessionId: collabPluginState === null || collabPluginState === void 0 ? void 0 : collabPluginState.sessionId
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
actions: {
|
|
47
|
+
getAvatarColor
|
|
48
|
+
},
|
|
49
|
+
pmPlugins() {
|
|
50
|
+
const {
|
|
51
|
+
useNativePlugin = false,
|
|
52
|
+
userId
|
|
53
|
+
} = options || {};
|
|
54
|
+
return [...(useNativePlugin ? [{
|
|
55
|
+
name: 'pmCollab',
|
|
56
|
+
plugin: () => collab({
|
|
57
|
+
clientID: userId
|
|
58
|
+
})
|
|
59
|
+
}, {
|
|
60
|
+
name: 'nativeCollabProviderPlugin',
|
|
61
|
+
plugin: () => nativeCollabProviderPlugin({
|
|
62
|
+
providerPromise: collabEditProviderPromise
|
|
63
|
+
})
|
|
64
|
+
}] : []), {
|
|
65
|
+
name: 'collab',
|
|
66
|
+
plugin: ({
|
|
67
|
+
dispatch,
|
|
68
|
+
providerFactory
|
|
69
|
+
}) => {
|
|
70
|
+
return createPlugin(dispatch, providerFactory, providerResolver, executeProviderCode, options, featureFlags, api);
|
|
71
|
+
}
|
|
72
|
+
}];
|
|
73
|
+
},
|
|
74
|
+
onEditorViewStateUpdated(props) {
|
|
75
|
+
var _api$analytics, _options$useNativePlu;
|
|
76
|
+
const addErrorAnalytics = addSynchronyErrorAnalytics(props.newEditorState, props.newEditorState.tr, featureFlags, api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions);
|
|
77
|
+
executeProviderCode(sendTransaction({
|
|
78
|
+
originalTransaction: props.originalTransaction,
|
|
79
|
+
transactions: props.transactions,
|
|
80
|
+
oldEditorState: props.oldEditorState,
|
|
81
|
+
newEditorState: props.newEditorState,
|
|
82
|
+
useNativePlugin: (_options$useNativePlu = options === null || options === void 0 ? void 0 : options.useNativePlugin) !== null && _options$useNativePlu !== void 0 ? _options$useNativePlu : false
|
|
83
|
+
}), addErrorAnalytics);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
2
|
+
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
|
|
3
|
+
const nativeCollabProviderPluginKey = new PluginKey('nativeCollabProviderPlugin');
|
|
4
|
+
export const nativeCollabProviderPlugin = ({
|
|
5
|
+
providerPromise
|
|
6
|
+
}) => {
|
|
7
|
+
return new SafePlugin({
|
|
8
|
+
key: nativeCollabProviderPluginKey,
|
|
9
|
+
state: {
|
|
10
|
+
init: () => null,
|
|
11
|
+
apply: (tr, currentPluginState) => {
|
|
12
|
+
const provider = tr.getMeta(nativeCollabProviderPluginKey);
|
|
13
|
+
return provider ? provider : currentPluginState;
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
view: editorView => {
|
|
17
|
+
providerPromise.then(provider => {
|
|
18
|
+
const {
|
|
19
|
+
dispatch,
|
|
20
|
+
state
|
|
21
|
+
} = editorView;
|
|
22
|
+
const tr = state.tr;
|
|
23
|
+
tr.setMeta(nativeCollabProviderPluginKey, provider);
|
|
24
|
+
dispatch(tr);
|
|
25
|
+
});
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
export const getCollabProvider = editorState => {
|
|
31
|
+
return nativeCollabProviderPluginKey.getState(editorState);
|
|
32
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export class Participants {
|
|
2
|
+
constructor(participants = new Map()) {
|
|
3
|
+
this.participants = participants;
|
|
4
|
+
}
|
|
5
|
+
add(data) {
|
|
6
|
+
const newSet = new Map(this.participants);
|
|
7
|
+
data.forEach(participant => {
|
|
8
|
+
newSet.set(participant.sessionId, participant);
|
|
9
|
+
});
|
|
10
|
+
return new Participants(newSet);
|
|
11
|
+
}
|
|
12
|
+
remove(sessionIds) {
|
|
13
|
+
const newSet = new Map(this.participants);
|
|
14
|
+
sessionIds.forEach(sessionId => {
|
|
15
|
+
newSet.delete(sessionId);
|
|
16
|
+
});
|
|
17
|
+
return new Participants(newSet);
|
|
18
|
+
}
|
|
19
|
+
update(sessionId, lastActive) {
|
|
20
|
+
const newSet = new Map(this.participants);
|
|
21
|
+
const data = newSet.get(sessionId);
|
|
22
|
+
if (!data) {
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
newSet.set(sessionId, {
|
|
26
|
+
...data,
|
|
27
|
+
lastActive
|
|
28
|
+
});
|
|
29
|
+
return new Participants(newSet);
|
|
30
|
+
}
|
|
31
|
+
updateCursorPos(sessionId, cursorPos) {
|
|
32
|
+
const newSet = new Map(this.participants);
|
|
33
|
+
const data = newSet.get(sessionId);
|
|
34
|
+
if (!data) {
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
newSet.set(sessionId, {
|
|
38
|
+
...data,
|
|
39
|
+
cursorPos
|
|
40
|
+
});
|
|
41
|
+
return new Participants(newSet);
|
|
42
|
+
}
|
|
43
|
+
toArray() {
|
|
44
|
+
return Array.from(this.participants.values());
|
|
45
|
+
}
|
|
46
|
+
get(sessionId) {
|
|
47
|
+
return this.participants.get(sessionId);
|
|
48
|
+
}
|
|
49
|
+
size() {
|
|
50
|
+
return this.participants.size;
|
|
51
|
+
}
|
|
52
|
+
eq(other) {
|
|
53
|
+
const left = this.toArray().map(p => p.sessionId).sort((a, b) => a > b ? -1 : 1).join('');
|
|
54
|
+
const right = other.toArray().map(p => p.sessionId).sort((a, b) => a > b ? -1 : 1).join('');
|
|
55
|
+
return left === right;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
2
|
+
import { TELEPOINTER_DIM_CLASS } from '@atlaskit/editor-common/collab';
|
|
3
|
+
import { browser } from '@atlaskit/editor-common/utils';
|
|
4
|
+
import { Selection } from '@atlaskit/editor-prosemirror/state';
|
|
5
|
+
import { ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
|
|
6
|
+
import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
|
|
7
|
+
import { Participants } from './participants';
|
|
8
|
+
import { createTelepointers, findPointers, getPositionOfTelepointer } from './utils';
|
|
9
|
+
const isReplaceStep = step => step instanceof ReplaceStep;
|
|
10
|
+
export { TELEPOINTER_DIM_CLASS };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns position where it's possible to place a decoration.
|
|
14
|
+
*/
|
|
15
|
+
export const getValidPos = (tr, pos) => {
|
|
16
|
+
const endOfDocPos = tr.doc.nodeSize - 2;
|
|
17
|
+
if (pos <= endOfDocPos) {
|
|
18
|
+
const resolvedPos = tr.doc.resolve(pos);
|
|
19
|
+
const backwardSelection = Selection.findFrom(resolvedPos, -1, true);
|
|
20
|
+
// if there's no correct cursor position before the `pos`, we try to find it after the `pos`
|
|
21
|
+
const forwardSelection = Selection.findFrom(resolvedPos, 1, true);
|
|
22
|
+
return backwardSelection ? backwardSelection.from : forwardSelection ? forwardSelection.from : pos;
|
|
23
|
+
}
|
|
24
|
+
return endOfDocPos;
|
|
25
|
+
};
|
|
26
|
+
export class PluginState {
|
|
27
|
+
get decorations() {
|
|
28
|
+
return this.decorationSet;
|
|
29
|
+
}
|
|
30
|
+
get activeParticipants() {
|
|
31
|
+
return this.participants;
|
|
32
|
+
}
|
|
33
|
+
get sessionId() {
|
|
34
|
+
return this.sid;
|
|
35
|
+
}
|
|
36
|
+
constructor(decorations, participants, sessionId, collabInitalised = false, onError) {
|
|
37
|
+
// eslint-disable-next-line no-console
|
|
38
|
+
_defineProperty(this, "onError", error => console.error(error));
|
|
39
|
+
this.decorationSet = decorations;
|
|
40
|
+
this.participants = participants;
|
|
41
|
+
this.sid = sessionId;
|
|
42
|
+
this.isReady = collabInitalised;
|
|
43
|
+
this.onError = onError || this.onError;
|
|
44
|
+
}
|
|
45
|
+
getInitial(sessionId) {
|
|
46
|
+
const participant = this.participants.get(sessionId);
|
|
47
|
+
return participant ? participant.name.substring(0, 1).toUpperCase() : 'X';
|
|
48
|
+
}
|
|
49
|
+
apply(tr) {
|
|
50
|
+
let {
|
|
51
|
+
participants,
|
|
52
|
+
sid,
|
|
53
|
+
isReady
|
|
54
|
+
} = this;
|
|
55
|
+
const presenceData = tr.getMeta('presence');
|
|
56
|
+
const telepointerData = tr.getMeta('telepointer');
|
|
57
|
+
const sessionIdData = tr.getMeta('sessionId');
|
|
58
|
+
let collabInitialised = tr.getMeta('collabInitialised');
|
|
59
|
+
if (typeof collabInitialised !== 'boolean') {
|
|
60
|
+
collabInitialised = isReady;
|
|
61
|
+
}
|
|
62
|
+
if (sessionIdData) {
|
|
63
|
+
sid = sessionIdData.sid;
|
|
64
|
+
}
|
|
65
|
+
let add = [];
|
|
66
|
+
let remove = [];
|
|
67
|
+
if (presenceData) {
|
|
68
|
+
const {
|
|
69
|
+
joined = [],
|
|
70
|
+
left = []
|
|
71
|
+
} = presenceData;
|
|
72
|
+
participants = participants.remove(left.map(i => i.sessionId));
|
|
73
|
+
participants = participants.add(joined);
|
|
74
|
+
|
|
75
|
+
// Remove telepointers for users that left
|
|
76
|
+
left.forEach(i => {
|
|
77
|
+
const pointers = findPointers(i.sessionId, this.decorationSet);
|
|
78
|
+
if (pointers) {
|
|
79
|
+
remove = remove.concat(pointers);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (telepointerData) {
|
|
84
|
+
const {
|
|
85
|
+
sessionId
|
|
86
|
+
} = telepointerData;
|
|
87
|
+
if (participants.get(sessionId) && sessionId !== sid) {
|
|
88
|
+
const oldPointers = findPointers(telepointerData.sessionId, this.decorationSet);
|
|
89
|
+
if (oldPointers) {
|
|
90
|
+
remove = remove.concat(oldPointers);
|
|
91
|
+
}
|
|
92
|
+
const endOfDocPos = tr.doc.nodeSize - 2;
|
|
93
|
+
const anchor = telepointerData.selection.anchor;
|
|
94
|
+
const head = telepointerData.selection.head;
|
|
95
|
+
let rawFrom = anchor < head ? anchor : head;
|
|
96
|
+
let rawTo = anchor >= head ? anchor : head;
|
|
97
|
+
if (rawFrom > endOfDocPos) {
|
|
98
|
+
rawFrom = endOfDocPos;
|
|
99
|
+
}
|
|
100
|
+
if (rawTo > endOfDocPos) {
|
|
101
|
+
rawTo = endOfDocPos;
|
|
102
|
+
}
|
|
103
|
+
const isSelection = rawTo - rawFrom > 0;
|
|
104
|
+
let from = 1;
|
|
105
|
+
let to = 1;
|
|
106
|
+
try {
|
|
107
|
+
from = getValidPos(tr, isSelection ? Math.max(rawFrom - 1, 0) : rawFrom);
|
|
108
|
+
to = isSelection ? getValidPos(tr, rawTo) : from;
|
|
109
|
+
} catch (err) {
|
|
110
|
+
this.onError(err);
|
|
111
|
+
}
|
|
112
|
+
add = add.concat(createTelepointers(from, to, sessionId, isSelection, this.getInitial(sessionId)));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (tr.docChanged) {
|
|
116
|
+
// Adjust decoration positions to changes made by the transaction
|
|
117
|
+
try {
|
|
118
|
+
this.decorationSet = this.decorationSet.map(tr.mapping, tr.doc, {
|
|
119
|
+
// Reapplies decorators those got removed by the state change
|
|
120
|
+
onRemove: spec => {
|
|
121
|
+
if (spec.pointer && spec.pointer.sessionId && spec.key === `telepointer-${spec.pointer.sessionId}`) {
|
|
122
|
+
const step = tr.steps.filter(isReplaceStep)[0];
|
|
123
|
+
if (step) {
|
|
124
|
+
const {
|
|
125
|
+
sessionId
|
|
126
|
+
} = spec.pointer;
|
|
127
|
+
const {
|
|
128
|
+
slice: {
|
|
129
|
+
content: {
|
|
130
|
+
size
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
from
|
|
134
|
+
} = step;
|
|
135
|
+
const pos = getValidPos(tr, size ? Math.min(from + size, tr.doc.nodeSize - 3) : Math.max(from, 1));
|
|
136
|
+
add = add.concat(createTelepointers(pos, pos, sessionId, false, this.getInitial(sessionId)));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
} catch (err) {
|
|
142
|
+
this.onError(err);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Remove any selection decoration within the change range,
|
|
146
|
+
// takes care of the issue when after pasting we end up with a dead selection
|
|
147
|
+
tr.steps.filter(isReplaceStep).forEach(s => {
|
|
148
|
+
const {
|
|
149
|
+
from,
|
|
150
|
+
to
|
|
151
|
+
} = s;
|
|
152
|
+
this.decorationSet.find(from, to).forEach(deco => {
|
|
153
|
+
// `type` is private, `from` and `to` are public in latest version
|
|
154
|
+
// `from` != `to` means it's a selection
|
|
155
|
+
if (deco.from !== deco.to) {
|
|
156
|
+
remove.push(deco);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const {
|
|
162
|
+
selection
|
|
163
|
+
} = tr;
|
|
164
|
+
this.decorationSet.find().forEach(deco => {
|
|
165
|
+
if (deco.type.toDOM) {
|
|
166
|
+
const hasTelepointerDimClass = deco.type.toDOM.classList.contains(TELEPOINTER_DIM_CLASS);
|
|
167
|
+
if (deco.from === selection.from && deco.to === selection.to) {
|
|
168
|
+
if (!hasTelepointerDimClass) {
|
|
169
|
+
deco.type.toDOM.classList.add(TELEPOINTER_DIM_CLASS);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Browser condition here to fix ED-14722 where telepointer
|
|
173
|
+
// decorations with side -1 in Firefox causes backspace issues.
|
|
174
|
+
// This is likely caused by contenteditable quirks in Firefox
|
|
175
|
+
if (!browser.gecko) {
|
|
176
|
+
deco.type.side = -1;
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
if (hasTelepointerDimClass) {
|
|
180
|
+
deco.type.toDOM.classList.remove(TELEPOINTER_DIM_CLASS);
|
|
181
|
+
}
|
|
182
|
+
deco.type.side = 0;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
if (remove.length) {
|
|
187
|
+
this.decorationSet = this.decorationSet.remove(remove);
|
|
188
|
+
}
|
|
189
|
+
if (add.length) {
|
|
190
|
+
this.decorationSet = this.decorationSet.add(tr.doc, add);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// This piece needs to be after the decorationSet adjustments,
|
|
194
|
+
// otherwise it's always one step behind where the cursor is
|
|
195
|
+
if (telepointerData) {
|
|
196
|
+
const {
|
|
197
|
+
sessionId
|
|
198
|
+
} = telepointerData;
|
|
199
|
+
if (participants.get(sessionId)) {
|
|
200
|
+
const positionForScroll = getPositionOfTelepointer(sessionId, this.decorationSet);
|
|
201
|
+
if (positionForScroll) {
|
|
202
|
+
participants = participants.updateCursorPos(sessionId, positionForScroll);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const nextState = new PluginState(this.decorationSet, participants, sid, collabInitialised);
|
|
207
|
+
return PluginState.eq(nextState, this) ? this : nextState;
|
|
208
|
+
}
|
|
209
|
+
static eq(a, b) {
|
|
210
|
+
return a.participants === b.participants && a.sessionId === b.sessionId && a.isReady === b.isReady;
|
|
211
|
+
}
|
|
212
|
+
static init(config) {
|
|
213
|
+
const {
|
|
214
|
+
doc,
|
|
215
|
+
onError
|
|
216
|
+
} = config;
|
|
217
|
+
return new PluginState(DecorationSet.create(doc, []), new Participants(), undefined, undefined, onError);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ACTION, ACTION_SUBJECT, EVENT_TYPE, fireAnalyticsEvent } from '@atlaskit/editor-common/analytics';
|
|
2
|
+
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
3
|
+
import { addSynchronyErrorAnalytics } from './analytics';
|
|
4
|
+
import { initialize } from './events/initialize';
|
|
5
|
+
import { pluginKey } from './plugin-key';
|
|
6
|
+
import { PluginState } from './plugin-state';
|
|
7
|
+
export { PluginState, pluginKey };
|
|
8
|
+
export const createPlugin = (dispatch, providerFactory, providerResolver, collabProviderCallback, options, featureFlags, pluginInjectionApi) => {
|
|
9
|
+
return new SafePlugin({
|
|
10
|
+
key: pluginKey,
|
|
11
|
+
state: {
|
|
12
|
+
init(config) {
|
|
13
|
+
return PluginState.init(config);
|
|
14
|
+
},
|
|
15
|
+
apply(transaction, prevPluginState, _oldEditorState, _newEditorState) {
|
|
16
|
+
const pluginState = prevPluginState.apply(transaction);
|
|
17
|
+
dispatch(pluginKey, pluginState);
|
|
18
|
+
return pluginState;
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
props: {
|
|
22
|
+
decorations(state) {
|
|
23
|
+
var _pluginKey$getState;
|
|
24
|
+
return (_pluginKey$getState = pluginKey.getState(state)) === null || _pluginKey$getState === void 0 ? void 0 : _pluginKey$getState.decorations;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
filterTransaction(tr, state) {
|
|
28
|
+
const pluginState = pluginKey.getState(state);
|
|
29
|
+
const collabInitialiseTr = tr.getMeta('collabInitialised');
|
|
30
|
+
|
|
31
|
+
// Don't allow transactions that modifies the document before
|
|
32
|
+
// collab-plugin is ready.
|
|
33
|
+
if (collabInitialiseTr) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
if (!(pluginState !== null && pluginState !== void 0 && pluginState.isReady) && tr.docChanged) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
},
|
|
41
|
+
view(view) {
|
|
42
|
+
var _pluginInjectionApi$a, _pluginInjectionApi$a5;
|
|
43
|
+
const addErrorAnalytics = addSynchronyErrorAnalytics(view.state, view.state.tr, featureFlags, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a === void 0 ? void 0 : _pluginInjectionApi$a.actions);
|
|
44
|
+
const onSyncUpError = attributes => {
|
|
45
|
+
var _pluginInjectionApi$a2, _pluginInjectionApi$a3, _pluginInjectionApi$a4;
|
|
46
|
+
const fireAnalyticsCallback = fireAnalyticsEvent((_pluginInjectionApi$a2 = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a3 = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a3 === void 0 ? void 0 : (_pluginInjectionApi$a4 = _pluginInjectionApi$a3.sharedState.currentState()) === null || _pluginInjectionApi$a4 === void 0 ? void 0 : _pluginInjectionApi$a4.createAnalyticsEvent) !== null && _pluginInjectionApi$a2 !== void 0 ? _pluginInjectionApi$a2 : undefined);
|
|
47
|
+
fireAnalyticsCallback({
|
|
48
|
+
payload: {
|
|
49
|
+
action: ACTION.NEW_COLLAB_SYNC_UP_ERROR_NO_STEPS,
|
|
50
|
+
actionSubject: ACTION_SUBJECT.EDITOR,
|
|
51
|
+
eventType: EVENT_TYPE.OPERATIONAL,
|
|
52
|
+
attributes
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
options.onSyncUpError = onSyncUpError;
|
|
57
|
+
const cleanup = collabProviderCallback(initialize({
|
|
58
|
+
view,
|
|
59
|
+
options,
|
|
60
|
+
providerFactory,
|
|
61
|
+
featureFlags,
|
|
62
|
+
editorAnalyticsApi: pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a5 = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a5 === void 0 ? void 0 : _pluginInjectionApi$a5.actions
|
|
63
|
+
}), addErrorAnalytics);
|
|
64
|
+
providerFactory && providerFactory.subscribe('collabEditProvider', (_name, providerPromise) => {
|
|
65
|
+
if (providerPromise) {
|
|
66
|
+
providerPromise.then(provider => providerResolver(provider));
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
destroy() {
|
|
71
|
+
providerFactory.unsubscribeAll('collabEditProvider');
|
|
72
|
+
if (cleanup) {
|
|
73
|
+
cleanup.then(unsubscribe => {
|
|
74
|
+
if (unsubscribe) {
|
|
75
|
+
unsubscribe();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|