UNPKG

@atlaskit/editor-plugin-collab-edit

Version:

Collab Edit plugin for @atlaskit/editor-core

302 lines (294 loc) 13.9 kB
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'; import { ADD_AGENT_SHIMMER_META, buildAgentShimmerDecorations, HIGHLIGHT_AGENT_SHIMMER_META, reduceAgentShimmers, REMOVE_AGENT_SHIMMER_META } from './agent-shimmer-decorations'; /** * 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(); var agentShimmers = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : []; _classCallCheck(this, PluginState); // Active agent-edit shimmer ranges (pure data). Replaced with a fresh array of fresh // objects whenever it changes — never mutated in place. // 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; this.agentShimmers = agentShimmers; } 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 agentShimmerData = tr.getMeta(ADD_AGENT_SHIMMER_META); var removeAgentShimmerId = tr.getMeta(REMOVE_AGENT_SHIMMER_META); var highlightAgentShimmerId = tr.getMeta(HIGHLIGHT_AGENT_SHIMMER_META); 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) { var _deco$spec; // Never let telepointer dim/side logic touch agent-shimmer decorations. if ((_deco$spec = deco.spec) !== null && _deco$spec !== void 0 && _deco$spec.isAgentShimmer) { return; } 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$spec2, _deco$spec3, _deco$spec4; // Never let telepointer nudge logic touch agent-shimmer decorations. if ((_deco$spec2 = deco.spec) !== null && _deco$spec2 !== void 0 && _deco$spec2.isAgentShimmer) { return; } if (deco.type.toDOM && participants.get(nudgeSessionId) && ((_deco$spec3 = deco.spec) === null || _deco$spec3 === void 0 || (_deco$spec3 = _deco$spec3.pointer) === null || _deco$spec3 === void 0 ? void 0 : _deco$spec3.sessionId) === nudgeSessionId && ((_deco$spec4 = deco.spec) === null || _deco$spec4 === void 0 ? void 0 : _deco$spec4.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()); } }); } // Register / keep-aligned / remove the agent-edit purple highlight // decorations. Fully fault-isolated — it works on local copies and only merges into the shared // `add`/`remove` (and commits `this.agentShimmers`) after the whole block succeeds, so a fault // here can never corrupt telepointer decorations or the shared decoration set. Never mutates // shimmer entries in place: each change produces a fresh array of fresh objects. try { var _reduceAgentShimmers = reduceAgentShimmers(this.agentShimmers, tr, agentShimmerData, removeAgentShimmerId, highlightAgentShimmerId), changed = _reduceAgentShimmers.changed, next = _reduceAgentShimmers.next; if (changed) { // Build into local arrays; only merge into the shared add/remove once the block succeeds. var agentRemove = this.decorationSet.find(undefined, undefined, function (spec) { return spec.isAgentShimmer; }); var agentAdd = buildAgentShimmerDecorations(tr, next, getValidPos, this.onError); // Commit only after the whole block succeeded. remove = remove.concat(agentRemove); add = add.concat(agentAdd); this.agentShimmers = next; } } catch (err) { this.onError(err); // Degrade to no shimmer without corrupting telepointer decorations: drop all agent // decorations and clear agent state, leaving the telepointer `add`/`remove` untouched. this.agentShimmers = []; try { remove = remove.concat(this.decorationSet.find(undefined, undefined, function (spec) { return spec.isAgentShimmer; })); } catch (_unused) { // Teardown must not re-throw. } } 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, this.agentShimmers); 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); } }]); }();