UNPKG

@atlaskit/editor-plugin-block-controls

Version:

Block controls plugin for @atlaskit/editor-core

192 lines 6.31 kB
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(); } }; };