mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-09-17 10:56:49 +00:00
The watch label, version selectors, diff options and tabs move into a sticky #diff-header, and the top menu above it (watch URL, EDIT, theme and GitHub links) becomes sticky too, so the whole header stays on screen while the diff scrolls. The minimap and anchor jumps offset by the measured height of both bars instead of a fixed value. Heights are measured on load and kept current with a ResizeObserver where it is available, falling back to the window resize and hashchange events as the restock graph and queue sparkline already do, plus one deferred measure at load - the tab switch hides #settings and so resizes the header without a window resize, and opening a link already at #screenshot does the same with no hashchange to follow. The app header is stored before the diff header is measured, since the latter's cap is derived from the former. A sticky bar must be opaque to the content scrolling beneath it, but the page's own backdrop is a fixed full-viewport gradient layer over the page colour, so a flat --color-background panel reads as a white band cutting across it. Instead each bar repaints that same backdrop via a shared page-surface-gradient mixin: the three gradient stops pre-composited against --color-background-page with color-mix() at the layer's 0.91 opacity, with background-attachment: fixed so they stay registered with body::after. Children of #diff-header are centred with a flex column, mirroring the align-items: center that section.content applies to the non-sticky siblings, so the controls panel and tabs keep their intrinsic width and standard colours. The top menu is made sticky only under body.difference-page. Upstream already wants this globally (see the @todo in parts/_top_menu.scss) but held off because the bar has no background of its own; scoping it here keeps that decision separate. Sticky makes .header a stacking context, so its mobile drawer is capped at the bar's z-index of 30 - high enough to cover #diff-header, low enough that the action rail's hover flyout and toast notifications still paint over the bar as before. The activity strip is fixed on body in the root stacking context, so it would have painted through the capped drawer; on this page it drops just below the bar rather than raising the bar past the rail. Dropping the 40px section.content padding and tightening the title's own padding closes the gap between the two bars, and #diff-header's half-viewport cap now excludes the top menu so the sticky stack stays within the same budget on short viewports. .app-main's 0.55rem gap goes too on this page: it sits between the two sticky bars, so the diff header would otherwise start 8.8px below the top menu and slide up to meet it over the first 8.8px of scroll. That gap cannot survive sticking - it is outside both bars, so the diff would scroll through it - and the bars are flush at every scroll position instead. Co-authored-by: Engineer <engineer@agents.matrixsi.com> Co-authored-by: Jeff Hedlund <jhedlund@gmail.com> Signed-off-by: Jeff Hedlund <jhedlund@gmail.com>
202 lines
8.3 KiB
JavaScript
202 lines
8.3 KiB
JavaScript
$(document).ready(function () {
|
|
|
|
// Find all <span> elements inside pre#difference
|
|
var inputs = $('#difference span').toArray();
|
|
inputs.current = 0;
|
|
|
|
// Setup visual minimap of difference locations (cells are pre-built in Python)
|
|
var $visualizer = $('#cell-diff-jump-visualiser');
|
|
var $difference = $('#difference');
|
|
var $cells = $visualizer.find('> div');
|
|
var visualizerResolutionCells = $cells.length;
|
|
var cellHeight;
|
|
|
|
var header = document.getElementById('diff-header');
|
|
// The app top menu, sticky above #diff-header on the diff page.
|
|
var appHeader = document.querySelector('.app-main > .header');
|
|
|
|
// Controls can wrap or disappear when switching tabs, and the top menu
|
|
// rewraps on narrow viewports. Keep each sticky layer, the minimap and the
|
|
// anchor links below the actual measured heights instead of fixed offsets.
|
|
// Order matters: #diff-header's max-height is calc(50dvh - the app header),
|
|
// so store that first and measure the diff header against the new cap. The
|
|
// other way round reads it under the previous cap and one pass is not enough
|
|
// to settle.
|
|
function updateHeaderHeight() {
|
|
if (appHeader) {
|
|
document.body.style.setProperty('--app-header-height', appHeader.offsetHeight + 'px');
|
|
}
|
|
if (header) {
|
|
document.body.style.setProperty('--diff-header-height', header.offsetHeight + 'px');
|
|
}
|
|
}
|
|
if (header || appHeader) {
|
|
// Measure once regardless of observer support, so the offsets are never
|
|
// left at their 0 fallback.
|
|
updateHeaderHeight();
|
|
if (typeof ResizeObserver !== 'undefined') {
|
|
var headerObserver = new ResizeObserver(updateHeaderHeight);
|
|
if (header) headerObserver.observe(header);
|
|
if (appHeader) headerObserver.observe(appHeader);
|
|
} else {
|
|
// Without it, catch the two things that actually resize the bars:
|
|
// the window rewrapping them, and the tab switch that shows or hides
|
|
// #settings. diff-overview.js toggles that from its own hashchange
|
|
// handler, so defer past it rather than depend on listener order.
|
|
$(window).on('resize.diffheader', updateHeaderHeight.debounce(100));
|
|
$(window).on('hashchange.diffheader', function () {
|
|
setTimeout(updateHeaderHeight, 0);
|
|
});
|
|
// Opening a link straight at #screenshot / #extract hides #settings
|
|
// from diff-overview.js's ready handler, with no hashchange to
|
|
// follow. Re-measure once the whole ready pass has run.
|
|
setTimeout(updateHeaderHeight, 0);
|
|
}
|
|
}
|
|
|
|
// Centre of the region left visible below the sticky stack, not of the whole
|
|
// viewport - every sticky layer above the diff has to be counted or jumps
|
|
// land half its height too high.
|
|
function viewportCenterOffset() {
|
|
var appHeaderHeight = appHeader ? appHeader.offsetHeight : 0;
|
|
var headerHeight = header ? header.offsetHeight : 0;
|
|
var visualizerHeight = $visualizer.is(':visible') ? $visualizer.outerHeight() : 0;
|
|
return (appHeaderHeight + headerHeight + visualizerHeight + $(window).height()) / 2;
|
|
}
|
|
|
|
if ($difference.length && visualizerResolutionCells > 0) {
|
|
var docHeight = $difference[0].scrollHeight;
|
|
cellHeight = docHeight / visualizerResolutionCells;
|
|
|
|
// Add click handlers to pre-built cells
|
|
$cells.each(function(i) {
|
|
$(this).data('cellIndex', i);
|
|
$(this).on('click', function() {
|
|
var cellIndex = $(this).data('cellIndex');
|
|
var targetPositionInDifference = cellIndex * cellHeight;
|
|
|
|
// Scroll so target is at viewport center (where eyes expect it)
|
|
window.scrollTo({
|
|
top: $difference.offset().top + targetPositionInDifference - viewportCenterOffset(),
|
|
behavior: "smooth"
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
$('#jump-next-diff').click(function () {
|
|
if (!inputs || inputs.length === 0) return;
|
|
|
|
// Find the next change after current scroll position
|
|
var currentScrollPos = $(window).scrollTop();
|
|
var currentCenter = currentScrollPos + viewportCenterOffset();
|
|
|
|
// Add small buffer (50px) to jump past changes already near center
|
|
var searchFromPosition = currentCenter + 50;
|
|
|
|
var nextElement = null;
|
|
for (var i = 0; i < inputs.length; i++) {
|
|
var elementTop = $(inputs[i]).offset().top;
|
|
if (elementTop > searchFromPosition) {
|
|
nextElement = inputs[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If no element found ahead, wrap to first element
|
|
if (!nextElement) {
|
|
nextElement = inputs[0];
|
|
}
|
|
|
|
// Scroll to position the element at viewport center
|
|
var elementTop = $(nextElement).offset().top;
|
|
var targetScrollPos = elementTop - viewportCenterOffset();
|
|
|
|
window.scrollTo({
|
|
top: targetScrollPos,
|
|
behavior: "smooth",
|
|
});
|
|
});
|
|
|
|
// Track current scroll position in visualizer
|
|
function updateVisualizerPosition() {
|
|
if (!$difference.length || visualizerResolutionCells === 0) return;
|
|
|
|
var scrollTop = $(window).scrollTop();
|
|
var viewportHeight = $(window).height();
|
|
var viewportCenter = scrollTop + viewportCenterOffset();
|
|
var differenceTop = $difference.offset().top;
|
|
var differenceHeight = $difference[0].scrollHeight;
|
|
var positionInDifference = viewportCenter - differenceTop;
|
|
|
|
// Handle edge case: if we're at max scroll, show last cell
|
|
// This prevents shorter documents from never reaching 100%
|
|
var maxScrollTop = $(document).height() - viewportHeight;
|
|
var isAtBottom = scrollTop >= maxScrollTop - 10; // 10px tolerance
|
|
|
|
// Calculate which cell we're currently viewing
|
|
var currentCell;
|
|
if (isAtBottom) {
|
|
currentCell = visualizerResolutionCells - 1;
|
|
} else {
|
|
currentCell = Math.floor(positionInDifference / cellHeight);
|
|
currentCell = Math.max(0, Math.min(currentCell, visualizerResolutionCells - 1));
|
|
}
|
|
|
|
// Remove previous active marker and add to current cell
|
|
$visualizer.find('> div').removeClass('current-position');
|
|
$visualizer.find('> div').eq(currentCell).addClass('current-position');
|
|
}
|
|
|
|
// Recalculate cellHeight on window resize
|
|
function handleResize() {
|
|
if ($difference.length) {
|
|
var docHeight = $difference[0].scrollHeight;
|
|
cellHeight = docHeight / visualizerResolutionCells;
|
|
updateVisualizerPosition();
|
|
}
|
|
}
|
|
|
|
// Debounce scroll and resize events to reduce CPU usage
|
|
$(window).on('scroll', updateVisualizerPosition.debounce(5));
|
|
$(window).on('resize', handleResize.debounce(100));
|
|
|
|
// Initial scroll to specific line if requested
|
|
if (typeof initialScrollToLineNumber !== 'undefined' && initialScrollToLineNumber !== null && $difference.length) {
|
|
// Convert line number to text position and scroll to it
|
|
var diffText = $difference.text();
|
|
var lines = diffText.split('\n');
|
|
|
|
if (initialScrollToLineNumber > 0 && initialScrollToLineNumber <= lines.length) {
|
|
// Calculate character position of the target line
|
|
var charPosition = 0;
|
|
for (var i = 0; i < initialScrollToLineNumber - 1; i++) {
|
|
charPosition += lines[i].length + 1; // +1 for newline
|
|
}
|
|
|
|
// Estimate vertical position based on average line height
|
|
var totalChars = diffText.length;
|
|
var totalHeight = $difference[0].scrollHeight;
|
|
var estimatedTop = (charPosition / totalChars) * totalHeight;
|
|
|
|
// Scroll to position with line at viewport center
|
|
setTimeout(function() {
|
|
window.scrollTo({
|
|
top: $difference.offset().top + estimatedTop - viewportCenterOffset(),
|
|
behavior: "smooth"
|
|
});
|
|
}, 100); // Small delay to ensure page is fully loaded
|
|
}
|
|
}
|
|
|
|
// Initial position update
|
|
if ($difference.length && cellHeight) {
|
|
updateVisualizerPosition();
|
|
}
|
|
|
|
function changed() {
|
|
//$('#jump-next-diff').click();
|
|
}
|
|
|
|
});
|