UNPKG

@atlaskit/renderer

Version:
253 lines (239 loc) • 10.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useScrollToBlock = void 0; var _react = require("react"); var _browserApis = require("@atlaskit/browser-apis"); var _blockMenu = require("@atlaskit/editor-common/block-menu"); var _platformFeatureFlags = require("@atlaskit/platform-feature-flags"); var _useStableScroll2 = require("./useStableScroll"); 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; } /** * 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 */ var useScrollToBlock = exports.useScrollToBlock = function useScrollToBlock(containerRef, adfDoc, scrollToBlock) { var _useStableScroll = (0, _useStableScroll2.useStableScroll)({ stabilityWaitTime: 750, maxStabilityWaitTime: 10000 }), waitForStability = _useStableScroll.waitForStability, cleanupStability = _useStableScroll.cleanup; (0, _react.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(_blockMenu.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 = (0, _blockMenu.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 ((0, _blockMenu.isExpandCollapsed)(expandContainer)) { anyExpandsCollapsed = true; // Expand it. (0, _blockMenu.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 = (0, _blockMenu.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 ((0, _platformFeatureFlags.fg)('platform_editor_block_menu_v2_patch_4')) { cancelExpandAndScroll = (0, _blockMenu.expandAllParentsThenScroll)(element, 0, function (el) { if (scrollToBlock) { scrollToBlock(el); } else { el.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); } else { cancelExpandAndScroll = (0, _blockMenu.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 = (0, _blockMenu.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 ((0, _blockMenu.isExpandCollapsed)(expandContainer)) { (0, _blockMenu.expandElement)(expandContainer); allReady = false; break; } } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } } var element = (0, _blockMenu.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 = (0, _browserApis.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 = (0, _browserApis.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]); };