This commit is contained in:
Simon Huber 2025-12-02 14:33:58 +01:00 committed by GitHub
commit e95f885831
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 127 additions and 12 deletions

View file

@ -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);
}

View file

@ -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() [<navigateTo>escapecss[]]
\procedure linkcatcher-actions()
<$action-sendmessage $message="tm-scroll" selector={{{ [[.tc-scrollable-container-example-scroll-container ]addsuffix<openingBracket>addsuffix[data-tiddler-title=]addsuffix<quote>addsuffix<tf.navigate-to-css-escaped>addsuffix<quote>addsuffix<closingBracket>addsuffix[.tc-tiddler-frame]] }}}/>
\end linkcatcher-actions
<div class="tc-scrollable-container-example" style="height: 75vh; border: 1px solid #ccc; display: flex;">
<div style="width: 25%; overflow: auto;">
<$linkcatcher actions=<<linkcatcher-actions>>>
{{$:/core/ui/SideBar/Recent}}
</$linkcatcher>
</div>
<div class="tc-scrollable-container-example-scroll-container" style="width: 75%; overflow: auto; padding: 1rem;">
<$list filter="[all[tiddlers]!is[system]!sort[modified]limit{$:/config/RecentLimit}] -[<currentTiddler>]" storyview="classic" template="$:/core/ui/ViewTemplate"/>
</div>
</div>
\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<br>
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=<<example-content>>/>