UNPKG

@atlaskit/editor-plugin-block-controls

Version:

Block controls plugin for @atlaskit/editor-core

287 lines (279 loc) • 17.1 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.createSparseSurfacesViewWithLifecycle = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); var _surfaceKeys = require("@atlaskit/editor-common/block-controls/surface-keys"); var _nodeAnchorProvider = require("@atlaskit/editor-common/node-anchor-provider"); var _surfaceRenderer = require("@atlaskit/editor-ui-control-model/surface-renderer"); var _decorationsSurfaceAnchor = require("../pm-plugins/decorations-surface-anchor"); var _main = require("../pm-plugins/main"); var _sparseGutterHover = require("../pm-plugins/sparse-gutter-hover"); var _sparseSurfaceCandidates = require("../pm-plugins/utils/sparse-surface-candidates"); var _surfaceViewport = require("../pm-plugins/utils/surface-viewport"); var _blockControlsSurfaceTargets = require("../ui/block-controls-surface-targets"); var _blockControlsSurfaceDescriptors = require("./block-controls-surface-descriptors"); var _surfaceLifecycle = require("./surface-lifecycle"); 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) { (0, _defineProperty2.default)(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; } 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 && (0, _toConsumableArray2.default)(a).every(function (_ref) { var _ref2 = (0, _slicedToArray2.default)(_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 = _main.key.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 (0, _sparseSurfaceCandidates.reconcileSparseSurfaceCandidates)({ activeNode: activeNode, activeNodesByPosition: activeNodesByPosition, newState: state, state: previous.candidates, visiblePositions: visiblePositions, protectedPositions: (0, _toConsumableArray2.default)(activeNodesByPosition.keys()), resolvedSurfaces: [_surfaceKeys.BLOCK_CONTROLS_LEFT_SURFACE, _surfaceKeys.BLOCK_CONTROLS_RIGHT_SURFACE].map(function (surface) { var _api$uiControlRegistr, _api$uiControlRegistr2; return (0, _surfaceRenderer.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 === _decorationsSurfaceAnchor.TYPE_SURFACE_ANCHOR_DEC; }).map(function (decoration) { return [decoration.from, String(decoration.spec.anchorName)]; })); var nodeIdProvider = (0, _nodeAnchorProvider.getNodeIdProvider)(view); var _createSurfaceAnchorD = (0, _decorationsSurfaceAnchor.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: (0, _blockControlsSurfaceTargets.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(_surfaceKeys.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(_surfaceKeys.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. */ var createSparseSurfacesViewWithLifecycle = exports.createSparseSurfacesViewWithLifecycle = function createSparseSurfacesViewWithLifecycle(_ref6) { var api = _ref6.api, pluginKey = _ref6.pluginKey, view = _ref6.view; var win = view.dom.ownerDocument.defaultView; var lifecycle = (0, _surfaceLifecycle.getSurfaceLifecycle)(view); var resolvers = { left: (0, _blockControlsSurfaceDescriptors.createSurfaceDescriptorResolver)('left'), right: (0, _blockControlsSurfaceDescriptors.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 = _main.key.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 = (0, _surfaceViewport.observeSurfaceViewport)(view, function (positions) { visiblePositions = positions; schedule(); }); var unsubscribe = subscribeToInvalidation(api, invalidate); var unbindGutterHover = (0, _sparseGutterHover.bindSparseGutterHover)({ api: api, view: view }); return { update: function update(currentView, previousState) { updateAvailableAnchors(); observer.update(); var controls = _main.key.getState(currentView.state); var previousControls = _main.key.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(); } }; };