@atlaskit/editor-plugin-block-controls
Version:
Block controls plugin for @atlaskit/editor-core
192 lines • 6.31 kB
JavaScript
const NODE_NAME_ATTRIBUTE = 'data-prosemirror-node-name';
/**
* Tracks block DOM nodes that intersect the browser viewport. The browser owns clipping and
* visibility decisions; this helper only translates observer targets back to PM positions.
*/
export const observeSurfaceViewport = (view, onChange) => {
const win = view.dom.ownerDocument.defaultView;
if (!win || typeof win.IntersectionObserver === 'undefined') {
return {
update: () => {},
destroy: () => {}
};
}
const targets = new Map();
const visibleTargets = new Set();
let previousPositions = [];
let previousDoc = view.state.doc;
let destroyed = false;
const resolvePosition = element => {
try {
const domPosition = view.posAtDOM(element, 0);
const nodeName = element.getAttribute(NODE_NAME_ATTRIBUTE);
const directNode = view.state.doc.nodeAt(domPosition);
if (directNode !== null && directNode !== void 0 && directNode.isBlock && directNode.type.name === nodeName && view.nodeDOM(domPosition) === element) {
return domPosition;
}
const $position = view.state.doc.resolve(domPosition);
for (let depth = $position.depth; depth > 0; depth--) {
const node = $position.node(depth);
if (!node.isBlock || node.type.name !== nodeName) {
continue;
}
const position = $position.before(depth);
const nodeDOM = view.nodeDOM(position);
if (nodeDOM === element || nodeDOM instanceof Node && (nodeDOM.contains(element) || element.contains(nodeDOM))) {
return position;
}
}
return undefined;
} catch {
return undefined;
}
};
const getVisiblePositions = () => {
const positions = new Set();
visibleTargets.forEach(element => {
var _targets$get;
const position = (_targets$get = targets.get(element)) === null || _targets$get === void 0 ? void 0 : _targets$get.position;
if (position !== undefined) {
positions.add(position);
}
});
return Array.from(positions).sort((first, second) => first - second);
};
const emit = () => {
if (destroyed) {
return;
}
const positions = getVisiblePositions();
if (positions.length !== previousPositions.length || positions.some((position, index) => position !== previousPositions[index])) {
previousPositions = positions;
onChange(positions);
}
};
const intersectionObserver = new win.IntersectionObserver(entries => {
let changed = false;
for (const entry of entries) {
const element = entry.target;
const target = targets.get(element);
if (!target || !view.dom.contains(element)) {
continue;
}
const isIntersecting = entry.isIntersecting;
if (target.isIntersecting !== isIntersecting) {
target.isIntersecting = isIntersecting;
if (isIntersecting) {
target.position = resolvePosition(element);
if (target.position !== undefined) {
visibleTargets.add(element);
} else {
target.isIntersecting = false;
}
} else {
visibleTargets.delete(element);
}
changed = true;
}
}
if (changed) {
emit();
}
}, {
root: null,
threshold: 0
});
const removeTarget = element => {
const target = targets.get(element);
if (!target) {
return;
}
targets.delete(element);
visibleTargets.delete(element);
intersectionObserver.unobserve(element);
};
const observeElement = element => {
var _view$state$schema$no;
const nodeName = element.getAttribute(NODE_NAME_ATTRIBUTE);
if (element === view.dom || !nodeName || !((_view$state$schema$no = view.state.schema.nodes[nodeName]) !== null && _view$state$schema$no !== void 0 && _view$state$schema$no.isBlock) || !view.dom.contains(element)) {
return;
}
if (targets.has(element)) {
return;
}
targets.set(element, {
isIntersecting: false
});
intersectionObserver.observe(element);
};
const observeSubtree = node => {
if (node.nodeType !== Node.ELEMENT_NODE) {
return;
}
const element = node;
observeElement(element);
const descendants = element.querySelectorAll(`[${NODE_NAME_ATTRIBUTE}]`);
for (let index = 0; index < descendants.length; index++) {
observeElement(descendants[index]);
}
};
const unobserveSubtree = node => {
if (node.nodeType !== Node.ELEMENT_NODE) {
return;
}
const element = node;
removeTarget(element);
const descendants = element.querySelectorAll(`[${NODE_NAME_ATTRIBUTE}]`);
for (let index = 0; index < descendants.length; index++) {
removeTarget(descendants[index]);
}
};
const mutationObserver = new win.MutationObserver(records => {
let removedVisibleTarget = false;
for (const record of records) {
for (let index = 0; index < record.removedNodes.length; index++) {
const node = record.removedNodes[index];
if (node.nodeType === Node.ELEMENT_NODE) {
if (view.dom.contains(node)) {
continue;
}
const before = visibleTargets.size;
unobserveSubtree(node);
removedVisibleTarget ||= before !== visibleTargets.size;
}
}
for (let index = 0; index < record.addedNodes.length; index++) {
observeSubtree(record.addedNodes[index]);
}
}
if (removedVisibleTarget) {
emit();
}
});
const initialTargets = view.dom.querySelectorAll(`[${NODE_NAME_ATTRIBUTE}]`);
for (let index = 0; index < initialTargets.length; index++) {
observeElement(initialTargets[index]);
}
mutationObserver.observe(view.dom, {
childList: true,
subtree: true
});
return {
update() {
if (previousDoc !== view.state.doc) {
previousDoc = view.state.doc;
visibleTargets.forEach(element => {
const target = targets.get(element);
if (target) {
target.position = resolvePosition(element);
}
});
emit();
}
},
destroy() {
destroyed = true;
mutationObserver.disconnect();
intersectionObserver.disconnect();
targets.clear();
visibleTargets.clear();
}
};
};