@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
280 lines (273 loc) • 16.4 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
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 { BLOCK_CONTROLS_LEFT_SURFACE, BLOCK_CONTROLS_RIGHT_SURFACE } from '@atlaskit/editor-common/block-controls/surface-keys';
import { getNodeIdProvider } from '@atlaskit/editor-common/node-anchor-provider';
import { resolveSurface } from '@atlaskit/editor-ui-control-model/surface-renderer';
import { createSurfaceAnchorDecorations, TYPE_SURFACE_ANCHOR_DEC } from '../pm-plugins/decorations-surface-anchor';
import { key as blockControlsKey } from '../pm-plugins/main';
import { bindSparseGutterHover } from '../pm-plugins/sparse-gutter-hover';
import { reconcileSparseSurfaceCandidates } from '../pm-plugins/utils/sparse-surface-candidates';
import { observeSurfaceViewport } from '../pm-plugins/utils/surface-viewport';
import { getBlockControlsSurfaceTargets } from '../ui/block-controls-surface-targets';
import { createSurfaceDescriptorResolver } from './block-controls-surface-descriptors';
import { getSurfaceLifecycle } from './surface-lifecycle';
var haveAnchorPositionsChanged = function haveAnchorPositionsChanged(positions, anchors) {
return positions.length !== anchors.size || positions.some(function (position) {
return !anchors.has(position);
});
};
var areAnchorsEqual = function areAnchorsEqual(a, b) {
return a.size === b.size && _toConsumableArray(a).every(function (_ref) {
var _ref2 = _slicedToArray(_ref, 2),
position = _ref2[0],
name = _ref2[1];
return b.get(position) === name;
});
};
/** Limited mode and dragging remove every sparse anchor decoration from the document. */
var areSurfacesSuppressed = function areSurfacesSuppressed(api, state) {
var _api$limitedMode, _blockControlsKey$get;
return Boolean((api === null || api === void 0 || (_api$limitedMode = api.limitedMode) === null || _api$limitedMode === void 0 || (_api$limitedMode = _api$limitedMode.sharedState.currentState()) === null || _api$limitedMode === void 0 ? void 0 : _api$limitedMode.enabled) || ((_blockControlsKey$get = blockControlsKey.getState(state)) === null || _blockControlsKey$get === void 0 ? void 0 : _blockControlsKey$get.isDragging));
};
/** An open block menu keeps its trigger node, and that node's root, as active surface positions. */
var getMenuActiveNodes = function getMenuActiveNodes(controls, doc) {
var activeNodesByPosition = new Map();
var menu = controls !== null && controls !== void 0 && controls.isMenuOpen ? controls.menuTriggerByNode : undefined;
if (menu && controls !== null && controls !== void 0 && controls.menuTriggerBy && doc.nodeAt(menu.pos)) {
var _menu$rootPos;
var menuNode = _objectSpread(_objectSpread({}, menu), {}, {
anchorName: controls.menuTriggerBy
});
activeNodesByPosition.set(menu.pos, menuNode);
activeNodesByPosition.set((_menu$rootPos = menu.rootPos) !== null && _menu$rootPos !== void 0 ? _menu$rootPos : menu.pos, menuNode);
}
return activeNodesByPosition;
};
var reconcileCandidates = function reconcileCandidates(_ref3) {
var activeNode = _ref3.activeNode,
activeNodesByPosition = _ref3.activeNodesByPosition,
api = _ref3.api,
previous = _ref3.previous,
registryChanged = _ref3.registryChanged,
state = _ref3.state,
visiblePositions = _ref3.visiblePositions;
return reconcileSparseSurfaceCandidates({
activeNode: activeNode,
activeNodesByPosition: activeNodesByPosition,
newState: state,
state: previous.candidates,
visiblePositions: visiblePositions,
protectedPositions: _toConsumableArray(activeNodesByPosition.keys()),
resolvedSurfaces: [BLOCK_CONTROLS_LEFT_SURFACE, BLOCK_CONTROLS_RIGHT_SURFACE].map(function (surface) {
var _api$uiControlRegistr, _api$uiControlRegistr2;
return resolveSurface((_api$uiControlRegistr = api === null || api === void 0 || (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 ? void 0 : _api$uiControlRegistr2.actions.getComponents(surface)) !== null && _api$uiControlRegistr !== void 0 ? _api$uiControlRegistr : [], surface);
}),
documentChanged: previous.documentChanged,
invalidation: _objectSpread(_objectSpread({}, previous.invalidation), {}, {
all: registryChanged
})
});
};
/** True when the committed state already matches the candidates and no anchor awaits retirement. */
var isReconciled = function isReconciled(previous, candidates, lifecycle) {
return !haveAnchorPositionsChanged(candidates.positions, previous.anchors) && candidates === previous.candidates && !previous.documentChanged && !previous.invalidation && !lifecycle.hasRetirableAnchors(previous.decorations);
};
/**
* Builds anchor decorations for the candidate positions, keeping each node's existing anchor name
* and any anchor a committed or pending surface still uses.
*/
var createReconciledDecorations = function createReconciledDecorations(view, previous, positions, lifecycle) {
var previousAnchors = new Map(previous.decorations.find(undefined, undefined, function (spec) {
return spec.type === TYPE_SURFACE_ANCHOR_DEC;
}).map(function (decoration) {
return [decoration.from, String(decoration.spec.anchorName)];
}));
var nodeIdProvider = getNodeIdProvider(view);
var _createSurfaceAnchorD = createSurfaceAnchorDecorations(view.state, positions, function (node, position, previousAnchorName) {
if (previousAnchorName) {
nodeIdProvider.setIdForNode(node, previousAnchorName);
return previousAnchorName;
}
return nodeIdProvider.getOrGenerateId(node, position);
}, previousAnchors),
nextAnchors = _createSurfaceAnchorD.anchors,
desiredDecorations = _createSurfaceAnchorD.decorations;
return {
// Retiring anchors after a commit leaves the anchor map unchanged, so keep its identity.
anchors: areAnchorsEqual(nextAnchors, previous.anchors) ? previous.anchors : nextAnchors,
decorations: lifecycle.retainAnchors(view.state.doc, desiredDecorations, previous.decorations)
};
};
var resolveSurfaceSnapshot = function resolveSurfaceSnapshot(_ref4) {
var _activeNode$handleOpt, _api$uiControlRegistr3, _api$uiControlRegistr4, _api$uiControlRegistr5, _api$uiControlRegistr6;
var activeNode = _ref4.activeNode,
activeNodesByPosition = _ref4.activeNodesByPosition,
anchors = _ref4.anchors,
api = _ref4.api,
invalidated = _ref4.invalidated,
positions = _ref4.positions,
resolvers = _ref4.resolvers,
state = _ref4.state;
var candidatePositions = new Set(positions);
var input = {
activeNode: activeNode,
state: state,
surfaceActiveNodes: activeNodesByPosition,
surfaceAnchors: anchors,
targets: getBlockControlsSurfaceTargets(activeNode, positions).filter(function (_ref5) {
var position = _ref5.position;
return candidatePositions.has(position);
})
};
return {
forceVisibleOnMouseOut: Boolean(activeNode === null || activeNode === void 0 || (_activeNode$handleOpt = activeNode.handleOptions) === null || _activeNode$handleOpt === void 0 ? void 0 : _activeNode$handleOpt.isFocused),
left: resolvers.left(_objectSpread(_objectSpread({}, input), {}, {
components: (_api$uiControlRegistr3 = api === null || api === void 0 || (_api$uiControlRegistr4 = api.uiControlRegistry) === null || _api$uiControlRegistr4 === void 0 ? void 0 : _api$uiControlRegistr4.actions.getComponents(BLOCK_CONTROLS_LEFT_SURFACE)) !== null && _api$uiControlRegistr3 !== void 0 ? _api$uiControlRegistr3 : []
}), invalidated),
right: resolvers.right(_objectSpread(_objectSpread({}, input), {}, {
components: (_api$uiControlRegistr5 = api === null || api === void 0 || (_api$uiControlRegistr6 = api.uiControlRegistry) === null || _api$uiControlRegistr6 === void 0 ? void 0 : _api$uiControlRegistr6.actions.getComponents(BLOCK_CONTROLS_RIGHT_SURFACE)) !== null && _api$uiControlRegistr5 !== void 0 ? _api$uiControlRegistr5 : []
}), invalidated)
};
};
/** Changes that can add or remove anchor suppression, which also invalidates control visibility. */
var haveSuppressionInputsChanged = function haveSuppressionInputsChanged(controls, previousControls) {
return (controls === null || controls === void 0 ? void 0 : controls.isDragging) !== (previousControls === null || previousControls === void 0 ? void 0 : previousControls.isDragging) || (controls === null || controls === void 0 ? void 0 : controls.isResizerResizing) !== (previousControls === null || previousControls === void 0 ? void 0 : previousControls.isResizerResizing);
};
/** Changes that can move surfaces between blocks without invalidating control visibility. */
var haveSurfaceInputsChanged = function haveSurfaceInputsChanged(sparse, controls, previousControls) {
return Boolean((sparse === null || sparse === void 0 ? void 0 : sparse.documentChanged) || (sparse === null || sparse === void 0 ? void 0 : sparse.invalidation)) || (controls === null || controls === void 0 ? void 0 : controls.activeNode) !== (previousControls === null || previousControls === void 0 ? void 0 : previousControls.activeNode) || (controls === null || controls === void 0 ? void 0 : controls.isMenuOpen) !== (previousControls === null || previousControls === void 0 ? void 0 : previousControls.isMenuOpen) || (controls === null || controls === void 0 ? void 0 : controls.menuTriggerByNode) !== (previousControls === null || previousControls === void 0 ? void 0 : previousControls.menuTriggerByNode);
};
var subscribeToInvalidation = function subscribeToInvalidation(api, invalidate) {
var _api$uiControlRegistr7, _api$editorDisabled, _api$editorViewMode, _api$limitedMode2, _api$showDiff, _api$userIntent;
var unsubscribers = [api === null || api === void 0 || (_api$uiControlRegistr7 = api.uiControlRegistry) === null || _api$uiControlRegistr7 === void 0 ? void 0 : _api$uiControlRegistr7.actions.subscribe(invalidate), api === null || api === void 0 || (_api$editorDisabled = api.editorDisabled) === null || _api$editorDisabled === void 0 ? void 0 : _api$editorDisabled.sharedState.onChange(invalidate), api === null || api === void 0 || (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.sharedState.onChange(invalidate), api === null || api === void 0 || (_api$limitedMode2 = api.limitedMode) === null || _api$limitedMode2 === void 0 ? void 0 : _api$limitedMode2.sharedState.onChange(invalidate), api === null || api === void 0 || (_api$showDiff = api.showDiff) === null || _api$showDiff === void 0 ? void 0 : _api$showDiff.sharedState.onChange(invalidate), api === null || api === void 0 || (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 ? void 0 : _api$userIntent.sharedState.onChange(invalidate)];
return function () {
return unsubscribers.forEach(function (unsubscribe) {
return unsubscribe === null || unsubscribe === void 0 ? void 0 : unsubscribe();
});
};
};
/**
* Sparse surfaces plugin view for the CLS fix. Each animation frame reconciles which blocks have
* surfaces, commits their anchor decorations, and publishes the surfaces React renders through
* `SurfaceLifecycle`, which retains anchors until React commits their replacement.
*/
export var createSparseSurfacesViewWithLifecycle = function createSparseSurfacesViewWithLifecycle(_ref6) {
var api = _ref6.api,
pluginKey = _ref6.pluginKey,
view = _ref6.view;
var win = view.dom.ownerDocument.defaultView;
var lifecycle = getSurfaceLifecycle(view);
var resolvers = {
left: createSurfaceDescriptorResolver('left'),
right: createSurfaceDescriptorResolver('right')
};
var frame;
var visiblePositions = [];
var registryChanged = true;
var surfaceStateChanged = false;
var updateAvailableAnchors = function updateAvailableAnchors() {
var sparse = pluginKey.getState(view.state);
if (sparse) {
lifecycle.updateAnchors(sparse.decorations, areSurfacesSuppressed(api, view.state));
}
};
var reconcile = function reconcile() {
var _api$limitedMode3;
frame = undefined;
var previous = pluginKey.getState(view.state);
if (!previous || api !== null && api !== void 0 && (_api$limitedMode3 = api.limitedMode) !== null && _api$limitedMode3 !== void 0 && (_api$limitedMode3 = _api$limitedMode3.sharedState.currentState()) !== null && _api$limitedMode3 !== void 0 && _api$limitedMode3.enabled) {
return;
}
var controls = blockControlsKey.getState(view.state);
var activeNode = controls === null || controls === void 0 ? void 0 : controls.activeNode;
var activeNodesByPosition = getMenuActiveNodes(controls, view.state.doc);
var candidates = reconcileCandidates({
activeNode: activeNode,
activeNodesByPosition: activeNodesByPosition,
api: api,
previous: previous,
registryChanged: registryChanged,
state: view.state,
visiblePositions: visiblePositions
});
var visibilityChanged = registryChanged;
registryChanged = false;
if (!surfaceStateChanged && !visibilityChanged && isReconciled(previous, candidates, lifecycle)) {
return;
}
surfaceStateChanged = false;
var _createReconciledDeco = createReconciledDecorations(view, previous, candidates.positions, lifecycle),
anchors = _createReconciledDeco.anchors,
decorations = _createReconciledDeco.decorations;
view.dispatch(view.state.tr.setMeta(pluginKey, {
reconciliation: {
activeNodesByPosition: activeNodesByPosition,
anchors: anchors,
candidates: candidates,
decorations: decorations
},
type: 'commitReconciliation'
}));
lifecycle.publish(resolveSurfaceSnapshot({
activeNode: activeNode,
activeNodesByPosition: activeNodesByPosition,
anchors: anchors,
api: api,
invalidated: visibilityChanged || Boolean(previous.invalidation),
positions: candidates.positions,
resolvers: resolvers,
state: view.state
}), anchors);
};
var schedule = function schedule() {
if (frame === undefined && win) {
frame = win.requestAnimationFrame(reconcile);
}
};
var invalidate = function invalidate() {
if (areSurfacesSuppressed(api, view.state)) {
updateAvailableAnchors();
}
registryChanged = true;
schedule();
};
updateAvailableAnchors();
// A committed tree may no longer need older anchors; the next reconcile retires them.
lifecycle.setOnCommit(schedule);
var observer = observeSurfaceViewport(view, function (positions) {
visiblePositions = positions;
schedule();
});
var unsubscribe = subscribeToInvalidation(api, invalidate);
var unbindGutterHover = bindSparseGutterHover({
api: api,
view: view
});
return {
update: function update(currentView, previousState) {
updateAvailableAnchors();
observer.update();
var controls = blockControlsKey.getState(currentView.state);
var previousControls = blockControlsKey.getState(previousState);
if (haveSuppressionInputsChanged(controls, previousControls)) {
invalidate();
}
if (haveSurfaceInputsChanged(pluginKey.getState(currentView.state), controls, previousControls)) {
surfaceStateChanged = true;
schedule();
}
},
destroy: function destroy() {
if (frame !== undefined) {
win === null || win === void 0 || win.cancelAnimationFrame(frame);
}
lifecycle.destroy();
observer.destroy();
unbindGutterHover === null || unbindGutterHover === void 0 || unbindGutterHover();
unsubscribe();
}
};
};