UNPKG

@atlaskit/editor-plugin-collab-edit

Version:

Collab Edit plugin for @atlaskit/editor-core

237 lines (232 loc) 9.69 kB
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 const getValidPos = (tr, pos) => { const endOfDocPos = tr.doc.nodeSize - 2; if (pos <= endOfDocPos) { const resolvedPos = tr.doc.resolve(pos); const backwardSelection = Selection.findFrom(resolvedPos, -1, true); // if there's no correct cursor position before the `pos`, we try to find it after the `pos` const forwardSelection = Selection.findFrom(resolvedPos, 1, true); return backwardSelection ? backwardSelection.from : forwardSelection ? forwardSelection.from : pos; } return endOfDocPos; }; export class PluginState { get decorations() { return this.decorationSet; } get activeParticipants() { return this.participants; } get sessionId() { return this.sid; } constructor(decorations, participants, sessionId, collabInitalised = false, onError, nudgeAnimations = new Map()) { // eslint-disable-next-line no-console _defineProperty(this, "onError", error => console.error(error)); this.decorationSet = decorations; this.participants = participants; this.sid = sessionId; this.isReady = collabInitalised; this.onError = onError || this.onError; this.nudgeAnimations = nudgeAnimations; } getFullName(sessionId) { const participant = this.participants.get(sessionId); return participant ? participant.name : 'X'; } getInitial(sessionId) { return this.getFullName(sessionId).substring(0, 1).toUpperCase(); } getPresenceId(sessionId) { var _participant$presence; const 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; } apply(tr) { // Ignored via go/ees005 // eslint-disable-next-line prefer-const let { participants, sid, isReady } = this; const presenceData = tr.getMeta('presence'); const telepointerData = tr.getMeta('telepointer'); const nudgeTelepointerData = tr.getMeta('nudgeTelepointer'); const sessionIdData = tr.getMeta('sessionId'); let collabInitialised = tr.getMeta('collabInitialised'); if (typeof collabInitialised !== 'boolean') { collabInitialised = isReady; } if (sessionIdData) { sid = sessionIdData.sid; } let add = []; let remove = []; if (presenceData) { const { joined = [], left = [] } = presenceData; participants = participants.remove(left.map(i => i.sessionId)); participants = participants.add(joined); // Remove telepointers for users that left left.forEach(i => { const pointers = findPointers(this.getPresenceId(i.sessionId), this.decorationSet); if (pointers) { remove = remove.concat(pointers); } }); } if (telepointerData) { const { sessionId } = telepointerData; if (participants.get(sessionId) && sessionId !== sid) { const oldPointers = findPointers(this.getPresenceId(sessionId), this.decorationSet); if (oldPointers) { remove = remove.concat(oldPointers); } const endOfDocPos = tr.doc.nodeSize - 2; const anchor = telepointerData.selection.anchor; const head = telepointerData.selection.head; let rawFrom = anchor < head ? anchor : head; let rawTo = anchor >= head ? anchor : head; if (rawFrom > endOfDocPos) { rawFrom = endOfDocPos; } if (rawTo > endOfDocPos) { rawTo = endOfDocPos; } const isSelection = rawTo - rawFrom > 0; let from = 1; let 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: spec => { if (spec.pointer && spec.pointer.sessionId && spec.key === `telepointer-${spec.pointer.sessionId}`) { const step = tr.steps.filter(isReplaceStep)[0]; if (step) { const { sessionId, presenceId } = spec.pointer; const { slice: { content: { size } }, from // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any } = step; const 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); } } const { selection } = tr; // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any this.decorationSet.find().forEach(deco => { if (deco.type.toDOM) { const hasTelepointerDimClass = deco.type.toDOM.classList.contains(TELEPOINTER_DIM_CLASS); const 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) { const 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(deco => { var _deco$spec, _deco$spec$pointer, _deco$spec2; if (deco.type.toDOM && participants.get(nudgeSessionId) && ((_deco$spec = deco.spec) === null || _deco$spec === void 0 ? void 0 : (_deco$spec$pointer = _deco$spec.pointer) === null || _deco$spec$pointer === void 0 ? void 0 : _deco$spec$pointer.sessionId) === nudgeSessionId && ((_deco$spec2 = deco.spec) === null || _deco$spec2 === void 0 ? void 0 : _deco$spec2.key) === `telepointer-${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) { const { sessionId } = telepointerData; if (participants.get(sessionId)) { const positionForScroll = getPositionOfTelepointer(sessionId, this.decorationSet); if (positionForScroll) { participants = participants.updateCursorPos(sessionId, positionForScroll); } } } const nextState = new PluginState(this.decorationSet, participants, sid, collabInitialised, this.onError, this.nudgeAnimations); return PluginState.eq(nextState, this) ? this : nextState; } static 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 static init(config) { const { doc, onError } = config; return new PluginState(DecorationSet.create(doc, []), new Participants(), undefined, undefined, onError); } }