@atlaskit/editor-plugin-collab-edit
Version:
Collab Edit plugin for @atlaskit/editor-core
249 lines (244 loc) • 11 kB
JavaScript
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _createClass from "@babel/runtime/helpers/createClass";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import { getBrowserInfo } from '@atlaskit/editor-common/browser';
import { TELEPOINTER_DIM_CLASS, TELEPOINTER_PULSE_CLASS, TELEPOINTER_PULSE_DURING_TR_CLASS } from '@atlaskit/editor-common/collab';
import { Selection } from '@atlaskit/editor-prosemirror/state';
import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
import { Participants } from '../participants';
import { createTelepointers, findPointers, getPositionOfTelepointer, isReplaceStep, hasExistingNudge } from '../utils';
/**
* Returns position where it's possible to place a decoration.
*/
export var getValidPos = function getValidPos(tr, pos) {
var endOfDocPos = tr.doc.nodeSize - 2;
if (pos <= endOfDocPos) {
var resolvedPos = tr.doc.resolve(pos);
var backwardSelection = Selection.findFrom(resolvedPos, -1, true);
// if there's no correct cursor position before the `pos`, we try to find it after the `pos`
var forwardSelection = Selection.findFrom(resolvedPos, 1, true);
return backwardSelection ? backwardSelection.from : forwardSelection ? forwardSelection.from : pos;
}
return endOfDocPos;
};
export var PluginState = /*#__PURE__*/function () {
function PluginState(decorations, participants, sessionId) {
var collabInitalised = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
var onError = arguments.length > 4 ? arguments[4] : undefined;
var nudgeAnimations = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : new Map();
_classCallCheck(this, PluginState);
// eslint-disable-next-line no-console
_defineProperty(this, "onError", function (error) {
return console.error(error);
});
this.decorationSet = decorations;
this.participants = participants;
this.sid = sessionId;
this.isReady = collabInitalised;
this.onError = onError || this.onError;
this.nudgeAnimations = nudgeAnimations;
}
return _createClass(PluginState, [{
key: "decorations",
get: function get() {
return this.decorationSet;
}
}, {
key: "activeParticipants",
get: function get() {
return this.participants;
}
}, {
key: "sessionId",
get: function get() {
return this.sid;
}
}, {
key: "getFullName",
value: function getFullName(sessionId) {
var participant = this.participants.get(sessionId);
return participant ? participant.name : 'X';
}
}, {
key: "getInitial",
value: function getInitial(sessionId) {
return this.getFullName(sessionId).substring(0, 1).toUpperCase();
}
}, {
key: "getPresenceId",
value: function getPresenceId(sessionId) {
var _participant$presence;
var participant = this.participants.get(sessionId);
return (_participant$presence = participant === null || participant === void 0 ? void 0 : participant.presenceId) !== null && _participant$presence !== void 0 ? _participant$presence : sessionId;
}
}, {
key: "apply",
value: function apply(tr) {
var _this = this;
// Ignored via go/ees005
// eslint-disable-next-line prefer-const
var participants = this.participants,
sid = this.sid,
isReady = this.isReady;
var presenceData = tr.getMeta('presence');
var telepointerData = tr.getMeta('telepointer');
var nudgeTelepointerData = tr.getMeta('nudgeTelepointer');
var sessionIdData = tr.getMeta('sessionId');
var collabInitialised = tr.getMeta('collabInitialised');
if (typeof collabInitialised !== 'boolean') {
collabInitialised = isReady;
}
if (sessionIdData) {
sid = sessionIdData.sid;
}
var add = [];
var remove = [];
if (presenceData) {
var _presenceData$joined = presenceData.joined,
joined = _presenceData$joined === void 0 ? [] : _presenceData$joined,
_presenceData$left = presenceData.left,
left = _presenceData$left === void 0 ? [] : _presenceData$left;
participants = participants.remove(left.map(function (i) {
return i.sessionId;
}));
participants = participants.add(joined);
// Remove telepointers for users that left
left.forEach(function (i) {
var pointers = findPointers(_this.getPresenceId(i.sessionId), _this.decorationSet);
if (pointers) {
remove = remove.concat(pointers);
}
});
}
if (telepointerData) {
var sessionId = telepointerData.sessionId;
if (participants.get(sessionId) && sessionId !== sid) {
var oldPointers = findPointers(this.getPresenceId(sessionId), this.decorationSet);
if (oldPointers) {
remove = remove.concat(oldPointers);
}
var endOfDocPos = tr.doc.nodeSize - 2;
var anchor = telepointerData.selection.anchor;
var head = telepointerData.selection.head;
var rawFrom = anchor < head ? anchor : head;
var rawTo = anchor >= head ? anchor : head;
if (rawFrom > endOfDocPos) {
rawFrom = endOfDocPos;
}
if (rawTo > endOfDocPos) {
rawTo = endOfDocPos;
}
var isSelection = rawTo - rawFrom > 0;
var from = 1;
var to = 1;
try {
from = getValidPos(tr, isSelection ? Math.max(rawFrom, 0) : rawFrom);
to = isSelection ? getValidPos(tr, rawTo) : from;
} catch (err) {
this.onError(err);
}
add = add.concat(createTelepointers(from, to, sessionId, isSelection, this.getInitial(sessionId), this.getPresenceId(sessionId), this.getFullName(sessionId), hasExistingNudge(sessionId, this.nudgeAnimations)));
}
}
if (tr.docChanged) {
// Adjust decoration positions to changes made by the transaction
try {
this.decorationSet = this.decorationSet.map(tr.mapping, tr.doc, {
// Reapplies decorators those got removed by the state change
onRemove: function onRemove(spec) {
if (spec.pointer && spec.pointer.sessionId && spec.key === "telepointer-".concat(spec.pointer.sessionId)) {
var step = tr.steps.filter(isReplaceStep)[0];
if (step) {
var _spec$pointer = spec.pointer,
_sessionId = _spec$pointer.sessionId,
presenceId = _spec$pointer.presenceId;
var _ref = step,
size = _ref.slice.content.size,
_from = _ref.from;
var pos = getValidPos(tr, size ? Math.min(_from + size, tr.doc.nodeSize - 3) : Math.max(_from, 1));
add = add.concat(createTelepointers(pos, pos, _sessionId, false, _this.getInitial(_sessionId), presenceId, _this.getFullName(_sessionId), hasExistingNudge(_sessionId, _this.nudgeAnimations)));
}
}
}
});
} catch (err) {
this.onError(err);
}
}
var selection = tr.selection;
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.decorationSet.find().forEach(function (deco) {
if (deco.type.toDOM) {
var hasTelepointerDimClass = deco.type.toDOM.classList.contains(TELEPOINTER_DIM_CLASS);
var browser = getBrowserInfo();
if (deco.from === selection.from && deco.to === selection.to) {
if (!hasTelepointerDimClass) {
deco.type.toDOM.classList.add(TELEPOINTER_DIM_CLASS);
}
// Browser condition here to fix ED-14722 where telepointer
// decorations with side -1 in Firefox causes backspace issues.
// This is likely caused by contenteditable quirks in Firefox
if (!browser.gecko) {
deco.type.side = -1;
}
} else {
if (hasTelepointerDimClass) {
deco.type.toDOM.classList.remove(TELEPOINTER_DIM_CLASS);
}
deco.type.side = 0;
}
}
});
if (nudgeTelepointerData) {
var nudgeSessionId = nudgeTelepointerData === null || nudgeTelepointerData === void 0 ? void 0 : nudgeTelepointerData.sessionId;
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.decorationSet.find().forEach(function (deco) {
var _deco$spec, _deco$spec2;
if (deco.type.toDOM && participants.get(nudgeSessionId) && ((_deco$spec = deco.spec) === null || _deco$spec === void 0 || (_deco$spec = _deco$spec.pointer) === null || _deco$spec === void 0 ? void 0 : _deco$spec.sessionId) === nudgeSessionId && ((_deco$spec2 = deco.spec) === null || _deco$spec2 === void 0 ? void 0 : _deco$spec2.key) === "telepointer-".concat(nudgeSessionId)) {
// Restart animation by removing and re-adding the class
deco.type.toDOM.classList.remove(TELEPOINTER_PULSE_DURING_TR_CLASS);
deco.type.toDOM.classList.remove(TELEPOINTER_PULSE_CLASS);
void deco.type.toDOM.offsetWidth; // Force reflow
deco.type.toDOM.classList.add(TELEPOINTER_PULSE_CLASS);
_this.nudgeAnimations.set(nudgeSessionId, Date.now());
}
});
}
if (remove.length) {
this.decorationSet = this.decorationSet.remove(remove);
}
if (add.length) {
this.decorationSet = this.decorationSet.add(tr.doc, add);
}
// This piece needs to be after the decorationSet adjustments,
// otherwise it's always one step behind where the cursor is
if (telepointerData) {
var _sessionId2 = telepointerData.sessionId;
if (participants.get(_sessionId2)) {
var positionForScroll = getPositionOfTelepointer(_sessionId2, this.decorationSet);
if (positionForScroll) {
participants = participants.updateCursorPos(_sessionId2, positionForScroll);
}
}
}
var nextState = new PluginState(this.decorationSet, participants, sid, collabInitialised, this.onError, this.nudgeAnimations);
return PluginState.eq(nextState, this) ? this : nextState;
}
}], [{
key: "eq",
value: function eq(a, b) {
return a.participants === b.participants && a.sessionId === b.sessionId && a.isReady === b.isReady;
}
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}, {
key: "init",
value: function init(config) {
var doc = config.doc,
onError = config.onError;
return new PluginState(DecorationSet.create(doc, []), new Participants(), undefined, undefined, onError);
}
}]);
}();