diff --git a/core/modules/utils/dom/scroller.js b/core/modules/utils/dom/scroller.js index 84be12c50..73c9b323f 100644 --- a/core/modules/utils/dom/scroller.js +++ b/core/modules/utils/dom/scroller.js @@ -60,6 +60,25 @@ PageScroller.prototype.handleEvent = function(event) { return true; }; +/* +Find the scrollable parent of an element +*/ +PageScroller.prototype.findScrollableParent = function(element) { + if(!element) { + return window; + } + var parent = element.parentElement; + while(parent) { + var overflowY = window.getComputedStyle(parent).overflowY; + var isScrollable = overflowY === "auto" || overflowY === "scroll"; + if(isScrollable && parent.scrollHeight > parent.clientHeight) { + return parent; + } + parent = parent.parentElement; + } + return window; +}; + /* Handle a scroll event hitting the page document */ @@ -67,6 +86,9 @@ PageScroller.prototype.scrollIntoView = function(element,callback,options) { var self = this, duration = $tw.utils.hop(options,"animationDuration") ? parseInt(options.animationDuration) : $tw.utils.getAnimationDuration(), srcWindow = element ? element.ownerDocument.defaultView : window; + // Find the scrollable parent + var scrollContainer = this.findScrollableParent(element); + var isWindowScroll = scrollContainer === window || scrollContainer === srcWindow; // Now get ready to scroll the body this.cancelScroll(srcWindow); this.startTime = Date.now(); @@ -76,16 +98,46 @@ PageScroller.prototype.scrollIntoView = function(element,callback,options) { if(toolbar) { offset = toolbar.offsetHeight; } + // Get the scroll-margin-top and scroll-margin-left values from the element + var scrollMarginTop = 0, scrollMarginLeft = 0; + if(element) { + var computedStyle = srcWindow.getComputedStyle(element); + var marginTop = computedStyle.getPropertyValue("scroll-margin-top"); + if(marginTop) { + scrollMarginTop = parseFloat(marginTop) || 0; + } + var marginLeft = computedStyle.getPropertyValue("scroll-margin-left"); + if(marginLeft) { + scrollMarginLeft = parseFloat(marginLeft) || 0; + } + } // Get the client bounds of the element and adjust by the scroll position var getBounds = function() { var clientBounds = typeof callback === 'function' ? callback() : element.getBoundingClientRect(), + scrollPosition; + + if(isWindowScroll) { scrollPosition = $tw.utils.getScrollPosition(srcWindow); - return { - left: clientBounds.left + scrollPosition.x, - top: clientBounds.top + scrollPosition.y - offset, - width: clientBounds.width, - height: clientBounds.height - }; + return { + left: clientBounds.left + scrollPosition.x - scrollMarginLeft, + top: clientBounds.top + scrollPosition.y - offset - scrollMarginTop, + width: clientBounds.width, + height: clientBounds.height + }; + } else { + // For container scroll, calculate position relative to container + var containerBounds = scrollContainer.getBoundingClientRect(); + scrollPosition = { + x: scrollContainer.scrollLeft, + y: scrollContainer.scrollTop + }; + return { + left: clientBounds.left - containerBounds.left + scrollPosition.x - scrollMarginLeft, + top: clientBounds.top - containerBounds.top + scrollPosition.y - offset - scrollMarginTop, + width: clientBounds.width, + height: clientBounds.height + }; + } }, // We'll consider the horizontal and vertical scroll directions separately via this function // targetPos/targetSize - position and size of the target element @@ -111,11 +163,30 @@ PageScroller.prototype.scrollIntoView = function(element,callback,options) { t = 1; } t = $tw.utils.slowInSlowOut(t); - var scrollPosition = $tw.utils.getScrollPosition(srcWindow), - bounds = getBounds(), - endX = getEndPos(bounds.left,bounds.width,scrollPosition.x,srcWindow.innerWidth), - endY = getEndPos(bounds.top,bounds.height,scrollPosition.y,srcWindow.innerHeight); - srcWindow.scrollTo(scrollPosition.x + (endX - scrollPosition.x) * t,scrollPosition.y + (endY - scrollPosition.y) * t); + var scrollPosition, bounds, endX, endY, viewportWidth, viewportHeight; + + if(isWindowScroll) { + scrollPosition = $tw.utils.getScrollPosition(srcWindow); + bounds = getBounds(); + viewportWidth = srcWindow.innerWidth; + viewportHeight = srcWindow.innerHeight; + endX = getEndPos(bounds.left,bounds.width,scrollPosition.x,viewportWidth); + endY = getEndPos(bounds.top,bounds.height,scrollPosition.y,viewportHeight); + srcWindow.scrollTo(scrollPosition.x + (endX - scrollPosition.x) * t,scrollPosition.y + (endY - scrollPosition.y) * t); + } else { + scrollPosition = { + x: scrollContainer.scrollLeft, + y: scrollContainer.scrollTop + }; + bounds = getBounds(); + viewportWidth = scrollContainer.clientWidth; + viewportHeight = scrollContainer.clientHeight; + endX = getEndPos(bounds.left,bounds.width,scrollPosition.x,viewportWidth); + endY = getEndPos(bounds.top,bounds.height,scrollPosition.y,viewportHeight); + scrollContainer.scrollLeft = scrollPosition.x + (endX - scrollPosition.x) * t; + scrollContainer.scrollTop = scrollPosition.y + (endY - scrollPosition.y) * t; + } + if(t < 1) { self.idRequestFrame = self.requestAnimationFrame.call(srcWindow,drawFrame); } diff --git a/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-scroll.tid b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-scroll.tid index 047b7e45f..0150c7fbe 100644 --- a/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-scroll.tid +++ b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-scroll.tid @@ -1,15 +1,59 @@ caption: tm-scroll created: 20160425000906330 -modified: 20201014152456174 +modified: 20250809084836027 tags: Messages title: WidgetMessage: tm-scroll type: text/vnd.tiddlywiki +\procedure example-content() +\procedure openingBracket() [ +\procedure closingBracket() ] +\procedure quote() " +\function tf.navigate-to-css-escaped() [escapecss[]] +\procedure linkcatcher-actions() +<$action-sendmessage $message="tm-scroll" selector={{{ [[.tc-scrollable-container-example-scroll-container ]addsuffixaddsuffix[data-tiddler-title=]addsuffixaddsuffixaddsuffixaddsuffixaddsuffix[.tc-tiddler-frame]] }}}/> +\end linkcatcher-actions + +
+
+<$linkcatcher actions=<>> +{{$:/core/ui/SideBar/Recent}} + +
+
+<$list filter="[all[tiddlers]!is[system]!sort[modified]limit{$:/config/RecentLimit}] -[]" storyview="classic" template="$:/core/ui/ViewTemplate"/> +
+
+\end example-content + The `tm-scroll` message causes the surrounding scrollable container to scroll to the specified DOM node into view. The `tm-scroll` is handled in various places in the core itself, but can also be handled by a [[ScrollableWidget]]. +The core scroller implementation (`$:/core/modules/utils/dom/scroller.js`) provides intelligent scrolling behavior that: + +* Automatically detects the nearest scrollable parent container (elements with `overflow: auto` or `overflow: scroll`) +* Falls back to window scrolling if no scrollable parent is found +* Respects CSS `scroll-margin-top` property on target elements for proper spacing +* Accounts for fixed toolbars with class `tc-adjust-top-of-scroll` to prevent content being hidden +* Uses smooth animation with configurable duration via the `slowInSlowOut` easing function +* Supports both window-level and container-level scrolling with proper coordinate calculations +* Snaps to top/left edges when scrolling within 50 pixels of them for cleaner positioning + |!Name |!Description | |target |Target DOM node the scrollable container should scroll to (note that this parameter can only be set via JavaScript code) | |selector |<<.from-version "5.1.23">> Optional string [[CSS selector|https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors]] as an alternate means of identifying the target DOM node | |animationDuration |<<.from-version "5.2.2">> Optional number specifying the animation duration in milliseconds for the scrolling. Defaults to the [[global animation duration|$:/config/AnimationDuration]]. | <<.tip "Set `animationDuration` to `0` to scroll without animation">> + +!! Implementation Details + +The scroller uses `requestAnimationFrame` for smooth scrolling performance and provides methods to: + +* Check if scrolling is currently in progress via `isScrolling()` +* Cancel an in-progress scroll animation via `cancelScroll()` +* Handle both direct element references and CSS selector-based targeting + +Here's an example how <<.from-version "5.4.0">> the `tm-scroll` message can be used to scroll any scrollable container
+When clicking any of the links within the left pane you can observe that the container at the right scrolls to the navigated element + +<$transclude $variable=".example" n="1" eg=<>/>