@atlaskit/renderer
Version:
Renderer component
248 lines (234 loc) • 10.6 kB
JavaScript
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
import { useEffect } from 'react';
import { getDocument } from '@atlaskit/browser-apis';
import { DEFAULT_BLOCK_LINK_HASH_PREFIX, expandAllParentsThenScroll, expandElement, isExpandCollapsed, findNodeWithExpandParents, getLocalIdSelector } from '@atlaskit/editor-common/block-menu';
import { fg } from '@atlaskit/platform-feature-flags';
import { useStableScroll } from './useStableScroll';
/**
* useScrollToBlock - Handler for block link scrolling in the renderer with expand support
*
* This hook enables scroll-to-block functionality when blocks may be hidden inside collapsed expands.
* It searches the ADF document for the target block, identifies any parent expand nodes,
* expands them if needed, and waits for layout stability before scrolling to the block.
*
* This implementation waits for the container to stabilize (no layout shifts) before scrolling,
* which prevents issues with images loading, dynamic content, or other async operations that
* cause layout changes.
*
* @param containerRef - Optional ref to the renderer container (RendererStyleContainer)
* @param adfDoc - The ADF document to search for nodes and expand parents
*/
export var useScrollToBlock = function useScrollToBlock(containerRef, adfDoc, scrollToBlock) {
var _useStableScroll = useStableScroll({
stabilityWaitTime: 750,
maxStabilityWaitTime: 10000
}),
waitForStability = _useStableScroll.waitForStability,
cleanupStability = _useStableScroll.cleanup;
useEffect(function () {
var _getDocument;
// Only run in browser environment.
if (typeof window === 'undefined' || !(containerRef !== null && containerRef !== void 0 && containerRef.current)) {
return;
}
// Parse hash fragment for block ID (format: #block-{localId}).
var hash = window.location.hash;
var defaultPrefixWithHash = "#".concat(DEFAULT_BLOCK_LINK_HASH_PREFIX);
var blockId = hash && hash.startsWith(defaultPrefixWithHash) ? hash.slice(defaultPrefixWithHash.length) : null;
if (!blockId) {
return;
}
var retryCount = 0;
var maxRetries = 40;
var retryInterval = 250;
var intervalId = null;
var hasScrolled = false;
var cancelExpandAndScroll = null;
var scrollToElement = function scrollToElement() {
// Step 1: Search the ADF document for the node with the given blockId.
// This works even if the node is hidden inside a collapsed expand.
if (!adfDoc || !(containerRef !== null && containerRef !== void 0 && containerRef.current)) {
return false;
}
var nodeWithExpandParents = findNodeWithExpandParents(adfDoc, blockId);
if (!nodeWithExpandParents) {
// Node not found in ADF document.
return false;
}
var expandParentLocalIds = nodeWithExpandParents.expandParentLocalIds;
// Step 2: If the node has expand parents, we need to expand them first.
if (expandParentLocalIds.length > 0) {
// Find the expand elements in the DOM using their localIds.
// Note: We need to expand from outermost to innermost.
var allExpandsFound = true;
var anyExpandsCollapsed = false;
var _iterator = _createForOfIteratorHelper(expandParentLocalIds),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var expandLocalId = _step.value;
var expandContainer = containerRef.current.querySelector("[data-local-id=\"".concat(expandLocalId, "\"]"));
if (!expandContainer) {
// Expand not found in DOM yet (shouldn't happen but handle it).
allExpandsFound = false;
break;
}
// Check if this expand is collapsed.
if (isExpandCollapsed(expandContainer)) {
anyExpandsCollapsed = true;
// Expand it.
expandElement(expandContainer);
// After expanding, we need to retry to handle nested expands.
// The DOM needs time to update.
return false; // Will retry after interval.
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
if (!allExpandsFound) {
// Retry later when expands are in DOM.
return false;
}
// All parent expands are now open (or we just expanded one and need to wait).
if (anyExpandsCollapsed) {
// Just expanded something, wait for DOM update.
return false;
}
}
// Step 3: Now the target element should be visible in the DOM, find it and scroll.
var element = getLocalIdSelector(blockId, containerRef.current);
if (!element) {
// Element still not in DOM, retry.
return false;
}
// Element found and all parent expands are open! Use the utility to scroll.
// (This will handle any final edge cases and do the actual scrolling).
// Capture cleanup function to cancel pending timeouts.
if (fg('platform_editor_block_menu_v2_patch_4')) {
cancelExpandAndScroll = expandAllParentsThenScroll(element, 0, function (el) {
if (scrollToBlock) {
scrollToBlock(el);
} else {
el.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
} else {
cancelExpandAndScroll = expandAllParentsThenScroll(element);
}
return true;
};
var performScroll = function performScroll() {
if (hasScrolled) {
return;
}
// Try to scroll to element.
if (scrollToElement()) {
hasScrolled = true;
cleanup();
}
};
var attemptScroll = function attemptScroll() {
retryCount++;
// Try to find the element first.
if (!adfDoc || !(containerRef !== null && containerRef !== void 0 && containerRef.current)) {
return false;
}
var nodeWithExpandParents = findNodeWithExpandParents(adfDoc, blockId);
if (!nodeWithExpandParents) {
return false;
}
var expandParentLocalIds = nodeWithExpandParents.expandParentLocalIds;
// Check if all expands are expanded and element exists.
var allReady = true;
if (expandParentLocalIds.length > 0) {
var _iterator2 = _createForOfIteratorHelper(expandParentLocalIds),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var expandLocalId = _step2.value;
var expandContainer = containerRef.current.querySelector("[data-local-id=\"".concat(expandLocalId, "\"]"));
if (!expandContainer) {
allReady = false;
break;
}
if (isExpandCollapsed(expandContainer)) {
expandElement(expandContainer);
allReady = false;
break;
}
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
}
var element = getLocalIdSelector(blockId, containerRef.current);
if (!element) {
allReady = false;
}
// If everything is ready, start monitoring for stability.
if (allReady) {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
waitForStability(containerRef.current, performScroll);
return true;
}
// Stop retrying if we've exceeded max retries.
if (retryCount >= maxRetries) {
cleanup();
return false;
}
return false;
};
var cleanup = function cleanup() {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
cleanupStability();
// Cancel any pending expand and scroll operations.
if (cancelExpandAndScroll) {
cancelExpandAndScroll();
cancelExpandAndScroll = null;
}
};
// Try to scroll immediately.
if (attemptScroll()) {
return cleanup;
}
if (((_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.readyState) === 'complete') {
// Document is already ready, try a few more times with delays.
// This handles cases where elements are added after document ready.
intervalId = setInterval(function () {
attemptScroll();
}, retryInterval);
} else {
// Document not ready yet, wait for it and then retry.
intervalId = setInterval(function () {
var _getDocument2;
if (((_getDocument2 = getDocument()) === null || _getDocument2 === void 0 ? void 0 : _getDocument2.readyState) === 'complete') {
attemptScroll();
} else if (retryCount >= maxRetries) {
cleanup();
} else {
retryCount++;
}
}, retryInterval);
}
// Cleanup function.
return cleanup;
// Intentionally not including adfDoc in the dependency array to avoid unnecessary re-renders.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [containerRef, waitForStability, cleanupStability, scrollToBlock]);
};