@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
305 lines (301 loc) • 15.6 kB
JavaScript
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
};
};