@atlaskit/editor-plugin-collab-edit
Version:
Collab Edit plugin for @atlaskit/editor-core
159 lines (150 loc) • 8.11 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
import { Decoration } from '@atlaskit/editor-prosemirror/view';
// Kept out of `plugin-state.ts` so all the agent-shimmer plumbing lives together and is easy to
// remove if the approach changes. `plugin-state` just reduces the active ranges and asks this module
// to build the decorations.
/** Default time the skeleton shimmer stays on the agent-authored content (ms). */
export var AGENT_SHIMMER_DEFAULT_DURATION_MS = 3000;
/**
* Default time the purple "just edited" highlight stays after the skeleton clears (ms); `0` disables
* the highlight phase so the shimmer just reveals the content.
*/
export var AGENT_EDIT_HIGHLIGHT_DEFAULT_DURATION_MS = 2000;
// Skeleton-loader bar over the agent-authored range, plus a Rovo AI telepointer at the end.
export var AGENT_SHIMMER_CLASS = 'collab-agent-shimmer';
// Purple "just edited" highlight shown over the range after the skeleton clears — same style as the
// editor AI "improve writing" in-editor highlight (subtle purple background + dashed purple underline).
export var AGENT_EDIT_HIGHLIGHT_CLASS = 'collab-agent-edit-highlight';
export var ROVO_AGENT_TELEPOINTER_CLASS = 'ai-in-editor-telepointer';
export var ROVO_AGENT_TELEPOINTER_LABEL_CLASS = 'ai-in-editor-telepointer-label';
export var ADD_AGENT_SHIMMER_META = 'addAgentShimmer'; // register the shimmer decorations
export var HIGHLIGHT_AGENT_SHIMMER_META = 'highlightAgentShimmer'; // skeleton → purple highlight phase
export var REMOVE_AGENT_SHIMMER_META = 'removeAgentShimmer'; // remove them once the shimmer ends
// A shimmer runs in two phases: the `skeleton` loader, then (optionally) the purple `highlight` over
// the revealed content, before removal.
// A range an agent step wrote; the shimmer decorations are drawn over `from`..`to` and kept until
// removal (so positions can be re-mapped). Pure data only. `phase` selects skeleton vs purple
// highlight. `telepointerLabel` is the label for the trailing agent telepointer (shown through both
// phases); when absent, no telepointer is shown. `highlightDurationMs` is the purple-highlight
// lifetime, used to size its ease in/out animation so it matches the removal timer.
// Rovo AI in-editor telepointer/cursor shown at the end of an agent-authored range (same DOM/style
// pattern as editor-plugin-ai's in-editor direct-streaming telepointer).
var createRovoAgentTelepointer = function createRovoAgentTelepointer(label) {
var element = document.createElement('span');
element.setAttribute('data-testid', 'ai-in-editor-telepointer-widget');
element.className = ROVO_AGENT_TELEPOINTER_CLASS;
var labelElement = document.createElement('span');
labelElement.setAttribute('data-testid', 'ai-in-editor-telepointer-widget-label');
labelElement.className = ROVO_AGENT_TELEPOINTER_LABEL_CLASS;
labelElement.append(label);
element.appendChild(labelElement);
return element;
};
/**
* Pure reducer for the active shimmer ranges from a transaction's changes. Maps existing ranges
* forward as the doc changes, replaces them wholesale when a new agent batch lands (a new batch
* supersedes any still-in-flight shimmer), and drops a range when its removal timer fires. Returns a
* fresh array (never mutates in place) plus whether anything changed.
*/
export var reduceAgentShimmers = function reduceAgentShimmers(current, tr, added, removedShimmerId, highlightShimmerId) {
var next = current;
var changed = false;
// Ranges added in THIS transaction are already in post-change coords, so map the pre-existing
// ones BEFORE replacing with any new batch.
if (tr.docChanged && next.length) {
next = next.map(function (shimmer) {
return _objectSpread(_objectSpread({}, shimmer), {}, {
from: tr.mapping.map(shimmer.from, -1),
to: tr.mapping.map(shimmer.to, 1)
});
});
changed = true;
}
if (added !== null && added !== void 0 && added.length) {
next = added.map(function (shimmer) {
return _objectSpread({}, shimmer);
});
changed = true;
}
// Transition a shimmer from the skeleton phase to the purple highlight phase (skeleton timer fired).
// Guard on an actual match so a timer firing after the shimmer was already removed (superseded by a
// new batch, or cleared) doesn't rebuild the array and trigger a pointless decoration rebuild.
if (highlightShimmerId && next.some(function (shimmer) {
return shimmer.shimmerId === highlightShimmerId;
})) {
next = next.map(function (shimmer) {
return shimmer.shimmerId === highlightShimmerId ? _objectSpread(_objectSpread({}, shimmer), {}, {
phase: 'highlight'
}) : shimmer;
});
changed = true;
}
if (removedShimmerId) {
next = next.filter(function (shimmer) {
return shimmer.shimmerId !== removedShimmerId;
});
changed = true;
}
return {
changed: changed,
next: next
};
};
/**
* Builds the inline decorations (skeleton bar or purple highlight, per phase) plus the trailing
* telepointer for the active shimmer ranges. `getValidPos` clamps a raw position to a valid
* decoration position (owned by `plugin-state`). One bad range is isolated via `onError` so it can't
* kill the others.
*/
export var buildAgentShimmerDecorations = function buildAgentShimmerDecorations(tr, shimmers, getValidPos, onError) {
var decorations = [];
shimmers.forEach(function (_ref) {
var shimmerId = _ref.shimmerId,
from = _ref.from,
to = _ref.to,
telepointerLabel = _ref.telepointerLabel,
phase = _ref.phase,
highlightDurationMs = _ref.highlightDurationMs;
try {
// `getValidPos` already clamps to the last valid position, so only the lower bound needs
// guarding here (a raw `from < 1` would throw in `doc.resolve`).
var validFrom = getValidPos(tr, Math.max(from, 1));
var validTo = getValidPos(tr, to);
if (validTo <= validFrom) {
return;
}
// Inline decoration over the whole range: the grey skeleton loader, or (once revealed) the
// purple "just edited" highlight. The highlight eases in and out over its lifetime via a CSS
// animation whose duration is set inline so it matches the removal timer.
var isHighlight = phase === 'highlight';
var inlineAttrs = {
class: isHighlight ? AGENT_EDIT_HIGHLIGHT_CLASS : AGENT_SHIMMER_CLASS
};
if (isHighlight && highlightDurationMs > 0) {
inlineAttrs.style = "animation-duration: ".concat(highlightDurationMs, "ms");
}
decorations.push(Decoration.inline(validFrom, validTo, inlineAttrs, {
isAgentShimmer: true,
shimmerId: shimmerId
}));
// Rovo AI telepointer/cursor (labelled with the agent's type) at the end of the range, shown
// through BOTH the skeleton and purple-highlight phases so the agent's cursor stays put until
// the edit is fully revealed.
if (telepointerLabel) {
decorations.push(Decoration.widget(validTo, createRovoAgentTelepointer(telepointerLabel), {
isAgentShimmer: true,
shimmerId: shimmerId,
class: ROVO_AGENT_TELEPOINTER_CLASS,
key: "agent-telepointer-".concat(shimmerId),
side: 1
}));
}
} catch (err) {
// One bad range must not kill the others.
onError(err);
}
});
return decorations;
};