UNPKG

@atlaskit/editor-plugin-block-controls

Version:

Block controls plugin for @atlaskit/editor-core

305 lines (301 loc) 15.6 kB
import { BLOCK_CONTROL_UI_CONTEXT } from '@atlaskit/editor-common/block-controls/block-control-ui-context'; import { createSurfaceContext } from '@atlaskit/editor-ui-control-model/create-surface-context'; import { willComponentRender } from '@atlaskit/editor-ui-control-model/surface-renderer'; import { createBlockControlsSurfaceContextForPosition } from '../../ui/block-controls-surface-context'; import { hasSurfaceControls } from '../../ui/utils/has-surface-controls'; /** * Viewport positions are post-transaction positions supplied by the DOM observer. The reducer * never needs to inspect DOM or search document IDs for viewport discovery. */ const NO_ACTIVE = 'none'; const EMPTY_CANDIDATES = new Map(); const EMPTY_POSITIONS = []; const EMPTY_PROTECTED_POSITIONS = []; export const emptySparseSurfaceCandidateState = { activeRevision: NO_ACTIVE, candidates: EMPTY_CANDIDATES, positions: EMPTY_POSITIONS, protectedPositions: EMPTY_PROTECTED_POSITIONS, visiblePositions: EMPTY_POSITIONS }; const getActiveRevision = activeNode => { var _activeNode$rootPos, _activeNode$rootNodeT; return activeNode ? [activeNode.pos, activeNode.nodeType, (_activeNode$rootPos = activeNode.rootPos) !== null && _activeNode$rootPos !== void 0 ? _activeNode$rootPos : activeNode.pos, (_activeNode$rootNodeT = activeNode.rootNodeType) !== null && _activeNode$rootNodeT !== void 0 ? _activeNode$rootNodeT : ''].join(':') : NO_ACTIVE; }; const isInRange = (position, range) => position >= range.from && position <= range.to; const normalizeRange = (range, contentSize) => ({ from: Math.max(0, Math.min(contentSize, Math.min(range.from, range.to))), to: Math.max(0, Math.min(contentSize, Math.max(range.from, range.to))) }); const getSurfaces = resolvedSurfaces => resolvedSurfaces.flatMap(({ childrenMap, components, root }) => root && hasSurfaceControls(components) ? [{ childrenMap, root }] : []); const getNodeId = node => { var _node$attrs; const localId = (_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.localId; return typeof localId === 'string' ? localId : undefined; }; const sameCandidate = (first, second) => (first === null || first === void 0 ? void 0 : first.id) === (second === null || second === void 0 ? void 0 : second.id) && (first === null || first === void 0 ? void 0 : first.contextRevision) === (second === null || second === void 0 ? void 0 : second.contextRevision) && (first === null || first === void 0 ? void 0 : first.isPersistent) === (second === null || second === void 0 ? void 0 : second.isPersistent) && (first === null || first === void 0 ? void 0 : first.nodeRef) === (second === null || second === void 0 ? void 0 : second.nodeRef) && (first === null || first === void 0 ? void 0 : first.nodeType) === (second === null || second === void 0 ? void 0 : second.nodeType) && (first === null || first === void 0 ? void 0 : first.nodeSize) === (second === null || second === void 0 ? void 0 : second.nodeSize) && (first === null || first === void 0 ? void 0 : first.pos) === (second === null || second === void 0 ? void 0 : second.pos) && (first === null || first === void 0 ? void 0 : first.renders) === (second === null || second === void 0 ? void 0 : second.renders); const sameCandidateMap = (first, second) => { if (first.size !== second.size) { return false; } for (const [position, candidate] of first) { if (!sameCandidate(candidate, second.get(position))) { return false; } } return true; }; const samePositions = (first, second) => first.length === second.length && first.every((position, index) => position === second[index]); const normalizePositions = positions => Array.from(new Set(positions)).sort((first, second) => first - second); const mapPositions = (positions, tr) => normalizePositions(positions.map(position => tr.docChanged ? tr.mapping.mapResult(position, 1) : { deleted: false, pos: position }).filter(result => !result.deleted).map(result => result.pos)); const createSurfaceContextAtPosition = (state, position, activeNode) => { const blockControlsContext = createBlockControlsSurfaceContextForPosition(state, position, activeNode); return blockControlsContext ? createSurfaceContext(BLOCK_CONTROL_UI_CONTEXT, blockControlsContext) : undefined; }; const willAnySurfaceRenderAt = (surfaces, state, position, activeNode) => { const surfaceContext = createSurfaceContextAtPosition(state, position, activeNode); return surfaces.some(({ childrenMap, root }) => willComponentRender(root, childrenMap, surfaceContext)); }; const addCandidateAt = ({ candidates, position, state, surfaces, activeNode }) => { const node = state.doc.nodeAt(position); if (!(node !== null && node !== void 0 && node.isBlock)) { candidates.delete(position); return; } const isPersistent = willAnySurfaceRenderAt(surfaces, state, position, undefined); const renders = isPersistent || activeNode !== undefined && willAnySurfaceRenderAt(surfaces, state, position, activeNode); candidates.set(position, { contextRevision: getActiveRevision(activeNode), id: getNodeId(node), isPersistent, nodeRef: node, nodeType: node.type.name, nodeSize: node.nodeSize, renders, pos: position }); }; const isInvalidated = (candidate, position, invalidation, changedRange) => { var _invalidation$ids, _invalidation$positio, _invalidation$ranges; if (invalidation !== null && invalidation !== void 0 && invalidation.all) { return true; } if (invalidation !== null && invalidation !== void 0 && (_invalidation$ids = invalidation.ids) !== null && _invalidation$ids !== void 0 && _invalidation$ids.some(id => id === candidate.id)) { return true; } if (invalidation !== null && invalidation !== void 0 && (_invalidation$positio = invalidation.positions) !== null && _invalidation$positio !== void 0 && _invalidation$positio.includes(position)) { return true; } return Boolean(changedRange && isInRange(position, changedRange) || (invalidation === null || invalidation === void 0 ? void 0 : (_invalidation$ranges = invalidation.ranges) === null || _invalidation$ranges === void 0 ? void 0 : _invalidation$ranges.some(range => isInRange(position, range)))); }; const getPositions = candidates => Array.from(candidates.values()).filter(candidate => candidate.renders).map(candidate => candidate.pos).sort((first, second) => first - second); const mapCandidates = (state, tr, retain) => { const mapped = new Map(); for (const candidate of state.candidates.values()) { const result = tr.docChanged ? tr.mapping.mapResult(candidate.pos, 1) : { deleted: false, pos: candidate.pos }; if (!result.deleted && retain(result.pos, candidate)) { mapped.set(result.pos, result.pos === candidate.pos ? candidate : { ...candidate, pos: result.pos }); } } return mapped; }; /** Map cache state during PM apply. This phase never calls isHidden or scans doc. */ export const mapSparseSurfaceCandidates = ({ newState, previousState, tr }) => { if (!tr.docChanged) { return previousState; } const candidates = new Map(); for (const candidate of previousState.candidates.values()) { const result = tr.mapping.mapResult(candidate.pos, 1); if (!result.deleted) { candidates.set(result.pos, result.pos === candidate.pos ? candidate : { ...candidate, pos: result.pos }); } } const protectedPositions = normalizePositions(previousState.protectedPositions.map(position => tr.mapping.mapResult(position, 1)).filter(result => !result.deleted).map(result => result.pos)); const visiblePositions = mapPositions(previousState.visiblePositions, tr); const positions = getPositions(candidates); const candidatesUnchanged = sameCandidateMap(previousState.candidates, candidates); const positionsUnchanged = samePositions(previousState.positions, positions); const protectedUnchanged = samePositions(previousState.protectedPositions, protectedPositions); if (candidatesUnchanged && positionsUnchanged && protectedUnchanged && samePositions(previousState.visiblePositions, visiblePositions)) { return previousState; } return { ...previousState, candidates: candidatesUnchanged ? previousState.candidates : candidates, positions: positionsUnchanged ? previousState.positions : positions, protectedPositions: protectedUnchanged ? previousState.protectedPositions : protectedPositions, visiblePositions }; }; /** * Reconcile after view state is current. Predicate work stays bounded to cache, * exact visible positions, explicit ranges, and protected active/menu positions. */ export const reconcileSparseSurfaceCandidates = ({ activeNodesByPosition, activeNode, changedRange, documentChanged = false, invalidation, newState, protectedPositions, resolvedSurfaces, state, visiblePositions }) => reduceSparseSurfaceCandidates({ activeNode, meta: { activeNodesByPosition, changedRange, documentChanged, invalidation, protectedPositions, visiblePositions }, newState, previousState: state, resolvedSurfaces, tr: newState.tr }); /** * Update sparse candidates. Viewport and invalidation metadata are the only * discovery inputs; active changes re-evaluate retained cache, never document. */ export const reduceSparseSurfaceCandidates = ({ activeNode, meta, newState, previousState, resolvedSurfaces, tr }) => { var _meta$visiblePosition, _meta$protectedPositi, _meta$documentChanged, _invalidation$positio2; const activeRevision = getActiveRevision(activeNode); const activeChanged = activeRevision !== previousState.activeRevision; const hasVisiblePositionsUpdate = Boolean(meta && Object.prototype.hasOwnProperty.call(meta, 'visiblePositions')); const visiblePositions = hasVisiblePositionsUpdate ? normalizePositions((_meta$visiblePosition = meta === null || meta === void 0 ? void 0 : meta.visiblePositions) !== null && _meta$visiblePosition !== void 0 ? _meta$visiblePosition : EMPTY_POSITIONS) : tr.docChanged ? mapPositions(previousState.visiblePositions, tr) : previousState.visiblePositions; const visiblePositionsChanged = !samePositions(visiblePositions, previousState.visiblePositions); const requestedProtectedPositions = normalizePositions((_meta$protectedPositi = meta === null || meta === void 0 ? void 0 : meta.protectedPositions) !== null && _meta$protectedPositi !== void 0 ? _meta$protectedPositi : previousState.protectedPositions); const protectedPositions = samePositions(previousState.protectedPositions, requestedProtectedPositions) ? previousState.protectedPositions : requestedProtectedPositions; const protectedSet = new Set(protectedPositions); if (activeNode) { var _activeNode$rootPos2; protectedSet.add(activeNode.pos); protectedSet.add((_activeNode$rootPos2 = activeNode.rootPos) !== null && _activeNode$rootPos2 !== void 0 ? _activeNode$rootPos2 : activeNode.pos); } const visibleSet = new Set(visiblePositions); const retain = (position, candidate) => ((candidate === null || candidate === void 0 ? void 0 : candidate.isPersistent) || protectedSet.has(position) || visibleSet.has(position)) && position >= 0 && position <= newState.doc.content.size; const surfaces = getSurfaces(resolvedSurfaces); const activeNodeAt = position => { var _ref, _meta$activeNodesByPo, _meta$activeNodesByPo2; return (_ref = (_meta$activeNodesByPo = meta === null || meta === void 0 ? void 0 : (_meta$activeNodesByPo2 = meta.activeNodesByPosition) === null || _meta$activeNodesByPo2 === void 0 ? void 0 : _meta$activeNodesByPo2.get(position)) !== null && _meta$activeNodesByPo !== void 0 ? _meta$activeNodesByPo : activeNode) !== null && _ref !== void 0 ? _ref : undefined; }; const changedRange = meta !== null && meta !== void 0 && meta.changedRange ? normalizeRange(meta.changedRange, newState.doc.content.size) : undefined; const invalidation = meta === null || meta === void 0 ? void 0 : meta.invalidation; const documentChanged = (_meta$documentChanged = meta === null || meta === void 0 ? void 0 : meta.documentChanged) !== null && _meta$documentChanged !== void 0 ? _meta$documentChanged : tr.docChanged; const previousCandidates = mapCandidates(previousState, tr, retain); const dirty = (candidate, position) => { const currentNode = newState.doc.nodeAt(position); const currentContextRevision = getActiveRevision(activeNodeAt(position)); return activeChanged || candidate.contextRevision !== currentContextRevision || candidate.nodeRef !== currentNode || isInvalidated(candidate, position, invalidation, changedRange); }; if (surfaces.length === 0) { const noCandidates = previousCandidates.size === 0; if (noCandidates && previousState.positions.length === 0 && samePositions(visiblePositions, previousState.visiblePositions) && activeRevision === previousState.activeRevision && protectedPositions === previousState.protectedPositions) { return previousState; } return { activeRevision, candidates: noCandidates ? previousState.candidates : EMPTY_CANDIDATES, positions: EMPTY_POSITIONS, protectedPositions, visiblePositions }; } for (const [position, candidate] of previousCandidates) { if (dirty(candidate, position)) { addCandidateAt({ candidates: previousCandidates, position, state: newState, surfaces, activeNode: activeNodeAt(position) }); const updatedCandidate = previousCandidates.get(position); if (updatedCandidate && !retain(position, updatedCandidate)) { previousCandidates.delete(position); } } } // IO supplies exact visible positions. Iterate those positions directly so a long visible // container cannot cause a traversal of its entire nested document. if (visiblePositionsChanged || documentChanged || invalidation !== null && invalidation !== void 0 && invalidation.all || changedRange) { for (const position of visiblePositions) { if (!previousCandidates.has(position)) { addCandidateAt({ candidates: previousCandidates, position, state: newState, surfaces, activeNode: activeNodeAt(position) }); } } } const directPositions = new Set([...protectedSet, ...((_invalidation$positio2 = invalidation === null || invalidation === void 0 ? void 0 : invalidation.positions) !== null && _invalidation$positio2 !== void 0 ? _invalidation$positio2 : [])]); for (const position of directPositions) { if (retain(position) && !previousCandidates.has(position)) { addCandidateAt({ candidates: previousCandidates, position, state: newState, surfaces, activeNode: activeNodeAt(position) }); } } const positions = getPositions(previousCandidates); if (sameCandidateMap(previousState.candidates, previousCandidates) && samePositions(previousState.positions, positions) && samePositions(previousState.visiblePositions, visiblePositions) && activeRevision === previousState.activeRevision && protectedPositions === previousState.protectedPositions) { return previousState; } return { activeRevision, candidates: sameCandidateMap(previousState.candidates, previousCandidates) ? previousState.candidates : previousCandidates, positions: samePositions(previousState.positions, positions) ? previousState.positions : positions, protectedPositions, visiblePositions }; };